281 lines
9.5 KiB
Python
281 lines
9.5 KiB
Python
"""Support for August sensors."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
import logging
|
|
from typing import Generic, TypeVar
|
|
|
|
from yalexs.activity import ActivityType
|
|
from yalexs.keypad import KeypadDetail
|
|
from yalexs.lock import LockDetail
|
|
|
|
from homeassistant.components.august import AugustData
|
|
from homeassistant.components.sensor import (
|
|
SensorDeviceClass,
|
|
SensorEntity,
|
|
SensorEntityDescription,
|
|
)
|
|
from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE
|
|
from homeassistant.core import callback
|
|
from homeassistant.helpers.entity import EntityCategory
|
|
from homeassistant.helpers.entity_registry import async_get_registry
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
|
|
|
from .const import (
|
|
ATTR_OPERATION_AUTORELOCK,
|
|
ATTR_OPERATION_KEYPAD,
|
|
ATTR_OPERATION_METHOD,
|
|
ATTR_OPERATION_REMOTE,
|
|
DATA_AUGUST,
|
|
DOMAIN,
|
|
OPERATION_METHOD_AUTORELOCK,
|
|
OPERATION_METHOD_KEYPAD,
|
|
OPERATION_METHOD_MOBILE_DEVICE,
|
|
OPERATION_METHOD_REMOTE,
|
|
)
|
|
from .entity import AugustEntityMixin
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
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: KeypadDetail) -> int | None:
|
|
"""Get the latest state of the sensor."""
|
|
return detail.battery_percentage
|
|
|
|
|
|
T = TypeVar("T", LockDetail, KeypadDetail)
|
|
|
|
|
|
@dataclass
|
|
class AugustRequiredKeysMixin(Generic[T]):
|
|
"""Mixin for required keys."""
|
|
|
|
value_fn: 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",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=_retrieve_device_battery_state,
|
|
)
|
|
|
|
SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail](
|
|
key="linked_keypad_battery",
|
|
name="Battery",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
value_fn=_retrieve_linked_keypad_battery_state,
|
|
)
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
"""Set up the August sensors."""
|
|
data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
|
|
entities = []
|
|
migrate_unique_id_devices = []
|
|
operation_sensors = []
|
|
batteries = {
|
|
"device_battery": [],
|
|
"linked_keypad_battery": [],
|
|
}
|
|
for device in data.doorbells:
|
|
batteries["device_battery"].append(device)
|
|
for device in data.locks:
|
|
batteries["device_battery"].append(device)
|
|
batteries["linked_keypad_battery"].append(device)
|
|
operation_sensors.append(device)
|
|
|
|
for device in batteries["device_battery"]:
|
|
detail = data.get_device_detail(device.device_id)
|
|
if detail is None or SENSOR_TYPE_DEVICE_BATTERY.value_fn(detail) is None:
|
|
_LOGGER.debug(
|
|
"Not adding battery sensor for %s because it is not present",
|
|
device.device_name,
|
|
)
|
|
continue
|
|
_LOGGER.debug(
|
|
"Adding battery sensor for %s",
|
|
device.device_name,
|
|
)
|
|
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)
|
|
|
|
if detail.keypad is None:
|
|
_LOGGER.debug(
|
|
"Not adding keypad battery sensor for %s because it is not present",
|
|
device.device_name,
|
|
)
|
|
continue
|
|
_LOGGER.debug(
|
|
"Adding keypad battery sensor for %s",
|
|
device.device_name,
|
|
)
|
|
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)
|
|
|
|
for device in operation_sensors:
|
|
entities.append(AugustOperatorSensor(data, device))
|
|
|
|
await _async_migrate_old_unique_ids(hass, migrate_unique_id_devices)
|
|
|
|
async_add_entities(entities)
|
|
|
|
|
|
async def _async_migrate_old_unique_ids(hass, devices):
|
|
"""Keypads now have their own serial number."""
|
|
registry = await async_get_registry(hass)
|
|
for device in devices:
|
|
old_entity_id = registry.async_get_entity_id(
|
|
"sensor", DOMAIN, device.old_unique_id
|
|
)
|
|
if old_entity_id is not None:
|
|
_LOGGER.debug(
|
|
"Migrating unique_id from [%s] to [%s]",
|
|
device.old_unique_id,
|
|
device.unique_id,
|
|
)
|
|
registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id)
|
|
|
|
|
|
class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity):
|
|
"""Representation of an August lock operation sensor."""
|
|
|
|
def __init__(self, data, device):
|
|
"""Initialize the sensor."""
|
|
super().__init__(data, device)
|
|
self._data = data
|
|
self._device = device
|
|
self._operated_remote = None
|
|
self._operated_keypad = None
|
|
self._operated_autorelock = None
|
|
self._operated_time = None
|
|
self._entity_picture = None
|
|
self._update_from_data()
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"{self._device.device_name} Operator"
|
|
|
|
@callback
|
|
def _update_from_data(self):
|
|
"""Get the latest state of the sensor and update activity."""
|
|
lock_activity = self._data.activity_stream.get_latest_device_activity(
|
|
self._device_id, {ActivityType.LOCK_OPERATION}
|
|
)
|
|
|
|
self._attr_available = True
|
|
if lock_activity is not None:
|
|
self._attr_native_value = lock_activity.operated_by
|
|
self._operated_remote = lock_activity.operated_remote
|
|
self._operated_keypad = lock_activity.operated_keypad
|
|
self._operated_autorelock = lock_activity.operated_autorelock
|
|
self._entity_picture = lock_activity.operator_thumbnail_url
|
|
|
|
@property
|
|
def extra_state_attributes(self):
|
|
"""Return the device specific state attributes."""
|
|
attributes = {}
|
|
|
|
if self._operated_remote is not None:
|
|
attributes[ATTR_OPERATION_REMOTE] = self._operated_remote
|
|
if self._operated_keypad is not None:
|
|
attributes[ATTR_OPERATION_KEYPAD] = self._operated_keypad
|
|
if self._operated_autorelock is not None:
|
|
attributes[ATTR_OPERATION_AUTORELOCK] = self._operated_autorelock
|
|
|
|
if self._operated_remote:
|
|
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_REMOTE
|
|
elif self._operated_keypad:
|
|
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_KEYPAD
|
|
elif self._operated_autorelock:
|
|
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_AUTORELOCK
|
|
else:
|
|
attributes[ATTR_OPERATION_METHOD] = OPERATION_METHOD_MOBILE_DEVICE
|
|
|
|
return attributes
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log."""
|
|
await super().async_added_to_hass()
|
|
|
|
last_state = await self.async_get_last_state()
|
|
if not last_state or last_state.state == STATE_UNAVAILABLE:
|
|
return
|
|
|
|
self._attr_state = last_state.state
|
|
if ATTR_ENTITY_PICTURE in last_state.attributes:
|
|
self._entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE]
|
|
if ATTR_OPERATION_REMOTE in last_state.attributes:
|
|
self._operated_remote = last_state.attributes[ATTR_OPERATION_REMOTE]
|
|
if ATTR_OPERATION_KEYPAD in last_state.attributes:
|
|
self._operated_keypad = last_state.attributes[ATTR_OPERATION_KEYPAD]
|
|
if ATTR_OPERATION_AUTORELOCK in last_state.attributes:
|
|
self._operated_autorelock = last_state.attributes[ATTR_OPERATION_AUTORELOCK]
|
|
|
|
@property
|
|
def entity_picture(self):
|
|
"""Return the entity picture to use in the frontend, if any."""
|
|
return self._entity_picture
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Get the unique id of the device sensor."""
|
|
return f"{self._device_id}_lock_operator"
|
|
|
|
|
|
class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]):
|
|
"""Representation of an August sensor."""
|
|
|
|
entity_description: AugustSensorEntityDescription[T]
|
|
_attr_device_class = SensorDeviceClass.BATTERY
|
|
_attr_native_unit_of_measurement = PERCENTAGE
|
|
|
|
def __init__(
|
|
self,
|
|
data: AugustData,
|
|
device,
|
|
old_device,
|
|
description: AugustSensorEntityDescription[T],
|
|
):
|
|
"""Initialize the sensor."""
|
|
super().__init__(data, device)
|
|
self.entity_description = description
|
|
self._old_device = old_device
|
|
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."""
|
|
self._attr_native_value = self.entity_description.value_fn(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.entity_description.key}"
|