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
@@ -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