From a4f6c3336fc598a51a3adce81349b15320e1a3e2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 19 Sep 2021 01:10:15 +0200 Subject: [PATCH] Use EntityDescription - august (#56395) --- .../components/august/binary_sensor.py | 132 +++++++++++------- homeassistant/components/august/sensor.py | 84 ++++++++--- 2 files changed, 145 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 27a115a0823..6a2c9a2ff6d 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -1,17 +1,29 @@ """Support for August binary sensors.""" +from __future__ import annotations + +from dataclasses import dataclass from datetime import datetime, timedelta import logging +from typing import Callable, cast -from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, SOURCE_PUBNUB, ActivityType +from yalexs.activity import ( + ACTION_DOORBELL_CALL_MISSED, + SOURCE_PUBNUB, + Activity, + ActivityType, +) +from yalexs.doorbell import DoorbellDetail from yalexs.lock import LockDoorStatus from yalexs.util import update_lock_detail_from_activity +from homeassistant.components.august import AugustData from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -27,7 +39,7 @@ TIME_TO_RECHECK_DETECTION = timedelta( ) -def _retrieve_online_state(data, detail): +def _retrieve_online_state(data: AugustData, detail: DoorbellDetail) -> bool: """Get the latest state of the sensor.""" # The doorbell will go into standby mode when there is no motion # for a short while. It will wake by itself when needed so we need @@ -36,7 +48,7 @@ def _retrieve_online_state(data, detail): return detail.is_online or detail.is_standby -def _retrieve_motion_state(data, detail): +def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool: latest = data.activity_stream.get_latest_device_activity( detail.device_id, {ActivityType.DOORBELL_MOTION} ) @@ -47,7 +59,7 @@ def _retrieve_motion_state(data, detail): return _activity_time_based_state(latest) -def _retrieve_ding_state(data, detail): +def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool: latest = data.activity_stream.get_latest_device_activity( detail.device_id, {ActivityType.DOORBELL_DING} ) @@ -64,34 +76,62 @@ def _retrieve_ding_state(data, detail): return _activity_time_based_state(latest) -def _activity_time_based_state(latest): +def _activity_time_based_state(latest: Activity) -> bool: """Get the latest state of the sensor.""" start = latest.activity_start_time end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION return start <= _native_datetime() <= end -def _native_datetime(): +def _native_datetime() -> datetime: """Return time in the format august uses without timezone.""" return datetime.now() -SENSOR_NAME = 0 -SENSOR_DEVICE_CLASS = 1 -SENSOR_STATE_PROVIDER = 2 -SENSOR_STATE_IS_TIME_BASED = 3 +@dataclass +class AugustRequiredKeysMixin: + """Mixin for required keys.""" -# sensor_type: [name, device_class, state_provider, is_time_based] -SENSOR_TYPES_DOORBELL = { - "doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _retrieve_ding_state, True], - "doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _retrieve_motion_state, True], - "doorbell_online": [ - "Online", - DEVICE_CLASS_CONNECTIVITY, - _retrieve_online_state, - False, - ], -} + state_provider: Callable[[AugustData, DoorbellDetail], bool] + is_time_based: bool + + +@dataclass +class AugustBinarySensorEntityDescription( + BinarySensorEntityDescription, AugustRequiredKeysMixin +): + """Describes August binary_sensor entity.""" + + +SENSOR_TYPE_DOOR = BinarySensorEntityDescription( + key="door_open", + name="Open", +) + + +SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( + AugustBinarySensorEntityDescription( + key="doorbell_ding", + name="Ding", + device_class=DEVICE_CLASS_OCCUPANCY, + state_provider=_retrieve_ding_state, + is_time_based=True, + ), + AugustBinarySensorEntityDescription( + key="doorbell_motion", + name="Motion", + device_class=DEVICE_CLASS_MOTION, + state_provider=_retrieve_motion_state, + is_time_based=True, + ), + AugustBinarySensorEntityDescription( + key="doorbell_online", + name="Online", + device_class=DEVICE_CLASS_CONNECTIVITY, + state_provider=_retrieve_online_state, + is_time_based=False, + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -109,16 +149,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): continue _LOGGER.debug("Adding sensor class door for %s", door.device_name) - entities.append(AugustDoorBinarySensor(data, "door_open", door)) + entities.append(AugustDoorBinarySensor(data, door, SENSOR_TYPE_DOOR)) for doorbell in data.doorbells: - for sensor_type, sensor in SENSOR_TYPES_DOORBELL.items(): + for description in SENSOR_TYPES_DOORBELL: _LOGGER.debug( "Adding doorbell sensor class %s for %s", - sensor[SENSOR_DEVICE_CLASS], + description.device_class, doorbell.device_name, ) - entities.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) + entities.append(AugustDoorbellBinarySensor(data, doorbell, description)) async_add_entities(entities) @@ -128,14 +168,16 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): _attr_device_class = DEVICE_CLASS_DOOR - def __init__(self, data, sensor_type, device): + def __init__(self, data, device, description: BinarySensorEntityDescription): """Initialize the sensor.""" super().__init__(data, device) + self.entity_description = description self._data = data - self._sensor_type = sensor_type self._device = device - self._attr_name = f"{device.device_name} Open" - self._attr_unique_id = f"{self._device_id}_open" + self._attr_name = f"{device.device_name} {description.name}" + self._attr_unique_id = ( + f"{self._device_id}_{cast(str, description.name).lower()}" + ) self._update_from_data() @callback @@ -164,41 +206,29 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Representation of an August binary sensor.""" - def __init__(self, data, sensor_type, device): + entity_description: AugustBinarySensorEntityDescription + + def __init__(self, data, device, description: AugustBinarySensorEntityDescription): """Initialize the sensor.""" super().__init__(data, device) + self.entity_description = description self._check_for_off_update_listener = None self._data = data - self._sensor_type = sensor_type - self._attr_device_class = self._sensor_config[SENSOR_DEVICE_CLASS] - self._attr_name = f"{device.device_name} {self._sensor_config[SENSOR_NAME]}" + self._attr_name = f"{device.device_name} {description.name}" self._attr_unique_id = ( - f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}" + f"{self._device_id}_{cast(str, description.name).lower()}" ) self._update_from_data() - @property - def _sensor_config(self): - """Return the config for the sensor.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type] - - @property - def _state_provider(self): - """Return the state provider for the binary sensor.""" - return self._sensor_config[SENSOR_STATE_PROVIDER] - - @property - def _is_time_based(self): - """Return true of false if the sensor is time based.""" - return self._sensor_config[SENSOR_STATE_IS_TIME_BASED] - @callback def _update_from_data(self): """Get the latest state of the sensor.""" self._cancel_any_pending_updates() - self._attr_is_on = self._state_provider(self._data, self._detail) + self._attr_is_on = self.entity_description.state_provider( + self._data, self._detail + ) - if self._is_time_based: + if self.entity_description.is_time_based: self._attr_available = _retrieve_online_state(self._data, self._detail) self._schedule_update_to_recheck_turn_off_sensor() else: diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index b6d93d3b3b1..e78ae520034 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -1,9 +1,20 @@ """Support for August sensors.""" +from __future__ import annotations + +from dataclasses import dataclass import logging +from typing import Callable, Generic, TypeVar from yalexs.activity import ActivityType +from yalexs.keypad import KeypadDetail +from yalexs.lock import LockDetail -from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity +from homeassistant.components.august import AugustData +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.helpers.entity_registry import async_get_registry @@ -26,20 +37,44 @@ from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) -def _retrieve_device_battery_state(detail): +def _retrieve_device_battery_state(detail: LockDetail) -> int: """Get the latest state of the sensor.""" return detail.battery_level -def _retrieve_linked_keypad_battery_state(detail): +def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None: """Get the latest state of the sensor.""" return detail.battery_percentage -SENSOR_TYPES_BATTERY = { - "device_battery": {"state_provider": _retrieve_device_battery_state}, - "linked_keypad_battery": {"state_provider": _retrieve_linked_keypad_battery_state}, -} +T = TypeVar("T", LockDetail, KeypadDetail) + + +@dataclass +class AugustRequiredKeysMixin(Generic[T]): + """Mixin for required keys.""" + + state_provider: Callable[[T], int | None] + + +@dataclass +class AugustSensorEntityDescription( + SensorEntityDescription, AugustRequiredKeysMixin[T] +): + """Describes August sensor entity.""" + + +SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( + key="device_battery", + name="Battery", + state_provider=_retrieve_device_battery_state, +) + +SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail]( + key="linked_keypad_battery", + name="Battery", + state_provider=_retrieve_linked_keypad_battery_state, +) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -60,9 +95,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): operation_sensors.append(device) for device in batteries["device_battery"]: - state_provider = SENSOR_TYPES_BATTERY["device_battery"]["state_provider"] detail = data.get_device_detail(device.device_id) - if detail is None or state_provider(detail) is None: + if detail is None or SENSOR_TYPE_DEVICE_BATTERY.state_provider(detail) is None: _LOGGER.debug( "Not adding battery sensor for %s because it is not present", device.device_name, @@ -72,7 +106,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "Adding battery sensor for %s", device.device_name, ) - entities.append(AugustBatterySensor(data, "device_battery", device, device)) + entities.append( + AugustBatterySensor[LockDetail]( + data, device, device, SENSOR_TYPE_DEVICE_BATTERY + ) + ) for device in batteries["linked_keypad_battery"]: detail = data.get_device_detail(device.device_id) @@ -87,8 +125,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "Adding keypad battery sensor for %s", device.device_name, ) - keypad_battery_sensor = AugustBatterySensor( - data, "linked_keypad_battery", detail.keypad, device + keypad_battery_sensor = AugustBatterySensor[KeypadDetail]( + data, detail.keypad, device, SENSOR_TYPE_KEYPAD_BATTERY ) entities.append(keypad_battery_sensor) migrate_unique_id_devices.append(keypad_battery_sensor) @@ -204,29 +242,35 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): return f"{self._device_id}_lock_operator" -class AugustBatterySensor(AugustEntityMixin, SensorEntity): +class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]): """Representation of an August sensor.""" + entity_description: AugustSensorEntityDescription[T] _attr_device_class = DEVICE_CLASS_BATTERY _attr_native_unit_of_measurement = PERCENTAGE - def __init__(self, data, sensor_type, device, old_device): + def __init__( + self, + data: AugustData, + device, + old_device, + description: AugustSensorEntityDescription[T], + ): """Initialize the sensor.""" super().__init__(data, device) - self._sensor_type = sensor_type + self.entity_description = description self._old_device = old_device - self._attr_name = f"{device.device_name} Battery" - self._attr_unique_id = f"{self._device_id}_{sensor_type}" + self._attr_name = f"{device.device_name} {description.name}" + self._attr_unique_id = f"{self._device_id}_{description.key}" self._update_from_data() @callback def _update_from_data(self): """Get the latest state of the sensor.""" - state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"] - self._attr_native_value = state_provider(self._detail) + self._attr_native_value = self.entity_description.state_provider(self._detail) self._attr_available = self._attr_native_value is not None @property def old_unique_id(self) -> str: """Get the old unique id of the device sensor.""" - return f"{self._old_device.device_id}_{self._sensor_type}" + return f"{self._old_device.device_id}_{self.entity_description.key}"