Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Aranet sensors."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from typing import Any
7 
8 from aranet4.client import Aranet4Advertisement
9 from bleak.backends.device import BLEDevice
10 
12  PassiveBluetoothDataProcessor,
13  PassiveBluetoothDataUpdate,
14  PassiveBluetoothEntityKey,
15  PassiveBluetoothProcessorEntity,
16 )
18  SensorDeviceClass,
19  SensorEntity,
20  SensorEntityDescription,
21  SensorStateClass,
22 )
23 from homeassistant.const import (
24  ATTR_MANUFACTURER,
25  ATTR_NAME,
26  ATTR_SW_VERSION,
27  CONCENTRATION_PARTS_PER_MILLION,
28  PERCENTAGE,
29  EntityCategory,
30  UnitOfPressure,
31  UnitOfTemperature,
32  UnitOfTime,
33 )
34 from homeassistant.core import HomeAssistant
35 from homeassistant.helpers.device_registry import DeviceInfo
36 from homeassistant.helpers.entity import EntityDescription
37 from homeassistant.helpers.entity_platform import AddEntitiesCallback
38 
39 from . import AranetConfigEntry
40 from .const import ARANET_MANUFACTURER_NAME
41 
42 
43 @dataclass(frozen=True)
45  """Class to describe an Aranet sensor entity."""
46 
47  # PassiveBluetoothDataUpdate does not support UNDEFINED
48  # Restrict the type to satisfy the type checker and catch attempts
49  # to use UNDEFINED in the entity descriptions.
50  name: str | None = None
51  scale: float | int = 1
52 
53 
54 SENSOR_DESCRIPTIONS = {
55  "temperature": AranetSensorEntityDescription(
56  key="temperature",
57  name="Temperature",
58  device_class=SensorDeviceClass.TEMPERATURE,
59  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
60  state_class=SensorStateClass.MEASUREMENT,
61  ),
63  key="humidity",
64  name="Humidity",
65  device_class=SensorDeviceClass.HUMIDITY,
66  native_unit_of_measurement=PERCENTAGE,
67  state_class=SensorStateClass.MEASUREMENT,
68  ),
70  key="pressure",
71  name="Pressure",
72  device_class=SensorDeviceClass.PRESSURE,
73  native_unit_of_measurement=UnitOfPressure.HPA,
74  state_class=SensorStateClass.MEASUREMENT,
75  ),
77  key="co2",
78  name="Carbon Dioxide",
79  device_class=SensorDeviceClass.CO2,
80  native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
81  state_class=SensorStateClass.MEASUREMENT,
82  ),
83  "radiation_rate": AranetSensorEntityDescription(
84  key="radiation_rate",
85  translation_key="radiation_rate",
86  name="Radiation Dose Rate",
87  native_unit_of_measurement="μSv/h",
88  state_class=SensorStateClass.MEASUREMENT,
89  suggested_display_precision=2,
90  scale=0.001,
91  ),
92  "radiation_total": AranetSensorEntityDescription(
93  key="radiation_total",
94  translation_key="radiation_total",
95  name="Radiation Total Dose",
96  native_unit_of_measurement="mSv",
97  state_class=SensorStateClass.MEASUREMENT,
98  suggested_display_precision=4,
99  scale=0.000001,
100  ),
101  "radon_concentration": AranetSensorEntityDescription(
102  key="radon_concentration",
103  translation_key="radon_concentration",
104  name="Radon Concentration",
105  native_unit_of_measurement="Bq/m³",
106  state_class=SensorStateClass.MEASUREMENT,
107  ),
109  key="battery",
110  name="Battery",
111  device_class=SensorDeviceClass.BATTERY,
112  native_unit_of_measurement=PERCENTAGE,
113  state_class=SensorStateClass.MEASUREMENT,
114  entity_category=EntityCategory.DIAGNOSTIC,
115  ),
116  "interval": AranetSensorEntityDescription(
117  key="update_interval",
118  name="Update Interval",
119  device_class=SensorDeviceClass.DURATION,
120  native_unit_of_measurement=UnitOfTime.SECONDS,
121  state_class=SensorStateClass.MEASUREMENT,
122  # The interval setting is not a generally useful entity for most users.
123  entity_registry_enabled_default=False,
124  entity_category=EntityCategory.DIAGNOSTIC,
125  ),
126 }
127 
128 
130  device: BLEDevice,
131  key: str,
132 ) -> PassiveBluetoothEntityKey:
133  """Convert a device key to an entity key."""
134  return PassiveBluetoothEntityKey(key, device.address)
135 
136 
138  adv: Aranet4Advertisement,
139 ) -> DeviceInfo:
140  """Convert a sensor device info to hass device info."""
141  hass_device_info = DeviceInfo({})
142  if adv.readings and adv.readings.name:
143  hass_device_info[ATTR_NAME] = adv.readings.name
144  hass_device_info[ATTR_MANUFACTURER] = ARANET_MANUFACTURER_NAME
145  if adv.manufacturer_data:
146  hass_device_info[ATTR_SW_VERSION] = str(adv.manufacturer_data.version)
147  return hass_device_info
148 
149 
151  adv: Aranet4Advertisement,
152 ) -> PassiveBluetoothDataUpdate[Any]:
153  """Convert a sensor update to a Bluetooth data update."""
154  data: dict[PassiveBluetoothEntityKey, Any] = {}
155  names: dict[PassiveBluetoothEntityKey, str | None] = {}
156  descs: dict[PassiveBluetoothEntityKey, EntityDescription] = {}
157  for key, desc in SENSOR_DESCRIPTIONS.items():
158  tag = _device_key_to_bluetooth_entity_key(adv.device, key)
159  val = getattr(adv.readings, key)
160  if val == -1:
161  continue
162  val *= desc.scale
163  data[tag] = val
164  names[tag] = desc.name
165  descs[tag] = desc
167  devices={adv.device.address: _sensor_device_info_to_hass(adv)},
168  entity_descriptions=descs,
169  entity_data=data,
170  entity_names=names,
171  )
172 
173 
175  hass: HomeAssistant,
176  entry: AranetConfigEntry,
177  async_add_entities: AddEntitiesCallback,
178 ) -> None:
179  """Set up the Aranet sensors."""
180  processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
181  entry.async_on_unload(
182  processor.async_add_entities_listener(
183  Aranet4BluetoothSensorEntity, async_add_entities
184  )
185  )
186  entry.async_on_unload(entry.runtime_data.async_register_processor(processor))
187 
188 
190  PassiveBluetoothProcessorEntity[
191  PassiveBluetoothDataProcessor[float | int | None, Aranet4Advertisement],
192  ],
193  SensorEntity,
194 ):
195  """Representation of an Aranet sensor."""
196 
197  @property
198  def available(self) -> bool:
199  """Return whether the entity was available in the last update."""
200  # Our superclass covers "did the device disappear entirely", but if the
201  # device has smart home integrations disabled, it will send BLE beacons
202  # without data, which we turn into Nones here. Because None is never a
203  # valid value for any of the Aranet sensors, that means the entity is
204  # actually unavailable.
205  return (
206  super().available
207  and self.processor.entity_data.get(self.entity_key) is not None
208  )
209 
210  @property
211  def native_value(self) -> int | float | None:
212  """Return the native value."""
213  return self.processor.entity_data.get(self.entity_key)
PassiveBluetoothEntityKey _device_key_to_bluetooth_entity_key(BLEDevice device, str key)
Definition: sensor.py:132
None async_setup_entry(HomeAssistant hass, AranetConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:178
PassiveBluetoothDataUpdate[Any] sensor_update_to_bluetooth_data_update(Aranet4Advertisement adv)
Definition: sensor.py:152
DeviceInfo _sensor_device_info_to_hass(Aranet4Advertisement adv)
Definition: sensor.py:139