Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for deCONZ sensors."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import datetime
8 from typing import Generic, TypeVar
9 
10 from pydeconz.interfaces.sensors import SensorResources
11 from pydeconz.models.event import EventType
12 from pydeconz.models.sensor import SensorBase as PydeconzSensorBase
13 from pydeconz.models.sensor.air_purifier import AirPurifier
14 from pydeconz.models.sensor.air_quality import AirQuality
15 from pydeconz.models.sensor.carbon_dioxide import CarbonDioxide
16 from pydeconz.models.sensor.consumption import Consumption
17 from pydeconz.models.sensor.daylight import DAYLIGHT_STATUS, Daylight
18 from pydeconz.models.sensor.formaldehyde import Formaldehyde
19 from pydeconz.models.sensor.generic_status import GenericStatus
20 from pydeconz.models.sensor.humidity import Humidity
21 from pydeconz.models.sensor.light_level import LightLevel
22 from pydeconz.models.sensor.moisture import Moisture
23 from pydeconz.models.sensor.particulate_matter import ParticulateMatter
24 from pydeconz.models.sensor.power import Power
25 from pydeconz.models.sensor.pressure import Pressure
26 from pydeconz.models.sensor.switch import Switch
27 from pydeconz.models.sensor.temperature import Temperature
28 from pydeconz.models.sensor.time import Time
29 
31  DOMAIN as SENSOR_DOMAIN,
32  SensorDeviceClass,
33  SensorEntity,
34  SensorEntityDescription,
35  SensorStateClass,
36 )
37 from homeassistant.config_entries import ConfigEntry
38 from homeassistant.const import (
39  ATTR_TEMPERATURE,
40  ATTR_VOLTAGE,
41  CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
42  CONCENTRATION_PARTS_PER_BILLION,
43  CONCENTRATION_PARTS_PER_MILLION,
44  LIGHT_LUX,
45  PERCENTAGE,
46  EntityCategory,
47  UnitOfEnergy,
48  UnitOfPower,
49  UnitOfPressure,
50  UnitOfTemperature,
51  UnitOfTime,
52 )
53 from homeassistant.core import HomeAssistant, callback
54 from homeassistant.helpers.entity_platform import AddEntitiesCallback
55 from homeassistant.helpers.typing import StateType
56 import homeassistant.util.dt as dt_util
57 
58 from .const import ATTR_DARK, ATTR_ON
59 from .entity import DeconzDevice
60 from .hub import DeconzHub
61 
62 PROVIDES_EXTRA_ATTRIBUTES = (
63  "battery",
64  "consumption",
65  "daylight_status",
66  "humidity",
67  "light_level",
68  "power",
69  "pressure",
70  "status",
71  "temperature",
72 )
73 
74 ATTR_CURRENT = "current"
75 ATTR_POWER = "power"
76 ATTR_DAYLIGHT = "daylight"
77 ATTR_EVENT_ID = "event_id"
78 
79 
80 T = TypeVar(
81  "T",
82  AirPurifier,
83  AirQuality,
84  CarbonDioxide,
85  Consumption,
86  Daylight,
87  Formaldehyde,
88  GenericStatus,
89  Humidity,
90  LightLevel,
91  Moisture,
92  ParticulateMatter,
93  Power,
94  Pressure,
95  Temperature,
96  Time,
97  PydeconzSensorBase,
98 )
99 
100 
101 @dataclass(frozen=True, kw_only=True)
102 class DeconzSensorDescription(Generic[T], SensorEntityDescription):
103  """Class describing deCONZ binary sensor entities."""
104 
105  instance_check: type[T] | None = None
106  name_suffix: str = ""
107  old_unique_id_suffix: str = ""
108  supported_fn: Callable[[T], bool]
109  update_key: str
110  value_fn: Callable[[T], datetime | StateType]
111 
112 
113 ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
114  DeconzSensorDescription[AirPurifier](
115  key="air_purifier_filter_run_time",
116  supported_fn=lambda device: True,
117  update_key="filterruntime",
118  name_suffix="Filter time",
119  value_fn=lambda device: device.filter_run_time,
120  instance_check=AirPurifier,
121  device_class=SensorDeviceClass.DURATION,
122  entity_category=EntityCategory.DIAGNOSTIC,
123  native_unit_of_measurement=UnitOfTime.SECONDS,
124  suggested_unit_of_measurement=UnitOfTime.DAYS,
125  suggested_display_precision=1,
126  ),
127  DeconzSensorDescription[AirQuality](
128  key="air_quality",
129  supported_fn=lambda device: device.supports_air_quality,
130  update_key="airquality",
131  value_fn=lambda device: device.air_quality,
132  instance_check=AirQuality,
133  ),
134  DeconzSensorDescription[AirQuality](
135  key="air_quality_ppb",
136  supported_fn=lambda device: device.air_quality_ppb is not None,
137  update_key="airqualityppb",
138  value_fn=lambda device: device.air_quality_ppb,
139  instance_check=AirQuality,
140  name_suffix="PPB",
141  old_unique_id_suffix="ppb",
142  state_class=SensorStateClass.MEASUREMENT,
143  native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
144  ),
145  DeconzSensorDescription[AirQuality](
146  key="air_quality_formaldehyde",
147  supported_fn=lambda device: device.air_quality_formaldehyde is not None,
148  update_key="airquality_formaldehyde_density",
149  value_fn=lambda device: device.air_quality_formaldehyde,
150  instance_check=AirQuality,
151  name_suffix="CH2O",
152  device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
153  state_class=SensorStateClass.MEASUREMENT,
154  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
155  ),
156  DeconzSensorDescription[AirQuality](
157  key="air_quality_co2",
158  supported_fn=lambda device: device.air_quality_co2 is not None,
159  update_key="airquality_co2_density",
160  value_fn=lambda device: device.air_quality_co2,
161  instance_check=AirQuality,
162  name_suffix="CO2",
163  device_class=SensorDeviceClass.CO2,
164  state_class=SensorStateClass.MEASUREMENT,
165  native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
166  ),
167  DeconzSensorDescription[AirQuality](
168  key="air_quality_pm2_5",
169  supported_fn=lambda device: device.pm_2_5 is not None,
170  update_key="pm2_5",
171  value_fn=lambda device: device.pm_2_5,
172  instance_check=AirQuality,
173  name_suffix="PM25",
174  device_class=SensorDeviceClass.PM25,
175  state_class=SensorStateClass.MEASUREMENT,
176  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
177  ),
178  DeconzSensorDescription[CarbonDioxide](
179  key="carbon_dioxide",
180  supported_fn=lambda device: True,
181  update_key="measured_value",
182  value_fn=lambda device: device.carbon_dioxide,
183  instance_check=CarbonDioxide,
184  device_class=SensorDeviceClass.CO2,
185  state_class=SensorStateClass.MEASUREMENT,
186  native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
187  ),
188  DeconzSensorDescription[Consumption](
189  key="consumption",
190  supported_fn=lambda device: device.consumption is not None,
191  update_key="consumption",
192  value_fn=lambda device: device.scaled_consumption,
193  instance_check=Consumption,
194  device_class=SensorDeviceClass.ENERGY,
195  state_class=SensorStateClass.TOTAL_INCREASING,
196  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
197  ),
198  DeconzSensorDescription[Daylight](
199  key="daylight_status",
200  supported_fn=lambda device: True,
201  update_key="status",
202  value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status],
203  instance_check=Daylight,
204  icon="mdi:white-balance-sunny",
205  entity_registry_enabled_default=False,
206  ),
207  DeconzSensorDescription[Formaldehyde](
208  key="formaldehyde",
209  supported_fn=lambda device: True,
210  update_key="measured_value",
211  value_fn=lambda device: device.formaldehyde,
212  instance_check=Formaldehyde,
213  device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
214  state_class=SensorStateClass.MEASUREMENT,
215  native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
216  ),
217  DeconzSensorDescription[GenericStatus](
218  key="status",
219  supported_fn=lambda device: device.status is not None,
220  update_key="status",
221  value_fn=lambda device: device.status,
222  instance_check=GenericStatus,
223  ),
224  DeconzSensorDescription[Humidity](
225  key="humidity",
226  supported_fn=lambda device: device.humidity is not None,
227  update_key="humidity",
228  value_fn=lambda device: device.scaled_humidity,
229  instance_check=Humidity,
230  device_class=SensorDeviceClass.HUMIDITY,
231  state_class=SensorStateClass.MEASUREMENT,
232  native_unit_of_measurement=PERCENTAGE,
233  suggested_display_precision=1,
234  ),
235  DeconzSensorDescription[LightLevel](
236  key="light_level",
237  supported_fn=lambda device: device.light_level is not None,
238  update_key="lightlevel",
239  value_fn=lambda device: device.scaled_light_level,
240  instance_check=LightLevel,
241  device_class=SensorDeviceClass.ILLUMINANCE,
242  state_class=SensorStateClass.MEASUREMENT,
243  native_unit_of_measurement=LIGHT_LUX,
244  ),
245  DeconzSensorDescription[Moisture](
246  key="moisture",
247  supported_fn=lambda device: device.moisture is not None,
248  update_key="moisture",
249  value_fn=lambda device: device.scaled_moisture,
250  instance_check=Moisture,
251  device_class=SensorDeviceClass.MOISTURE,
252  state_class=SensorStateClass.MEASUREMENT,
253  native_unit_of_measurement=PERCENTAGE,
254  suggested_display_precision=1,
255  ),
256  DeconzSensorDescription[ParticulateMatter](
257  key="particulate_matter_pm2_5",
258  supported_fn=lambda device: device.measured_value is not None,
259  update_key="measured_value",
260  value_fn=lambda device: device.measured_value,
261  instance_check=ParticulateMatter,
262  name_suffix="PM25",
263  device_class=SensorDeviceClass.PM25,
264  state_class=SensorStateClass.MEASUREMENT,
265  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
266  ),
267  DeconzSensorDescription[Power](
268  key="power",
269  supported_fn=lambda device: device.power is not None,
270  update_key="power",
271  value_fn=lambda device: device.power,
272  instance_check=Power,
273  device_class=SensorDeviceClass.POWER,
274  state_class=SensorStateClass.MEASUREMENT,
275  native_unit_of_measurement=UnitOfPower.WATT,
276  ),
277  DeconzSensorDescription[Pressure](
278  key="pressure",
279  supported_fn=lambda device: device.pressure is not None,
280  update_key="pressure",
281  value_fn=lambda device: device.pressure,
282  instance_check=Pressure,
283  device_class=SensorDeviceClass.PRESSURE,
284  state_class=SensorStateClass.MEASUREMENT,
285  native_unit_of_measurement=UnitOfPressure.HPA,
286  ),
287  DeconzSensorDescription[Temperature](
288  key="temperature",
289  supported_fn=lambda device: device.temperature is not None,
290  update_key="temperature",
291  value_fn=lambda device: device.scaled_temperature,
292  instance_check=Temperature,
293  device_class=SensorDeviceClass.TEMPERATURE,
294  state_class=SensorStateClass.MEASUREMENT,
295  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
296  suggested_display_precision=1,
297  ),
298  DeconzSensorDescription[Time](
299  key="last_set",
300  supported_fn=lambda device: device.last_set is not None,
301  update_key="lastset",
302  value_fn=lambda device: dt_util.parse_datetime(device.last_set),
303  instance_check=Time,
304  device_class=SensorDeviceClass.TIMESTAMP,
305  ),
306  DeconzSensorDescription[SensorResources](
307  key="battery",
308  supported_fn=lambda device: device.battery is not None,
309  update_key="battery",
310  value_fn=lambda device: device.battery,
311  name_suffix="Battery",
312  old_unique_id_suffix="battery",
313  device_class=SensorDeviceClass.BATTERY,
314  state_class=SensorStateClass.MEASUREMENT,
315  native_unit_of_measurement=PERCENTAGE,
316  entity_category=EntityCategory.DIAGNOSTIC,
317  ),
318  DeconzSensorDescription[SensorResources](
319  key="internal_temperature",
320  supported_fn=lambda device: device.internal_temperature is not None,
321  update_key="temperature",
322  value_fn=lambda device: device.internal_temperature,
323  name_suffix="Temperature",
324  old_unique_id_suffix="temperature",
325  device_class=SensorDeviceClass.TEMPERATURE,
326  state_class=SensorStateClass.MEASUREMENT,
327  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
328  ),
329 )
330 
331 
333  hass: HomeAssistant,
334  config_entry: ConfigEntry,
335  async_add_entities: AddEntitiesCallback,
336 ) -> None:
337  """Set up the deCONZ sensors."""
338  hub = DeconzHub.get_hub(hass, config_entry)
339  hub.entities[SENSOR_DOMAIN] = set()
340 
341  known_device_entities: dict[str, set[str]] = {
342  description.key: set()
343  for description in ENTITY_DESCRIPTIONS
344  if description.instance_check is None
345  }
346 
347  @callback
348  def async_add_sensor(_: EventType, sensor_id: str) -> None:
349  """Add sensor from deCONZ."""
350  sensor = hub.api.sensors[sensor_id]
351  entities: list[DeconzSensor] = []
352 
353  for description in ENTITY_DESCRIPTIONS:
354  if description.instance_check and not isinstance(
355  sensor, description.instance_check
356  ):
357  continue
358 
359  no_sensor_data = False
360  if not description.supported_fn(sensor):
361  no_sensor_data = True
362 
363  if description.instance_check is None:
364  if (
365  sensor.type.startswith("CLIP")
366  or (no_sensor_data and description.key != "battery")
367  or (
368  (unique_id := sensor.unique_id.rpartition("-")[0])
369  in known_device_entities[description.key]
370  )
371  ):
372  continue
373  known_device_entities[description.key].add(unique_id)
374  if no_sensor_data and description.key == "battery":
376  sensor_id, hub, description, async_add_entities
377  )
378  continue
379 
380  if no_sensor_data:
381  continue
382 
383  entities.append(DeconzSensor(sensor, hub, description))
384 
385  async_add_entities(entities)
386 
387  hub.register_platform_add_device_callback(
388  async_add_sensor,
389  hub.api.sensors,
390  )
391 
392 
393 class DeconzSensor(DeconzDevice[SensorResources], SensorEntity):
394  """Representation of a deCONZ sensor."""
395 
396  TYPE = SENSOR_DOMAIN
397  entity_description: DeconzSensorDescription
398 
399  def __init__(
400  self,
401  device: SensorResources,
402  hub: DeconzHub,
403  description: DeconzSensorDescription,
404  ) -> None:
405  """Initialize deCONZ sensor."""
406  self.entity_descriptionentity_description = description
407  self.unique_id_suffixunique_id_suffix = description.key
408  self._update_key_update_key = description.update_key
409  if description.name_suffix:
410  self._name_suffix_name_suffix = description.name_suffix
411  super().__init__(device, hub)
412 
413  if (
414  self.entity_descriptionentity_description.key in PROVIDES_EXTRA_ATTRIBUTES
415  and self._update_keys is not None
416  ):
417  self._update_keys.update({"on", "state"})
418 
419  @property
420  def native_value(self) -> StateType | datetime:
421  """Return the state of the sensor."""
422  return self.entity_descriptionentity_description.value_fn(self._device_device)
423 
424  @property
425  def extra_state_attributes(self) -> dict[str, bool | float | int | str | None]:
426  """Return the state attributes of the sensor."""
427  attr: dict[str, bool | float | int | str | None] = {}
428 
429  if self.entity_descriptionentity_description.key not in PROVIDES_EXTRA_ATTRIBUTES:
430  return attr
431 
432  if self._device_device.on is not None:
433  attr[ATTR_ON] = self._device_device.on
434 
435  if self._device_device.internal_temperature is not None:
436  attr[ATTR_TEMPERATURE] = self._device_device.internal_temperature
437 
438  if isinstance(self._device_device, Consumption):
439  attr[ATTR_POWER] = self._device_device.power
440 
441  elif isinstance(self._device_device, Daylight):
442  attr[ATTR_DAYLIGHT] = self._device_device.daylight
443 
444  elif isinstance(self._device_device, LightLevel):
445  if self._device_device.dark is not None:
446  attr[ATTR_DARK] = self._device_device.dark
447 
448  if self._device_device.daylight is not None:
449  attr[ATTR_DAYLIGHT] = self._device_device.daylight
450 
451  elif isinstance(self._device_device, Power):
452  attr[ATTR_CURRENT] = self._device_device.current
453  attr[ATTR_VOLTAGE] = self._device_device.voltage
454 
455  elif isinstance(self._device_device, Switch):
456  for event in self.hub.events:
457  if self._device_device == event.device:
458  attr[ATTR_EVENT_ID] = event.event_id
459 
460  return attr
461 
462 
464  """Track sensors without a battery state and add entity when battery state exist."""
465 
466  def __init__(
467  self,
468  sensor_id: str,
469  hub: DeconzHub,
470  description: DeconzSensorDescription,
471  async_add_entities: AddEntitiesCallback,
472  ) -> None:
473  """Set up tracker."""
474  self.sensorsensor = hub.api.sensors[sensor_id]
475  self.hubhub = hub
476  self.descriptiondescription = description
477  self.async_add_entitiesasync_add_entities = async_add_entities
478  self.unsubscribeunsubscribe = self.sensorsensor.subscribe(self.async_update_callbackasync_update_callback)
479 
480  @callback
481  def async_update_callback(self) -> None:
482  """Update the device's state."""
483  if self.descriptiondescription.update_key in self.sensorsensor.changed_keys:
484  self.unsubscribeunsubscribe()
485  self.async_add_entitiesasync_add_entities(
486  [DeconzSensor(self.sensorsensor, self.hubhub, self.descriptiondescription)]
487  )
None __init__(self, str sensor_id, DeconzHub hub, DeconzSensorDescription description, AddEntitiesCallback async_add_entities)
Definition: sensor.py:472
None __init__(self, SensorResources device, DeconzHub hub, DeconzSensorDescription description)
Definition: sensor.py:404
dict[str, bool|float|int|str|None] extra_state_attributes(self)
Definition: sensor.py:425
bool add(self, _T matcher)
Definition: match.py:185
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:336
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
Callable[[], None] subscribe(HomeAssistant hass, str topic, MessageCallbackType msg_callback, int qos=DEFAULT_QOS, str encoding="utf-8")
Definition: client.py:247