Use EntityDescription - august (#56395)

This commit is contained in:
Marc Mueller 2021-09-19 01:10:15 +02:00 committed by GitHub
parent 9b710cad5d
commit a4f6c3336f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 145 additions and 71 deletions

View file

@ -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:

View file

@ -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}"