Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Sensor component that handles additional Tomorrowio data for your location."""
2 
3 from __future__ import annotations
4 
5 from abc import abstractmethod
6 from collections.abc import Callable
7 from dataclasses import dataclass
8 from typing import Any
9 
10 from pytomorrowio.const import (
11  HealthConcernType,
12  PollenIndex,
13  PrecipitationType,
14  PrimaryPollutantType,
15  UVDescription,
16 )
17 
19  SensorDeviceClass,
20  SensorEntity,
21  SensorEntityDescription,
22  SensorStateClass,
23 )
24 from homeassistant.config_entries import ConfigEntry
25 from homeassistant.const import (
26  CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
27  CONCENTRATION_PARTS_PER_MILLION,
28  CONF_API_KEY,
29  PERCENTAGE,
30  UnitOfIrradiance,
31  UnitOfLength,
32  UnitOfPressure,
33  UnitOfSpeed,
34  UnitOfTemperature,
35 )
36 from homeassistant.core import HomeAssistant
37 from homeassistant.helpers.entity_platform import AddEntitiesCallback
38 from homeassistant.util.unit_conversion import DistanceConverter, SpeedConverter
39 from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
40 
41 from .const import (
42  DOMAIN,
43  TMRW_ATTR_CARBON_MONOXIDE,
44  TMRW_ATTR_CHINA_AQI,
45  TMRW_ATTR_CHINA_HEALTH_CONCERN,
46  TMRW_ATTR_CHINA_PRIMARY_POLLUTANT,
47  TMRW_ATTR_CLOUD_BASE,
48  TMRW_ATTR_CLOUD_CEILING,
49  TMRW_ATTR_CLOUD_COVER,
50  TMRW_ATTR_DEW_POINT,
51  TMRW_ATTR_EPA_AQI,
52  TMRW_ATTR_EPA_HEALTH_CONCERN,
53  TMRW_ATTR_EPA_PRIMARY_POLLUTANT,
54  TMRW_ATTR_FEELS_LIKE,
55  TMRW_ATTR_FIRE_INDEX,
56  TMRW_ATTR_NITROGEN_DIOXIDE,
57  TMRW_ATTR_OZONE,
58  TMRW_ATTR_PARTICULATE_MATTER_10,
59  TMRW_ATTR_PARTICULATE_MATTER_25,
60  TMRW_ATTR_POLLEN_GRASS,
61  TMRW_ATTR_POLLEN_TREE,
62  TMRW_ATTR_POLLEN_WEED,
63  TMRW_ATTR_PRECIPITATION_TYPE,
64  TMRW_ATTR_PRESSURE_SURFACE_LEVEL,
65  TMRW_ATTR_SOLAR_GHI,
66  TMRW_ATTR_SULPHUR_DIOXIDE,
67  TMRW_ATTR_UV_HEALTH_CONCERN,
68  TMRW_ATTR_UV_INDEX,
69  TMRW_ATTR_WIND_GUST,
70 )
71 from .coordinator import TomorrowioDataUpdateCoordinator
72 from .entity import TomorrowioEntity
73 
74 
75 @dataclass(frozen=True)
77  """Describes a Tomorrow.io sensor entity."""
78 
79  attribute: str = ""
80  unit_imperial: str | None = None
81  unit_metric: str | None = None
82  multiplication_factor: Callable[[float], float] | float | None = None
83  imperial_conversion: Callable[[float], float] | float | None = None
84  value_map: Any | None = None
85 
86  def __post_init__(self) -> None:
87  """Handle post init."""
88  if (self.unit_imperial is None and self.unit_metric is not None) or (
89  self.unit_imperial is not None and self.unit_metric is None
90  ):
91  raise ValueError(
92  "Entity descriptions must include both imperial and metric units or "
93  "they must both be None"
94  )
95 
96  if self.value_map is not None:
97  options = [item.name.lower() for item in self.value_map]
98  object.__setattr__(self, "device_class", SensorDeviceClass.ENUM)
99  object.__setattr__(self, "options", options)
100 
101 
102 # From https://cfpub.epa.gov/ncer_abstracts/index.cfm/fuseaction/display.files/fileID/14285
103 # x ug/m^3 = y ppb * molecular weight / 24.45
104 def convert_ppb_to_ugm3(molecular_weight: float) -> Callable[[float], float]:
105  """Return function to convert ppb to ug/m^3."""
106  return lambda x: (x * molecular_weight) / 24.45
107 
108 
109 SENSOR_TYPES = (
111  key="feels_like",
112  translation_key="feels_like",
113  attribute=TMRW_ATTR_FEELS_LIKE,
114  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
115  device_class=SensorDeviceClass.TEMPERATURE,
116  state_class=SensorStateClass.MEASUREMENT,
117  ),
119  key="dew_point",
120  translation_key="dew_point",
121  attribute=TMRW_ATTR_DEW_POINT,
122  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
123  device_class=SensorDeviceClass.TEMPERATURE,
124  state_class=SensorStateClass.MEASUREMENT,
125  ),
126  # Data comes in as hPa
128  key="pressure_surface_level",
129  attribute=TMRW_ATTR_PRESSURE_SURFACE_LEVEL,
130  native_unit_of_measurement=UnitOfPressure.HPA,
131  device_class=SensorDeviceClass.PRESSURE,
132  state_class=SensorStateClass.MEASUREMENT,
133  ),
134  # Data comes in as W/m^2, convert to BTUs/(hr * ft^2) for imperial
135  # https://www.theunitconverter.com/watt-square-meter-to-btu-hour-square-foot-conversion/
137  key="global_horizontal_irradiance",
138  attribute=TMRW_ATTR_SOLAR_GHI,
139  unit_imperial=UnitOfIrradiance.BTUS_PER_HOUR_SQUARE_FOOT,
140  unit_metric=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
141  imperial_conversion=(1 / 3.15459),
142  device_class=SensorDeviceClass.IRRADIANCE,
143  state_class=SensorStateClass.MEASUREMENT,
144  ),
145  # Data comes in as km, convert to miles for imperial
147  key="cloud_base",
148  translation_key="cloud_base",
149  attribute=TMRW_ATTR_CLOUD_BASE,
150  unit_imperial=UnitOfLength.MILES,
151  unit_metric=UnitOfLength.KILOMETERS,
152  device_class=SensorDeviceClass.DISTANCE,
153  state_class=SensorStateClass.MEASUREMENT,
154  imperial_conversion=lambda val: DistanceConverter.convert(
155  val,
156  UnitOfLength.KILOMETERS,
157  UnitOfLength.MILES,
158  ),
159  ),
160  # Data comes in as km, convert to miles for imperial
162  key="cloud_ceiling",
163  translation_key="cloud_ceiling",
164  attribute=TMRW_ATTR_CLOUD_CEILING,
165  unit_imperial=UnitOfLength.MILES,
166  unit_metric=UnitOfLength.KILOMETERS,
167  device_class=SensorDeviceClass.DISTANCE,
168  state_class=SensorStateClass.MEASUREMENT,
169  imperial_conversion=lambda val: DistanceConverter.convert(
170  val,
171  UnitOfLength.KILOMETERS,
172  UnitOfLength.MILES,
173  ),
174  ),
176  key="cloud_cover",
177  translation_key="cloud_cover",
178  attribute=TMRW_ATTR_CLOUD_COVER,
179  native_unit_of_measurement=PERCENTAGE,
180  ),
181  # Data comes in as m/s, convert to mi/h for imperial
183  key="wind_gust",
184  translation_key="wind_gust",
185  attribute=TMRW_ATTR_WIND_GUST,
186  unit_imperial=UnitOfSpeed.MILES_PER_HOUR,
187  unit_metric=UnitOfSpeed.METERS_PER_SECOND,
188  device_class=SensorDeviceClass.SPEED,
189  state_class=SensorStateClass.MEASUREMENT,
190  imperial_conversion=lambda val: SpeedConverter.convert(
191  val, UnitOfSpeed.METERS_PER_SECOND, UnitOfSpeed.MILES_PER_HOUR
192  ),
193  ),
195  key="precipitation_type",
196  translation_key="precipitation_type",
197  attribute=TMRW_ATTR_PRECIPITATION_TYPE,
198  value_map=PrecipitationType,
199  ),
200  # Data comes in as ppb, convert to µg/m^3
201  # Molecular weight of Ozone is 48
203  key="ozone",
204  attribute=TMRW_ATTR_OZONE,
205  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
206  multiplication_factor=convert_ppb_to_ugm3(48),
207  device_class=SensorDeviceClass.OZONE,
208  state_class=SensorStateClass.MEASUREMENT,
209  ),
211  key="particulate_matter_2_5_mm",
212  attribute=TMRW_ATTR_PARTICULATE_MATTER_25,
213  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
214  device_class=SensorDeviceClass.PM25,
215  state_class=SensorStateClass.MEASUREMENT,
216  ),
218  key="particulate_matter_10_mm",
219  attribute=TMRW_ATTR_PARTICULATE_MATTER_10,
220  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
221  device_class=SensorDeviceClass.PM10,
222  state_class=SensorStateClass.MEASUREMENT,
223  ),
224  # Data comes in as ppb, convert to µg/m^3
225  # Molecular weight of Nitrogen Dioxide is 46.01
227  key="nitrogen_dioxide",
228  attribute=TMRW_ATTR_NITROGEN_DIOXIDE,
229  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
230  multiplication_factor=convert_ppb_to_ugm3(46.01),
231  device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
232  state_class=SensorStateClass.MEASUREMENT,
233  ),
234  # Data comes in as ppb, convert to ppm
236  key="carbon_monoxide",
237  attribute=TMRW_ATTR_CARBON_MONOXIDE,
238  native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
239  multiplication_factor=1 / 1000,
240  device_class=SensorDeviceClass.CO,
241  state_class=SensorStateClass.MEASUREMENT,
242  ),
243  # Data comes in as ppb, convert to µg/m^3
244  # Molecular weight of Sulphur Dioxide is 64.07
246  key="sulphur_dioxide",
247  attribute=TMRW_ATTR_SULPHUR_DIOXIDE,
248  native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
249  multiplication_factor=convert_ppb_to_ugm3(64.07),
250  device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
251  state_class=SensorStateClass.MEASUREMENT,
252  ),
254  key="us_epa_air_quality_index",
255  translation_key="us_epa_air_quality_index",
256  attribute=TMRW_ATTR_EPA_AQI,
257  device_class=SensorDeviceClass.AQI,
258  state_class=SensorStateClass.MEASUREMENT,
259  ),
261  key="us_epa_primary_pollutant",
262  translation_key="primary_pollutant",
263  attribute=TMRW_ATTR_EPA_PRIMARY_POLLUTANT,
264  value_map=PrimaryPollutantType,
265  ),
267  key="us_epa_health_concern",
268  translation_key="health_concern",
269  attribute=TMRW_ATTR_EPA_HEALTH_CONCERN,
270  value_map=HealthConcernType,
271  ),
273  key="china_mep_air_quality_index",
274  translation_key="china_mep_air_quality_index",
275  attribute=TMRW_ATTR_CHINA_AQI,
276  device_class=SensorDeviceClass.AQI,
277  ),
279  key="china_mep_primary_pollutant",
280  translation_key="china_mep_primary_pollutant",
281  attribute=TMRW_ATTR_CHINA_PRIMARY_POLLUTANT,
282  value_map=PrimaryPollutantType,
283  ),
285  key="china_mep_health_concern",
286  translation_key="china_mep_health_concern",
287  attribute=TMRW_ATTR_CHINA_HEALTH_CONCERN,
288  value_map=HealthConcernType,
289  ),
291  key="tree_pollen_index",
292  translation_key="pollen_index",
293  attribute=TMRW_ATTR_POLLEN_TREE,
294  value_map=PollenIndex,
295  ),
297  key="weed_pollen_index",
298  translation_key="weed_pollen_index",
299  attribute=TMRW_ATTR_POLLEN_WEED,
300  value_map=PollenIndex,
301  ),
303  key="grass_pollen_index",
304  translation_key="grass_pollen_index",
305  attribute=TMRW_ATTR_POLLEN_GRASS,
306  value_map=PollenIndex,
307  ),
309  key="fire_index",
310  translation_key="fire_index",
311  attribute=TMRW_ATTR_FIRE_INDEX,
312  ),
314  key="uv_index",
315  translation_key="uv_index",
316  attribute=TMRW_ATTR_UV_INDEX,
317  state_class=SensorStateClass.MEASUREMENT,
318  ),
320  key="uv_radiation_health_concern",
321  translation_key="uv_radiation_health_concern",
322  attribute=TMRW_ATTR_UV_HEALTH_CONCERN,
323  value_map=UVDescription,
324  ),
325 )
326 
327 
329  hass: HomeAssistant,
330  config_entry: ConfigEntry,
331  async_add_entities: AddEntitiesCallback,
332 ) -> None:
333  """Set up a config entry."""
334  coordinator = hass.data[DOMAIN][config_entry.data[CONF_API_KEY]]
335  entities = [
336  TomorrowioSensorEntity(hass, config_entry, coordinator, 4, description)
337  for description in SENSOR_TYPES
338  ]
339  async_add_entities(entities)
340 
341 
343  value: float, conversion: Callable[[float], float] | float
344 ) -> float:
345  """Handle conversion of a value based on conversion type."""
346  if callable(conversion):
347  return round(conversion(float(value)), 2)
348 
349  return round(float(value) * conversion, 2)
350 
351 
353  """Base Tomorrow.io sensor entity."""
354 
355  entity_description: TomorrowioSensorEntityDescription
356  _attr_entity_registry_enabled_default = False
357 
358  def __init__(
359  self,
360  hass: HomeAssistant,
361  config_entry: ConfigEntry,
362  coordinator: TomorrowioDataUpdateCoordinator,
363  api_version: int,
364  description: TomorrowioSensorEntityDescription,
365  ) -> None:
366  """Initialize Tomorrow.io Sensor Entity."""
367  super().__init__(config_entry, coordinator, api_version)
368  self.entity_descriptionentity_description = description
369  self._attr_unique_id_attr_unique_id = f"{self._config_entry.unique_id}_{description.key}"
370  if self.entity_descriptionentity_description.native_unit_of_measurement is None:
371  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = description.unit_metric
372  if hass.config.units is US_CUSTOMARY_SYSTEM:
373  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = description.unit_imperial
374 
375  @property
376  @abstractmethod
377  def _state(self) -> int | float | None:
378  """Return the raw state."""
379 
380  @property
381  def native_value(self) -> str | int | float | None:
382  """Return the state."""
383  state = self._state_state
384  desc = self.entity_descriptionentity_description
385 
386  if state is None:
387  return state
388 
389  if desc.value_map is not None:
390  return desc.value_map(state).name.lower()
391 
392  if desc.multiplication_factor is not None:
393  state = handle_conversion(state, desc.multiplication_factor)
394 
395  # If there is an imperial conversion needed and the instance is using imperial,
396  # apply the conversion logic.
397  if (
398  desc.imperial_conversion
399  and desc.unit_imperial is not None
400  and desc.unit_imperial != desc.unit_metric
401  and self.hasshasshass.config.units is US_CUSTOMARY_SYSTEM
402  ):
403  return handle_conversion(state, desc.imperial_conversion)
404 
405  return state
406 
407 
409  """Sensor entity that talks to Tomorrow.io v4 API to retrieve non-weather data."""
410 
411  @property
412  def _state(self) -> int | float | None:
413  """Return the raw state."""
414  val = self._get_current_property_get_current_property(self.entity_descriptionentity_description.attribute)
415  assert not isinstance(val, str)
416  return val
int|str|float|None _get_current_property(self, str property_name)
Definition: entity.py:39
None __init__(self, HomeAssistant hass, ConfigEntry config_entry, TomorrowioDataUpdateCoordinator coordinator, int api_version, TomorrowioSensorEntityDescription description)
Definition: sensor.py:365
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:332
Callable[[float], float] convert_ppb_to_ugm3(float molecular_weight)
Definition: sensor.py:104
float handle_conversion(float value, Callable[[float], float]|float conversion)
Definition: sensor.py:344