Use EntityDescription - august (#56395)
This commit is contained in:
parent
9b710cad5d
commit
a4f6c3336f
2 changed files with 145 additions and 71 deletions
|
@ -1,17 +1,29 @@
|
||||||
"""Support for August binary sensors."""
|
"""Support for August binary sensors."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
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.lock import LockDoorStatus
|
||||||
from yalexs.util import update_lock_detail_from_activity
|
from yalexs.util import update_lock_detail_from_activity
|
||||||
|
|
||||||
|
from homeassistant.components.august import AugustData
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASS_CONNECTIVITY,
|
DEVICE_CLASS_CONNECTIVITY,
|
||||||
DEVICE_CLASS_DOOR,
|
DEVICE_CLASS_DOOR,
|
||||||
DEVICE_CLASS_MOTION,
|
DEVICE_CLASS_MOTION,
|
||||||
DEVICE_CLASS_OCCUPANCY,
|
DEVICE_CLASS_OCCUPANCY,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.event import async_call_later
|
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."""
|
"""Get the latest state of the sensor."""
|
||||||
# The doorbell will go into standby mode when there is no motion
|
# 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
|
# 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
|
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(
|
latest = data.activity_stream.get_latest_device_activity(
|
||||||
detail.device_id, {ActivityType.DOORBELL_MOTION}
|
detail.device_id, {ActivityType.DOORBELL_MOTION}
|
||||||
)
|
)
|
||||||
|
@ -47,7 +59,7 @@ def _retrieve_motion_state(data, detail):
|
||||||
return _activity_time_based_state(latest)
|
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(
|
latest = data.activity_stream.get_latest_device_activity(
|
||||||
detail.device_id, {ActivityType.DOORBELL_DING}
|
detail.device_id, {ActivityType.DOORBELL_DING}
|
||||||
)
|
)
|
||||||
|
@ -64,34 +76,62 @@ def _retrieve_ding_state(data, detail):
|
||||||
return _activity_time_based_state(latest)
|
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."""
|
"""Get the latest state of the sensor."""
|
||||||
start = latest.activity_start_time
|
start = latest.activity_start_time
|
||||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||||
return start <= _native_datetime() <= end
|
return start <= _native_datetime() <= end
|
||||||
|
|
||||||
|
|
||||||
def _native_datetime():
|
def _native_datetime() -> datetime:
|
||||||
"""Return time in the format august uses without timezone."""
|
"""Return time in the format august uses without timezone."""
|
||||||
return datetime.now()
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
SENSOR_NAME = 0
|
@dataclass
|
||||||
SENSOR_DEVICE_CLASS = 1
|
class AugustRequiredKeysMixin:
|
||||||
SENSOR_STATE_PROVIDER = 2
|
"""Mixin for required keys."""
|
||||||
SENSOR_STATE_IS_TIME_BASED = 3
|
|
||||||
|
|
||||||
# sensor_type: [name, device_class, state_provider, is_time_based]
|
state_provider: Callable[[AugustData, DoorbellDetail], bool]
|
||||||
SENSOR_TYPES_DOORBELL = {
|
is_time_based: bool
|
||||||
"doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _retrieve_ding_state, True],
|
|
||||||
"doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _retrieve_motion_state, True],
|
|
||||||
"doorbell_online": [
|
@dataclass
|
||||||
"Online",
|
class AugustBinarySensorEntityDescription(
|
||||||
DEVICE_CLASS_CONNECTIVITY,
|
BinarySensorEntityDescription, AugustRequiredKeysMixin
|
||||||
_retrieve_online_state,
|
):
|
||||||
False,
|
"""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):
|
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
|
continue
|
||||||
|
|
||||||
_LOGGER.debug("Adding sensor class door for %s", door.device_name)
|
_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 doorbell in data.doorbells:
|
||||||
for sensor_type, sensor in SENSOR_TYPES_DOORBELL.items():
|
for description in SENSOR_TYPES_DOORBELL:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Adding doorbell sensor class %s for %s",
|
"Adding doorbell sensor class %s for %s",
|
||||||
sensor[SENSOR_DEVICE_CLASS],
|
description.device_class,
|
||||||
doorbell.device_name,
|
doorbell.device_name,
|
||||||
)
|
)
|
||||||
entities.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
|
entities.append(AugustDoorbellBinarySensor(data, doorbell, description))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
@ -128,14 +168,16 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||||
|
|
||||||
_attr_device_class = DEVICE_CLASS_DOOR
|
_attr_device_class = DEVICE_CLASS_DOOR
|
||||||
|
|
||||||
def __init__(self, data, sensor_type, device):
|
def __init__(self, data, device, description: BinarySensorEntityDescription):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(data, device)
|
super().__init__(data, device)
|
||||||
|
self.entity_description = description
|
||||||
self._data = data
|
self._data = data
|
||||||
self._sensor_type = sensor_type
|
|
||||||
self._device = device
|
self._device = device
|
||||||
self._attr_name = f"{device.device_name} Open"
|
self._attr_name = f"{device.device_name} {description.name}"
|
||||||
self._attr_unique_id = f"{self._device_id}_open"
|
self._attr_unique_id = (
|
||||||
|
f"{self._device_id}_{cast(str, description.name).lower()}"
|
||||||
|
)
|
||||||
self._update_from_data()
|
self._update_from_data()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -164,41 +206,29 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||||
class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
|
||||||
"""Representation of an August binary sensor."""
|
"""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."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(data, device)
|
super().__init__(data, device)
|
||||||
|
self.entity_description = description
|
||||||
self._check_for_off_update_listener = None
|
self._check_for_off_update_listener = None
|
||||||
self._data = data
|
self._data = data
|
||||||
self._sensor_type = sensor_type
|
self._attr_name = f"{device.device_name} {description.name}"
|
||||||
self._attr_device_class = self._sensor_config[SENSOR_DEVICE_CLASS]
|
|
||||||
self._attr_name = f"{device.device_name} {self._sensor_config[SENSOR_NAME]}"
|
|
||||||
self._attr_unique_id = (
|
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()
|
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
|
@callback
|
||||||
def _update_from_data(self):
|
def _update_from_data(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
self._cancel_any_pending_updates()
|
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._attr_available = _retrieve_online_state(self._data, self._detail)
|
||||||
self._schedule_update_to_recheck_turn_off_sensor()
|
self._schedule_update_to_recheck_turn_off_sensor()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
"""Support for August sensors."""
|
"""Support for August sensors."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable, Generic, TypeVar
|
||||||
|
|
||||||
from yalexs.activity import ActivityType
|
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.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.entity_registry import async_get_registry
|
from homeassistant.helpers.entity_registry import async_get_registry
|
||||||
|
@ -26,20 +37,44 @@ from .entity import AugustEntityMixin
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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."""
|
"""Get the latest state of the sensor."""
|
||||||
return detail.battery_level
|
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."""
|
"""Get the latest state of the sensor."""
|
||||||
return detail.battery_percentage
|
return detail.battery_percentage
|
||||||
|
|
||||||
|
|
||||||
SENSOR_TYPES_BATTERY = {
|
T = TypeVar("T", LockDetail, KeypadDetail)
|
||||||
"device_battery": {"state_provider": _retrieve_device_battery_state},
|
|
||||||
"linked_keypad_battery": {"state_provider": _retrieve_linked_keypad_battery_state},
|
|
||||||
}
|
@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):
|
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)
|
operation_sensors.append(device)
|
||||||
|
|
||||||
for device in batteries["device_battery"]:
|
for device in batteries["device_battery"]:
|
||||||
state_provider = SENSOR_TYPES_BATTERY["device_battery"]["state_provider"]
|
|
||||||
detail = data.get_device_detail(device.device_id)
|
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(
|
_LOGGER.debug(
|
||||||
"Not adding battery sensor for %s because it is not present",
|
"Not adding battery sensor for %s because it is not present",
|
||||||
device.device_name,
|
device.device_name,
|
||||||
|
@ -72,7 +106,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"Adding battery sensor for %s",
|
"Adding battery sensor for %s",
|
||||||
device.device_name,
|
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"]:
|
for device in batteries["linked_keypad_battery"]:
|
||||||
detail = data.get_device_detail(device.device_id)
|
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",
|
"Adding keypad battery sensor for %s",
|
||||||
device.device_name,
|
device.device_name,
|
||||||
)
|
)
|
||||||
keypad_battery_sensor = AugustBatterySensor(
|
keypad_battery_sensor = AugustBatterySensor[KeypadDetail](
|
||||||
data, "linked_keypad_battery", detail.keypad, device
|
data, detail.keypad, device, SENSOR_TYPE_KEYPAD_BATTERY
|
||||||
)
|
)
|
||||||
entities.append(keypad_battery_sensor)
|
entities.append(keypad_battery_sensor)
|
||||||
migrate_unique_id_devices.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"
|
return f"{self._device_id}_lock_operator"
|
||||||
|
|
||||||
|
|
||||||
class AugustBatterySensor(AugustEntityMixin, SensorEntity):
|
class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]):
|
||||||
"""Representation of an August sensor."""
|
"""Representation of an August sensor."""
|
||||||
|
|
||||||
|
entity_description: AugustSensorEntityDescription[T]
|
||||||
_attr_device_class = DEVICE_CLASS_BATTERY
|
_attr_device_class = DEVICE_CLASS_BATTERY
|
||||||
_attr_native_unit_of_measurement = PERCENTAGE
|
_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."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(data, device)
|
super().__init__(data, device)
|
||||||
self._sensor_type = sensor_type
|
self.entity_description = description
|
||||||
self._old_device = old_device
|
self._old_device = old_device
|
||||||
self._attr_name = f"{device.device_name} Battery"
|
self._attr_name = f"{device.device_name} {description.name}"
|
||||||
self._attr_unique_id = f"{self._device_id}_{sensor_type}"
|
self._attr_unique_id = f"{self._device_id}_{description.key}"
|
||||||
self._update_from_data()
|
self._update_from_data()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_from_data(self):
|
def _update_from_data(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"]
|
self._attr_native_value = self.entity_description.state_provider(self._detail)
|
||||||
self._attr_native_value = state_provider(self._detail)
|
|
||||||
self._attr_available = self._attr_native_value is not None
|
self._attr_available = self._attr_native_value is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def old_unique_id(self) -> str:
|
def old_unique_id(self) -> str:
|
||||||
"""Get the old unique id of the device sensor."""
|
"""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}"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue