hass.tibber_prices/custom_components/tibber_prices/migrations.py
Julian Pawlowski 565397b8ca feat(migrations): add entity auto-migration system with HA repairs
Adds migrations.py with automatic entity registry migration for renamed
sensor keys. Separated from coordinator/repairs.py (runtime issues) and
__init__.py _migrate_config_options() (config format changes).

- ENTITY_KEY_RENAMES dict maps old→new entity keys (extensible)
- _auto_migrate_entity_keys() updates unique_id, preserves entity_id
- Handles partial migration (new entity already exists → remove old)
- Creates persistent HA repair issue after migration via ir.async_create_issue()
- Called in async_setup_entry() after _migrate_config_options()

Migrates: trend_change_in_minutes → next_price_trend_change_in

Repair issue informs users about:
- Auto-migrated entity renames (entity_id preserved, no action needed)
- Duration sensor value unit change (hours → minutes): update automation
  thresholds from `state < 0.25` to `state < 15` for 15-minute checks

All 5 language files (en, de, nb, nl, sv) updated with translations.

BREAKING CHANGE: Duration sensors (remaining time, starts in, period
duration, trend change countdown) now report state values in minutes
instead of hours. Display unit in dashboards remains hours by default.
Update numeric comparisons in automations accordingly.

Impact: Users upgrading from previous releases see an informational
repair notice guiding them through any required automation updates.
Entity renames are handled transparently with no loss of history.
2026-04-10 12:21:49 +00:00

147 lines
4.8 KiB
Python

"""
Entity migration checks for Tibber Prices integration.
Detects obsolete entity keys in the entity registry after upgrades and
performs automatic migration where possible. Creates repair issues to
notify users about breaking changes that require manual action.
Separation of concerns:
- This module: One-time upgrade migrations (entity renames, breaking changes)
- coordinator/repairs.py: Runtime repairs (API issues, missing data)
- __init__.py _migrate_config_options(): Config option format changes
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from homeassistant.core import callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import issue_registry as ir
from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
_LOGGER = logging.getLogger(__name__)
# ============================================================================
# ENTITY KEY RENAMES
# Add entries here when renaming sensors in future releases.
# old_entity_key -> new_entity_key (auto-migrated, entity_id preserved)
# ============================================================================
ENTITY_KEY_RENAMES: dict[str, str] = {
"trend_change_in_minutes": "next_price_trend_change_in",
}
@callback
def check_entity_migrations(
hass: HomeAssistant,
entry: ConfigEntry,
) -> None:
"""
Check for entity migrations and create repairs if needed.
Called during async_setup_entry, before platform forwarding.
Performs auto-migration of renamed entities and creates
informational repairs about breaking changes.
"""
ent_reg = er.async_get(hass)
# Auto-migrate renamed entity keys
migrated = _auto_migrate_entity_keys(ent_reg, entry)
# Create persistent repair about breaking changes
issue_id = f"entity_migration_{entry.entry_id}"
if migrated:
rename_lines = [f"- `{old_key}` → `{new_key}`" for old_key, new_key, _ in migrated]
entity_list = "\n".join(rename_lines)
_LOGGER.info(
"Auto-migrated %d entity key(s) for '%s': %s",
len(migrated),
entry.title,
", ".join(f"{old}{new}" for old, new, _ in migrated),
)
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
is_fixable=False,
is_persistent=True,
severity=ir.IssueSeverity.WARNING,
translation_key="entity_migration",
translation_placeholders={
"home_name": entry.title,
"entity_list": entity_list,
"count": str(len(migrated)),
},
learn_more_url="https://github.com/jpawlowski/hass.tibber_prices/releases",
)
@callback
def _auto_migrate_entity_keys(
ent_reg: er.EntityRegistry,
entry: ConfigEntry,
) -> list[tuple[str, str, str]]:
"""
Auto-migrate renamed entity keys in the entity registry.
Updates unique_ids for renamed entities while preserving entity_id
and all user customizations (history, dashboard references, etc.).
Returns:
List of (old_key, new_key, entity_id) tuples for migrated entities
"""
migrated: list[tuple[str, str, str]] = []
prefix = f"{entry.entry_id}_"
# Get all entities for this config entry
entry_entities = er.async_entries_for_config_entry(ent_reg, entry.entry_id)
for entity_entry in entry_entities:
if not entity_entry.unique_id.startswith(prefix):
continue
entity_key = entity_entry.unique_id[len(prefix) :]
if entity_key not in ENTITY_KEY_RENAMES:
continue
new_key = ENTITY_KEY_RENAMES[entity_key]
new_unique_id = f"{prefix}{new_key}"
# Check if new entity already exists (e.g., from a partial migration)
new_entity_id = ent_reg.async_get_entity_id(entity_entry.domain, DOMAIN, new_unique_id)
if new_entity_id:
# New entity already exists — remove the obsolete old one
_LOGGER.debug(
"Removing obsolete entity '%s' (new entity '%s' already exists)",
entity_entry.entity_id,
new_entity_id,
)
ent_reg.async_remove(entity_entry.entity_id)
else:
# Migrate: update unique_id (preserves entity_id and history)
_LOGGER.debug(
"Migrating entity '%s': unique_id '%s''%s'",
entity_entry.entity_id,
entity_entry.unique_id,
new_unique_id,
)
ent_reg.async_update_entity(
entity_entry.entity_id,
new_unique_id=new_unique_id,
)
migrated.append((entity_key, new_key, entity_entry.entity_id))
return migrated