Home Assistant Unofficial Reference 2024.12.1
type_sensors.py
Go to the documentation of this file.
1 """Class to hold all sensor accessories."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import logging
7 from typing import Any, NamedTuple
8 
9 from pyhap.const import CATEGORY_SENSOR
10 from pyhap.service import Service
11 
12 from homeassistant.components.binary_sensor import BinarySensorDeviceClass
13 from homeassistant.const import (
14  ATTR_DEVICE_CLASS,
15  ATTR_UNIT_OF_MEASUREMENT,
16  STATE_HOME,
17  STATE_ON,
18  UnitOfTemperature,
19 )
20 from homeassistant.core import State, callback
21 
22 from .accessories import TYPES, HomeAccessory
23 from .const import (
24  CHAR_AIR_PARTICULATE_DENSITY,
25  CHAR_AIR_QUALITY,
26  CHAR_CARBON_DIOXIDE_DETECTED,
27  CHAR_CARBON_DIOXIDE_LEVEL,
28  CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
29  CHAR_CARBON_MONOXIDE_DETECTED,
30  CHAR_CARBON_MONOXIDE_LEVEL,
31  CHAR_CARBON_MONOXIDE_PEAK_LEVEL,
32  CHAR_CONTACT_SENSOR_STATE,
33  CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
34  CHAR_CURRENT_HUMIDITY,
35  CHAR_CURRENT_TEMPERATURE,
36  CHAR_LEAK_DETECTED,
37  CHAR_MOTION_DETECTED,
38  CHAR_NITROGEN_DIOXIDE_DENSITY,
39  CHAR_OCCUPANCY_DETECTED,
40  CHAR_PM10_DENSITY,
41  CHAR_PM25_DENSITY,
42  CHAR_SMOKE_DETECTED,
43  CHAR_VOC_DENSITY,
44  CONF_THRESHOLD_CO,
45  CONF_THRESHOLD_CO2,
46  PROP_CELSIUS,
47  PROP_MAX_VALUE,
48  PROP_MIN_VALUE,
49  SERV_AIR_QUALITY_SENSOR,
50  SERV_CARBON_DIOXIDE_SENSOR,
51  SERV_CARBON_MONOXIDE_SENSOR,
52  SERV_CONTACT_SENSOR,
53  SERV_HUMIDITY_SENSOR,
54  SERV_LEAK_SENSOR,
55  SERV_LIGHT_SENSOR,
56  SERV_MOTION_SENSOR,
57  SERV_OCCUPANCY_SENSOR,
58  SERV_SMOKE_SENSOR,
59  SERV_TEMPERATURE_SENSOR,
60  THRESHOLD_CO,
61  THRESHOLD_CO2,
62 )
63 from .util import (
64  convert_to_float,
65  density_to_air_quality,
66  density_to_air_quality_nitrogen_dioxide,
67  density_to_air_quality_pm10,
68  density_to_air_quality_voc,
69  temperature_to_homekit,
70 )
71 
72 _LOGGER = logging.getLogger(__name__)
73 
74 
75 class SI(NamedTuple):
76  """Service info."""
77 
78  service: str
79  char: str
80  format: Callable[[bool], int | bool]
81 
82 
83 BINARY_SENSOR_SERVICE_MAP: dict[str, SI] = {
84  BinarySensorDeviceClass.CO: SI(
85  SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int
86  ),
87  BinarySensorDeviceClass.DOOR: SI(
88  SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int
89  ),
90  BinarySensorDeviceClass.GARAGE_DOOR: SI(
91  SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int
92  ),
93  BinarySensorDeviceClass.GAS: SI(
94  SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int
95  ),
96  BinarySensorDeviceClass.MOISTURE: SI(SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, int),
97  BinarySensorDeviceClass.MOTION: SI(SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, bool),
98  BinarySensorDeviceClass.OCCUPANCY: SI(
99  SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, int
100  ),
101  BinarySensorDeviceClass.OPENING: SI(
102  SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int
103  ),
104  BinarySensorDeviceClass.SMOKE: SI(SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED, int),
105  BinarySensorDeviceClass.WINDOW: SI(
106  SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int
107  ),
108 }
109 
110 
111 @TYPES.register("TemperatureSensor")
113  """Generate a TemperatureSensor accessory for a temperature sensor.
114 
115  Sensor entity must return temperature in °C, °F.
116  """
117 
118  def __init__(self, *args: Any) -> None:
119  """Initialize a TemperatureSensor accessory object."""
120  super().__init__(*args, category=CATEGORY_SENSOR)
121  state = self.hasshass.states.get(self.entity_identity_id)
122  assert state
123  serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
124  self.char_tempchar_temp = serv_temp.configure_char(
125  CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS
126  )
127  # Set the state so it is in sync on initial
128  # GET to avoid an event storm after homekit startup
129  self.async_update_stateasync_update_stateasync_update_state(state)
130 
131  @callback
132  def async_update_state(self, new_state: State) -> None:
133  """Update temperature after state changed."""
134  unit = new_state.attributes.get(
135  ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature.CELSIUS
136  )
137  if (temperature := convert_to_float(new_state.state)) is not None:
138  temperature = temperature_to_homekit(temperature, unit)
139  self.char_tempchar_temp.set_value(temperature)
140  _LOGGER.debug(
141  "%s: Current temperature set to %.1f°C", self.entity_identity_id, temperature
142  )
143 
144 
145 @TYPES.register("HumiditySensor")
147  """Generate a HumiditySensor accessory as humidity sensor."""
148 
149  def __init__(self, *args: Any) -> None:
150  """Initialize a HumiditySensor accessory object."""
151  super().__init__(*args, category=CATEGORY_SENSOR)
152  state = self.hasshass.states.get(self.entity_identity_id)
153  assert state
154  serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
155  self.char_humiditychar_humidity = serv_humidity.configure_char(
156  CHAR_CURRENT_HUMIDITY, value=0
157  )
158  # Set the state so it is in sync on initial
159  # GET to avoid an event storm after homekit startup
160  self.async_update_stateasync_update_stateasync_update_state(state)
161 
162  @callback
163  def async_update_state(self, new_state: State) -> None:
164  """Update accessory after state change."""
165  if (humidity := convert_to_float(new_state.state)) is not None:
166  self.char_humiditychar_humidity.set_value(humidity)
167  _LOGGER.debug("%s: Percent set to %d%%", self.entity_identity_id, humidity)
168 
169 
170 @TYPES.register("AirQualitySensor")
172  """Generate a AirQualitySensor accessory as air quality sensor."""
173 
174  def __init__(self, *args: Any) -> None:
175  """Initialize a AirQualitySensor accessory object."""
176  super().__init__(*args, category=CATEGORY_SENSOR)
177  state = self.hasshass.states.get(self.entity_identity_id)
178  assert state
179  self.create_servicescreate_services()
180 
181  # Set the state so it is in sync on initial
182  # GET to avoid an event storm after homekit startup
183  self.async_update_stateasync_update_stateasync_update_state(state)
184 
185  def create_services(self) -> None:
186  """Initialize a AirQualitySensor accessory object."""
187  serv_air_quality = self.add_preload_service(
188  SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]
189  )
190  self.char_qualitychar_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0)
191  self.char_densitychar_density = serv_air_quality.configure_char(
192  CHAR_AIR_PARTICULATE_DENSITY, value=0
193  )
194 
195  @callback
196  def async_update_state(self, new_state: State) -> None:
197  """Update accessory after state change."""
198  if (density := convert_to_float(new_state.state)) is not None:
199  if self.char_densitychar_density.value != density:
200  self.char_densitychar_density.set_value(density)
201  _LOGGER.debug("%s: Set density to %d", self.entity_identity_id, density)
202  air_quality = density_to_air_quality(density)
203  self.char_qualitychar_quality.set_value(air_quality)
204  _LOGGER.debug("%s: Set air_quality to %d", self.entity_identity_id, air_quality)
205 
206 
207 @TYPES.register("PM10Sensor")
209  """Generate a PM10Sensor accessory as PM 10 sensor."""
210 
211  def create_services(self) -> None:
212  """Override the init function for PM 10 Sensor."""
213  serv_air_quality = self.add_preload_service(
214  SERV_AIR_QUALITY_SENSOR, [CHAR_PM10_DENSITY]
215  )
216  self.char_qualitychar_qualitychar_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0)
217  self.char_densitychar_densitychar_density = serv_air_quality.configure_char(CHAR_PM10_DENSITY, value=0)
218 
219  @callback
220  def async_update_state(self, new_state: State) -> None:
221  """Update accessory after state change."""
222  density = convert_to_float(new_state.state)
223  if density is None:
224  return
225  if self.char_densitychar_densitychar_density.value != density:
226  self.char_densitychar_densitychar_density.set_value(density)
227  _LOGGER.debug("%s: Set density to %d", self.entity_identity_id, density)
228  air_quality = density_to_air_quality_pm10(density)
229  if self.char_qualitychar_qualitychar_quality.value != air_quality:
230  self.char_qualitychar_qualitychar_quality.set_value(air_quality)
231  _LOGGER.debug("%s: Set air_quality to %d", self.entity_identity_id, air_quality)
232 
233 
234 @TYPES.register("PM25Sensor")
236  """Generate a PM25Sensor accessory as PM 2.5 sensor."""
237 
238  def create_services(self) -> None:
239  """Override the init function for PM 2.5 Sensor."""
240  serv_air_quality = self.add_preload_service(
241  SERV_AIR_QUALITY_SENSOR, [CHAR_PM25_DENSITY]
242  )
243  self.char_qualitychar_qualitychar_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0)
244  self.char_densitychar_densitychar_density = serv_air_quality.configure_char(CHAR_PM25_DENSITY, value=0)
245 
246  @callback
247  def async_update_state(self, new_state: State) -> None:
248  """Update accessory after state change."""
249  density = convert_to_float(new_state.state)
250  if density is None:
251  return
252  if self.char_densitychar_densitychar_density.value != density:
253  self.char_densitychar_densitychar_density.set_value(density)
254  _LOGGER.debug("%s: Set density to %d", self.entity_identity_id, density)
255  air_quality = density_to_air_quality(density)
256  if self.char_qualitychar_qualitychar_quality.value != air_quality:
257  self.char_qualitychar_qualitychar_quality.set_value(air_quality)
258  _LOGGER.debug("%s: Set air_quality to %d", self.entity_identity_id, air_quality)
259 
260 
261 @TYPES.register("NitrogenDioxideSensor")
263  """Generate a NitrogenDioxideSensor accessory as NO2 sensor."""
264 
265  def create_services(self) -> None:
266  """Override the init function for PM 2.5 Sensor."""
267  serv_air_quality = self.add_preload_service(
268  SERV_AIR_QUALITY_SENSOR, [CHAR_NITROGEN_DIOXIDE_DENSITY]
269  )
270  self.char_qualitychar_qualitychar_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0)
271  self.char_densitychar_densitychar_density = serv_air_quality.configure_char(
272  CHAR_NITROGEN_DIOXIDE_DENSITY, value=0
273  )
274 
275  @callback
276  def async_update_state(self, new_state: State) -> None:
277  """Update accessory after state change."""
278  density = convert_to_float(new_state.state)
279  if density is None:
280  return
281  if self.char_densitychar_densitychar_density.value != density:
282  self.char_densitychar_densitychar_density.set_value(density)
283  _LOGGER.debug("%s: Set density to %d", self.entity_identity_id, density)
284  air_quality = density_to_air_quality_nitrogen_dioxide(density)
285  if self.char_qualitychar_qualitychar_quality.value != air_quality:
286  self.char_qualitychar_qualitychar_quality.set_value(air_quality)
287  _LOGGER.debug("%s: Set air_quality to %d", self.entity_identity_id, air_quality)
288 
289 
290 @TYPES.register("VolatileOrganicCompoundsSensor")
292  """Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor.
293 
294  Sensor entity must return VOC in µg/m3.
295  """
296 
297  def create_services(self) -> None:
298  """Override the init function for VOC Sensor."""
299  serv_air_quality: Service = self.add_preload_service(
300  SERV_AIR_QUALITY_SENSOR, [CHAR_VOC_DENSITY]
301  )
302  self.char_qualitychar_qualitychar_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0)
303  self.char_densitychar_densitychar_density = serv_air_quality.configure_char(
304  CHAR_VOC_DENSITY,
305  value=0,
306  properties={
307  PROP_MIN_VALUE: 0,
308  PROP_MAX_VALUE: 5000,
309  },
310  )
311 
312  @callback
313  def async_update_state(self, new_state: State) -> None:
314  """Update accessory after state change."""
315  density = convert_to_float(new_state.state)
316  if density is None:
317  return
318  if self.char_densitychar_densitychar_density.value != density:
319  self.char_densitychar_densitychar_density.set_value(density)
320  _LOGGER.debug("%s: Set density to %d", self.entity_identity_id, density)
321  air_quality = density_to_air_quality_voc(density)
322  if self.char_qualitychar_qualitychar_quality.value != air_quality:
323  self.char_qualitychar_qualitychar_quality.set_value(air_quality)
324  _LOGGER.debug("%s: Set air_quality to %d", self.entity_identity_id, air_quality)
325 
326 
327 @TYPES.register("CarbonMonoxideSensor")
329  """Generate a CarbonMonoxidSensor accessory as CO sensor."""
330 
331  def __init__(self, *args: Any) -> None:
332  """Initialize a CarbonMonoxideSensor accessory object."""
333  super().__init__(*args, category=CATEGORY_SENSOR)
334  state = self.hasshass.states.get(self.entity_identity_id)
335  assert state
336  serv_co = self.add_preload_service(
337  SERV_CARBON_MONOXIDE_SENSOR,
338  [CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL],
339  )
340 
341  self.threshold_cothreshold_co = self.configconfig.get(CONF_THRESHOLD_CO, THRESHOLD_CO)
342  _LOGGER.debug("%s: Set CO threshold to %d", self.entity_identity_id, self.threshold_cothreshold_co)
343 
344  self.char_levelchar_level = serv_co.configure_char(CHAR_CARBON_MONOXIDE_LEVEL, value=0)
345  self.char_peakchar_peak = serv_co.configure_char(
346  CHAR_CARBON_MONOXIDE_PEAK_LEVEL, value=0
347  )
348  self.char_detectedchar_detected = serv_co.configure_char(
349  CHAR_CARBON_MONOXIDE_DETECTED, value=0
350  )
351  # Set the state so it is in sync on initial
352  # GET to avoid an event storm after homekit startup
353  self.async_update_stateasync_update_stateasync_update_state(state)
354 
355  @callback
356  def async_update_state(self, new_state: State) -> None:
357  """Update accessory after state change."""
358  if (value := convert_to_float(new_state.state)) is not None:
359  self.char_levelchar_level.set_value(value)
360  if value > self.char_peakchar_peak.value:
361  self.char_peakchar_peak.set_value(value)
362  co_detected = value > self.threshold_cothreshold_co
363  self.char_detectedchar_detected.set_value(co_detected)
364  _LOGGER.debug("%s: Set to %d", self.entity_identity_id, value)
365 
366 
367 @TYPES.register("CarbonDioxideSensor")
369  """Generate a CarbonDioxideSensor accessory as CO2 sensor."""
370 
371  def __init__(self, *args: Any) -> None:
372  """Initialize a CarbonDioxideSensor accessory object."""
373  super().__init__(*args, category=CATEGORY_SENSOR)
374  state = self.hasshass.states.get(self.entity_identity_id)
375  assert state
376  serv_co2 = self.add_preload_service(
377  SERV_CARBON_DIOXIDE_SENSOR,
378  [CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL],
379  )
380 
381  self.threshold_co2threshold_co2 = self.configconfig.get(CONF_THRESHOLD_CO2, THRESHOLD_CO2)
382  _LOGGER.debug("%s: Set CO2 threshold to %d", self.entity_identity_id, self.threshold_co2threshold_co2)
383 
384  self.char_levelchar_level = serv_co2.configure_char(CHAR_CARBON_DIOXIDE_LEVEL, value=0)
385  self.char_peakchar_peak = serv_co2.configure_char(
386  CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0
387  )
388  self.char_detectedchar_detected = serv_co2.configure_char(
389  CHAR_CARBON_DIOXIDE_DETECTED, value=0
390  )
391  # Set the state so it is in sync on initial
392  # GET to avoid an event storm after homekit startup
393  self.async_update_stateasync_update_stateasync_update_state(state)
394 
395  @callback
396  def async_update_state(self, new_state: State) -> None:
397  """Update accessory after state change."""
398  if (value := convert_to_float(new_state.state)) is not None:
399  self.char_levelchar_level.set_value(value)
400  if value > self.char_peakchar_peak.value:
401  self.char_peakchar_peak.set_value(value)
402  co2_detected = value > self.threshold_co2threshold_co2
403  self.char_detectedchar_detected.set_value(co2_detected)
404  _LOGGER.debug("%s: Set to %d", self.entity_identity_id, value)
405 
406 
407 @TYPES.register("LightSensor")
409  """Generate a LightSensor accessory as light sensor."""
410 
411  def __init__(self, *args: Any) -> None:
412  """Initialize a LightSensor accessory object."""
413  super().__init__(*args, category=CATEGORY_SENSOR)
414  state = self.hasshass.states.get(self.entity_identity_id)
415  assert state
416  serv_light = self.add_preload_service(SERV_LIGHT_SENSOR)
417  self.char_lightchar_light = serv_light.configure_char(
418  CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0
419  )
420  # Set the state so it is in sync on initial
421  # GET to avoid an event storm after homekit startup
422  self.async_update_stateasync_update_stateasync_update_state(state)
423 
424  @callback
425  def async_update_state(self, new_state: State) -> None:
426  """Update accessory after state change."""
427  if (luminance := convert_to_float(new_state.state)) is not None:
428  self.char_lightchar_light.set_value(luminance)
429  _LOGGER.debug("%s: Set to %d", self.entity_identity_id, luminance)
430 
431 
432 @TYPES.register("BinarySensor")
434  """Generate a BinarySensor accessory as binary sensor."""
435 
436  def __init__(self, *args: Any) -> None:
437  """Initialize a BinarySensor accessory object."""
438  super().__init__(*args, category=CATEGORY_SENSOR)
439  state = self.hasshass.states.get(self.entity_identity_id)
440  assert state
441  device_class = state.attributes.get(ATTR_DEVICE_CLASS)
442  service_char = (
443  BINARY_SENSOR_SERVICE_MAP[device_class]
444  if device_class in BINARY_SENSOR_SERVICE_MAP
445  else BINARY_SENSOR_SERVICE_MAP[BinarySensorDeviceClass.OCCUPANCY]
446  )
447 
448  self.formatformat = service_char.format
449  service = self.add_preload_service(service_char.service)
450  initial_value = False if self.formatformat is bool else 0
451  self.char_detectedchar_detected = service.configure_char(
452  service_char.char, value=initial_value
453  )
454  # Set the state so it is in sync on initial
455  # GET to avoid an event storm after homekit startup
456  self.async_update_stateasync_update_stateasync_update_state(state)
457 
458  @callback
459  def async_update_state(self, new_state: State) -> None:
460  """Update accessory after state change."""
461  state = new_state.state
462  detected = self.formatformat(state in (STATE_ON, STATE_HOME))
463  self.char_detectedchar_detected.set_value(detected)
464  _LOGGER.debug("%s: Set to %d", self.entity_identity_id, detected)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88