257 files

This commit is contained in:
Home Assistant Version Control
2026-10-02 16:06:24 +00:00
parent 7c55603942
commit c29f5cc088
258 changed files with 24621 additions and 3136 deletions
@@ -1030,6 +1030,7 @@ def build_action_buttons(
entry_id: str | None,
task_id: str | None,
skip_allowed: bool,
complete_allowed: bool = True,
) -> list[dict[str, str]]:
"""The Companion-app action buttons a reminder carries, per the three
action toggles — translated labels, at most three (Android's cap).
@@ -1049,7 +1050,7 @@ def build_action_buttons(
return action_id(verb, None, None) if is_test else action_id(verb, entry_id, task_id)
actions: list[dict[str, str]] = []
if options.get(CONF_ACTION_COMPLETE_ENABLED, setting_default(CONF_ACTION_COMPLETE_ENABLED)):
if options.get(CONF_ACTION_COMPLETE_ENABLED, setting_default(CONF_ACTION_COMPLETE_ENABLED)) and complete_allowed:
actions.append({"action": _id("COMPLETE"), "title": f"✅ {_notif_t('action_complete', lang)}"})
if options.get(CONF_ACTION_SKIP_ENABLED, setting_default(CONF_ACTION_SKIP_ENABLED)) and skip_allowed:
actions.append({"action": _id("SKIP"), "title": f"⏭️ {_notif_t('action_skip', lang)}"})
@@ -1341,6 +1342,31 @@ class NotificationManager:
Unknown entries/tasks keep the button (nothing to check against)."""
return self._task_config(entry_id, task_id).get("allow_skip") is not False
def _complete_allowed(self, entry_id: str, task_id: str) -> bool:
"""A "Complete" button the action handler would refuse is not offered
(audit 2026-09-29): the tap only logged the refusal. Same rules as
``coordinator.complete_maintenance`` for an unattended surface — a
task only a tag scan may complete, one that demands details (nobody
is asked on a notification tap; phase-aware), and one whose completion
window has not opened yet (a lead-time reminder). Unknown entries or
tasks keep the button (nothing to check against)."""
from ..models.maintenance_task import MaintenanceTask
from .completion_requirements import required_completion_fields
entry = self.hass.config_entries.async_get_entry(entry_id)
coordinator = getattr(getattr(entry, "runtime_data", None), "coordinator", None)
if coordinator is None:
return True
task = coordinator._get_merged_tasks_data().get(task_id)
if not task:
return True
if task.get("require_tag_scan") or required_completion_fields(task):
return False
try:
return bool(MaintenanceTask.from_dict(task).can_complete_now)
except (KeyError, TypeError, ValueError):
return True
def _task_config(self, entry_id: str, task_id: str) -> Mapping[str, Any]:
"""The task's static config dict (``entry.data``) — carries the flags
the computed coordinator payload drops (``notify_enabled``,
@@ -1723,8 +1749,27 @@ class NotificationManager:
per-task gate's view of the snooze state — notification_gates.)"""
return self._is_snoozed(notification_key(entry_id, task_id, status))
def snooze_task(self, entry_id: str, task_id: str) -> None:
"""Snooze all notifications for a task."""
def task_snooze_state(self, entry_id: str, task_id: str) -> dict[str, datetime]:
"""The task's active snoozes, per status — what a voice undo restores."""
state: dict[str, datetime] = {}
for status in NOTIFIABLE_STATUSES:
until = self._snoozed_until.get(notification_key(entry_id, task_id, status))
if until is not None:
state[status] = until
return state
def restore_task_snooze_state(self, entry_id: str, task_id: str, state: dict[str, datetime]) -> None:
"""Put a task's snoozes back to *state* (from :meth:`task_snooze_state`)."""
for status in NOTIFIABLE_STATUSES:
key = notification_key(entry_id, task_id, status)
if status in state:
self._snoozed_until[key] = state[status]
else:
self._snoozed_until.pop(key, None)
self._dirty()
def snooze_task(self, entry_id: str, task_id: str) -> datetime:
"""Snooze all notifications for a task; returns when it ends."""
hours = self._opt(CONF_SNOOZE_DURATION_HOURS)
until = dt_util.now() + timedelta(hours=hours)
# Snooze for all status types
@@ -1732,6 +1777,7 @@ class NotificationManager:
self._snoozed_until[notification_key(entry_id, task_id, status)] = until
self._dirty()
_LOGGER.debug("Snoozed task %s for %s hours (until %s)", task_id, hours, until)
return until
async def async_task_status_changed(
self,
@@ -1996,6 +2042,7 @@ class NotificationManager:
entry_id=entry_id,
task_id=task_id,
skip_allowed=self._skip_allowed(entry_id, task_id),
complete_allowed=self._complete_allowed(entry_id, task_id),
)
service_data = _service_payload(