257 files
This commit is contained in:
@@ -47,7 +47,6 @@ from .const import (
|
||||
CONF_ADVANCED_CHECKLISTS,
|
||||
CONF_ADVANCED_ENVIRONMENTAL,
|
||||
CONF_ADVANCED_GROUPS,
|
||||
CONF_ADVANCED_PREDICTIONS,
|
||||
CONF_ADVANCED_SCHEDULE_TIME,
|
||||
CONF_ADVANCED_SEASONAL,
|
||||
CONF_BUDGET_MONTHLY,
|
||||
@@ -63,6 +62,7 @@ from .const import (
|
||||
DOCUMENT_TEXT_INDEX_KEY,
|
||||
DOMAIN,
|
||||
EVENT_UNSUBS_KEY,
|
||||
FLAT_SCHEDULE_TYPES,
|
||||
GLOBAL_UNIQUE_ID,
|
||||
MAX_COST,
|
||||
MAX_DURATION_MINUTES,
|
||||
@@ -214,12 +214,17 @@ SERVICE_ADD_OBJECT_SCHEMA = vol.Schema(
|
||||
# just a third UI onto the same storage, so a value the WS API rejects must not
|
||||
# be reachable from an automation. `vol.Coerce` stays (unlike the WS schemas)
|
||||
# because YAML/templated service data arrives as strings.
|
||||
# Only the recurrences the flat service fields can express. "sensor_based"
|
||||
# was offered but a service cannot set the trigger, so it silently became an
|
||||
# interval or manual task (audit 2026-09-29); calendar kinds go via `schedule`.
|
||||
_SERVICE_SCHEDULE_TYPE = vol.In(sorted(str(kind) for kind in FLAT_SCHEDULE_TYPES))
|
||||
|
||||
SERVICE_ADD_TASK_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("entry_id"): cv.string,
|
||||
vol.Required("name"): vol.All(cv.string, vol.Length(min=1, max=MAX_NAME_LENGTH)),
|
||||
vol.Optional("task_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)),
|
||||
vol.Optional("schedule_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)),
|
||||
vol.Optional("schedule_type"): _SERVICE_SCHEDULE_TYPE,
|
||||
vol.Optional("interval_days"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=INTERVAL_DAYS_RANGE[0], max=INTERVAL_DAYS_RANGE[1])
|
||||
),
|
||||
@@ -240,7 +245,7 @@ SERVICE_UPDATE_TASK_SCHEMA = vol.Schema(
|
||||
vol.Required("task_id"): cv.string,
|
||||
vol.Optional("name"): vol.All(cv.string, vol.Length(min=1, max=MAX_NAME_LENGTH)),
|
||||
vol.Optional("task_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)),
|
||||
vol.Optional("schedule_type"): vol.All(cv.string, vol.Length(max=MAX_TYPE_LENGTH)),
|
||||
vol.Optional("schedule_type"): _SERVICE_SCHEDULE_TYPE,
|
||||
vol.Optional("interval_days"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=INTERVAL_DAYS_RANGE[0], max=INTERVAL_DAYS_RANGE[1])
|
||||
),
|
||||
@@ -269,7 +274,7 @@ SERVICE_DELETE_TASK_SCHEMA = vol.Schema(
|
||||
SERVICE_LIST_TASKS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("entry_id"): cv.string,
|
||||
vol.Optional("status"): vol.In(["ok", "due_soon", "overdue", "triggered"]),
|
||||
vol.Optional("status"): vol.In(["ok", "due_soon", "overdue", "triggered", "paused"]),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -430,6 +435,8 @@ def _trigger_target_and_unit(hass: HomeAssistant, tc: dict[str, Any]) -> tuple[f
|
||||
return tc.get("trigger_runtime_hours"), "h"
|
||||
if ttype == TriggerType.STATE_CHANGE:
|
||||
return tc.get("trigger_target_changes"), None
|
||||
if ttype == TriggerType.DUE_DATE:
|
||||
return tc.get("trigger_days_before") or 0, "d"
|
||||
if ttype == TriggerType.COMPOUND:
|
||||
return None, None
|
||||
from .entity.triggers import primary_entity_id
|
||||
@@ -700,7 +707,9 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
include_history = call.data.get("include_history", True)
|
||||
|
||||
# Phase 1: gather data on the event loop (accesses HA APIs)
|
||||
data = build_export_data(hass, include_history=include_history)
|
||||
from .helpers.import_mapping import async_attach_move_hints
|
||||
|
||||
data = await async_attach_move_hints(hass, build_export_data(hass, include_history=include_history))
|
||||
|
||||
# Phase 2: serialize in executor (CPU-bound, no HA API calls)
|
||||
file_path = hass.config.path(f"maintenance_export.{fmt}")
|
||||
@@ -751,7 +760,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
entry_id=call.data["entry_id"],
|
||||
name=call.data["name"],
|
||||
task_type=call.data.get("task_type", "custom"),
|
||||
schedule_type=call.data.get("schedule_type", "time_based"),
|
||||
schedule_type=call.data.get("schedule_type"),
|
||||
interval_days=call.data.get("interval_days"),
|
||||
interval_unit=call.data.get("interval_unit", "days"),
|
||||
due_date=call.data.get("due_date"),
|
||||
@@ -1082,13 +1091,11 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
"""
|
||||
found: list[ConfigEntry] = []
|
||||
seen: set[str] = set()
|
||||
# 2026.8 gives a device exactly one config entry (`config_entry_id`);
|
||||
# the plural `config_entries` is a silently-deprecated compat shim
|
||||
# there — no report_usage, so the deprecation gate cannot see it — and
|
||||
# the only spelling 2026.7 has. Prefer the singular where it exists.
|
||||
single = getattr(device, "config_entry_id", None)
|
||||
owner_ids = (single,) if single is not None else tuple(getattr(device, "config_entries", ()) or ())
|
||||
for ce_id in owner_ids:
|
||||
# One config entry per device since 2026.8 (the plural is reported
|
||||
# from 2026.10) — helpers.device_link.device_owner_ids reads it.
|
||||
from .helpers.device_link import device_owner_ids
|
||||
|
||||
for ce_id in device_owner_ids(device):
|
||||
ce = hass.config_entries.async_get_entry(ce_id)
|
||||
if ce is not None and ce.domain == DOMAIN and ce.unique_id != GLOBAL_UNIQUE_ID:
|
||||
found.append(ce)
|
||||
@@ -1281,7 +1288,6 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
|
||||
def _detect_advanced_feature_usage(hass: HomeAssistant, global_options: dict[str, Any]) -> dict[str, bool]:
|
||||
"""Scan existing entries to detect which advanced features are in use."""
|
||||
adaptive = False
|
||||
predictions = False
|
||||
seasonal = False
|
||||
environmental = False
|
||||
checklists = False
|
||||
@@ -1295,8 +1301,6 @@ def _detect_advanced_feature_usage(hass: HomeAssistant, global_options: dict[str
|
||||
ac = task_data.get("adaptive_config") or {}
|
||||
if ac.get("enabled"):
|
||||
adaptive = True
|
||||
if ac.get("sensor_prediction_enabled"):
|
||||
predictions = True
|
||||
if ac.get("seasonal_enabled"):
|
||||
seasonal = True
|
||||
if ac.get("environmental_entity"):
|
||||
@@ -1311,7 +1315,6 @@ def _detect_advanced_feature_usage(hass: HomeAssistant, global_options: dict[str
|
||||
|
||||
return {
|
||||
CONF_ADVANCED_ADAPTIVE: adaptive,
|
||||
CONF_ADVANCED_PREDICTIONS: predictions,
|
||||
CONF_ADVANCED_SEASONAL: seasonal,
|
||||
CONF_ADVANCED_ENVIRONMENTAL: environmental,
|
||||
CONF_ADVANCED_BUDGET: budget,
|
||||
@@ -1527,6 +1530,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: MaintenanceSupporterConf
|
||||
# HA-started so the store has finished loading.
|
||||
entry.async_on_unload(async_at_started(hass, _check_document_storage_issues))
|
||||
|
||||
# Devices an object created in a mistaken area before 2.96 move back,
|
||||
# and the empty leftover areas are offered for removal (repair issue).
|
||||
from .helpers.area_leftovers import async_check_leftover_areas
|
||||
|
||||
entry.async_on_unload(async_at_started(hass, async_check_leftover_areas))
|
||||
|
||||
# Backfill the document search index (#171) a couple of minutes after
|
||||
# start — pypdf is CPU work, and boot is the wrong moment for it.
|
||||
entry.async_on_unload(async_at_started(hass, _schedule_document_text_backfill))
|
||||
@@ -1996,9 +2005,14 @@ async def _check_task_responsible_user_orphans(
|
||||
changed = False
|
||||
for tid, td in tasks.items():
|
||||
ruid = td.get("responsible_user_id")
|
||||
pool = [u for u in (td.get("assignee_pool") or []) if isinstance(u, str)]
|
||||
raw_pool = td.get("assignee_pool")
|
||||
pool = [u for u in raw_pool if isinstance(u, str)] if isinstance(raw_pool, list) else []
|
||||
pruned_pool = [u for u in pool if u in valid_ids]
|
||||
resp_orphaned = bool(ruid) and ruid not in valid_ids
|
||||
# A value that is no user id at all (a list from a malformed
|
||||
# backup) is as orphaned as a deleted user's — `in valid_ids`
|
||||
# raised on it and failed the global entry's setup on every start
|
||||
# (bug audit 2026-09-29).
|
||||
resp_orphaned = bool(ruid) and (not isinstance(ruid, str) or ruid not in valid_ids)
|
||||
if not resp_orphaned and pruned_pool == pool:
|
||||
new_tasks[tid] = td
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user