# WashData - Home Assistant integration for appliance cycle monitoring via smart plugs. # Copyright (C) 2026 Lukas Bandura # SPDX-License-Identifier: AGPL-3.0-or-later # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Button platform for WashData.""" from __future__ import annotations import logging from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, STATE_RUNNING, STATE_STARTING, STATE_PAUSED, STATE_ENDING, SIGNAL_WASHER_UPDATE from .manager import WashDataManager _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the WashData button.""" manager: WashDataManager = hass.data[DOMAIN][entry.entry_id] async_add_entities([ WashDataTerminateButton(manager, entry), WashDataPauseCycleButton(manager, entry), WashDataResumeCycleButton(manager, entry), WashDataRecordStartButton(manager, entry), WashDataRecordStopButton(manager, entry), WashDataMarkUnloadedButton(manager, entry), ]) class WashDataTerminateButton(ButtonEntity): """Button to force terminate the current cycle.""" _attr_has_entity_name = True _attr_translation_key = "force_end_cycle" _attr_icon = "mdi:stop-circle-outline" def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the button.""" self._manager = manager self._entry = entry self._attr_unique_id = f"{entry.entry_id}_force_end" self._attr_device_info = { "identifiers": {(DOMAIN, entry.entry_id)}, "name": entry.title, "manufacturer": "WashData", } def press(self) -> None: """Handle the button press.""" raise NotImplementedError() async def async_press(self) -> None: """Handle the button press.""" await self._manager.async_terminate_cycle() class WashDataPauseCycleButton(ButtonEntity): """Button to pause the current cycle (user-triggered).""" _attr_has_entity_name = True _attr_translation_key = "pause_cycle" _attr_icon = "mdi:pause-circle-outline" def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the button.""" self._manager = manager self._entry = entry self._attr_unique_id = f"{entry.entry_id}_pause_cycle" self._attr_device_info = { "identifiers": {(DOMAIN, entry.entry_id)}, "name": entry.title, "manufacturer": "WashData", } async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_WASHER_UPDATE.format(self._entry.entry_id), self._update_callback, ) ) @callback def _update_callback(self) -> None: self.async_write_ha_state() @property def available(self) -> bool: """Only available when a cycle is active and not already user-paused.""" return ( self._manager.check_state() in (STATE_RUNNING, STATE_STARTING, STATE_PAUSED, STATE_ENDING) and not self._manager.is_user_paused ) async def async_press(self) -> None: """Handle the button press.""" await self._manager.async_pause_cycle() class WashDataResumeCycleButton(ButtonEntity): """Button to resume a user-paused cycle.""" _attr_has_entity_name = True _attr_translation_key = "resume_cycle" _attr_icon = "mdi:play-circle-outline" def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the button.""" self._manager = manager self._entry = entry self._attr_unique_id = f"{entry.entry_id}_resume_cycle" self._attr_device_info = { "identifiers": {(DOMAIN, entry.entry_id)}, "name": entry.title, "manufacturer": "WashData", } async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_WASHER_UPDATE.format(self._entry.entry_id), self._update_callback, ) ) @callback def _update_callback(self) -> None: self.async_write_ha_state() @property def available(self) -> bool: """Only available when the cycle is user-paused.""" return self._manager.is_user_paused async def async_press(self) -> None: """Handle the button press.""" await self._manager.async_resume_cycle() class WashDataRecordStartButton(ButtonEntity): """Button to start manually recording a clean cycle.""" _attr_has_entity_name = True _attr_translation_key = "record_start" _attr_icon = "mdi:record-circle-outline" def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the button.""" self._manager = manager self._entry = entry self._attr_unique_id = f"{entry.entry_id}_record_start" self._attr_device_info = { "identifiers": {(DOMAIN, entry.entry_id)}, "name": entry.title, "manufacturer": "WashData", } async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_WASHER_UPDATE.format(self._entry.entry_id), self._update_callback, ) ) @callback def _update_callback(self) -> None: self.async_write_ha_state() @property def available(self) -> bool: """Only available when not already recording and no active cycle is running.""" return ( not self._manager.recorder.is_recording and self._manager.detector.state == "off" ) async def async_press(self) -> None: """Handle the button press.""" await self._manager.async_start_recording() class WashDataRecordStopButton(ButtonEntity): """Button to stop manual recording.""" _attr_has_entity_name = True _attr_translation_key = "record_stop" _attr_icon = "mdi:stop-circle" def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the button.""" self._manager = manager self._entry = entry self._attr_unique_id = f"{entry.entry_id}_record_stop" self._attr_device_info = { "identifiers": {(DOMAIN, entry.entry_id)}, "name": entry.title, "manufacturer": "WashData", } async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_WASHER_UPDATE.format(self._entry.entry_id), self._update_callback, ) ) @callback def _update_callback(self) -> None: self.async_write_ha_state() @property def available(self) -> bool: """Only available while a recording is active.""" return self._manager.recorder.is_recording async def async_press(self) -> None: """Handle the button press.""" await self._manager.async_stop_recording() class WashDataMarkUnloadedButton(ButtonEntity): """Button to confirm the load has been taken out (#451). The automation-facing half of unload confirmation: a physical button, an NFC tag or any other automation can press this instead of the door sensor WashData would otherwise need. Available only while the machine is in the Clean state (cycle finished, load still inside) - there is nothing to confirm otherwise, and Home Assistant skips an unavailable entity in a service call, so an automation that presses it unconditionally is a harmless no-op. """ _attr_has_entity_name = True _attr_translation_key = "mark_unloaded" _attr_icon = "mdi:basket-check-outline" def __init__(self, manager: WashDataManager, entry: ConfigEntry) -> None: """Initialize the button.""" self._manager = manager self._entry = entry self._attr_unique_id = f"{entry.entry_id}_mark_unloaded" self._attr_device_info = { "identifiers": {(DOMAIN, entry.entry_id)}, "name": entry.title, "manufacturer": "WashData", } async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_WASHER_UPDATE.format(self._entry.entry_id), self._update_callback, ) ) @callback def _update_callback(self) -> None: self.async_write_ha_state() @property def available(self) -> bool: """Only available while the finished load is still waiting to be taken out.""" return self._manager.is_clean_state async def async_press(self) -> None: """Handle the button press.""" self._manager.mark_unloaded("mark unloaded button")