Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """The Washer/Dryer Sensor for Whirlpool Appliances."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import datetime, timedelta
8 import logging
9 
10 from whirlpool.washerdryer import MachineState, WasherDryer
11 
13  RestoreSensor,
14  SensorDeviceClass,
15  SensorEntity,
16  SensorEntityDescription,
17 )
18 from homeassistant.config_entries import ConfigEntry
19 from homeassistant.core import HomeAssistant, callback
20 from homeassistant.helpers.aiohttp_client import async_get_clientsession
21 from homeassistant.helpers.device_registry import DeviceInfo
22 from homeassistant.helpers.entity_platform import AddEntitiesCallback
23 from homeassistant.helpers.typing import StateType
24 from homeassistant.util.dt import utcnow
25 
26 from . import WhirlpoolData
27 from .const import DOMAIN
28 
29 TANK_FILL = {
30  "0": "unknown",
31  "1": "empty",
32  "2": "25",
33  "3": "50",
34  "4": "100",
35  "5": "active",
36 }
37 
38 MACHINE_STATE = {
39  MachineState.Standby: "standby",
40  MachineState.Setting: "setting",
41  MachineState.DelayCountdownMode: "delay_countdown",
42  MachineState.DelayPause: "delay_paused",
43  MachineState.SmartDelay: "smart_delay",
44  MachineState.SmartGridPause: "smart_grid_pause",
45  MachineState.Pause: "pause",
46  MachineState.RunningMainCycle: "running_maincycle",
47  MachineState.RunningPostCycle: "running_postcycle",
48  MachineState.Exceptions: "exception",
49  MachineState.Complete: "complete",
50  MachineState.PowerFailure: "power_failure",
51  MachineState.ServiceDiagnostic: "service_diagnostic_mode",
52  MachineState.FactoryDiagnostic: "factory_diagnostic_mode",
53  MachineState.LifeTest: "life_test",
54  MachineState.CustomerFocusMode: "customer_focus_mode",
55  MachineState.DemoMode: "demo_mode",
56  MachineState.HardStopOrError: "hard_stop_or_error",
57  MachineState.SystemInit: "system_initialize",
58 }
59 
60 CYCLE_FUNC = [
61  (WasherDryer.get_cycle_status_filling, "cycle_filling"),
62  (WasherDryer.get_cycle_status_rinsing, "cycle_rinsing"),
63  (WasherDryer.get_cycle_status_sensing, "cycle_sensing"),
64  (WasherDryer.get_cycle_status_soaking, "cycle_soaking"),
65  (WasherDryer.get_cycle_status_spinning, "cycle_spinning"),
66  (WasherDryer.get_cycle_status_washing, "cycle_washing"),
67 ]
68 
69 DOOR_OPEN = "door_open"
70 ICON_D = "mdi:tumble-dryer"
71 ICON_W = "mdi:washing-machine"
72 
73 _LOGGER = logging.getLogger(__name__)
74 SCAN_INTERVAL = timedelta(minutes=5)
75 
76 
77 def washer_state(washer: WasherDryer) -> str | None:
78  """Determine correct states for a washer."""
79 
80  if washer.get_attribute("Cavity_OpStatusDoorOpen") == "1":
81  return DOOR_OPEN
82 
83  machine_state = washer.get_machine_state()
84 
85  if machine_state == MachineState.RunningMainCycle:
86  for func, cycle_name in CYCLE_FUNC:
87  if func(washer):
88  return cycle_name
89 
90  return MACHINE_STATE.get(machine_state)
91 
92 
93 @dataclass(frozen=True, kw_only=True)
95  """Describes Whirlpool Washer sensor entity."""
96 
97  value_fn: Callable
98 
99 
100 SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = (
102  key="state",
103  translation_key="whirlpool_machine",
104  device_class=SensorDeviceClass.ENUM,
105  options=(
106  list(MACHINE_STATE.values())
107  + [value for _, value in CYCLE_FUNC]
108  + [DOOR_OPEN]
109  ),
110  value_fn=washer_state,
111  ),
113  key="DispenseLevel",
114  translation_key="whirlpool_tank",
115  entity_registry_enabled_default=False,
116  device_class=SensorDeviceClass.ENUM,
117  options=list(TANK_FILL.values()),
118  value_fn=lambda WasherDryer: TANK_FILL.get(
119  WasherDryer.get_attribute("WashCavity_OpStatusBulkDispense1Level")
120  ),
121  ),
122 )
123 
124 SENSOR_TIMER: tuple[SensorEntityDescription] = (
126  key="timeremaining",
127  translation_key="end_time",
128  device_class=SensorDeviceClass.TIMESTAMP,
129  ),
130 )
131 
132 
134  hass: HomeAssistant,
135  config_entry: ConfigEntry,
136  async_add_entities: AddEntitiesCallback,
137 ) -> None:
138  """Config flow entry for Whrilpool Laundry."""
139  entities: list = []
140  whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id]
141  for appliance in whirlpool_data.appliances_manager.washer_dryers:
142  _wd = WasherDryer(
143  whirlpool_data.backend_selector,
144  whirlpool_data.auth,
145  appliance["SAID"],
147  )
148  await _wd.connect()
149 
150  entities.extend(
151  [
153  appliance["SAID"],
154  appliance["NAME"],
155  description,
156  _wd,
157  )
158  for description in SENSORS
159  ]
160  )
161  entities.extend(
162  [
164  appliance["SAID"],
165  appliance["NAME"],
166  description,
167  _wd,
168  )
169  for description in SENSOR_TIMER
170  ]
171  )
172  async_add_entities(entities)
173 
174 
176  """A class for the whirlpool/maytag washer account."""
177 
178  _attr_should_poll = False
179  _attr_has_entity_name = True
180 
181  def __init__(
182  self,
183  said: str,
184  name: str,
185  description: WhirlpoolSensorEntityDescription,
186  washdry: WasherDryer,
187  ) -> None:
188  """Initialize the washer sensor."""
189  self._wd: WasherDryer = washdry
190 
191  if name == "dryer":
192  self._attr_icon_attr_icon = ICON_D
193  else:
194  self._attr_icon_attr_icon = ICON_W
195 
196  self.entity_description: WhirlpoolSensorEntityDescription = description
197  self._attr_device_info_attr_device_info = DeviceInfo(
198  identifiers={(DOMAIN, said)},
199  name=name.capitalize(),
200  manufacturer="Whirlpool",
201  )
202  self._attr_unique_id_attr_unique_id = f"{said}-{description.key}"
203 
204  async def async_added_to_hass(self) -> None:
205  """Connect washer/dryer to the cloud."""
206  self._wd.register_attr_callback(self.async_write_ha_stateasync_write_ha_state)
207 
208  async def async_will_remove_from_hass(self) -> None:
209  """Close Whirlpool Appliance sockets before removing."""
210  self._wd.unregister_attr_callback(self.async_write_ha_stateasync_write_ha_state)
211 
212  @property
213  def available(self) -> bool:
214  """Return True if entity is available."""
215  return self._wd.get_online()
216 
217  @property
218  def native_value(self) -> StateType | str:
219  """Return native value of sensor."""
220  return self.entity_description.value_fn(self._wd)
221 
222 
224  """A timestamp class for the whirlpool/maytag washer account."""
225 
226  _attr_should_poll = True
227  _attr_has_entity_name = True
228 
229  def __init__(
230  self,
231  said: str,
232  name: str,
233  description: SensorEntityDescription,
234  washdry: WasherDryer,
235  ) -> None:
236  """Initialize the washer sensor."""
237  self._wd: WasherDryer = washdry
238 
239  if name == "dryer":
240  self._attr_icon_attr_icon = ICON_D
241  else:
242  self._attr_icon_attr_icon = ICON_W
243 
244  self.entity_description: SensorEntityDescription = description
245  self._running_running: bool | None = None
246  self._attr_device_info_attr_device_info = DeviceInfo(
247  identifiers={(DOMAIN, said)},
248  name=name.capitalize(),
249  manufacturer="Whirlpool",
250  )
251  self._attr_unique_id_attr_unique_id = f"{said}-{description.key}"
252 
253  async def async_added_to_hass(self) -> None:
254  """Connect washer/dryer to the cloud."""
255  if restored_data := await self.async_get_last_sensor_dataasync_get_last_sensor_data():
256  self._attr_native_value_attr_native_value = restored_data.native_value
257  await super().async_added_to_hass()
258  self._wd.register_attr_callback(self.update_from_latest_dataupdate_from_latest_data)
259 
260  async def async_will_remove_from_hass(self) -> None:
261  """Close Whrilpool Appliance sockets before removing."""
262  self._wd.unregister_attr_callback(self.update_from_latest_dataupdate_from_latest_data)
263  await self._wd.disconnect()
264 
265  @property
266  def available(self) -> bool:
267  """Return True if entity is available."""
268  return self._wd.get_online()
269 
270  async def async_update(self) -> None:
271  """Update status of Whirlpool."""
272  await self._wd.fetch_data()
273 
274  @callback
275  def update_from_latest_data(self) -> None:
276  """Calculate the time stamp for completion."""
277  machine_state = self._wd.get_machine_state()
278  now = utcnow()
279  if (
280  machine_state.value
281  in {MachineState.Complete.value, MachineState.Standby.value}
282  and self._running_running
283  ):
284  self._running_running = False
285  self._attr_native_value_attr_native_value = now
286  self._async_write_ha_state_async_write_ha_state()
287 
288  if machine_state is MachineState.RunningMainCycle:
289  self._running_running = True
290 
291  new_timestamp = now + timedelta(
292  seconds=int(self._wd.get_attribute("Cavity_TimeStatusEstTimeRemaining"))
293  )
294 
295  if (
296  self._attr_native_value_attr_native_value is None
297  or isinstance(self._attr_native_value_attr_native_value, datetime)
298  and abs(new_timestamp - self._attr_native_value_attr_native_value) > timedelta(seconds=60)
299  ):
300  self._attr_native_value_attr_native_value = new_timestamp
301  self._async_write_ha_state_async_write_ha_state()
SensorExtraStoredData|None async_get_last_sensor_data(self)
Definition: __init__.py:934
None __init__(self, str said, str name, WhirlpoolSensorEntityDescription description, WasherDryer washdry)
Definition: sensor.py:187
None __init__(self, str said, str name, SensorEntityDescription description, WasherDryer washdry)
Definition: sensor.py:235
MetOfficeData fetch_data(datapoint.Manager connection, Site site, str mode)
Definition: helpers.py:32
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:137
str|None washer_state(WasherDryer washer)
Definition: sensor.py:77
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)