Enforce strict typing for SimpliSafe (#53417)

This commit is contained in:
Aaron Bach 2021-07-27 14:11:54 -06:00 committed by GitHub
parent f71980a634
commit f92ba75791
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 211 additions and 97 deletions

View File

@ -84,6 +84,7 @@ homeassistant.components.scene.*
homeassistant.components.select.*
homeassistant.components.sensor.*
homeassistant.components.shelly.*
homeassistant.components.simplisafe.*
homeassistant.components.slack.*
homeassistant.components.sonos.media_player
homeassistant.components.ssdp.*

View File

@ -1,17 +1,28 @@
"""Support for SimpliSafe alarm systems."""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable
from typing import Callable, cast
from uuid import UUID
from simplipy import get_api
from simplipy.api import API
from simplipy.errors import (
EndpointUnavailableError,
InvalidCredentialsError,
SimplipyError,
)
from simplipy.sensor.v2 import SensorV2
from simplipy.sensor.v3 import SensorV3
from simplipy.system import SystemNotification
from simplipy.system.v2 import SystemV2
from simplipy.system.v3 import SystemV3
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import CoreState, callback
from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
@ -109,7 +120,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend(
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
async def async_get_client_id(hass):
async def async_get_client_id(hass: HomeAssistant) -> str:
"""Get a client ID (based on the HASS unique ID) for the SimpliSafe API.
Note that SimpliSafe requires full, "dashed" versions of UUIDs.
@ -118,7 +129,9 @@ async def async_get_client_id(hass):
return str(UUID(hass_id))
async def async_register_base_station(hass, system, config_entry_id):
async def async_register_base_station(
hass: HomeAssistant, system: SystemV2 | SystemV3, config_entry_id: str
) -> None:
"""Register a new bridge."""
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
@ -130,11 +143,11 @@ async def async_register_base_station(hass, system, config_entry_id):
)
async def async_setup_entry(hass, config_entry): # noqa: C901
"""Set up SimpliSafe as config entry."""
hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}})
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = []
@callback
def _async_standardize_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Bring a config entry up to current standards."""
if CONF_PASSWORD not in config_entry.data:
raise ConfigEntryAuthFailed("Config schema change requires re-authentication")
@ -154,6 +167,14 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
if entry_updates:
hass.config_entries.async_update_entry(config_entry, **entry_updates)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up SimpliSafe as config entry."""
hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}})
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = []
_async_standardize_config_entry(hass, config_entry)
_verify_domain_control = verify_domain_control(hass, DOMAIN)
client_id = await async_get_client_id(hass)
@ -183,10 +204,12 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
@callback
def verify_system_exists(coro):
def verify_system_exists(
coro: Callable[..., Awaitable]
) -> Callable[..., Awaitable]:
"""Log an error if a service call uses an invalid system ID."""
async def decorator(call):
async def decorator(call: ServiceCall) -> None:
"""Decorate."""
system_id = int(call.data[ATTR_SYSTEM_ID])
if system_id not in simplisafe.systems:
@ -197,10 +220,10 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
return decorator
@callback
def v3_only(coro):
def v3_only(coro: Callable[..., Awaitable]) -> Callable[..., Awaitable]:
"""Log an error if the decorated coroutine is called with a v2 system."""
async def decorator(call):
async def decorator(call: ServiceCall) -> None:
"""Decorate."""
system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])]
if system.version != 3:
@ -212,43 +235,40 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
@verify_system_exists
@_verify_domain_control
async def clear_notifications(call):
async def clear_notifications(call: ServiceCall) -> None:
"""Clear all active notifications."""
system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.clear_notifications()
except SimplipyError as err:
LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@_verify_domain_control
async def remove_pin(call):
async def remove_pin(call: ServiceCall) -> None:
"""Remove a PIN."""
system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE])
except SimplipyError as err:
LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@_verify_domain_control
async def set_pin(call):
async def set_pin(call: ServiceCall) -> None:
"""Set a PIN."""
system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]]
try:
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE])
except SimplipyError as err:
LOGGER.error("Error during service call: %s", err)
return
@verify_system_exists
@v3_only
@_verify_domain_control
async def set_system_properties(call):
async def set_system_properties(call: ServiceCall) -> None:
"""Set one or more system parameters."""
system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]]
system = cast(SystemV3, simplisafe.systems[call.data[ATTR_SYSTEM_ID]])
try:
await system.set_properties(
{
@ -259,7 +279,6 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
)
except SimplipyError as err:
LOGGER.error("Error during service call: %s", err)
return
for service, method, schema in (
("clear_notifications", clear_notifications, None),
@ -278,7 +297,7 @@ async def async_setup_entry(hass, config_entry): # noqa: C901
return True
async def async_unload_entry(hass, entry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a SimpliSafe config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
@ -287,7 +306,7 @@ async def async_unload_entry(hass, entry):
return unload_ok
async def async_reload_entry(hass, config_entry):
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Handle an options update."""
await hass.config_entries.async_reload(config_entry.entry_id)
@ -295,17 +314,19 @@ async def async_reload_entry(hass, config_entry):
class SimpliSafe:
"""Define a SimpliSafe data object."""
def __init__(self, hass, config_entry, api):
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: API
) -> None:
"""Initialize."""
self._api = api
self._hass = hass
self._system_notifications = {}
self._system_notifications: dict[int, set[SystemNotification]] = {}
self.config_entry = config_entry
self.coordinator = None
self.systems = {}
self.coordinator: DataUpdateCoordinator | None = None
self.systems: dict[int, SystemV2 | SystemV3] = {}
@callback
def _async_process_new_notifications(self, system):
def _async_process_new_notifications(self, system: SystemV2 | SystemV3) -> None:
"""Act on any new system notifications."""
if self._hass.state != CoreState.running:
# If HASS isn't fully running yet, it may cause the SIMPLISAFE_NOTIFICATION
@ -324,8 +345,6 @@ class SimpliSafe:
LOGGER.debug("New system notifications: %s", to_add)
self._system_notifications[system.system_id].update(to_add)
for notification in to_add:
text = notification.text
if notification.link:
@ -341,7 +360,9 @@ class SimpliSafe:
},
)
async def async_init(self):
self._system_notifications[system.system_id] = latest_notifications
async def async_init(self) -> None:
"""Initialize the data class."""
self.systems = await self._api.get_systems()
for system in self.systems.values():
@ -361,10 +382,10 @@ class SimpliSafe:
update_method=self.async_update,
)
async def async_update(self):
async def async_update(self) -> None:
"""Get updated data from SimpliSafe."""
async def async_update_system(system):
async def async_update_system(system: SystemV2 | SystemV3) -> None:
"""Update a system."""
await system.update(cached=system.version != 3)
self._async_process_new_notifications(system)
@ -389,8 +410,16 @@ class SimpliSafe:
class SimpliSafeEntity(CoordinatorEntity):
"""Define a base SimpliSafe entity."""
def __init__(self, simplisafe, system, name, *, serial=None):
def __init__(
self,
simplisafe: SimpliSafe,
system: SystemV2 | SystemV3,
name: str,
*,
serial: str | None = None,
) -> None:
"""Initialize."""
assert simplisafe.coordinator
super().__init__(simplisafe.coordinator)
if serial:
@ -413,32 +442,33 @@ class SimpliSafeEntity(CoordinatorEntity):
self._system = system
@property
def available(self):
def available(self) -> bool:
"""Return whether the entity is available."""
# We can easily detect if the V3 system is offline, but no simple check exists
# for the V2 system. Therefore, assuming the coordinator hasn't failed, we mark
# the entity as available if:
# 1. We can verify that the system is online (assuming True if we can't)
# 2. We can verify that the entity is online
return (
super().available
and self._online
and not (self._system.version == 3 and self._system.offline)
)
if isinstance(self._system, SystemV3):
system_offline = self._system.offline
else:
system_offline = False
return super().available and self._online and not system_offline
@callback
def _handle_coordinator_update(self):
def _handle_coordinator_update(self) -> None:
"""Update the entity with new REST API data."""
self.async_update_from_rest_api()
self.async_write_ha_state()
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
await super().async_added_to_hass()
self.async_update_from_rest_api()
@callback
def async_update_from_rest_api(self):
def async_update_from_rest_api(self) -> None:
"""Update the entity with the provided REST API data."""
raise NotImplementedError()
@ -446,13 +476,22 @@ class SimpliSafeEntity(CoordinatorEntity):
class SimpliSafeBaseSensor(SimpliSafeEntity):
"""Define a SimpliSafe base (binary) sensor."""
def __init__(self, simplisafe, system, sensor):
def __init__(
self,
simplisafe: SimpliSafe,
system: SystemV2 | SystemV3,
sensor: SensorV2 | SensorV3,
) -> None:
"""Initialize."""
super().__init__(simplisafe, system, sensor.name, serial=sensor.serial)
self._attr_device_info["identifiers"] = {(DOMAIN, sensor.serial)}
self._attr_device_info["model"] = sensor.type.name
self._attr_device_info["name"] = sensor.name
self._attr_device_info = {
"identifiers": {(DOMAIN, sensor.serial)},
"manufacturer": "SimpliSafe",
"model": sensor.type.name,
"name": sensor.name,
"via_device": (DOMAIN, system.serial),
}
human_friendly_name = " ".join([w.title() for w in sensor.type.name.split("_")])
self._attr_name = f"{super().name} {human_friendly_name}"

