diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index c74867df5bb..0177c15e8ae 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,7 +1,8 @@ """Support for deCONZ sensors.""" from __future__ import annotations -from collections.abc import ValuesView +from collections.abc import Callable, ValuesView +from dataclasses import dataclass from pydeconz.sensor import ( AirQuality, @@ -31,6 +32,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, + CONCENTRATION_PARTS_PER_BILLION, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, @@ -69,6 +71,24 @@ ATTR_POWER = "power" ATTR_DAYLIGHT = "daylight" ATTR_EVENT_ID = "event_id" + +@dataclass +class DeconzSensorDescriptionMixin: + """Required values when describing secondary sensor attributes.""" + + suffix: str + update_key: str + value_fn: Callable[[PydeconzSensor], float | int | None] + + +@dataclass +class DeconzSensorDescription( + SensorEntityDescription, + DeconzSensorDescriptionMixin, +): + """Class describing deCONZ binary sensor entities.""" + + ENTITY_DESCRIPTIONS = { Battery: SensorEntityDescription( key="battery", @@ -119,6 +139,27 @@ ENTITY_DESCRIPTIONS = { ), } +SENSOR_DESCRIPTIONS = [ + DeconzSensorDescription( + key="temperature", + value_fn=lambda device: device.secondary_temperature, + suffix="Temperature", + update_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + DeconzSensorDescription( + key="air_quality_ppb", + value_fn=lambda device: device.air_quality_ppb, + suffix="PPB", + update_key="airqualityppb", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + ), +] + async def async_setup_entry( hass: HomeAssistant, @@ -141,7 +182,7 @@ async def async_setup_entry( Create DeconzBattery if sensor has a battery attribute. Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. """ - entities: list[DeconzBattery | DeconzSensor | DeconzTemperature] = [] + entities: list[DeconzBattery | DeconzSensor | DeconzPropertySensor] = [] for sensor in sensors: @@ -166,11 +207,18 @@ async def async_setup_entry( ): entities.append(DeconzSensor(sensor, gateway)) - if sensor.secondary_temperature: - known_temperature_sensors = set(gateway.entities[DOMAIN]) - new_temperature_sensor = DeconzTemperature(sensor, gateway) - if new_temperature_sensor.unique_id not in known_temperature_sensors: - entities.append(new_temperature_sensor) + for sensor_description in SENSOR_DESCRIPTIONS: + + try: + if sensor_description.value_fn(sensor): + known_sensors = set(gateway.entities[DOMAIN]) + new_sensor = DeconzPropertySensor( + sensor, gateway, sensor_description + ) + if new_sensor.unique_id not in known_sensors: + entities.append(new_sensor) + except AttributeError: + continue if entities: async_add_entities(entities) @@ -245,38 +293,41 @@ class DeconzSensor(DeconzDevice, SensorEntity): return attr -class DeconzTemperature(DeconzDevice, SensorEntity): - """Representation of a deCONZ temperature sensor. - - Extra temperature sensor on certain Xiaomi devices. - """ +class DeconzPropertySensor(DeconzDevice, SensorEntity): + """Representation of a deCONZ secondary attribute sensor.""" TYPE = DOMAIN _device: PydeconzSensor + entity_description: DeconzSensorDescription - def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: - """Initialize deCONZ temperature sensor.""" + def __init__( + self, + device: PydeconzSensor, + gateway: DeconzGateway, + description: DeconzSensorDescription, + ) -> None: + """Initialize deCONZ sensor.""" + self.entity_description = description super().__init__(device, gateway) - self.entity_description = ENTITY_DESCRIPTIONS[Temperature] - self._attr_name = f"{self._device.name} Temperature" + self._attr_name = f"{self._device.name} {description.suffix}" + self._update_keys = {description.update_key, "reachable"} @property def unique_id(self) -> str: """Return a unique identifier for this device.""" - return f"{self.serial}-temperature" + return f"{self.serial}-{self.entity_description.suffix.lower()}" @callback def async_update_callback(self) -> None: """Update the sensor's state.""" - keys = {"temperature", "reachable"} - if self._device.changed_keys.intersection(keys): + if self._device.changed_keys.intersection(self._update_keys): super().async_update_callback() @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self._device.secondary_temperature # type: ignore[no-any-return] + return self.entity_description.value_fn(self._device) class DeconzBattery(DeconzDevice, SensorEntity): diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index d188d5504ca..ee66a159c18 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -476,8 +476,9 @@ async def test_air_quality_sensor(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 2 assert hass.states.get("sensor.air_quality").state == "poor" + assert hass.states.get("sensor.air_quality_ppb").state == "809" async def test_daylight_sensor(hass, aioclient_mock):