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
@@ -40,6 +40,7 @@ from homeassistant.util import dt as dt_util
from .const import (
BATTERY_FLEET_OBJECT_FLAG,
COMPLETION_PROVENANCE_NOTES,
CONF_ADMIN_PANEL_USER_IDS,
CONF_ADVANCED_ADAPTIVE,
CONF_ADVANCED_BUDGET,
@@ -99,10 +100,12 @@ from .entity.summary_coordinator import MaintenanceSummaryCoordinator
from .frontend import async_register_card
from .helpers.aggregate import object_name as aggregate_object_name
from .helpers.assist_sentences import async_sync as async_sync_assist_sentences
from .helpers.dates import INTERVAL_UNITS
from .helpers.dates import INTERVAL_UNITS, local_date_from_iso
from .helpers.documents import DocumentStore
from .helpers.global_options import get_global_entry
from .helpers.notification_gates import task_may_notify
from .helpers.notification_manager import NotificationManager
from .helpers.notify_hooks import KIND_LEAD_TIME
from .helpers.schedule import normalize_task_storage
from .helpers.task_fields import (
INTERVAL_DAYS_RANGE,
@@ -345,7 +348,7 @@ async def async_maybe_send_lead_reminders(hass: HomeAssistant) -> None:
per cycle overall (the day-match only occurs on one day per lead).
Complements — does not replace — the warning_days status-change path.
"""
from .const import CONF_REMINDER_LEAD_DAYS
from .const import CONF_REMINDER_LEAD_DAYS, MaintenanceStatus
from .models.maintenance_task import MaintenanceTask
global_entry = get_global_entry(hass)
@@ -383,8 +386,10 @@ async def async_maybe_send_lead_reminders(hass: HomeAssistant) -> None:
continue
if task_data.get("archived_at") is not None:
continue
if task_data.get("notify_enabled") is False:
continue # #173: muted task — no lead reminders either
# The per-task gates the lead-time kind declares (#173 mute,
# vacation, snooze) — before the model is even built.
if not task_may_notify(hass, entry.entry_id, task_id, MaintenanceStatus.DUE_SOON, task_data, kind=KIND_LEAD_TIME, manager=nm):
continue
task = MaintenanceTask.from_dict(task_data)
days = task.days_until_due
# days == 0 is a valid lead ("on the due date"); negatives are the
@@ -454,6 +459,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
# Create the notification manager (shared across all entries)
hass.data[DOMAIN][NOTIFICATION_MANAGER_KEY] = NotificationManager(hass)
await hass.data[DOMAIN][NOTIFICATION_MANAGER_KEY].async_load()
# Create + load the global document store (per-object doc metadata + the
# content-addressed blob registry; binaries live on disk under /config).
@@ -595,6 +601,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
raise ServiceValidationError(str(err)) from err
await coordinator.complete_maintenance(
task_id=task_id,
source="service",
notes=call.data.get("notes"),
cost=call.data.get("cost"),
duration=call.data.get("duration"),
@@ -901,7 +908,9 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
_LOGGER.info("Completing task %s via notification action", task_id)
await runtime_data.coordinator.complete_maintenance(
task_id=task_id,
notes=COMPLETION_PROVENANCE_NOTES["notification_action"],
unattended=True,
source="notification_action",
# mobile_app fires the action with the registration's
# user in the context - credit them (#128 attribution,
# feeds least_completed rotation); bug audit 2026-08-29.
@@ -936,7 +945,9 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
# Action succeeded — dismiss notification and clear NM rate-limit state
if nm is not None:
await nm.async_dismiss_task_notification(task_id)
cfg_entry = hass.config_entries.async_get_entry(entry_id)
task_cfg = ((cfg_entry.data.get(CONF_TASKS) or {}).get(task_id) or {}) if cfg_entry is not None else {}
await nm.async_dismiss_task_notification(task_id, responsible_user_id=task_cfg.get("responsible_user_id"))
nm.clear_task_state(entry_id, task_id)
unsub_notification = hass.bus.async_listen("mobile_app_notification_action", _handle_notification_action)
@@ -974,8 +985,9 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
await runtime_data.coordinator.complete_maintenance(
task_id=task_id,
completed_by=user_id,
notes="Completed via NFC tag",
notes=COMPLETION_PROVENANCE_NOTES["nfc"],
unattended=True,
source="nfc",
tag_verified=True,
)
except ServiceValidationError as err:
@@ -1302,8 +1314,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
ts for h in history if isinstance(h, dict) and isinstance((ts := h.get("timestamp")), str) and ts
]
if timestamps:
# ISO timestamps sort lexicographically; take YYYY-MM-DD
anchor = min(timestamps)[:10]
# ISO timestamps sort lexicographically; the earliest one's local day
anchor = local_date_from_iso(min(timestamps)) or today_iso
new_td = dict(td)
new_td["created_at"] = anchor
new_tasks[task_id] = new_td
@@ -1630,7 +1642,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
result["pruned"],
result["orphans_removed"],
)
if result.get("trigger_healed") or result["added"]:
if result.get("trigger_healed") or result["added"] or result.get("migrated"):
# #156: the recovery flag only reaches the LIVE trigger
# after a reload — and so does the stock SENSOR of a part
# added here: the sensor platform was set up before this
@@ -2007,6 +2019,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: MaintenanceSupporterCon
nm = hass.data.get(DOMAIN, {}).get(NOTIFICATION_MANAGER_KEY)
if nm is not None:
await nm.async_unload()
# The index's backfill timer / extraction tasks must not outlive the
# integration when the LAST entry to go is an object entry (the
# global entry may already be gone) - bug audit 2026-09-12.
text_index = hass.data.get(DOMAIN, {}).get(DOCUMENT_TEXT_INDEX_KEY)
if text_index is not None:
text_index.cancel()
for unsub in hass.data.get(DOMAIN, {}).get(EVENT_UNSUBS_KEY, []):
unsub()
hass.data.pop(DOMAIN, None)