New apps Added
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"""To-do platform for the Maintenance Supporter integration.
|
||||
|
||||
A single global ``todo`` list entity aggregates every active maintenance task
|
||||
across all objects, so maintenance shows up in Home Assistant's native **To-do**
|
||||
card and is reachable through **Assist/voice** ("what maintenance is due?",
|
||||
"mark the filter change done").
|
||||
|
||||
Each task becomes one to-do item whose status mirrors its due state: a task
|
||||
that is due-soon / overdue / triggered is ``needs_action`` (actionable), an
|
||||
up-to-date task is ``completed``. Checking a ``needs_action`` item off routes to
|
||||
the same ``complete_maintenance`` coordinator method the panel, buttons, and
|
||||
notification actions use — one action layer, no duplicated logic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.todo import TodoItem, TodoListEntity
|
||||
from homeassistant.components.todo.const import (
|
||||
TodoItemStatus,
|
||||
TodoListEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_OBJECT,
|
||||
CONF_TASK_ENABLED,
|
||||
CONF_TASKS,
|
||||
DOMAIN,
|
||||
GLOBAL_UNIQUE_ID,
|
||||
MaintenanceStatus,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MaintenanceSupporterConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
# Statuses that make a task "actionable" in the to-do list (needs_action).
|
||||
_ACTION_STATUSES = frozenset(
|
||||
{
|
||||
MaintenanceStatus.DUE_SOON,
|
||||
MaintenanceStatus.OVERDUE,
|
||||
MaintenanceStatus.TRIGGERED,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MaintenanceSupporterConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the single global maintenance to-do list.
|
||||
|
||||
Mirrors the calendar platform: the list lives on the global entry and
|
||||
aggregates every object's coordinator. Object entries register their
|
||||
coordinator so the list repaints when a task's status changes.
|
||||
"""
|
||||
if entry.unique_id != GLOBAL_UNIQUE_ID:
|
||||
runtime_data = entry.runtime_data
|
||||
if runtime_data and runtime_data.coordinator:
|
||||
todo = hass.data.get(DOMAIN, {}).get("_todo_entity")
|
||||
if todo:
|
||||
runtime_data.coordinator.register_todo_entity(todo)
|
||||
return
|
||||
|
||||
todo = MaintenanceTodoList(hass)
|
||||
hass.data.setdefault(DOMAIN, {})["_todo_entity"] = todo
|
||||
async_add_entities([todo])
|
||||
|
||||
for other_entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if other_entry.unique_id == GLOBAL_UNIQUE_ID:
|
||||
continue
|
||||
other_data = getattr(other_entry, "runtime_data", None)
|
||||
if other_data and getattr(other_data, "coordinator", None):
|
||||
other_data.coordinator.register_todo_entity(todo)
|
||||
|
||||
_LOGGER.debug("Maintenance to-do list entity created")
|
||||
|
||||
|
||||
class MaintenanceTodoList(TodoListEntity):
|
||||
"""A single list of every active maintenance task, keyed by due state."""
|
||||
|
||||
_attr_name = "Maintenance"
|
||||
_attr_unique_id = "maintenance_supporter_todo"
|
||||
_attr_translation_key = "maintenance_todo"
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the to-do list."""
|
||||
self._hass = hass
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Repaint the list (called by coordinators on update)."""
|
||||
if self.hass is not None:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def todo_items(self) -> list[TodoItem]:
|
||||
"""Build one item per active task across all objects."""
|
||||
items: list[TodoItem] = []
|
||||
for entry in self._hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.unique_id == GLOBAL_UNIQUE_ID:
|
||||
continue
|
||||
obj_data = entry.data.get(CONF_OBJECT, {})
|
||||
if obj_data.get("archived_at") is not None:
|
||||
continue
|
||||
obj_name = obj_data.get("name", "Unknown")
|
||||
|
||||
runtime_data = getattr(entry, "runtime_data", None)
|
||||
coordinator = getattr(runtime_data, "coordinator", None)
|
||||
live = coordinator.data.get(CONF_TASKS, {}) if coordinator and coordinator.data else {}
|
||||
|
||||
for task_id, task_cfg in entry.data.get(CONF_TASKS, {}).items():
|
||||
if not task_cfg.get(CONF_TASK_ENABLED, True):
|
||||
continue
|
||||
if task_cfg.get("archived_at") is not None:
|
||||
continue
|
||||
task_live = live.get(task_id, {})
|
||||
status = task_live.get("_status", MaintenanceStatus.OK)
|
||||
# Archived is retired; paused (v2.20, N3) is frozen — neither
|
||||
# belongs on the actionable To-do list.
|
||||
if status in (MaintenanceStatus.ARCHIVED, MaintenanceStatus.PAUSED):
|
||||
continue
|
||||
due_iso = task_live.get("_next_due")
|
||||
due: date | None = None
|
||||
if isinstance(due_iso, str):
|
||||
try:
|
||||
due = date.fromisoformat(due_iso)
|
||||
except ValueError:
|
||||
due = None
|
||||
items.append(
|
||||
TodoItem(
|
||||
uid=f"{entry.entry_id}:{task_id}",
|
||||
summary=f"{obj_name}: {task_cfg.get('name', '')}",
|
||||
status=(TodoItemStatus.NEEDS_ACTION if status in _ACTION_STATUSES else TodoItemStatus.COMPLETED),
|
||||
due=due,
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Handle a checkbox toggle: completing an item completes the task.
|
||||
|
||||
Un-checking (back to needs_action) is a no-op — a maintenance task can't
|
||||
be "un-completed"; its next cycle is driven by the schedule.
|
||||
"""
|
||||
if item.status != TodoItemStatus.COMPLETED or not item.uid:
|
||||
return
|
||||
entry_id, _, task_id = item.uid.partition(":")
|
||||
entry = self._hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None:
|
||||
return
|
||||
coordinator = getattr(getattr(entry, "runtime_data", None), "coordinator", None)
|
||||
if coordinator is None:
|
||||
return
|
||||
# Respect a task's completion window — silently no-op (the item stays
|
||||
# needs_action) rather than completing an item that's not yet due.
|
||||
from .models.maintenance_task import MaintenanceTask
|
||||
|
||||
td = coordinator._get_merged_tasks_data().get(task_id)
|
||||
if td and not MaintenanceTask.from_dict(td).can_complete_now:
|
||||
return
|
||||
await coordinator.complete_maintenance(task_id, notes="Completed from the To-do list")
|
||||
self.async_write_ha_state()
|
||||
Reference in New Issue
Block a user