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.
This commit is contained in:
Julian Pawlowski 2026-04-10 12:21:49 +00:00
parent 2a08515ba0
commit 565397b8ca
7 changed files with 171 additions and 0 deletions

View file

@ -41,6 +41,7 @@ from .interval_pool import (
async_remove_pool_storage, async_remove_pool_storage,
async_save_pool_state, async_save_pool_state,
) )
from .migrations import check_entity_migrations
from .services import async_setup_services from .services import async_setup_services
if TYPE_CHECKING: 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) # Migrate config options if needed (e.g., set default currency display mode for existing configs)
await _migrate_config_options(hass, entry) 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 # Preload translations to populate the cache
await async_load_translations(hass, "en") await async_load_translations(hass, "en")
await async_load_standard_translations(hass, "en") await async_load_standard_translations(hass, "en")

View file

@ -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

View file

@ -1049,6 +1049,10 @@
"home_not_found": { "home_not_found": {
"title": "Zuhause {home_name} nicht im Tibber-Konto gefunden", "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}." "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": { "services": {

View file

@ -1049,6 +1049,10 @@
"home_not_found": { "home_not_found": {
"title": "Home {home_name} not found in Tibber account", "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." "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": { "services": {

View file

@ -1049,6 +1049,10 @@
"home_not_found": { "home_not_found": {
"title": "Hjemmet {home_name} ble ikke funnet i Tibber-kontoen", "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." "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": { "services": {

View file

@ -1049,6 +1049,10 @@
"home_not_found": { "home_not_found": {
"title": "Huis {home_name} niet gevonden in Tibber-account", "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." "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": { "services": {

View file

@ -1049,6 +1049,10 @@
"home_not_found": { "home_not_found": {
"title": "Hem {home_name} hittades inte i Tibber-konto", "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." "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": { "services": {