Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Provides a sensor for Home Connect."""
2 
3 import contextlib
4 from dataclasses import dataclass
5 from datetime import datetime, timedelta
6 import logging
7 from typing import cast
8 
9 from homeconnect.api import HomeConnectError
10 
12  SensorDeviceClass,
13  SensorEntity,
14  SensorEntityDescription,
15  SensorStateClass,
16 )
17 from homeassistant.const import PERCENTAGE, UnitOfTime, UnitOfVolume
18 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.util import slugify
21 import homeassistant.util.dt as dt_util
22 
23 from . import HomeConnectConfigEntry
24 from .const import (
25  ATTR_VALUE,
26  BSH_DOOR_STATE,
27  BSH_OPERATION_STATE,
28  BSH_OPERATION_STATE_FINISHED,
29  BSH_OPERATION_STATE_PAUSE,
30  BSH_OPERATION_STATE_RUN,
31  COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
32  COFFEE_EVENT_DRIP_TRAY_FULL,
33  COFFEE_EVENT_WATER_TANK_EMPTY,
34  DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
35  DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
36  REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
37  REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
38  REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
39 )
40 from .entity import HomeConnectEntity
41 
42 _LOGGER = logging.getLogger(__name__)
43 
44 
45 EVENT_OPTIONS = ["confirmed", "off", "present"]
46 
47 
48 @dataclass(frozen=True, kw_only=True)
50  """Entity Description class for sensors."""
51 
52  default_value: str | None = None
53  appliance_types: tuple[str, ...] | None = None
54  sign: int = 1
55 
56 
57 BSH_PROGRAM_SENSORS = (
59  key="BSH.Common.Option.RemainingProgramTime",
60  device_class=SensorDeviceClass.TIMESTAMP,
61  sign=1,
62  translation_key="program_finish_time",
63  ),
65  key="BSH.Common.Option.Duration",
66  device_class=SensorDeviceClass.DURATION,
67  native_unit_of_measurement=UnitOfTime.SECONDS,
68  sign=1,
69  ),
71  key="BSH.Common.Option.ProgramProgress",
72  native_unit_of_measurement=PERCENTAGE,
73  sign=1,
74  translation_key="program_progress",
75  ),
76 )
77 
78 SENSORS = (
80  key=BSH_OPERATION_STATE,
81  device_class=SensorDeviceClass.ENUM,
82  options=[
83  "inactive",
84  "ready",
85  "delayedstart",
86  "run",
87  "pause",
88  "actionrequired",
89  "finished",
90  "error",
91  "aborting",
92  ],
93  translation_key="operation_state",
94  ),
96  key=BSH_DOOR_STATE,
97  device_class=SensorDeviceClass.ENUM,
98  options=[
99  "closed",
100  "locked",
101  "open",
102  ],
103  translation_key="door",
104  ),
106  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffee",
107  state_class=SensorStateClass.TOTAL_INCREASING,
108  translation_key="coffee_counter",
109  ),
111  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterPowderCoffee",
112  state_class=SensorStateClass.TOTAL_INCREASING,
113  translation_key="powder_coffee_counter",
114  ),
116  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWater",
117  native_unit_of_measurement=UnitOfVolume.MILLILITERS,
118  device_class=SensorDeviceClass.VOLUME,
119  state_class=SensorStateClass.TOTAL_INCREASING,
120  translation_key="hot_water_counter",
121  ),
123  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWaterCups",
124  state_class=SensorStateClass.TOTAL_INCREASING,
125  translation_key="hot_water_cups_counter",
126  ),
128  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotMilk",
129  state_class=SensorStateClass.TOTAL_INCREASING,
130  translation_key="hot_milk_counter",
131  ),
133  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterFrothyMilk",
134  state_class=SensorStateClass.TOTAL_INCREASING,
135  translation_key="frothy_milk_counter",
136  ),
138  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterMilk",
139  state_class=SensorStateClass.TOTAL_INCREASING,
140  translation_key="milk_counter",
141  ),
143  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffeeAndMilk",
144  state_class=SensorStateClass.TOTAL_INCREASING,
145  translation_key="coffee_and_milk_counter",
146  ),
148  key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterRistrettoEspresso",
149  state_class=SensorStateClass.TOTAL_INCREASING,
150  translation_key="ristretto_espresso_counter",
151  ),
153  key="BSH.Common.Status.BatteryLevel",
154  device_class=SensorDeviceClass.BATTERY,
155  translation_key="battery_level",
156  ),
158  key="BSH.Common.Status.Video.CameraState",
159  device_class=SensorDeviceClass.ENUM,
160  options=[
161  "disabled",
162  "sleeping",
163  "ready",
164  "streaminglocal",
165  "streamingcloud",
166  "streaminglocalancloud",
167  "error",
168  ],
169  translation_key="camera_state",
170  ),
172  key="ConsumerProducts.CleaningRobot.Status.LastSelectedMap",
173  device_class=SensorDeviceClass.ENUM,
174  options=[
175  "tempmap",
176  "map1",
177  "map2",
178  "map3",
179  ],
180  translation_key="last_selected_map",
181  ),
182 )
183 
184 EVENT_SENSORS = (
186  key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
187  device_class=SensorDeviceClass.ENUM,
188  options=EVENT_OPTIONS,
189  default_value="off",
190  translation_key="freezer_door_alarm",
191  appliance_types=("FridgeFreezer", "Freezer"),
192  ),
194  key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
195  device_class=SensorDeviceClass.ENUM,
196  options=EVENT_OPTIONS,
197  default_value="off",
198  translation_key="refrigerator_door_alarm",
199  appliance_types=("FridgeFreezer", "Refrigerator"),
200  ),
202  key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
203  device_class=SensorDeviceClass.ENUM,
204  options=EVENT_OPTIONS,
205  default_value="off",
206  translation_key="freezer_temperature_alarm",
207  appliance_types=("FridgeFreezer", "Freezer"),
208  ),
210  key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
211  device_class=SensorDeviceClass.ENUM,
212  options=EVENT_OPTIONS,
213  default_value="off",
214  translation_key="bean_container_empty",
215  appliance_types=("CoffeeMaker",),
216  ),
218  key=COFFEE_EVENT_WATER_TANK_EMPTY,
219  device_class=SensorDeviceClass.ENUM,
220  options=EVENT_OPTIONS,
221  default_value="off",
222  translation_key="water_tank_empty",
223  appliance_types=("CoffeeMaker",),
224  ),
226  key=COFFEE_EVENT_DRIP_TRAY_FULL,
227  device_class=SensorDeviceClass.ENUM,
228  options=EVENT_OPTIONS,
229  default_value="off",
230  translation_key="drip_tray_full",
231  appliance_types=("CoffeeMaker",),
232  ),
234  key=DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
235  device_class=SensorDeviceClass.ENUM,
236  options=EVENT_OPTIONS,
237  default_value="off",
238  translation_key="salt_nearly_empty",
239  appliance_types=("Dishwasher",),
240  ),
242  key=DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
243  device_class=SensorDeviceClass.ENUM,
244  options=EVENT_OPTIONS,
245  default_value="off",
246  translation_key="rinse_aid_nearly_empty",
247  appliance_types=("Dishwasher",),
248  ),
249 )
250 
251 
253  hass: HomeAssistant,
254  entry: HomeConnectConfigEntry,
255  async_add_entities: AddEntitiesCallback,
256 ) -> None:
257  """Set up the Home Connect sensor."""
258 
259  def get_entities() -> list[SensorEntity]:
260  """Get a list of entities."""
261  entities: list[SensorEntity] = []
262  for device in entry.runtime_data.devices:
263  entities.extend(
265  device,
266  description,
267  )
268  for description in EVENT_SENSORS
269  if description.appliance_types
270  and device.appliance.type in description.appliance_types
271  )
272  with contextlib.suppress(HomeConnectError):
273  if device.appliance.get_programs_available():
274  entities.extend(
275  HomeConnectSensor(device, desc) for desc in BSH_PROGRAM_SENSORS
276  )
277  entities.extend(
278  HomeConnectSensor(device, description)
279  for description in SENSORS
280  if description.key in device.appliance.status
281  )
282  return entities
283 
284  async_add_entities(await hass.async_add_executor_job(get_entities), True)
285 
286 
288  """Sensor class for Home Connect."""
289 
290  entity_description: HomeConnectSensorEntityDescription
291 
292  @property
293  def available(self) -> bool:
294  """Return true if the sensor is available."""
295  return self._attr_native_value_attr_native_value is not None
296 
297  async def async_update(self) -> None:
298  """Update the sensor's status."""
299  appliance_status = self.devicedevice.appliance.status
300  if (
301  self.bsh_keybsh_key not in appliance_status
302  or ATTR_VALUE not in appliance_status[self.bsh_keybsh_key]
303  ):
304  self._attr_native_value_attr_native_value = self.entity_descriptionentity_description.default_value
305  _LOGGER.debug("Updated, new state: %s", self._attr_native_value_attr_native_value)
306  return
307  status = appliance_status[self.bsh_keybsh_key]
308  match self.device_classdevice_classdevice_class:
309  case SensorDeviceClass.TIMESTAMP:
310  if ATTR_VALUE not in status:
311  self._attr_native_value_attr_native_value = None
312  elif (
313  self._attr_native_value_attr_native_value is not None
314  and self.entity_descriptionentity_description.sign == 1
315  and isinstance(self._attr_native_value_attr_native_value, datetime)
316  and self._attr_native_value_attr_native_value < dt_util.utcnow()
317  ):
318  # if the date is supposed to be in the future but we're
319  # already past it, set state to None.
320  self._attr_native_value_attr_native_value = None
321  elif (
322  BSH_OPERATION_STATE
323  in (appliance_status := self.devicedevice.appliance.status)
324  and ATTR_VALUE in appliance_status[BSH_OPERATION_STATE]
325  and appliance_status[BSH_OPERATION_STATE][ATTR_VALUE]
326  in [
327  BSH_OPERATION_STATE_RUN,
328  BSH_OPERATION_STATE_PAUSE,
329  BSH_OPERATION_STATE_FINISHED,
330  ]
331  ):
332  seconds = self.entity_descriptionentity_description.sign * float(status[ATTR_VALUE])
333  self._attr_native_value_attr_native_value = dt_util.utcnow() + timedelta(
334  seconds=seconds
335  )
336  else:
337  self._attr_native_value_attr_native_value = None
338  case SensorDeviceClass.ENUM:
339  # Value comes back as an enum, we only really care about the
340  # last part, so split it off
341  # https://developer.home-connect.com/docs/status/operation_state
342  self._attr_native_value_attr_native_value = slugify(
343  cast(str, status.get(ATTR_VALUE)).split(".")[-1]
344  )
345  case _:
346  self._attr_native_value_attr_native_value = status.get(ATTR_VALUE)
347  _LOGGER.debug("Updated, new state: %s", self._attr_native_value_attr_native_value)
SensorDeviceClass|None device_class(self)
Definition: __init__.py:313
None async_setup_entry(HomeAssistant hass, HomeConnectConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:256
list[OneWireSensor] get_entities(OneWireHub onewire_hub, MappingProxyType[str, Any] options)
Definition: sensor.py:364