View File

@ -1,8 +1,12 @@
"""Support for SimpliSafe alarm control panels."""
from __future__ import annotations
import re
from simplipy.errors import SimplipyError
from simplipy.system import SystemStates
from simplipy.system.v2 import SystemV2
from simplipy.system.v3 import SystemV3
from homeassistant.components.alarm_control_panel import (
FORMAT_NUMBER,
@ -13,6 +17,7 @@ from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_AWAY,
SUPPORT_ALARM_ARM_HOME,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_CODE,
STATE_ALARM_ARMED_AWAY,
@ -21,9 +26,10 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SimpliSafeEntity
from . import SimpliSafe, SimpliSafeEntity
from .const import (
ATTR_ALARM_DURATION,
ATTR_ALARM_VOLUME,
@ -48,7 +54,9 @@ ATTR_WALL_POWER_LEVEL = "wall_power_level"
ATTR_WIFI_STRENGTH = "wifi_strength"
async def async_setup_entry(hass, entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up a SimpliSafe alarm control panel based on a config entry."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
async_add_entities(
@ -60,7 +68,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
"""Representation of a SimpliSafe alarm."""
def __init__(self, simplisafe, system):
def __init__(self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3) -> None:
"""Initialize the SimpliSafe alarm."""
super().__init__(simplisafe, system, "Alarm Control Panel")
@ -91,7 +99,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
self._attr_state = None
@callback
def _is_code_valid(self, code, state):
def _is_code_valid(self, code: str | None, state: str) -> bool:
"""Validate that a code matches the required one."""
if not self._simplisafe.config_entry.options.get(CONF_CODE):
return True
@ -104,7 +112,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
return True
async def async_alarm_disarm(self, code=None):
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
if not self._is_code_valid(code, STATE_ALARM_DISARMED):
return
@ -118,7 +126,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
self._attr_state = STATE_ALARM_DISARMED
self.async_write_ha_state()
async def async_alarm_arm_home(self, code=None):
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
if not self._is_code_valid(code, STATE_ALARM_ARMED_HOME):
return
@ -134,7 +142,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
self._attr_state = STATE_ALARM_ARMED_HOME
self.async_write_ha_state()
async def async_alarm_arm_away(self, code=None):
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
if not self._is_code_valid(code, STATE_ALARM_ARMED_AWAY):
return
@ -151,9 +159,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
self.async_write_ha_state()
@callback
def async_update_from_rest_api(self):
def async_update_from_rest_api(self) -> None:
"""Update the entity with the provided REST API data."""
if self._system.version == 3:
if isinstance(self._system, SystemV3):
self._attr_extra_state_attributes.update(
{
ATTR_ALARM_DURATION: self._system.alarm_duration,
@ -175,9 +183,6 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
}
)
# Although system state updates are designed the come via the websocket, the
# SimpliSafe cloud can sporadically fail to send those updates as expected; so,
# just in case, we synchronize the state via the REST API, too:
if self._system.state == SystemStates.alarm:
self._attr_state = STATE_ALARM_TRIGGERED
elif self._system.state == SystemStates.away:

View File

@ -1,5 +1,9 @@
"""Support for SimpliSafe binary sensors."""
from simplipy.entity import EntityTypes
from __future__ import annotations
from simplipy.entity import Entity as SimplipyEntity, EntityTypes
from simplipy.system.v2 import SystemV2
from simplipy.system.v3 import SystemV3
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY,
@ -11,9 +15,11 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_SMOKE,
BinarySensorEntity,
)
from homeassistant.core import callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SimpliSafeBaseSensor
from . import SimpliSafe, SimpliSafeBaseSensor
from .const import DATA_CLIENT, DOMAIN, LOGGER
SUPPORTED_BATTERY_SENSOR_TYPES = [
@ -39,10 +45,13 @@ TRIGGERED_SENSOR_TYPES = {
}
async def async_setup_entry(hass, entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up SimpliSafe binary sensors based on a config entry."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
sensors = []
sensors: list[BatteryBinarySensor | TriggeredBinarySensor] = []
for system in simplisafe.systems.values():
if system.version == 2:
@ -68,14 +77,20 @@ async def async_setup_entry(hass, entry, async_add_entities):
class TriggeredBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity):
"""Define a binary sensor related to whether an entity has been triggered."""
def __init__(self, simplisafe, system, sensor, device_class):
def __init__(
self,
simplisafe: SimpliSafe,
system: SystemV2 | SystemV3,
sensor: SimplipyEntity,
device_class: str,
) -> None:
"""Initialize."""
super().__init__(simplisafe, system, sensor)
self._attr_device_class = device_class
@callback
def async_update_from_rest_api(self):
def async_update_from_rest_api(self) -> None:
"""Update the entity with the provided REST API data."""
self._attr_is_on = self._sensor.triggered
@ -85,13 +100,18 @@ class BatteryBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity):
_attr_device_class = DEVICE_CLASS_BATTERY
def __init__(self, simplisafe, system, sensor):
def __init__(
self,
simplisafe: SimpliSafe,
system: SystemV2 | SystemV3,
sensor: SimplipyEntity,
) -> None:
"""Initialize."""
super().__init__(simplisafe, system, sensor)
self._attr_unique_id = f"{super().unique_id}-battery"
@callback
def async_update_from_rest_api(self):
def async_update_from_rest_api(self) -> None:
"""Update the entity with the provided REST API data."""
self._attr_is_on = self._sensor.low_battery

View File

@ -1,5 +1,10 @@
"""Config flow to configure the SimpliSafe component."""
from __future__ import annotations
from typing import Any
from simplipy import get_api
from simplipy.api import API
from simplipy.errors import (
InvalidCredentialsError,
PendingAuthorizationError,
@ -8,9 +13,12 @@ from simplipy.errors import (
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.typing import ConfigType
from . import async_get_client_id
from .const import DOMAIN, LOGGER
@ -30,20 +38,25 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self):
def __init__(self) -> None:
"""Initialize the config flow."""
self._code = None
self._password = None
self._username = None
self._code: str | None = None
self._password: str | None = None
self._username: str | None = None
@staticmethod
@callback
def async_get_options_flow(config_entry):
def async_get_options_flow(
config_entry: ConfigEntry,
) -> SimpliSafeOptionsFlowHandler:
"""Define the config flow to handle options."""
return SimpliSafeOptionsFlowHandler(config_entry)
async def _async_get_simplisafe_api(self):
async def _async_get_simplisafe_api(self) -> API:
"""Get an authenticated SimpliSafe API client."""
assert self._username
assert self._password
client_id = await async_get_client_id(self.hass)
websession = aiohttp_client.async_get_clientsession(self.hass)
@ -54,7 +67,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
session=websession,
)
async def _async_login_during_step(self, *, step_id, form_schema):
async def _async_login_during_step(
self, *, step_id: str, form_schema: vol.Schema
) -> FlowResult:
"""Attempt to log into the API from within a config flow step."""
errors = {}
@ -84,8 +99,10 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
)
async def async_step_finish(self, user_input=None):
async def async_step_finish(self, user_input: dict[str, Any]) -> FlowResult:
"""Handle finish config entry setup."""
assert self._username
existing_entry = await self.async_set_unique_id(self._username)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
@ -95,7 +112,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=self._username, data=user_input)
async def async_step_mfa(self, user_input=None):
async def async_step_mfa(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle multi-factor auth confirmation."""
if user_input is None:
return self.async_show_form(step_id="mfa")
@ -116,14 +135,16 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
)
async def async_step_reauth(self, config):
async def async_step_reauth(self, config: ConfigType) -> FlowResult:
"""Handle configuration by re-auth."""
self._code = config.get(CONF_CODE)
self._username = config[CONF_USERNAME]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle re-auth completion."""
if not user_input:
return self.async_show_form(
@ -136,7 +157,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA
)
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the start of the config flow."""
if not user_input:
return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA)
@ -156,11 +179,13 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a SimpliSafe options flow."""
def __init__(self, config_entry):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

View File

@ -1,11 +1,18 @@
"""Support for SimpliSafe locks."""
from __future__ import annotations
from typing import Any
from simplipy.errors import SimplipyError
from simplipy.lock import LockStates
from simplipy.lock import Lock, LockStates
from simplipy.system.v3 import SystemV3
from homeassistant.components.lock import LockEntity
from homeassistant.core import callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SimpliSafeEntity
from . import SimpliSafe, SimpliSafeEntity
from .const import DATA_CLIENT, DOMAIN, LOGGER
ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
@ -13,7 +20,9 @@ ATTR_JAMMED = "jammed"
ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery"
async def async_setup_entry(hass, entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up SimpliSafe locks based on a config entry."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
locks = []
@ -32,13 +41,13 @@ async def async_setup_entry(hass, entry, async_add_entities):
class SimpliSafeLock(SimpliSafeEntity, LockEntity):
"""Define a SimpliSafe lock."""
def __init__(self, simplisafe, system, lock):
def __init__(self, simplisafe: SimpliSafe, system: SystemV3, lock: Lock) -> None:
"""Initialize."""
super().__init__(simplisafe, system, lock.name, serial=lock.serial)
self._lock = lock
async def async_lock(self, **kwargs):
async def async_lock(self, **kwargs: dict[str, Any]) -> None:
"""Lock the lock."""
try:
await self._lock.lock()
@ -49,7 +58,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity):
self._attr_is_locked = True
self.async_write_ha_state()
async def async_unlock(self, **kwargs):
async def async_unlock(self, **kwargs: dict[str, Any]) -> None:
"""Unlock the lock."""
try:
await self._lock.unlock()
@ -61,7 +70,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity):
self.async_write_ha_state()
@callback
def async_update_from_rest_api(self):
def async_update_from_rest_api(self) -> None:
"""Update the entity with the provided REST API data."""
self._attr_extra_state_attributes.update(
{

View File

@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==11.0.2"],
"requirements": ["simplisafe-python==11.0.3"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling"
}

View File

@ -2,14 +2,18 @@
from simplipy.entity import EntityTypes
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import SimpliSafeBaseSensor
from .const import DATA_CLIENT, DOMAIN, LOGGER
async def async_setup_entry(hass, entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up SimpliSafe freeze sensors based on a config entry."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
sensors = []
@ -33,6 +37,6 @@ class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity):
_attr_unit_of_measurement = TEMP_FAHRENHEIT
@callback
def async_update_from_rest_api(self):
def async_update_from_rest_api(self) -> None:
"""Update the entity with the provided REST API data."""
self._attr_state = self._sensor.temperature

View File

@ -935,6 +935,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.simplisafe.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.slack.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -2107,7 +2107,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==11.0.2
simplisafe-python==11.0.3
# homeassistant.components.sisyphus
sisyphus-control==3.0

View File

@ -1156,7 +1156,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==11.0.2
simplisafe-python==11.0.3
# homeassistant.components.slack
slackclient==2.5.0