393 files

This commit is contained in:
Home Assistant Version Control
2026-09-27 17:16:54 +00:00
parent b05b7a897e
commit 39d97ac2db
404 changed files with 54006 additions and 11034 deletions
@@ -28,11 +28,10 @@ from .const import (
ScheduleType,
)
from .helpers.aggregate import object_name
from .helpers.schedule import (
FLAT_RECURRENCE_KEYS,
normalize_task_storage,
read_legacy_fields,
)
from .helpers.entry_tasks import write_task
from .helpers.global_options import entry_option
from .helpers.issues import DEVICE_LINK_LOST_PREFIX, STALE_ACTION_PREFIX
from .helpers.schedule import FLAT_RECURRENCE_KEYS, read_legacy_fields
from .models.maintenance_task import MaintenanceTask
_LOGGER = logging.getLogger(__name__)
@@ -131,6 +130,34 @@ def _strip_entity_from_condition(cond: dict[str, Any], target: str) -> tuple[dic
return cond, top_has or nested_has
def _persist_trigger_repair(
hass: HomeAssistant, entry: Any, task_id: str, task_dict: dict[str, Any], entry_type: str, notes: str
) -> None:
"""Write a repaired task and record the repair in its history.
The static task goes through the one task-write chokepoint
(``write_task`` — normalized to the nested ``schedule``; the replace path
wrote it raw). The history entry and the trigger-runtime reset go to the
Store; an entry without one (legacy) keeps its history inline. The shared
epilogue of the replace and remove paths, which each carried a copy
(DRY audit 2026-09-26 B).
"""
write_task(hass, entry, task_id, task_dict)
rd = getattr(entry, "runtime_data", None)
store = getattr(rd, "store", None) if rd else None
if store is not None:
task = MaintenanceTask.from_dict(store.merge_task_data(task_id, task_dict))
task.add_history_entry(entry_type=entry_type, notes=notes)
store.set_history(task_id, task.to_dict().get("history", []))
store.clear_trigger_runtime(task_id)
store.async_delay_save()
else:
# Legacy: history via a full task round trip in ConfigEntry.
task = MaintenanceTask.from_dict(task_dict)
task.add_history_entry(entry_type=entry_type, notes=notes)
write_task(hass, entry, task_id, task.to_dict())
class MissingTriggerEntityRepairFlow(RepairsFlow):
"""Handle repair for a missing trigger entity.
@@ -267,37 +294,15 @@ class MissingTriggerEntityRepairFlow(RepairsFlow):
trigger_config.pop("trigger_baseline_value", None)
trigger_config.pop("trigger_change_count", None)
task_dict["trigger_config"] = trigger_config
tasks_data[task_id] = task_dict
# Write static changes to ConfigEntry
new_data = dict(entry.data)
new_data[CONF_TASKS] = tasks_data
self.hass.config_entries.async_update_entry(entry, data=new_data)
# Add history entry via Store (dynamic state)
rd = getattr(entry, "runtime_data", None)
store = getattr(rd, "store", None) if rd else None
if store is not None:
merged = store.merge_task_data(task_id, task_dict)
task = MaintenanceTask.from_dict(merged)
task.add_history_entry(
entry_type=HistoryEntryType.TRIGGER_REPLACED,
notes=f"Trigger entity replaced: {old_entity_id} → {new_entity_id}",
)
td = task.to_dict()
store.set_history(task_id, td.get("history", []))
store.clear_trigger_runtime(task_id)
store.async_delay_save()
else:
# Legacy: full task roundtrip via ConfigEntry
task = MaintenanceTask.from_dict(task_dict)
task.add_history_entry(
entry_type=HistoryEntryType.TRIGGER_REPLACED,
notes=f"Trigger entity replaced: {old_entity_id} → {new_entity_id}",
)
tasks_data[task_id] = task.to_dict()
new_data[CONF_TASKS] = tasks_data
self.hass.config_entries.async_update_entry(entry, data=new_data)
_persist_trigger_repair(
self.hass,
entry,
str(task_id),
task_dict,
HistoryEntryType.TRIGGER_REPLACED,
f"Trigger entity replaced: {old_entity_id} → {new_entity_id}",
)
# Reload entry so the trigger re-initialises with the new entity
await self.hass.config_entries.async_reload(entry_id)
@@ -341,36 +346,7 @@ class MissingTriggerEntityRepairFlow(RepairsFlow):
else:
history_notes = self._remove_from_flat(task_dict, trigger_config, missing_entity_id)
# Write static changes to ConfigEntry (recurrence normalized to nested)
tasks_data[task_id] = normalize_task_storage(task_dict)
new_data = dict(entry.data)
new_data[CONF_TASKS] = tasks_data
self.hass.config_entries.async_update_entry(entry, data=new_data)
# Add history entry via Store (dynamic state)
rd = getattr(entry, "runtime_data", None)
store = getattr(rd, "store", None) if rd else None
if store is not None:
merged = store.merge_task_data(task_id, task_dict)
task = MaintenanceTask.from_dict(merged)
task.add_history_entry(
entry_type=HistoryEntryType.TRIGGER_REMOVED,
notes=history_notes,
)
td = task.to_dict()
store.set_history(task_id, td.get("history", []))
store.clear_trigger_runtime(task_id)
store.async_delay_save()
else:
# Legacy: history via full task roundtrip in ConfigEntry
task = MaintenanceTask.from_dict(task_dict)
task.add_history_entry(
entry_type=HistoryEntryType.TRIGGER_REMOVED,
notes=history_notes,
)
tasks_data[task_id] = task.to_dict()
new_data[CONF_TASKS] = tasks_data
self.hass.config_entries.async_update_entry(entry, data=new_data)
_persist_trigger_repair(self.hass, entry, str(task_id), task_dict, HistoryEntryType.TRIGGER_REMOVED, history_notes)
await self.hass.config_entries.async_reload(entry_id)
@@ -508,7 +484,7 @@ class OrphanAdminPanelUserRepairFlow(RepairsFlow):
if entry is None:
return self.async_abort(reason="entry_gone")
target_uid = str(issue_data.get("user_id", ""))
ids = list(entry.options.get(CONF_ADMIN_PANEL_USER_IDS, []) or [])
ids = list(entry_option(entry, CONF_ADMIN_PANEL_USER_IDS) or [])
if target_uid in ids:
ids.remove(target_uid)
self.hass.config_entries.async_update_entry(
@@ -589,8 +565,6 @@ class StaleActionEntityRepairFlow(RepairsFlow):
return _entry_for_issue(self.hass, self.data)
def _patch_action_entity(self, entry: Any, new_entity_id: str) -> None:
from .helpers.entry_tasks import write_task
task_id = str((self.data or {}).get("task_id", ""))
task = dict(entry.data.get(CONF_TASKS, {}).get(task_id) or {})
action = dict(task.get("on_complete_action") or {})
@@ -601,8 +575,6 @@ class StaleActionEntityRepairFlow(RepairsFlow):
write_task(self.hass, entry, task_id, task)
def _clear_action(self, entry: Any) -> None:
from .helpers.entry_tasks import write_task
task_id = str((self.data or {}).get("task_id", ""))
task = dict(entry.data.get(CONF_TASKS, {}).get(task_id) or {})
task.pop("on_complete_action", None)
@@ -814,9 +786,9 @@ async def async_create_fix_flow(
"""Create a repair flow for the given issue."""
if issue_id.startswith("orphan_admin_panel_user_"):
return OrphanAdminPanelUserRepairFlow()
if issue_id.startswith("stale_action_entity_"):
if issue_id.startswith(STALE_ACTION_PREFIX):
return StaleActionEntityRepairFlow()
if issue_id.startswith("device_link_lost_"):
if issue_id.startswith(DEVICE_LINK_LOST_PREFIX):
return DeviceLinkRepairFlow()
if issue_id == "document_storage_issues":
return DocumentStorageRepairFlow()