diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 09d248756fc..1b24098f717 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,12 +1,15 @@ """Support for deCONZ sensors.""" + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime +from typing import Generic, TypeVar from pydeconz.interfaces.sensors import SensorResources from pydeconz.models.event import EventType +from pydeconz.models.sensor import SensorBase as PydeconzSensorBase from pydeconz.models.sensor.air_quality import AirQuality from pydeconz.models.sensor.consumption import Consumption from pydeconz.models.sensor.daylight import DAYLIGHT_STATUS, Daylight @@ -67,171 +70,163 @@ ATTR_DAYLIGHT = "daylight" ATTR_EVENT_ID = "event_id" +T = TypeVar( + "T", + AirQuality, + Consumption, + Daylight, + GenericStatus, + Humidity, + LightLevel, + Power, + Pressure, + Temperature, + Time, + PydeconzSensorBase, +) + + @dataclass -class DeconzSensorDescriptionMixin: +class DeconzSensorDescriptionMixin(Generic[T]): """Required values when describing secondary sensor attributes.""" + isinstance_fn: Callable[[T], bool] update_key: str - value_fn: Callable[[SensorResources], float | int | str | None] + value_fn: Callable[[T], datetime | StateType] @dataclass class DeconzSensorDescription( - SensorEntityDescription, - DeconzSensorDescriptionMixin, + SensorEntityDescription, DeconzSensorDescriptionMixin[T], Generic[T] ): """Class describing deCONZ binary sensor entities.""" - suffix: str = "" + common: bool = False + name_suffix: str = "" + old_unique_id_suffix: str = "" -ENTITY_DESCRIPTIONS = { - AirQuality: [ - DeconzSensorDescription( - key="air_quality", - value_fn=lambda device: device.air_quality - if isinstance(device, AirQuality) - else None, - update_key="airquality", - state_class=SensorStateClass.MEASUREMENT, - ), - DeconzSensorDescription( - key="air_quality_ppb", - value_fn=lambda device: device.air_quality_ppb - if isinstance(device, AirQuality) - else None, - suffix="PPB", - update_key="airqualityppb", - device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - ), - ], - Consumption: [ - DeconzSensorDescription( - key="consumption", - value_fn=lambda device: device.scaled_consumption - if isinstance(device, Consumption) and isinstance(device.consumption, int) - else None, - update_key="consumption", - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - ) - ], - Daylight: [ - DeconzSensorDescription( - key="daylight_status", - value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status] - if isinstance(device, Daylight) - else None, - update_key="status", - icon="mdi:white-balance-sunny", - entity_registry_enabled_default=False, - ) - ], - GenericStatus: [ - DeconzSensorDescription( - key="status", - value_fn=lambda device: device.status - if isinstance(device, GenericStatus) - else None, - update_key="status", - ) - ], - Humidity: [ - DeconzSensorDescription( - key="humidity", - value_fn=lambda device: device.scaled_humidity - if isinstance(device, Humidity) and isinstance(device.humidity, int) - else None, - update_key="humidity", - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ) - ], - LightLevel: [ - DeconzSensorDescription( - key="light_level", - value_fn=lambda device: device.scaled_light_level - if isinstance(device, LightLevel) and isinstance(device.light_level, int) - else None, - update_key="lightlevel", - device_class=SensorDeviceClass.ILLUMINANCE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=LIGHT_LUX, - ) - ], - Power: [ - DeconzSensorDescription( - key="power", - value_fn=lambda device: device.power if isinstance(device, Power) else None, - update_key="power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - ) - ], - Pressure: [ - DeconzSensorDescription( - key="pressure", - value_fn=lambda device: device.pressure - if isinstance(device, Pressure) - else None, - update_key="pressure", - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PRESSURE_HPA, - ) - ], - Temperature: [ - DeconzSensorDescription( - key="temperature", - value_fn=lambda device: device.scaled_temperature - if isinstance(device, Temperature) and isinstance(device.temperature, int) - else None, - update_key="temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - ) - ], - Time: [ - DeconzSensorDescription( - key="last_set", - value_fn=lambda device: device.last_set - if isinstance(device, Time) - else None, - update_key="lastset", - device_class=SensorDeviceClass.TIMESTAMP, - state_class=SensorStateClass.TOTAL_INCREASING, - ) - ], -} - - -COMMON_SENSOR_DESCRIPTIONS = [ - DeconzSensorDescription( - key="battery", - value_fn=lambda device: device.battery, - suffix="Battery", - update_key="battery", - device_class=SensorDeviceClass.BATTERY, +ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( + DeconzSensorDescription[AirQuality]( + key="air_quality", + isinstance_fn=lambda device: isinstance(device, AirQuality), + value_fn=lambda device: device.air_quality, + update_key="airquality", + state_class=SensorStateClass.MEASUREMENT, + ), + DeconzSensorDescription[AirQuality]( + key="air_quality_ppb", + isinstance_fn=lambda device: isinstance(device, AirQuality), + value_fn=lambda device: device.air_quality_ppb, + update_key="airqualityppb", + name_suffix="PPB", + old_unique_id_suffix="ppb", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + ), + DeconzSensorDescription[Consumption]( + key="consumption", + isinstance_fn=lambda device: isinstance(device, Consumption), + value_fn=lambda device: device.scaled_consumption, + update_key="consumption", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + DeconzSensorDescription[Daylight]( + key="daylight_status", + isinstance_fn=lambda device: isinstance(device, Daylight), + value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], + update_key="status", + icon="mdi:white-balance-sunny", + entity_registry_enabled_default=False, + ), + DeconzSensorDescription[GenericStatus]( + key="status", + isinstance_fn=lambda device: isinstance(device, GenericStatus), + value_fn=lambda device: device.status, + update_key="status", + ), + DeconzSensorDescription[Humidity]( + key="humidity", + isinstance_fn=lambda device: isinstance(device, Humidity), + value_fn=lambda device: device.scaled_humidity, + update_key="humidity", + device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - entity_category=EntityCategory.DIAGNOSTIC, ), - DeconzSensorDescription( - key="internal_temperature", - value_fn=lambda device: device.internal_temperature, - suffix="Temperature", + DeconzSensorDescription[LightLevel]( + key="light_level", + isinstance_fn=lambda device: isinstance(device, LightLevel), + value_fn=lambda device: device.scaled_light_level, + update_key="lightlevel", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=LIGHT_LUX, + ), + DeconzSensorDescription[Power]( + key="power", + isinstance_fn=lambda device: isinstance(device, Power), + value_fn=lambda device: device.power, + update_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + DeconzSensorDescription[Pressure]( + key="pressure", + isinstance_fn=lambda device: isinstance(device, Pressure), + value_fn=lambda device: device.pressure, + update_key="pressure", + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PRESSURE_HPA, + ), + DeconzSensorDescription[Temperature]( + key="temperature", + isinstance_fn=lambda device: isinstance(device, Temperature), + value_fn=lambda device: device.scaled_temperature, update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), -] + DeconzSensorDescription[Time]( + key="last_set", + isinstance_fn=lambda device: isinstance(device, Time), + value_fn=lambda device: dt_util.parse_datetime(device.last_set), + update_key="lastset", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DeconzSensorDescription[SensorResources]( + key="battery", + isinstance_fn=lambda device: isinstance(device, PydeconzSensorBase), + value_fn=lambda device: device.battery, + update_key="battery", + common=True, + name_suffix="Battery", + old_unique_id_suffix="battery", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DeconzSensorDescription[SensorResources]( + key="internal_temperature", + isinstance_fn=lambda device: isinstance(device, PydeconzSensorBase), + value_fn=lambda device: device.internal_temperature, + update_key="temperature", + common=True, + name_suffix="Temperature", + old_unique_id_suffix="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), +) @callback @@ -248,8 +243,8 @@ def async_update_unique_id( if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): return - if description.suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{description.suffix.lower()}' + if description.old_unique_id_suffix: + unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -265,7 +260,9 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() known_device_entities: dict[str, set[str]] = { - description.key: set() for description in COMMON_SENSOR_DESCRIPTIONS + description.key: set() + for description in ENTITY_DESCRIPTIONS + if description.common } @callback @@ -274,17 +271,15 @@ async def async_setup_entry( sensor = gateway.api.sensors[sensor_id] entities: list[DeconzSensor] = [] - for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + COMMON_SENSOR_DESCRIPTIONS - ): + for description in ENTITY_DESCRIPTIONS: + if not description.isinstance_fn(sensor): + continue + no_sensor_data = False - if ( - not hasattr(sensor, description.key) - or description.value_fn(sensor) is None - ): + if description.value_fn(sensor) is None: no_sensor_data = True - if description in COMMON_SENSOR_DESCRIPTIONS: + if description.common: if ( sensor.type.startswith("CLIP") or (no_sensor_data and description.key != "battery") @@ -296,7 +291,10 @@ async def async_setup_entry( continue known_device_entities[description.key].add(unique_id) if no_sensor_data and description.key == "battery": - DeconzBatteryTracker(sensor_id, gateway, async_add_entities) + async_update_unique_id(hass, sensor.unique_id, description) + DeconzBatteryTracker( + sensor_id, gateway, description, async_add_entities + ) continue if no_sensor_data: @@ -327,9 +325,10 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): ) -> None: """Initialize deCONZ sensor.""" self.entity_description = description + self.unique_id_suffix = description.key self._update_key = description.update_key - if description.suffix: - self._name_suffix = description.suffix + if description.name_suffix: + self._name_suffix = description.name_suffix super().__init__(device, gateway) if ( @@ -338,18 +337,9 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): ): self._update_keys.update({"on", "state"}) - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return f"{self._device.unique_id}-{self.entity_description.key}" - @property def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" - if self.entity_description.device_class is SensorDeviceClass.TIMESTAMP: - value = self.entity_description.value_fn(self._device) - assert isinstance(value, str) - return dt_util.parse_datetime(value) return self.entity_description.value_fn(self._device) @property @@ -399,19 +389,21 @@ class DeconzBatteryTracker: self, sensor_id: str, gateway: DeconzGateway, + description: DeconzSensorDescription, async_add_entities: AddEntitiesCallback, ) -> None: """Set up tracker.""" self.sensor = gateway.api.sensors[sensor_id] self.gateway = gateway + self.description = description self.async_add_entities = async_add_entities self.unsubscribe = self.sensor.subscribe(self.async_update_callback) @callback def async_update_callback(self) -> None: """Update the device's state.""" - if "battery" in self.sensor.changed_keys: + if self.description.update_key in self.sensor.changed_keys: self.unsubscribe() - desc = COMMON_SENSOR_DESCRIPTIONS[0] - async_update_unique_id(self.gateway.hass, self.sensor.unique_id, desc) - self.async_add_entities([DeconzSensor(self.sensor, self.gateway, desc)]) + self.async_add_entities( + [DeconzSensor(self.sensor, self.gateway, self.description)] + )