176 lines
6.9 KiB
Python
176 lines
6.9 KiB
Python
"""Helpers for rewriting entity_id references when HA renames an entity.
|
|
|
|
Used by the global ``EVENT_ENTITY_REGISTRY_UPDATED`` listener in ``__init__.py``
|
|
to keep ``trigger_config["entity_id"|"entity_ids"]`` and
|
|
``adaptive_config["environmental_entity"]`` in sync with the user's renames.
|
|
|
|
Without this, ``async_track_state_change_event`` (which subscribes by literal
|
|
entity_id) silently misses events on the new id — same dual-storage class of
|
|
bug as #48, but with feature breakage instead of a UI inconsistency.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def rewrite_trigger_config(config: dict[str, Any], old_id: str, new_id: str) -> tuple[dict[str, Any], bool]:
|
|
"""Return (new_config, changed) with all entity_id references rewritten.
|
|
|
|
Handles:
|
|
- flat ``entity_id`` (legacy single-entity)
|
|
- ``entity_ids`` list (multi-entity)
|
|
- ``_trigger_state`` keyed by entity_id (per-entity persisted state)
|
|
- compound triggers — recursive into ``conditions[].trigger_config``
|
|
"""
|
|
if not isinstance(config, dict):
|
|
return config, False
|
|
|
|
new_config = dict(config)
|
|
changed = False
|
|
|
|
if new_config.get("entity_id") == old_id:
|
|
new_config["entity_id"] = new_id
|
|
changed = True
|
|
|
|
eids = new_config.get("entity_ids")
|
|
if isinstance(eids, list) and old_id in eids:
|
|
new_config["entity_ids"] = [new_id if e == old_id else e for e in eids]
|
|
changed = True
|
|
|
|
state = new_config.get("_trigger_state")
|
|
if isinstance(state, dict) and old_id in state:
|
|
new_state = dict(state)
|
|
new_state[new_id] = new_state.pop(old_id)
|
|
new_config["_trigger_state"] = new_state
|
|
changed = True
|
|
|
|
if new_config.get("type") == "compound":
|
|
new_conditions: list[dict[str, Any]] = []
|
|
for cond in new_config.get("conditions", []) or []:
|
|
new_cond, cond_changed = rewrite_trigger_config(cond, old_id, new_id)
|
|
inner = new_cond.get("trigger_config") if isinstance(new_cond, dict) else None
|
|
if isinstance(inner, dict):
|
|
rewritten_inner, inner_changed = rewrite_trigger_config(inner, old_id, new_id)
|
|
if inner_changed:
|
|
new_cond = {**new_cond, "trigger_config": rewritten_inner}
|
|
cond_changed = True
|
|
if cond_changed:
|
|
changed = True
|
|
new_conditions.append(new_cond)
|
|
if changed:
|
|
new_config["conditions"] = new_conditions
|
|
|
|
return new_config, changed
|
|
|
|
|
|
def rewrite_task(task_data: dict[str, Any], old_id: str, new_id: str) -> tuple[dict[str, Any], bool]:
|
|
"""Rewrite trigger_config + adaptive_config.environmental_entity in a task.
|
|
|
|
Note: ``adaptive_config`` lives in the Store after the v1.x migration —
|
|
this helper still rewrites the inline copy for the legacy / pre-migration
|
|
path. The Store path is handled by ``rewrite_store``.
|
|
"""
|
|
new_task = dict(task_data)
|
|
changed = False
|
|
|
|
tc = new_task.get("trigger_config")
|
|
if isinstance(tc, dict):
|
|
new_tc, tc_changed = rewrite_trigger_config(tc, old_id, new_id)
|
|
if tc_changed:
|
|
new_task["trigger_config"] = new_tc
|
|
changed = True
|
|
|
|
ac = new_task.get("adaptive_config")
|
|
if isinstance(ac, dict) and ac.get("environmental_entity") == old_id:
|
|
new_task["adaptive_config"] = {**ac, "environmental_entity": new_id}
|
|
changed = True
|
|
|
|
return new_task, changed
|
|
|
|
|
|
def rewrite_tasks(tasks: dict[str, dict[str, Any]], old_id: str, new_id: str) -> tuple[dict[str, dict[str, Any]], bool]:
|
|
"""Rewrite all tasks in an entry; return (new_tasks, any_changed)."""
|
|
new_tasks: dict[str, dict[str, Any]] = {}
|
|
any_changed = False
|
|
for tid, td in tasks.items():
|
|
new_td, changed = rewrite_task(td, old_id, new_id)
|
|
new_tasks[tid] = new_td
|
|
if changed:
|
|
any_changed = True
|
|
return new_tasks, any_changed
|
|
|
|
|
|
def rewrite_store(store: Any, old_id: str, new_id: str) -> bool:
|
|
"""Rewrite ``adaptive_config.environmental_entity`` and ``trigger_runtime``
|
|
keys in a ``MaintenanceStore``.
|
|
|
|
These fields live in Store (not entry.data) after the v1.x migration —
|
|
`_DYNAMIC_TASK_FIELDS` includes ``adaptive_config``, and per-entity
|
|
trigger runtime is keyed by entity_id. Both must follow renames.
|
|
|
|
Returns True iff anything was rewritten.
|
|
"""
|
|
changed = False
|
|
tasks_state: dict[str, Any] = store._data.get("tasks", {})
|
|
for state in tasks_state.values():
|
|
ac = state.get("adaptive_config")
|
|
if isinstance(ac, dict) and ac.get("environmental_entity") == old_id:
|
|
state["adaptive_config"] = {**ac, "environmental_entity": new_id}
|
|
changed = True
|
|
runtime = state.get("trigger_runtime")
|
|
if isinstance(runtime, dict) and old_id in runtime:
|
|
runtime[new_id] = runtime.pop(old_id)
|
|
changed = True
|
|
return changed
|
|
|
|
|
|
def migrate_object_unique_ids(hass: Any, entry: Any, old_name: str | None, new_name: str | None) -> int:
|
|
"""Rewrite per-task entity unique_ids after an object RENAME.
|
|
|
|
Same dual-storage bug class as the trigger rewrites above, but for our
|
|
own entities: every per-task unique_id embeds the object's NAME SLUG
|
|
(``maintenance_supporter_{slug}_{task_id}[_suffix]``). Without this
|
|
migration a rename orphans all registry entries on the next reload —
|
|
the entities come back under NEW unique_ids (and new entity_ids), while
|
|
dashboards and automations keep pointing at the now-unavailable old ones.
|
|
|
|
Rewrites in place, preserving entity_ids, history, and user
|
|
customisations. Returns the number of migrated entries. A slug collision
|
|
(another object already using the new unique_id) is logged and skipped
|
|
rather than raised — the reload then falls back to fresh entities for
|
|
just that entry, which matches the pre-migration behaviour.
|
|
"""
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from ..const import slugify_object_name
|
|
|
|
old_slug = slugify_object_name(old_name or "")
|
|
new_slug = slugify_object_name(new_name or "")
|
|
if not old_slug or not new_slug or old_slug == new_slug:
|
|
return 0
|
|
|
|
old_prefix = f"maintenance_supporter_{old_slug}_"
|
|
new_prefix = f"maintenance_supporter_{new_slug}_"
|
|
ent_reg = er.async_get(hass)
|
|
migrated = 0
|
|
for reg_entry in er.async_entries_for_config_entry(ent_reg, entry.entry_id):
|
|
uid = reg_entry.unique_id or ""
|
|
if not uid.startswith(old_prefix):
|
|
continue
|
|
new_uid = new_prefix + uid[len(old_prefix) :]
|
|
try:
|
|
ent_reg.async_update_entity(reg_entry.entity_id, new_unique_id=new_uid)
|
|
migrated += 1
|
|
except ValueError:
|
|
import logging
|
|
|
|
logging.getLogger(__name__).warning(
|
|
"unique_id %s already taken while renaming %r -> %r; leaving %s",
|
|
new_uid,
|
|
old_name,
|
|
new_name,
|
|
reg_entry.entity_id,
|
|
)
|
|
return migrated
|