updated apps

This commit is contained in:
2026-07-14 23:57:03 -04:00
parent 6cc7212cef
commit 010e828e9c
797 changed files with 45153 additions and 4246 deletions
@@ -12,7 +12,9 @@ from ..const import (
CONF_TASKS,
MAX_CHECKLIST_ITEM_LENGTH,
MAX_CHECKLIST_ITEMS,
MAX_COST,
MAX_DATE_LENGTH,
MAX_DURATION_MINUTES,
MAX_ID_LENGTH,
MAX_TEXT_LENGTH,
)
@@ -26,6 +28,30 @@ from . import (
# ---------------------------------------------------------------------------
def _load_task_context(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
*,
need_coordinator: bool = True,
) -> tuple[Any, Any] | None:
"""Resolve ``(runtime_data, entry)`` for a task action, or send the standard
not-found error and return None.
Consolidates the identical prologue the task-action handlers copied inline
(the copies had already drifted — e.g. snooze omitted the coordinator check).
"""
rd = _get_runtime_data(hass, msg["entry_id"])
if need_coordinator and (rd is None or rd.coordinator is None):
connection.send_error(msg["id"], "not_found", "Coordinator not found")
return None
entry = hass.config_entries.async_get_entry(msg["entry_id"])
if entry is None or msg["task_id"] not in entry.data.get(CONF_TASKS, {}):
connection.send_error(msg["id"], "not_found", "Task not found")
return None
return rd, entry
def _completion_blocked(rd: Any, task_id: str) -> bool:
"""True iff the task's completion window forbids completing it right now.
@@ -48,8 +74,8 @@ def _completion_blocked(rd: Any, task_id: str) -> bool:
vol.Required("entry_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
vol.Required("task_id"): vol.All(str, vol.Length(max=MAX_ID_LENGTH)),
vol.Optional("notes"): vol.Any(vol.All(str, vol.Length(max=MAX_TEXT_LENGTH)), None),
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=0, max=1_000_000)), None),
vol.Optional("duration"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0, max=525_600)), None),
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)), None),
vol.Optional("duration"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)), None),
# Restrict checklist_state to {string-key (≤500): bool, ...} with
# a hard cap on entries. Without this, attackers (or bad clients)
# could inflate the per-task history with arbitrarily large dicts.
@@ -67,6 +93,9 @@ def _completion_blocked(rd: Any, task_id: str) -> bool:
# Meter readings (v2.20, #83): the recorded value for `reading` tasks.
# Wide numeric bounds — meters count high, temperatures go negative.
vol.Optional("reading_value"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=-1e12, max=1e12)), None),
# Spare parts: on an auto-created "buy" task, how many units were
# actually bought (dialog override of the part's restock_quantity).
vol.Optional("restock_quantity"): vol.Any(vol.All(int, vol.Range(min=1, max=9999)), None),
}
)
@websocket_api.async_response
@@ -76,15 +105,10 @@ async def ws_complete_task(
msg: dict[str, Any],
) -> None:
"""Mark a task as completed."""
rd = _get_runtime_data(hass, msg["entry_id"])
if rd is None or rd.coordinator is None:
connection.send_error(msg["id"], "not_found", "Coordinator not found")
return
entry = hass.config_entries.async_get_entry(msg["entry_id"])
if entry is None or msg["task_id"] not in entry.data.get(CONF_TASKS, {}):
connection.send_error(msg["id"], "not_found", "Task not found")
ctx = _load_task_context(hass, connection, msg)
if ctx is None:
return
rd, _entry = ctx
if _completion_blocked(rd, msg["task_id"]):
connection.send_error(
@@ -103,6 +127,7 @@ async def ws_complete_task(
feedback=msg.get("feedback"),
photo_doc_id=msg.get("photo_doc_id"),
reading_value=msg.get("reading_value"),
restock_quantity=msg.get("restock_quantity"),
)
connection.send_result(msg["id"], {"success": True})
@@ -186,15 +211,10 @@ async def ws_skip_task(
msg: dict[str, Any],
) -> None:
"""Skip the current maintenance cycle."""
rd = _get_runtime_data(hass, msg["entry_id"])
if rd is None or rd.coordinator is None:
connection.send_error(msg["id"], "not_found", "Coordinator not found")
return
entry = hass.config_entries.async_get_entry(msg["entry_id"])
if entry is None or msg["task_id"] not in entry.data.get(CONF_TASKS, {}):
connection.send_error(msg["id"], "not_found", "Task not found")
ctx = _load_task_context(hass, connection, msg)
if ctx is None:
return
rd, _entry = ctx
await rd.coordinator.skip_maintenance(
task_id=msg["task_id"],
@@ -221,15 +241,10 @@ async def ws_reset_task(
"""Reset the last performed date."""
from datetime import date as date_cls
rd = _get_runtime_data(hass, msg["entry_id"])
if rd is None or rd.coordinator is None:
connection.send_error(msg["id"], "not_found", "Coordinator not found")
return
entry = hass.config_entries.async_get_entry(msg["entry_id"])
if entry is None or msg["task_id"] not in entry.data.get(CONF_TASKS, {}):
connection.send_error(msg["id"], "not_found", "Task not found")
ctx = _load_task_context(hass, connection, msg)
if ctx is None:
return
rd, _entry = ctx
reset_date = None
if msg.get("date"):
@@ -263,15 +278,10 @@ async def ws_postpone_task(
"""Postpone the current occurrence to a chosen date (per-occurrence defer)."""
from datetime import date as date_cls
rd = _get_runtime_data(hass, msg["entry_id"])
if rd is None or rd.coordinator is None:
connection.send_error(msg["id"], "not_found", "Coordinator not found")
return
entry = hass.config_entries.async_get_entry(msg["entry_id"])
if entry is None or msg["task_id"] not in entry.data.get(CONF_TASKS, {}):
connection.send_error(msg["id"], "not_found", "Task not found")
ctx = _load_task_context(hass, connection, msg)
if ctx is None:
return
rd, _entry = ctx
try:
until = date_cls.fromisoformat(msg["until"])
@@ -304,9 +314,7 @@ async def ws_snooze_task(
"""
from .. import DOMAIN, NOTIFICATION_MANAGER_KEY
entry = hass.config_entries.async_get_entry(msg["entry_id"])
if entry is None or msg["task_id"] not in entry.data.get(CONF_TASKS, {}):
connection.send_error(msg["id"], "not_found", "Task not found")
if _load_task_context(hass, connection, msg, need_coordinator=False) is None:
return
nm = hass.data.get(DOMAIN, {}).get(NOTIFICATION_MANAGER_KEY)