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": {