From 565397b8ca46527e8725dd4a5a6db9494027f19e Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Fri, 10 Apr 2026 12:21:49 +0000 Subject: [PATCH] feat(migrations): add entity auto-migration system with HA repairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- custom_components/tibber_prices/__init__.py | 4 + custom_components/tibber_prices/migrations.py | 147 ++++++++++++++++++ .../tibber_prices/translations/de.json | 4 + .../tibber_prices/translations/en.json | 4 + .../tibber_prices/translations/nb.json | 4 + .../tibber_prices/translations/nl.json | 4 + .../tibber_prices/translations/sv.json | 4 + 7 files changed, 171 insertions(+) create mode 100644 custom_components/tibber_prices/migrations.py diff --git a/custom_components/tibber_prices/__init__.py b/custom_components/tibber_prices/__init__.py index c55674d..1854399 100644 --- a/custom_components/tibber_prices/__init__.py +++ b/custom_components/tibber_prices/__init__.py @@ -41,6 +41,7 @@ from .interval_pool import ( async_remove_pool_storage, async_save_pool_state, ) +from .migrations import check_entity_migrations from .services import async_setup_services if TYPE_CHECKING: @@ -222,6 +223,9 @@ async def async_setup_entry( # Migrate config options if needed (e.g., set default currency display mode for existing configs) await _migrate_config_options(hass, entry) + # Check for entity migrations (renames, breaking changes) and create repairs + check_entity_migrations(hass, entry) + # Preload translations to populate the cache await async_load_translations(hass, "en") await async_load_standard_translations(hass, "en") diff --git a/custom_components/tibber_prices/migrations.py b/custom_components/tibber_prices/migrations.py new file mode 100644 index 0000000..a5bc4ca --- /dev/null +++ b/custom_components/tibber_prices/migrations.py @@ -0,0 +1,147 @@ +""" +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 diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json index 245ff86..49594c0 100644 --- a/custom_components/tibber_prices/translations/de.json +++ b/custom_components/tibber_prices/translations/de.json @@ -1049,6 +1049,10 @@ "home_not_found": { "title": "Zuhause {home_name} nicht im Tibber-Konto gefunden", "description": "Das in dieser Integration konfigurierte Zuhause (Eintrag-ID: {entry_id}) ist nicht mehr in deinem Tibber-Konto verfügbar. Dies passiert normalerweise, wenn:\n- Das Zuhause aus deinem Tibber-Konto gelöscht wurde\n- Das Zuhause zu einem anderen Tibber-Konto verschoben wurde\n- Der Zugriff auf dieses Zuhause widerrufen wurde\n\nBitte entferne diesen Integrationseintrag und füge ihn erneut hinzu, falls das Zuhause weiterhin überwacht werden soll. Um diesen Eintrag zu entfernen, gehe zu Einstellungen → Geräte & Dienste → Tibber Prices und lösche die Konfiguration {home_name}." + }, + "entity_migration": { + "title": "Tibber Prices: Aktion nach Update erforderlich ({home_name})", + "description": "Dieses Update enthält Breaking Changes, die automatisch angewendet wurden.\n\n**Umbenannte Entitäten ({count})**\n\nDie folgenden Entity-Keys wurden umbenannt. Deine bestehenden Entity-IDs und Automationen bleiben erhalten:\n\n{entity_list}\n\n**Geänderte Dauer-Sensorwerte**\n\nAlle Dauer-Sensoren (verbleibende Zeit, startet in, Periodendauer, Trendänderungs-Countdown) geben ihren Zustandswert jetzt in **Minuten** statt Stunden an. Die Anzeigeeinheit in Dashboards bleibt standardmäßig Stunden.\n\nWenn du Automationen mit numerischen Vergleichen auf diesen Sensoren hast, aktualisiere deine Schwellwerte:\n- Alt: `state < 0.25` (15 Minuten als Stunden)\n- Neu: `state < 15` (15 Minuten)\n\nSchließe diesen Hinweis, nachdem du deine Automationen überprüft hast." } }, "services": { diff --git a/custom_components/tibber_prices/translations/en.json b/custom_components/tibber_prices/translations/en.json index f3f45f5..d8ae7e3 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -1049,6 +1049,10 @@ "home_not_found": { "title": "Home {home_name} not found in Tibber account", "description": "The home configured in this integration (entry ID: {entry_id}) is no longer available in your Tibber account. This typically happens when:\n- The home was deleted from your Tibber account\n- The home was moved to a different Tibber account\n- Access to this home was revoked\n\nPlease remove this integration entry and re-add it if the home should still be monitored. To remove this entry, go to Settings → Devices & Services → Tibber Prices and delete the {home_name} configuration." + }, + "entity_migration": { + "title": "Tibber Prices: Action required after update ({home_name})", + "description": "This update includes breaking changes that were applied automatically.\n\n**Renamed Entities ({count})**\n\nThe following entity keys were renamed. Your existing entity IDs and automations remain intact:\n\n{entity_list}\n\n**Duration Sensor Value Change**\n\nAll duration sensors (remaining time, starts in, period duration, trend change countdown) now report their state value in **minutes** instead of hours. The display unit in dashboards remains hours by default.\n\nIf you have automations using numeric comparisons on these sensors, update your thresholds:\n- Old: `state < 0.25` (15 minutes as hours)\n- New: `state < 15` (15 minutes)\n\nDismiss this notice after reviewing your automations." } }, "services": { diff --git a/custom_components/tibber_prices/translations/nb.json b/custom_components/tibber_prices/translations/nb.json index b678bda..b8acf07 100644 --- a/custom_components/tibber_prices/translations/nb.json +++ b/custom_components/tibber_prices/translations/nb.json @@ -1049,6 +1049,10 @@ "home_not_found": { "title": "Hjemmet {home_name} ble ikke funnet i Tibber-kontoen", "description": "Hjemmet konfigurert i denne integrasjonen (oppførings-ID: {entry_id}) er ikke lenger tilgjengelig i Tibber-kontoen din. Dette skjer vanligvis når:\n- Hjemmet ble slettet fra Tibber-kontoen din\n- Hjemmet ble flyttet til en annen Tibber-konto\n- Tilgang til dette hjemmet ble tilbakekalt\n\nVennligst fjern denne integrasjonsoppføringen og legg den til på nytt hvis hjemmet fortsatt skal overvåkes. For å fjerne denne oppføringen, gå til Innstillinger → Enheter og tjenester → Tibber Prices og slett {home_name}-konfigurasjonen." + }, + "entity_migration": { + "title": "Tibber Prices: Handling kreves etter oppdatering ({home_name})", + "description": "Denne oppdateringen inkluderer endringer som ble brukt automatisk.\n\n**Omdøpte entiteter ({count})**\n\nFølgende entity-nøkler ble omdøpt. Dine eksisterende entity-ID-er og automatiseringer forblir intakte:\n\n{entity_list}\n\n**Endrede varighetssensorverdier**\n\nAlle varighetssensorer (gjenværende tid, starter om, periodevarighet, trendendrings-nedtelling) rapporterer nå tilstandsverdien i **minutter** i stedet for timer. Visningsenheten i dashboards forblir timer som standard.\n\nHvis du har automatiseringer med numeriske sammenligninger på disse sensorene, oppdater tersklene:\n- Gammelt: `state < 0.25` (15 minutter som timer)\n- Nytt: `state < 15` (15 minutter)\n\nAvvis dette varselet etter å ha gjennomgått automatiseringene dine." } }, "services": { diff --git a/custom_components/tibber_prices/translations/nl.json b/custom_components/tibber_prices/translations/nl.json index e4e85c2..20e2a26 100644 --- a/custom_components/tibber_prices/translations/nl.json +++ b/custom_components/tibber_prices/translations/nl.json @@ -1049,6 +1049,10 @@ "home_not_found": { "title": "Huis {home_name} niet gevonden in Tibber-account", "description": "Het huis geconfigureerd in deze integratie (entry ID: {entry_id}) is niet langer beschikbaar in je Tibber-account. Dit gebeurt meestal wanneer:\n- Het huis is verwijderd uit je Tibber-account\n- Het huis is verplaatst naar een ander Tibber-account\n- Toegang tot dit huis is ingetrokken\n\nVerwijder dit integratie-item en voeg het opnieuw toe als het huis nog steeds gemonitord moet worden. Om dit item te verwijderen, ga naar Instellingen → Apparaten & Services → Tibber Prices en verwijder de {home_name} configuratie." + }, + "entity_migration": { + "title": "Tibber Prices: Actie vereist na update ({home_name})", + "description": "Deze update bevat wijzigingen die automatisch zijn toegepast.\n\n**Hernoemde entiteiten ({count})**\n\nDe volgende entity-sleutels zijn hernoemd. Je bestaande entity-ID's en automatiseringen blijven intact:\n\n{entity_list}\n\n**Gewijzigde duur-sensorwaarden**\n\nAlle duur-sensoren (resterende tijd, start over, periodeduur, trendwijzigings-aftelling) rapporteren hun statuswaarde nu in **minuten** in plaats van uren. De weergave-eenheid in dashboards blijft standaard uren.\n\nAls je automatiseringen hebt met numerieke vergelijkingen op deze sensoren, werk dan je drempelwaarden bij:\n- Oud: `state < 0.25` (15 minuten als uren)\n- Nieuw: `state < 15` (15 minuten)\n\nSluit deze melding nadat je je automatiseringen hebt gecontroleerd." } }, "services": { diff --git a/custom_components/tibber_prices/translations/sv.json b/custom_components/tibber_prices/translations/sv.json index a880521..fb47279 100644 --- a/custom_components/tibber_prices/translations/sv.json +++ b/custom_components/tibber_prices/translations/sv.json @@ -1049,6 +1049,10 @@ "home_not_found": { "title": "Hem {home_name} hittades inte i Tibber-konto", "description": "Hemmet som konfigurerats i denna integration (post-ID: {entry_id}) är inte längre tillgängligt i ditt Tibber-konto. Detta händer vanligtvis när:\n- Hemmet togs bort från ditt Tibber-konto\n- Hemmet flyttades till ett annat Tibber-konto\n- Åtkomst till detta hem återkallades\n\nTa bort denna integrationspost och lägg till den igen om hemmet fortfarande ska övervakas. För att ta bort denna post, gå till Inställningar → Enheter & Tjänster → Tibber-priser och radera {home_name}-konfigurationen." + }, + "entity_migration": { + "title": "Tibber Prices: Åtgärd krävs efter uppdatering ({home_name})", + "description": "Denna uppdatering innehåller ändringar som tillämpades automatiskt.\n\n**Omdöpta entiteter ({count})**\n\nFöljande entity-nycklar döptes om automatiskt. Dina befintliga entity-ID:n och automatiseringar förblir intakta:\n\n{entity_list}\n\n**Ändrade varaktighetssensorvärden**\n\nAlla varaktighetssensorer (återstående tid, startar om, periodvaraktighet, trendändrings-nedräkning) rapporterar nu sitt tillståndsvärde i **minuter** istället för timmar. Visningsenheten i dashboards förblir timmar som standard.\n\nOm du har automatiseringar med numeriska jämförelser på dessa sensorer, uppdatera dina tröskelvärden:\n- Gammalt: `state < 0.25` (15 minuter som timmar)\n- Nytt: `state < 15` (15 minuter)\n\nStäng detta meddelande efter att du har granskat dina automatiseringar." } }, "services": {