278 files
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user