Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """YoLink Sensor."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 
8 from yolink.const import (
9  ATTR_DEVICE_CO_SMOKE_SENSOR,
10  ATTR_DEVICE_DIMMER,
11  ATTR_DEVICE_DOOR_SENSOR,
12  ATTR_DEVICE_FINGER,
13  ATTR_DEVICE_LEAK_SENSOR,
14  ATTR_DEVICE_LOCK,
15  ATTR_DEVICE_MANIPULATOR,
16  ATTR_DEVICE_MOTION_SENSOR,
17  ATTR_DEVICE_MULTI_OUTLET,
18  ATTR_DEVICE_OUTLET,
19  ATTR_DEVICE_POWER_FAILURE_ALARM,
20  ATTR_DEVICE_SIREN,
21  ATTR_DEVICE_SMART_REMOTER,
22  ATTR_DEVICE_SWITCH,
23  ATTR_DEVICE_TH_SENSOR,
24  ATTR_DEVICE_THERMOSTAT,
25  ATTR_DEVICE_VIBRATION_SENSOR,
26  ATTR_DEVICE_WATER_DEPTH_SENSOR,
27  ATTR_DEVICE_WATER_METER_CONTROLLER,
28  ATTR_GARAGE_DOOR_CONTROLLER,
29 )
30 from yolink.device import YoLinkDevice
31 
33  SensorDeviceClass,
34  SensorEntity,
35  SensorEntityDescription,
36  SensorStateClass,
37 )
38 from homeassistant.config_entries import ConfigEntry
39 from homeassistant.const import (
40  PERCENTAGE,
41  SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
42  EntityCategory,
43  UnitOfEnergy,
44  UnitOfLength,
45  UnitOfPower,
46  UnitOfTemperature,
47  UnitOfVolume,
48 )
49 from homeassistant.core import HomeAssistant, callback
50 from homeassistant.helpers.entity_platform import AddEntitiesCallback
51 from homeassistant.util import percentage
52 
53 from .const import (
54  DEV_MODEL_PLUG_YS6602_EC,
55  DEV_MODEL_PLUG_YS6602_UC,
56  DEV_MODEL_PLUG_YS6803_EC,
57  DEV_MODEL_PLUG_YS6803_UC,
58  DEV_MODEL_TH_SENSOR_YS8004_EC,
59  DEV_MODEL_TH_SENSOR_YS8004_UC,
60  DEV_MODEL_TH_SENSOR_YS8008_EC,
61  DEV_MODEL_TH_SENSOR_YS8008_UC,
62  DEV_MODEL_TH_SENSOR_YS8014_EC,
63  DEV_MODEL_TH_SENSOR_YS8014_UC,
64  DEV_MODEL_TH_SENSOR_YS8017_EC,
65  DEV_MODEL_TH_SENSOR_YS8017_UC,
66  DOMAIN,
67 )
68 from .coordinator import YoLinkCoordinator
69 from .entity import YoLinkEntity
70 
71 
72 @dataclass(frozen=True, kw_only=True)
74  """YoLink SensorEntityDescription."""
75 
76  exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True
77  should_update_entity: Callable = lambda state: True
78  value: Callable = lambda state: state
79 
80 
81 SENSOR_DEVICE_TYPE = [
82  ATTR_DEVICE_DIMMER,
83  ATTR_DEVICE_DOOR_SENSOR,
84  ATTR_DEVICE_FINGER,
85  ATTR_DEVICE_LEAK_SENSOR,
86  ATTR_DEVICE_MOTION_SENSOR,
87  ATTR_DEVICE_MULTI_OUTLET,
88  ATTR_DEVICE_SMART_REMOTER,
89  ATTR_DEVICE_OUTLET,
90  ATTR_DEVICE_POWER_FAILURE_ALARM,
91  ATTR_DEVICE_SIREN,
92  ATTR_DEVICE_SWITCH,
93  ATTR_DEVICE_TH_SENSOR,
94  ATTR_DEVICE_THERMOSTAT,
95  ATTR_DEVICE_VIBRATION_SENSOR,
96  ATTR_DEVICE_WATER_DEPTH_SENSOR,
97  ATTR_DEVICE_WATER_METER_CONTROLLER,
98  ATTR_DEVICE_LOCK,
99  ATTR_DEVICE_MANIPULATOR,
100  ATTR_DEVICE_CO_SMOKE_SENSOR,
101  ATTR_GARAGE_DOOR_CONTROLLER,
102 ]
103 
104 BATTERY_POWER_SENSOR = [
105  ATTR_DEVICE_DOOR_SENSOR,
106  ATTR_DEVICE_FINGER,
107  ATTR_DEVICE_LEAK_SENSOR,
108  ATTR_DEVICE_MOTION_SENSOR,
109  ATTR_DEVICE_POWER_FAILURE_ALARM,
110  ATTR_DEVICE_SIREN,
111  ATTR_DEVICE_SMART_REMOTER,
112  ATTR_DEVICE_TH_SENSOR,
113  ATTR_DEVICE_VIBRATION_SENSOR,
114  ATTR_DEVICE_LOCK,
115  ATTR_DEVICE_MANIPULATOR,
116  ATTR_DEVICE_CO_SMOKE_SENSOR,
117  ATTR_DEVICE_WATER_DEPTH_SENSOR,
118  ATTR_DEVICE_WATER_METER_CONTROLLER,
119 ]
120 
121 MCU_DEV_TEMPERATURE_SENSOR = [
122  ATTR_DEVICE_LEAK_SENSOR,
123  ATTR_DEVICE_MOTION_SENSOR,
124  ATTR_DEVICE_CO_SMOKE_SENSOR,
125 ]
126 
127 NONE_HUMIDITY_SENSOR_MODELS = [
128  DEV_MODEL_TH_SENSOR_YS8004_EC,
129  DEV_MODEL_TH_SENSOR_YS8004_UC,
130  DEV_MODEL_TH_SENSOR_YS8008_EC,
131  DEV_MODEL_TH_SENSOR_YS8008_UC,
132  DEV_MODEL_TH_SENSOR_YS8014_EC,
133  DEV_MODEL_TH_SENSOR_YS8014_UC,
134  DEV_MODEL_TH_SENSOR_YS8017_UC,
135  DEV_MODEL_TH_SENSOR_YS8017_EC,
136 ]
137 
138 POWER_SUPPORT_MODELS = [
139  DEV_MODEL_PLUG_YS6602_UC,
140  DEV_MODEL_PLUG_YS6602_EC,
141  DEV_MODEL_PLUG_YS6803_UC,
142  DEV_MODEL_PLUG_YS6803_EC,
143 ]
144 
145 
146 def cvt_battery(val: int | None) -> int | None:
147  """Convert battery to percentage."""
148  if val is None:
149  return None
150  if val > 0:
151  return percentage.ordered_list_item_to_percentage([1, 2, 3, 4], val)
152  return 0
153 
154 
155 def cvt_volume(val: int | None) -> str | None:
156  """Convert volume to string."""
157  if val is None:
158  return None
159  volume_level = {1: "low", 2: "medium", 3: "high"}
160  return volume_level.get(val)
161 
162 
163 SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
165  key="battery",
166  device_class=SensorDeviceClass.BATTERY,
167  native_unit_of_measurement=PERCENTAGE,
168  state_class=SensorStateClass.MEASUREMENT,
169  value=cvt_battery,
170  exists_fn=lambda device: device.device_type in BATTERY_POWER_SENSOR,
171  should_update_entity=lambda value: value is not None,
172  ),
174  key="humidity",
175  device_class=SensorDeviceClass.HUMIDITY,
176  native_unit_of_measurement=PERCENTAGE,
177  state_class=SensorStateClass.MEASUREMENT,
178  exists_fn=lambda device: (
179  device.device_type in [ATTR_DEVICE_TH_SENSOR]
180  and device.device_model_name not in NONE_HUMIDITY_SENSOR_MODELS
181  ),
182  ),
184  key="temperature",
185  device_class=SensorDeviceClass.TEMPERATURE,
186  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
187  state_class=SensorStateClass.MEASUREMENT,
188  exists_fn=lambda device: device.device_type in [ATTR_DEVICE_TH_SENSOR],
189  ),
190  # mcu temperature
192  key="devTemperature",
193  device_class=SensorDeviceClass.TEMPERATURE,
194  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
195  state_class=SensorStateClass.MEASUREMENT,
196  exists_fn=lambda device: device.device_type in MCU_DEV_TEMPERATURE_SENSOR,
197  should_update_entity=lambda value: value is not None,
198  ),
200  key="loraInfo",
201  device_class=SensorDeviceClass.SIGNAL_STRENGTH,
202  native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
203  value=lambda value: value["signal"] if value is not None else None,
204  state_class=SensorStateClass.MEASUREMENT,
205  entity_category=EntityCategory.DIAGNOSTIC,
206  entity_registry_enabled_default=False,
207  should_update_entity=lambda value: value is not None,
208  ),
210  key="state",
211  translation_key="power_failure_alarm",
212  device_class=SensorDeviceClass.ENUM,
213  options=["normal", "alert", "off"],
214  exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
215  ),
217  key="mute",
218  translation_key="power_failure_alarm_mute",
219  device_class=SensorDeviceClass.ENUM,
220  options=["muted", "unmuted"],
221  exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
222  value=lambda value: "muted" if value is True else "unmuted",
223  ),
225  key="sound",
226  translation_key="power_failure_alarm_volume",
227  device_class=SensorDeviceClass.ENUM,
228  options=["low", "medium", "high"],
229  exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
230  value=cvt_volume,
231  ),
233  key="beep",
234  translation_key="power_failure_alarm_beep",
235  device_class=SensorDeviceClass.ENUM,
236  options=["enabled", "disabled"],
237  exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM,
238  value=lambda value: "enabled" if value is True else "disabled",
239  ),
241  key="waterDepth",
242  device_class=SensorDeviceClass.DISTANCE,
243  native_unit_of_measurement=UnitOfLength.METERS,
244  exists_fn=lambda device: device.device_type in ATTR_DEVICE_WATER_DEPTH_SENSOR,
245  ),
247  key="meter_reading",
248  translation_key="water_meter_reading",
249  device_class=SensorDeviceClass.WATER,
250  native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
251  state_class=SensorStateClass.TOTAL_INCREASING,
252  should_update_entity=lambda value: value is not None,
253  exists_fn=lambda device: (
254  device.device_type in ATTR_DEVICE_WATER_METER_CONTROLLER
255  ),
256  ),
258  key="power",
259  translation_key="current_power",
260  device_class=SensorDeviceClass.POWER,
261  native_unit_of_measurement=UnitOfPower.WATT,
262  state_class=SensorStateClass.MEASUREMENT,
263  should_update_entity=lambda value: value is not None,
264  exists_fn=lambda device: device.device_model_name in POWER_SUPPORT_MODELS,
265  value=lambda value: value / 10 if value is not None else None,
266  ),
268  key="watt",
269  translation_key="power_consumption",
270  device_class=SensorDeviceClass.ENERGY,
271  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
272  state_class=SensorStateClass.TOTAL,
273  should_update_entity=lambda value: value is not None,
274  exists_fn=lambda device: device.device_model_name in POWER_SUPPORT_MODELS,
275  value=lambda value: value / 100 if value is not None else None,
276  ),
277 )
278 
279 
281  hass: HomeAssistant,
282  config_entry: ConfigEntry,
283  async_add_entities: AddEntitiesCallback,
284 ) -> None:
285  """Set up YoLink Sensor from a config entry."""
286  device_coordinators = hass.data[DOMAIN][config_entry.entry_id].device_coordinators
287  sensor_device_coordinators = [
288  device_coordinator
289  for device_coordinator in device_coordinators.values()
290  if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE
291  ]
294  config_entry,
295  sensor_device_coordinator,
296  description,
297  )
298  for sensor_device_coordinator in sensor_device_coordinators
299  for description in SENSOR_TYPES
300  if description.exists_fn(sensor_device_coordinator.device)
301  )
302 
303 
305  """YoLink Sensor Entity."""
306 
307  entity_description: YoLinkSensorEntityDescription
308 
309  def __init__(
310  self,
311  config_entry: ConfigEntry,
312  coordinator: YoLinkCoordinator,
313  description: YoLinkSensorEntityDescription,
314  ) -> None:
315  """Init YoLink Sensor."""
316  super().__init__(config_entry, coordinator)
317  self.entity_descriptionentity_description = description
318  self._attr_unique_id_attr_unique_id = (
319  f"{coordinator.device.device_id} {self.entity_description.key}"
320  )
321 
322  @callback
323  def update_entity_state(self, state: dict) -> None:
324  """Update HA Entity State."""
325  if (
326  attr_val := self.entity_descriptionentity_description.value(
327  state.get(self.entity_descriptionentity_description.key)
328  )
329  ) is None and self.entity_descriptionentity_description.should_update_entity(attr_val) is False:
330  return
331  self._attr_native_value_attr_native_value = attr_val
332  self.async_write_ha_stateasync_write_ha_state()
333 
334  @property
335  def available(self) -> bool:
336  """Return true is device is available."""
337  return super().available and self.coordinator.dev_online