Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for airthings ble sensors."""
2 
3 from __future__ import annotations
4 
5 import dataclasses
6 import logging
7 
8 from airthings_ble import AirthingsDevice
9 
11  SensorDeviceClass,
12  SensorEntity,
13  SensorEntityDescription,
14  SensorStateClass,
15 )
16 from homeassistant.const import (
17  CONCENTRATION_PARTS_PER_BILLION,
18  CONCENTRATION_PARTS_PER_MILLION,
19  PERCENTAGE,
20  EntityCategory,
21  Platform,
22  UnitOfPressure,
23  UnitOfTemperature,
24 )
25 from homeassistant.core import HomeAssistant, callback
26 from homeassistant.helpers import device_registry as dr, entity_registry as er
27 from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
28 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30  RegistryEntry,
31  async_entries_for_device,
32 )
33 from homeassistant.helpers.typing import StateType
34 from homeassistant.helpers.update_coordinator import CoordinatorEntity
35 from homeassistant.util.unit_system import METRIC_SYSTEM
36 
37 from .const import DOMAIN, VOLUME_BECQUEREL, VOLUME_PICOCURIE
38 from .coordinator import AirthingsBLEConfigEntry, AirthingsBLEDataUpdateCoordinator
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
43  "radon_1day_avg": SensorEntityDescription(
44  key="radon_1day_avg",
45  translation_key="radon_1day_avg",
46  native_unit_of_measurement=VOLUME_BECQUEREL,
47  suggested_display_precision=0,
48  state_class=SensorStateClass.MEASUREMENT,
49  ),
50  "radon_longterm_avg": SensorEntityDescription(
51  key="radon_longterm_avg",
52  translation_key="radon_longterm_avg",
53  native_unit_of_measurement=VOLUME_BECQUEREL,
54  suggested_display_precision=0,
55  state_class=SensorStateClass.MEASUREMENT,
56  ),
57  "radon_1day_level": SensorEntityDescription(
58  key="radon_1day_level",
59  translation_key="radon_1day_level",
60  ),
61  "radon_longterm_level": SensorEntityDescription(
62  key="radon_longterm_level",
63  translation_key="radon_longterm_level",
64  ),
65  "temperature": SensorEntityDescription(
66  key="temperature",
67  device_class=SensorDeviceClass.TEMPERATURE,
68  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
69  state_class=SensorStateClass.MEASUREMENT,
70  ),
71  "humidity": SensorEntityDescription(
72  key="humidity",
73  device_class=SensorDeviceClass.HUMIDITY,
74  native_unit_of_measurement=PERCENTAGE,
75  state_class=SensorStateClass.MEASUREMENT,
76  ),
77  "pressure": SensorEntityDescription(
78  key="pressure",
79  device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
80  native_unit_of_measurement=UnitOfPressure.MBAR,
81  state_class=SensorStateClass.MEASUREMENT,
82  ),
83  "battery": SensorEntityDescription(
84  key="battery",
85  device_class=SensorDeviceClass.BATTERY,
86  native_unit_of_measurement=PERCENTAGE,
87  state_class=SensorStateClass.MEASUREMENT,
88  entity_category=EntityCategory.DIAGNOSTIC,
89  ),
91  key="co2",
92  device_class=SensorDeviceClass.CO2,
93  native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
94  state_class=SensorStateClass.MEASUREMENT,
95  ),
97  key="voc",
98  device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
99  native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
100  state_class=SensorStateClass.MEASUREMENT,
101  ),
102  "illuminance": SensorEntityDescription(
103  key="illuminance",
104  translation_key="illuminance",
105  native_unit_of_measurement=PERCENTAGE,
106  state_class=SensorStateClass.MEASUREMENT,
107  ),
108 }
109 
110 
111 @callback
112 def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None:
113  """Migrate entities to new unique ids (with BLE Address)."""
114  ent_reg = er.async_get(hass)
115  unique_id_trailer = f"_{sensor_name}"
116  new_unique_id = f"{address}{unique_id_trailer}"
117  if ent_reg.async_get_entity_id(DOMAIN, Platform.SENSOR, new_unique_id):
118  # New unique id already exists
119  return
120  dev_reg = dr.async_get(hass)
121  if not (
122  device := dev_reg.async_get_device(
123  connections={(CONNECTION_BLUETOOTH, address)}
124  )
125  ):
126  return
127  entities = async_entries_for_device(
128  ent_reg,
129  device_id=device.id,
130  include_disabled_entities=True,
131  )
132  matching_reg_entry: RegistryEntry | None = None
133  for entry in entities:
134  if entry.unique_id.endswith(unique_id_trailer) and (
135  not matching_reg_entry or "(" not in entry.unique_id
136  ):
137  matching_reg_entry = entry
138  if not matching_reg_entry or matching_reg_entry.unique_id == new_unique_id:
139  # Already has the newest unique id format
140  return
141  entity_id = matching_reg_entry.entity_id
142  ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id)
143  _LOGGER.debug("Migrated entity '%s' to unique id '%s'", entity_id, new_unique_id)
144 
145 
147  hass: HomeAssistant,
148  entry: AirthingsBLEConfigEntry,
149  async_add_entities: AddEntitiesCallback,
150 ) -> None:
151  """Set up the Airthings BLE sensors."""
152  is_metric = hass.config.units is METRIC_SYSTEM
153 
154  coordinator = entry.runtime_data
155 
156  # we need to change some units
157  sensors_mapping = SENSORS_MAPPING_TEMPLATE.copy()
158  if not is_metric:
159  for key, val in sensors_mapping.items():
160  if val.native_unit_of_measurement is not VOLUME_BECQUEREL:
161  continue
162  sensors_mapping[key] = dataclasses.replace(
163  val,
164  native_unit_of_measurement=VOLUME_PICOCURIE,
165  suggested_display_precision=1,
166  )
167 
168  entities = []
169  _LOGGER.debug("got sensors: %s", coordinator.data.sensors)
170  for sensor_type, sensor_value in coordinator.data.sensors.items():
171  if sensor_type not in sensors_mapping:
172  _LOGGER.debug(
173  "Unknown sensor type detected: %s, %s",
174  sensor_type,
175  sensor_value,
176  )
177  continue
178  async_migrate(hass, coordinator.data.address, sensor_type)
179  entities.append(
180  AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type])
181  )
182 
183  async_add_entities(entities)
184 
185 
187  CoordinatorEntity[AirthingsBLEDataUpdateCoordinator], SensorEntity
188 ):
189  """Airthings BLE sensors for the device."""
190 
191  _attr_has_entity_name = True
192 
193  def __init__(
194  self,
195  coordinator: AirthingsBLEDataUpdateCoordinator,
196  airthings_device: AirthingsDevice,
197  entity_description: SensorEntityDescription,
198  ) -> None:
199  """Populate the airthings entity with relevant data."""
200  super().__init__(coordinator)
201  self.entity_descriptionentity_description = entity_description
202 
203  name = airthings_device.name
204  if identifier := airthings_device.identifier:
205  name += f" ({identifier})"
206 
207  self._attr_unique_id_attr_unique_id = f"{airthings_device.address}_{entity_description.key}"
208  self._attr_device_info_attr_device_info = DeviceInfo(
209  connections={
210  (
211  CONNECTION_BLUETOOTH,
212  airthings_device.address,
213  )
214  },
215  name=name,
216  manufacturer=airthings_device.manufacturer,
217  hw_version=airthings_device.hw_version,
218  sw_version=airthings_device.sw_version,
219  model=airthings_device.model.product_name,
220  )
221 
222  @property
223  def available(self) -> bool:
224  """Check if device and sensor is available in data."""
225  return (
226  super().available
227  and self.entity_descriptionentity_description.key in self.coordinator.data.sensors
228  )
229 
230  @property
231  def native_value(self) -> StateType:
232  """Return the value reported by the sensor."""
233  return self.coordinator.data.sensors[self.entity_descriptionentity_description.key]
None __init__(self, AirthingsBLEDataUpdateCoordinator coordinator, AirthingsDevice airthings_device, SensorEntityDescription entity_description)
Definition: sensor.py:198
None async_setup_entry(HomeAssistant hass, AirthingsBLEConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:150
None async_migrate(HomeAssistant hass, str address, str sensor_name)
Definition: sensor.py:112
list[RegistryEntry] async_entries_for_device(EntityRegistry registry, str device_id, bool include_disabled_entities=False)