mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Enforce strict typing for SimpliSafe (#53417)
This commit is contained in:
parent
f71980a634
commit
f92ba75791
@ -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.*
|
||||
|
@ -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}"
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
11
mypy.ini
11
mypy.ini
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user