278 files

This commit is contained in:
Home Assistant Version Control
2026-09-13 20:38:58 +00:00
parent d7a0a08372
commit 1b4f5f68c6
278 changed files with 36892 additions and 7150 deletions
@@ -17,8 +17,10 @@ from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers.storage import Store
from .const import CONF_TASKS, DEFAULT_MAX_HISTORY_ENTRIES, DOMAIN
from .const import CONF_TASKS, DEFAULT_MAX_HISTORY_ENTRIES, DOMAIN, LIFECYCLE_HISTORY_TYPES
from .helpers.dates import local_date_from_iso
from .helpers.parts import round_qty
from .helpers.pause import write_anchor
_LOGGER = logging.getLogger(__name__)
@@ -233,6 +235,24 @@ class MaintenanceStore:
object's Store to another's."""
self._data.setdefault("tasks", {})[task_id] = dict(state)
def update_task_state(self, task_id: str, **fields: Any) -> None:
"""Write plain scalar fields of a task's dynamic state; a ``None``
value removes the key. The public door for the few callers that used
to reach into ``_ensure_task`` (the coordinator's cycle modifiers, the
reference-number counter)."""
state = self._ensure_task(task_id)
for key, value in fields.items():
if value is None:
state.pop(key, None)
else:
state[key] = value
def all_task_states(self) -> dict[str, dict[str, Any]]:
"""Every task's dynamic state, keyed by task id — the LIVE dicts, for
in-place rewrites that then save (entity renames)."""
tasks: dict[str, dict[str, Any]] = self._data.setdefault("tasks", {})
return tasks
# --- in-cycle checklist progress (#73) -----------------------------------
def set_checklist_progress(self, task_id: str, state: dict[str, bool]) -> None:
@@ -312,9 +332,21 @@ class MaintenanceStore:
return self.get_task_state(task_id).get("last_performed")
def set_last_performed(self, task_id: str, date_str: str) -> None:
"""Set last_performed date string."""
"""Set last_performed date string (modifiers untouched — the
coordinator's ``_persist_dynamic_state`` writes those itself)."""
self._ensure_task(task_id)["last_performed"] = date_str
def set_anchor(self, task_id: str, date_str: str | None, *, clear_modifiers: bool = True) -> None:
"""Move (or with ``None`` clear) a task's ``last_performed`` anchor.
Every anchor poke outside the coordinator goes through here: the
history edit/delete re-derivation, the dialog's "last performed"
field, unarchive/resume. With ``clear_modifiers`` (the default) the
cycle's per-occurrence modifiers (``due_override``,
``last_planned_due``) go too — see :func:`helpers.pause.write_anchor`.
"""
write_anchor(self._ensure_task(task_id), date_str, clear_modifiers=clear_modifiers)
def set_phase_cursor(self, task_id: str, cursor: int) -> None:
"""Set the phase cursor (#139) — which cycle step is due next."""
self._ensure_task(task_id)["phase_cursor"] = cursor
@@ -461,6 +493,23 @@ class MaintenanceStore:
# ------------------------------------------------------------------
def reanchor_from_history(store: MaintenanceStore, task_id: str, history: list[dict[str, Any]]) -> str | None:
"""Re-derive a task's ``last_performed`` from its history and write it.
The single rule behind the history edit AND delete commands: the anchor
is the day of the LATEST lifecycle entry (completed / reset / skipped /
missed — ``LIFECYCLE_HISTORY_TYPES``); with none left the task reads as
never performed. The cycle modifiers (a postpone's ``due_override``,
``last_planned_due``) are dropped only when the anchor actually MOVES —
editing a note on the last completion keeps a postponed occurrence,
deleting that completion abandons it. Returns the new anchor.
"""
latest = max((h.get("timestamp") or "" for h in history if h.get("type") in LIFECYCLE_HISTORY_TYPES), default="")
anchor = local_date_from_iso(latest) if latest else None # the stamp's local calendar day
store.set_anchor(task_id, anchor, clear_modifiers=anchor != store.get_last_performed(task_id))
return anchor
def extract_dynamic_from_task(
task_data: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any]]: