Files
HomeAssistantVS/custom_components/maintenance_supporter/button.py
T

133 lines
5.1 KiB
Python

"""Button platform for the Maintenance Supporter integration.
Per-task action buttons (complete / skip / reset) on each object's device. All
presses delegate to the same coordinator methods that the panel, services, and
mobile-notification actions use — one action layer, no duplicated logic (DRY).
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
COMPLETION_PROVENANCE_NOTES,
CONF_OBJECT,
CONF_TASKS,
DOMAIN,
GLOBAL_UNIQUE_ID,
task_unique_id,
)
from .coordinator import MaintenanceCoordinator
from .entity.entity_base import MaintenanceEntity
from .helpers.aggregate import object_slug as aggregate_object_slug
from .helpers.pause import is_task_inert
if TYPE_CHECKING:
from . import MaintenanceSupporterConfigEntry
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
# Per-task action buttons. The English suffix is also used in the entity_id /
# friendly name when the task has a custom entity_slug (mirrors binary_sensor's
# "{slug} overdue" convention).
_TASK_ACTIONS: tuple[tuple[str, str], ...] = (
("complete", "Complete"),
("skip", "Skip"),
("reset", "Reset"),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: MaintenanceSupporterConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up button entities for a maintenance object (or the global hub)."""
if entry.unique_id == GLOBAL_UNIQUE_ID:
# No global buttons — export belongs to the export_data service, not a
# button entity (a button can't trigger a browser download).
return
runtime_data = entry.runtime_data
if runtime_data is None or runtime_data.coordinator is None:
_LOGGER.error("No coordinator found for entry %s", entry.entry_id)
return
coordinator = runtime_data.coordinator
tasks = entry.data.get(CONF_TASKS, {})
entities = [
MaintenanceActionButton(coordinator, task_id, action, label) for task_id in tasks for action, label in _TASK_ACTIONS
]
async_add_entities(entities)
_LOGGER.debug("Added %d button entities for %s", len(entities), entry.title)
class MaintenanceActionButton(MaintenanceEntity, ButtonEntity):
"""A per-task action button (complete / skip / reset).
Pressing it calls the shared coordinator action method — the same path used
by the maintenance_supporter services and the mobile-notification actions.
"""
def __init__(
self,
coordinator: MaintenanceCoordinator,
task_id: str,
action: str,
label: str,
) -> None:
"""Initialize a per-task action button."""
super().__init__(coordinator, task_id)
self._action = action
obj_data = coordinator.entry.data.get(CONF_OBJECT, {})
task_data = coordinator.entry.data.get(CONF_TASKS, {}).get(task_id, {})
object_slug = aggregate_object_slug(obj_data)
self._attr_unique_id = task_unique_id(object_slug, task_id, action)
self._attr_translation_key = f"button_{action}"
# Custom entity_slug → stable, language-independent entity_id/name.
entity_slug = task_data.get("entity_slug")
if entity_slug:
self._attr_name = f"{entity_slug} {label}"
self._attr_translation_placeholders = {"task_name": task_data.get("name", "")}
@property
def available(self) -> bool:
"""Available only while the coordinator is up and the task is enabled."""
if not super().available:
return False
if not self._task_data:
return False
# An inert task — archived, disabled, or its object paused — has
# nothing to press: the coordinator refuses every action on it. The
# pause was missing here, so the buttons of a paused object looked
# usable and only raised on press (bug audit 2026-09-26).
task_cfg = self.coordinator.entry.data.get(CONF_TASKS, {}).get(self._task_id, {})
return not is_task_inert({**task_cfg, **self._task_data}, self.coordinator.entry.data.get(CONF_OBJECT, {}))
async def async_press(self) -> None:
"""Run the action via the shared coordinator method (single source)."""
if not self._task_data:
raise HomeAssistantError(translation_domain=DOMAIN, translation_key="task_no_longer_exists")
if self._action == "complete":
await self.coordinator.complete_maintenance(
self._task_id,
notes=COMPLETION_PROVENANCE_NOTES["button"],
unattended=True,
source="button",
completed_by=self._context.user_id if self._context else None,
)
elif self._action == "skip":
await self.coordinator.skip_maintenance(self._task_id, reason="Skipped from dashboard button")
elif self._action == "reset":
await self.coordinator.reset_maintenance(self._task_id)