Add Air Quality PPB sensor to deCONZ integration (#64164)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Robert Svensson 2022-01-17 20:25:55 +01:00 committed by GitHub
parent 7c110eeef4
commit 7e40707288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 73 additions and 21 deletions

View File

@ -1,7 +1,8 @@
"""Support for deCONZ sensors.""" """Support for deCONZ sensors."""
from __future__ import annotations from __future__ import annotations
from collections.abc import ValuesView from collections.abc import Callable, ValuesView
from dataclasses import dataclass
from pydeconz.sensor import ( from pydeconz.sensor import (
AirQuality, AirQuality,
@ -31,6 +32,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_VOLTAGE, ATTR_VOLTAGE,
CONCENTRATION_PARTS_PER_BILLION,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
LIGHT_LUX, LIGHT_LUX,
PERCENTAGE, PERCENTAGE,
@ -69,6 +71,24 @@ ATTR_POWER = "power"
ATTR_DAYLIGHT = "daylight" ATTR_DAYLIGHT = "daylight"
ATTR_EVENT_ID = "event_id" 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 = { ENTITY_DESCRIPTIONS = {
Battery: SensorEntityDescription( Battery: SensorEntityDescription(
key="battery", 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( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -141,7 +182,7 @@ async def async_setup_entry(
Create DeconzBattery if sensor has a battery attribute. Create DeconzBattery if sensor has a battery attribute.
Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. 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: for sensor in sensors:
@ -166,11 +207,18 @@ async def async_setup_entry(
): ):
entities.append(DeconzSensor(sensor, gateway)) entities.append(DeconzSensor(sensor, gateway))
if sensor.secondary_temperature: for sensor_description in SENSOR_DESCRIPTIONS:
known_temperature_sensors = set(gateway.entities[DOMAIN])
new_temperature_sensor = DeconzTemperature(sensor, gateway) try:
if new_temperature_sensor.unique_id not in known_temperature_sensors: if sensor_description.value_fn(sensor):
entities.append(new_temperature_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: if entities:
async_add_entities(entities) async_add_entities(entities)
@ -245,38 +293,41 @@ class DeconzSensor(DeconzDevice, SensorEntity):
return attr return attr
class DeconzTemperature(DeconzDevice, SensorEntity): class DeconzPropertySensor(DeconzDevice, SensorEntity):
"""Representation of a deCONZ temperature sensor. """Representation of a deCONZ secondary attribute sensor."""
Extra temperature sensor on certain Xiaomi devices.
"""
TYPE = DOMAIN TYPE = DOMAIN
_device: PydeconzSensor _device: PydeconzSensor
entity_description: DeconzSensorDescription
def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: def __init__(
"""Initialize deCONZ temperature sensor.""" self,
device: PydeconzSensor,
gateway: DeconzGateway,
description: DeconzSensorDescription,
) -> None:
"""Initialize deCONZ sensor."""
self.entity_description = description
super().__init__(device, gateway) super().__init__(device, gateway)
self.entity_description = ENTITY_DESCRIPTIONS[Temperature] self._attr_name = f"{self._device.name} {description.suffix}"
self._attr_name = f"{self._device.name} Temperature" self._update_keys = {description.update_key, "reachable"}
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique identifier for this device.""" """Return a unique identifier for this device."""
return f"{self.serial}-temperature" return f"{self.serial}-{self.entity_description.suffix.lower()}"
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the sensor's state.""" """Update the sensor's state."""
keys = {"temperature", "reachable"} if self._device.changed_keys.intersection(self._update_keys):
if self._device.changed_keys.intersection(keys):
super().async_update_callback() super().async_update_callback()
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the sensor.""" """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): class DeconzBattery(DeconzDevice, SensorEntity):

View File

@ -476,8 +476,9 @@ async def test_air_quality_sensor(hass, aioclient_mock):
with patch.dict(DECONZ_WEB_REQUEST, data): with patch.dict(DECONZ_WEB_REQUEST, data):
await setup_deconz_integration(hass, aioclient_mock) 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").state == "poor"
assert hass.states.get("sensor.air_quality_ppb").state == "809"
async def test_daylight_sensor(hass, aioclient_mock): async def test_daylight_sensor(hass, aioclient_mock):