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."""
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):

View File

@ -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):