Files
HomeAssistantVS/custom_components/ha_washdata/button.py
T

300 lines
10 KiB
Python

# 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 <https://www.gnu.org/licenses/>.
"""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")