"""The Washer/Dryer Sensor for Whirlpool Appliances.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta import logging from whirlpool.washerdryer import MachineState, WasherDryer from homeassistant.components.sensor import ( RestoreSensor, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utcnow from . import WhirlpoolData from .const import DOMAIN TANK_FILL = { "0": "unknown", "1": "empty", "2": "25%", "3": "50%", "4": "100%", "5": "active", } MACHINE_STATE = { MachineState.Standby: "standby", MachineState.Setting: "setting", MachineState.DelayCountdownMode: "delay_countdown", MachineState.DelayPause: "delay_paused", MachineState.SmartDelay: "smart_delay", MachineState.SmartGridPause: "smart_grid_pause", MachineState.Pause: "pause", MachineState.RunningMainCycle: "running_maincycle", MachineState.RunningPostCycle: "running_postcycle", MachineState.Exceptions: "exception", MachineState.Complete: "complete", MachineState.PowerFailure: "power_failure", MachineState.ServiceDiagnostic: "service_diagnostic_mode", MachineState.FactoryDiagnostic: "factory_diagnostic_mode", MachineState.LifeTest: "life_test", MachineState.CustomerFocusMode: "customer_focus_mode", MachineState.DemoMode: "demo_mode", MachineState.HardStopOrError: "hard_stop_or_error", MachineState.SystemInit: "system_initialize", } CYCLE_FUNC = [ (WasherDryer.get_cycle_status_filling, "cycle_filling"), (WasherDryer.get_cycle_status_rinsing, "cycle_rinsing"), (WasherDryer.get_cycle_status_sensing, "cycle_sensing"), (WasherDryer.get_cycle_status_soaking, "cycle_soaking"), (WasherDryer.get_cycle_status_spinning, "cycle_spinning"), (WasherDryer.get_cycle_status_washing, "cycle_washing"), ] DOOR_OPEN = "door_open" ICON_D = "mdi:tumble-dryer" ICON_W = "mdi:washing-machine" _LOGGER = logging.getLogger(__name__) def washer_state(washer: WasherDryer) -> str | None: """Determine correct states for a washer.""" if washer.get_attribute("Cavity_OpStatusDoorOpen") == "1": return DOOR_OPEN machine_state = washer.get_machine_state() if machine_state == MachineState.RunningMainCycle: for func, cycle_name in CYCLE_FUNC: if func(washer): return cycle_name return MACHINE_STATE.get(machine_state, None) @dataclass class WhirlpoolSensorEntityDescriptionMixin: """Mixin for required keys.""" value_fn: Callable @dataclass class WhirlpoolSensorEntityDescription( SensorEntityDescription, WhirlpoolSensorEntityDescriptionMixin ): """Describes Whirlpool Washer sensor entity.""" SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( WhirlpoolSensorEntityDescription( key="state", name="State", translation_key="whirlpool_machine", device_class=SensorDeviceClass.ENUM, options=( list(MACHINE_STATE.values()) + [value for _, value in CYCLE_FUNC] + [DOOR_OPEN] ), value_fn=washer_state, ), WhirlpoolSensorEntityDescription( key="DispenseLevel", name="Detergent Level", translation_key="whirlpool_tank", device_class=SensorDeviceClass.ENUM, options=list(TANK_FILL.values()), value_fn=lambda WasherDryer: TANK_FILL[ WasherDryer.get_attribute("WashCavity_OpStatusBulkDispense1Level") ], ), ) SENSOR_TIMER: tuple[SensorEntityDescription] = ( SensorEntityDescription( key="timeremaining", name="End Time", device_class=SensorDeviceClass.TIMESTAMP, ), ) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Config flow entry for Whrilpool Laundry.""" entities: list = [] whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] for appliance in whirlpool_data.appliances_manager.washer_dryers: _wd = WasherDryer( whirlpool_data.backend_selector, whirlpool_data.auth, appliance["SAID"], async_get_clientsession(hass), ) await _wd.connect() entities.extend( [ WasherDryerClass( appliance["SAID"], appliance["NAME"], description, _wd, ) for description in SENSORS ] ) entities.extend( [ WasherDryerTimeClass( appliance["SAID"], appliance["NAME"], description, _wd, ) for description in SENSOR_TIMER ] ) async_add_entities(entities) class WasherDryerClass(SensorEntity): """A class for the whirlpool/maytag washer account.""" _attr_should_poll = False def __init__( self, said: str, name: str, description: WhirlpoolSensorEntityDescription, washdry: WasherDryer, ) -> None: """Initialize the washer sensor.""" self._wd: WasherDryer = washdry if name == "dryer": self._attr_icon = ICON_D else: self._attr_icon = ICON_W self.entity_description: WhirlpoolSensorEntityDescription = description self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, said)}, name=name.capitalize(), manufacturer="Whirlpool", ) self._attr_has_entity_name = True self._attr_unique_id = f"{said}-{description.key}" async def async_added_to_hass(self) -> None: """Connect washer/dryer to the cloud.""" self._wd.register_attr_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: """Close Whrilpool Appliance sockets before removing.""" self._wd.unregister_attr_callback(self.async_write_ha_state) @property def available(self) -> bool: """Return True if entity is available.""" return self._wd.get_online() @property def native_value(self) -> StateType | str: """Return native value of sensor.""" return self.entity_description.value_fn(self._wd) class WasherDryerTimeClass(RestoreSensor): """A timestamp class for the whirlpool/maytag washer account.""" _attr_should_poll = False def __init__( self, said: str, name: str, description: SensorEntityDescription, washdry: WasherDryer, ) -> None: """Initialize the washer sensor.""" self._wd: WasherDryer = washdry if name == "dryer": self._attr_icon = ICON_D else: self._attr_icon = ICON_W self.entity_description: SensorEntityDescription = description self._running: bool | None = None self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, said)}, name=name.capitalize(), manufacturer="Whirlpool", ) self._attr_has_entity_name = True self._attr_unique_id = f"{said}-{description.key}" async def async_added_to_hass(self) -> None: """Connect washer/dryer to the cloud.""" if restored_data := await self.async_get_last_sensor_data(): self._attr_native_value = restored_data.native_value await super().async_added_to_hass() self._wd.register_attr_callback(self.update_from_latest_data) async def async_will_remove_from_hass(self) -> None: """Close Whrilpool Appliance sockets before removing.""" await self._wd.disconnect() @property def available(self) -> bool: """Return True if entity is available.""" return self._wd.get_online() @callback def update_from_latest_data(self) -> None: """Calculate the time stamp for completion.""" machine_state = self._wd.get_machine_state() now = utcnow() if ( machine_state.value in {MachineState.Complete.value, MachineState.Standby.value} and self._running ): self._running = False self._attr_native_value = now self._async_write_ha_state() if machine_state is MachineState.RunningMainCycle: self._running = True new_timestamp = now + timedelta( seconds=int(self._wd.get_attribute("Cavity_TimeStatusEstTimeRemaining")) ) if isinstance(self._attr_native_value, datetime) and abs( new_timestamp - self._attr_native_value ) > timedelta(seconds=60): self._attr_native_value = new_timestamp self._async_write_ha_state()