mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
Compare commits
3 commits
2a08515ba0
...
5314454a26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5314454a26 | ||
|
|
6e7b7b3ceb | ||
|
|
565397b8ca |
11 changed files with 471 additions and 2 deletions
14
AGENTS.md
14
AGENTS.md
|
|
@ -311,7 +311,7 @@ After successful refactoring:
|
||||||
|
|
||||||
**✅ ALLOWED in root:**
|
**✅ ALLOWED in root:**
|
||||||
- Platform modules: `__init__.py`, `sensor.py` (deprecated, now `sensor/`), `binary_sensor.py` (deprecated, now `binary_sensor/`), future platforms
|
- Platform modules: `__init__.py`, `sensor.py` (deprecated, now `sensor/`), `binary_sensor.py` (deprecated, now `binary_sensor/`), future platforms
|
||||||
- Core integration files: `const.py`, `manifest.json`, `services.yaml`, `diagnostics.py`, `data.py`
|
- Core integration files: `const.py`, `manifest.json`, `services.yaml`, `diagnostics.py`, `data.py`, `migrations.py`
|
||||||
- Translation directories: `translations/`, `custom_translations/`
|
- Translation directories: `translations/`, `custom_translations/`
|
||||||
- Brand images: `brand/` (icon.png, dark_icon.png, logo.png, dark_logo.png + `@2x` variants) — served via HA brands proxy API (HA ≥ 2026.4), silently ignored on older versions
|
- Brand images: `brand/` (icon.png, dark_icon.png, logo.png, dark_logo.png + `@2x` variants) — served via HA brands proxy API (HA ≥ 2026.4), silently ignored on older versions
|
||||||
|
|
||||||
|
|
@ -517,6 +517,7 @@ custom_components/tibber_prices/
|
||||||
│ ├── definitions.py # ENTITY_DESCRIPTIONS, constants
|
│ ├── definitions.py # ENTITY_DESCRIPTIONS, constants
|
||||||
│ └── attributes.py # Attribute builders
|
│ └── attributes.py # Attribute builders
|
||||||
├── entity.py # Base TibberPricesEntity class
|
├── entity.py # Base TibberPricesEntity class
|
||||||
|
├── migrations.py # Entity migration & breaking change repairs
|
||||||
├── entity_utils/ # Shared entity helpers (both platforms)
|
├── entity_utils/ # Shared entity helpers (both platforms)
|
||||||
│ ├── __init__.py # Package exports
|
│ ├── __init__.py # Package exports
|
||||||
│ ├── icons.py # Icon mapping logic
|
│ ├── icons.py # Icon mapping logic
|
||||||
|
|
@ -1824,6 +1825,15 @@ All entities MUST implement these patterns for proper HA integration:
|
||||||
|
|
||||||
**Why this matters**: Without `available`, entities show stale data during errors. Without state restore, history has gaps after HA restart. Without `force_update`, repeated state changes aren't visible in history.
|
**Why this matters**: Without `available`, entities show stale data during errors. Without state restore, history has gaps after HA restart. Without `force_update`, repeated state changes aren't visible in history.
|
||||||
|
|
||||||
|
**6. Entity Migration & Breaking Change Repairs:**
|
||||||
|
When renaming entity keys or changing sensor value units/semantics across releases:
|
||||||
|
|
||||||
|
- **Auto-migration** (`migrations.py`): Add old→new mapping to `ENTITY_KEY_RENAMES` dict. The `check_entity_migrations()` callback (called in `async_setup_entry` after `_migrate_config_options`) auto-updates `unique_id` in the entity registry while preserving `entity_id`, history, and user customizations.
|
||||||
|
- **Repair issues**: After migration, a persistent repair (`is_persistent=True`) is created via `ir.async_create_issue()` informing users about breaking changes. Users must dismiss manually after reviewing automations.
|
||||||
|
- **Partial migration handling**: If new entity already exists (e.g., user added manually), the old entity is removed instead of migrated.
|
||||||
|
- **Separation of concerns**: `migrations.py` handles one-time upgrade migrations. `coordinator/repairs.py` handles runtime repairs (API issues, missing data). `__init__.py _migrate_config_options()` handles config option format changes.
|
||||||
|
- **Duration sensor pattern**: Use `native_unit_of_measurement=UnitOfTime.MINUTES` + `suggested_unit_of_measurement=UnitOfTime.HOURS` — HA auto-converts for display, state value stays in minutes for intuitive automations (`state < 15` instead of `state < 0.25`).
|
||||||
|
|
||||||
## Code Quality Rules
|
## Code Quality Rules
|
||||||
|
|
||||||
**CRITICAL: See "Linting Best Practices" section for comprehensive type checking (Pyright) and linting (Ruff) guidelines.**
|
**CRITICAL: See "Linting Best Practices" section for comprehensive type checking (Pyright) and linting (Ruff) guidelines.**
|
||||||
|
|
@ -2720,6 +2730,8 @@ After the sensor.py refactoring (completed Nov 2025), sensors are organized by *
|
||||||
|
|
||||||
5. **Sync all language files** (de, nb, nl, sv)
|
5. **Sync all language files** (de, nb, nl, sv)
|
||||||
|
|
||||||
|
6. **If renaming**: Add old→new key mapping to `ENTITY_KEY_RENAMES` in `migrations.py` for auto-migration
|
||||||
|
|
||||||
**See** `sensor/definitions.py` for sensor grouping examples and `sensor/core.py` for handler implementations.
|
**See** `sensor/definitions.py` for sensor grouping examples and `sensor/core.py` for handler implementations.
|
||||||
|
|
||||||
**Unified Handler Methods (Post-Refactoring):**
|
**Unified Handler Methods (Post-Refactoring):**
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
147
custom_components/tibber_prices/migrations.py
Normal file
147
custom_components/tibber_prices/migrations.py
Normal 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
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
275
docs/user/docs/community-examples.md
Normal file
275
docs/user/docs/community-examples.md
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
---
|
||||||
|
comments: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Community Examples
|
||||||
|
|
||||||
|
This page collects **real-world examples** contributed by the community — templates, automations, dashboard cards, and creative solutions built with Tibber Prices.
|
||||||
|
|
||||||
|
> **Before you start:** All examples require adaptation to your setup. At minimum, replace entity IDs like `sensor.<home_name>_...` with your own. See **Settings → Devices & Services → Entities** to find the correct IDs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Country-Specific Price Calculations
|
||||||
|
|
||||||
|
The Tibber API provides the raw spot price (`energy_price` attribute) and tax/fee component (`tax` attribute) on every price sensor. Since the exact composition of `tax` varies by country, you can use these attributes to build **your own** country-specific calculations with Home Assistant templates.
|
||||||
|
|
||||||
|
:::tip Why templates instead of built-in calculations?
|
||||||
|
Tax rates and energy fees change regularly (often annually). Using `input_number` helpers in Home Assistant keeps your calculations up-to-date with a simple UI adjustment — no integration update needed.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::caution Adapt values to your country
|
||||||
|
The tax rates and fees shown below are **examples only**. Verify them against your energy provider's invoices and update them when rates change (usually January 1st).
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🇳🇱 Netherlands: Solar Feed-In Compensation
|
||||||
|
|
||||||
|
*Contributed by community member OdynBrouwer ([Discussion #105](https://github.com/jpawlowski/hass.tibber_prices/discussions/105))*
|
||||||
|
|
||||||
|
### Background
|
||||||
|
|
||||||
|
In the Netherlands, the electricity price paid to consumers includes:
|
||||||
|
|
||||||
|
| Component | Dutch Name | Typical Value (2025) |
|
||||||
|
|-----------|-----------|---------------------|
|
||||||
|
| Spot price | Inkoopprijs | Variable (= `energy_price` attribute) |
|
||||||
|
| Energy tax | Energiebelasting | ~0.0916 €/kWh (excl. VAT) |
|
||||||
|
| VAT | BTW | 21% |
|
||||||
|
| Purchase fee | Inkoopvergoeding | ~0.0205 €/kWh |
|
||||||
|
| Sales fee | Verkoopvergoeding | ~0.0205 €/kWh |
|
||||||
|
|
||||||
|
:::warning Rates change annually
|
||||||
|
The values above are examples. Check [Rijksoverheid.nl](https://www.rijksoverheid.nl/onderwerpen/belastingplan/energiebelasting) for current energy tax rates and your energy contract for purchase/sales fees.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Saldering (Net Metering) — Until 2027
|
||||||
|
|
||||||
|
The Netherlands currently uses **saldering** (net metering): solar feed-in is offset against consumption at the full consumer price. This effectively means you earn the `total` price for each kWh exported. [The Dutch government has confirmed this ends in 2027.](https://www.rijksoverheid.nl/onderwerpen/duurzame-energie/zonne-energie)
|
||||||
|
|
||||||
|
### Step 1: Create Input Number Helpers
|
||||||
|
|
||||||
|
Create `input_number` helpers in Home Assistant for each fee component. This way, when rates change (usually January 1st), you only need to update the values in the UI.
|
||||||
|
|
||||||
|
**Settings → Devices & Services → Helpers → Create Helper → Number**
|
||||||
|
|
||||||
|
| Helper | Entity ID | Min | Max | Step | Unit | Example Value |
|
||||||
|
|--------|-----------|-----|-----|------|------|---------------|
|
||||||
|
| Energiebelasting | `input_number.energiebelasting` | 0 | 1 | 0.0001 | €/kWh | 0.0916 |
|
||||||
|
| BTW percentage | `input_number.btw_percentage` | 0 | 100 | 0.01 | % | 21 |
|
||||||
|
| Inkoopvergoeding | `input_number.inkoopvergoeding` | 0 | 1 | 0.0001 | €/kWh | 0.0205 |
|
||||||
|
| Verkoopvergoeding | `input_number.verkoopvergoeding` | 0 | 1 | 0.0001 | €/kWh | 0.0205 |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Alternative: YAML configuration for input_number helpers</summary>
|
||||||
|
|
||||||
|
If you prefer YAML configuration over the UI, add these to your `configuration.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
input_number:
|
||||||
|
energiebelasting:
|
||||||
|
name: Energiebelasting
|
||||||
|
min: 0
|
||||||
|
max: 1
|
||||||
|
step: 0.0001
|
||||||
|
unit_of_measurement: "€/kWh"
|
||||||
|
icon: mdi:lightning-bolt
|
||||||
|
btw_percentage:
|
||||||
|
name: BTW Percentage
|
||||||
|
min: 0
|
||||||
|
max: 100
|
||||||
|
step: 0.01
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
icon: mdi:percent
|
||||||
|
inkoopvergoeding:
|
||||||
|
name: Inkoopvergoeding
|
||||||
|
min: 0
|
||||||
|
max: 1
|
||||||
|
step: 0.0001
|
||||||
|
unit_of_measurement: "€/kWh"
|
||||||
|
icon: mdi:cash-minus
|
||||||
|
verkoopvergoeding:
|
||||||
|
name: Verkoopvergoeding
|
||||||
|
min: 0
|
||||||
|
max: 1
|
||||||
|
step: 0.0001
|
||||||
|
unit_of_measurement: "€/kWh"
|
||||||
|
icon: mdi:cash-plus
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Step 2: Template Sensors for Feed-In Compensation
|
||||||
|
|
||||||
|
These template sensors calculate what you **earn** per kWh when feeding solar power back to the grid.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show YAML: Template sensors for feed-in with and without saldering</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
template:
|
||||||
|
- sensor:
|
||||||
|
# Feed-in compensation WITH saldering (current rules, until 2027)
|
||||||
|
# With saldering, you effectively earn the full consumer price
|
||||||
|
# minus the purchase fee, plus the sales fee.
|
||||||
|
- name: "Solar Feed-In Price (with Saldering)"
|
||||||
|
unique_id: solar_feed_in_saldering
|
||||||
|
unit_of_measurement: "€/kWh"
|
||||||
|
device_class: monetary
|
||||||
|
state: >
|
||||||
|
{% set energy = state_attr('sensor.<home_name>_current_electricity_price', 'energy_price') %}
|
||||||
|
{% set eb = states('input_number.energiebelasting') | float %}
|
||||||
|
{% set btw = states('input_number.btw_percentage') | float / 100 %}
|
||||||
|
{% set inkoop = states('input_number.inkoopvergoeding') | float %}
|
||||||
|
{% set verkoop = states('input_number.verkoopvergoeding') | float %}
|
||||||
|
{% if energy is not none %}
|
||||||
|
{{ ((energy + eb) * (1 + btw) - inkoop + verkoop) | round(4) }}
|
||||||
|
{% else %}
|
||||||
|
unavailable
|
||||||
|
{% endif %}
|
||||||
|
icon: mdi:solar-power-variant
|
||||||
|
|
||||||
|
# Feed-in compensation WITHOUT saldering (after 2027)
|
||||||
|
# Without saldering, you only earn the raw spot price
|
||||||
|
# minus the purchase fee, plus the sales fee.
|
||||||
|
- name: "Solar Feed-In Price (without Saldering)"
|
||||||
|
unique_id: solar_feed_in_no_saldering
|
||||||
|
unit_of_measurement: "€/kWh"
|
||||||
|
device_class: monetary
|
||||||
|
state: >
|
||||||
|
{% set energy = state_attr('sensor.<home_name>_current_electricity_price', 'energy_price') %}
|
||||||
|
{% set inkoop = states('input_number.inkoopvergoeding') | float %}
|
||||||
|
{% set verkoop = states('input_number.verkoopvergoeding') | float %}
|
||||||
|
{% if energy is not none %}
|
||||||
|
{{ (energy - inkoop + verkoop) | round(4) }}
|
||||||
|
{% else %}
|
||||||
|
unavailable
|
||||||
|
{% endif %}
|
||||||
|
icon: mdi:solar-power-variant-outline
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Step 3: Use in Automations
|
||||||
|
|
||||||
|
Now you can use these sensors to make smarter decisions about when to export solar power vs. charge a battery:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show YAML: Smart export automation</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Solar: Smart Export Decision"
|
||||||
|
description: >
|
||||||
|
When solar production exceeds consumption, decide whether to
|
||||||
|
export power or charge the home battery based on current
|
||||||
|
feed-in compensation vs. upcoming price forecasts.
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.solar_production_power
|
||||||
|
above: 2000
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: >
|
||||||
|
{# Export if feed-in price is above the next 3 hours average #}
|
||||||
|
{% set feed_in = states('sensor.solar_feed_in_price_with_saldering') | float(0) %}
|
||||||
|
{% set upcoming = states('sensor.<home_name>_next_3h_average_price') | float(0) %}
|
||||||
|
{{ feed_in > upcoming }}
|
||||||
|
action:
|
||||||
|
- service: switch.turn_off
|
||||||
|
entity_id: switch.battery_charging
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Preparing for the End of Saldering
|
||||||
|
|
||||||
|
To understand the financial impact of the saldering phase-out, you can create a dashboard comparing both scenarios side by side:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show YAML: Dashboard comparison card</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: entities
|
||||||
|
title: "Solar Feed-In Compensation Comparison"
|
||||||
|
entities:
|
||||||
|
- entity: sensor.<home_name>_current_electricity_price
|
||||||
|
name: "Consumer Price (total)"
|
||||||
|
- type: attribute
|
||||||
|
entity: sensor.<home_name>_current_electricity_price
|
||||||
|
attribute: energy_price
|
||||||
|
name: "Spot Price (energy)"
|
||||||
|
icon: mdi:transmission-tower
|
||||||
|
- entity: sensor.solar_feed_in_price_with_saldering
|
||||||
|
name: "Feed-In with Saldering"
|
||||||
|
icon: mdi:solar-power-variant
|
||||||
|
- entity: sensor.solar_feed_in_price_no_saldering
|
||||||
|
name: "Feed-In without Saldering (2027+)"
|
||||||
|
icon: mdi:solar-power-variant-outline
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🇩🇪 Germany: Price Composition
|
||||||
|
|
||||||
|
### Background
|
||||||
|
|
||||||
|
In Germany, the electricity price includes numerous components bundled into `tax`:
|
||||||
|
|
||||||
|
| Component | German Name | Description |
|
||||||
|
|-----------|-----------|-------------|
|
||||||
|
| Spot price | Börsenstrompreis | Variable (= `energy_price` attribute) |
|
||||||
|
| Grid fees | Netzentgelte | Varies by grid operator |
|
||||||
|
| Electricity tax | Stromsteuer | Fixed per kWh |
|
||||||
|
| Concession fee | Konzessionsabgabe | Varies by municipality |
|
||||||
|
| Surcharges | Umlagen (§19, Offshore, KWKG) | Various regulatory surcharges |
|
||||||
|
| VAT | Mehrwertsteuer | 19% |
|
||||||
|
|
||||||
|
### Template: Spot Price Share
|
||||||
|
|
||||||
|
A simple template sensor showing what percentage of your total price is the actual energy cost:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Show YAML: Spot price share template sensor</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
template:
|
||||||
|
- sensor:
|
||||||
|
- name: "Spot Price Share"
|
||||||
|
unique_id: spot_price_share
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
state: >
|
||||||
|
{% set energy = state_attr('sensor.<home_name>_current_electricity_price', 'energy_price') %}
|
||||||
|
{% set total = states('sensor.<home_name>_current_electricity_price') | float %}
|
||||||
|
{% if energy is not none and total > 0 %}
|
||||||
|
{{ ((energy / total) * 100) | round(1) }}
|
||||||
|
{% else %}
|
||||||
|
unavailable
|
||||||
|
{% endif %}
|
||||||
|
icon: mdi:chart-pie
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🇳🇴 Norway / 🇸🇪 Sweden: Grid & Tax Components
|
||||||
|
|
||||||
|
Norway and Sweden have their own fee structures, but the same pattern applies — use `input_number` helpers for the fixed/semi-fixed components and `energy_price` for the spot price.
|
||||||
|
|
||||||
|
**Contributions welcome!** If you have working template examples for Norway or Sweden, please share them in a [GitHub Discussion](https://github.com/jpawlowski/hass.tibber_prices/discussions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing Your Own Examples
|
||||||
|
|
||||||
|
Have a useful template, automation, or dashboard card built with Tibber Prices? We'd love to feature it here!
|
||||||
|
|
||||||
|
1. Share it in a [GitHub Discussion](https://github.com/jpawlowski/hass.tibber_prices/discussions)
|
||||||
|
2. Describe your use case and include the YAML code
|
||||||
|
3. Tested examples that work with the current version are preferred
|
||||||
|
|
||||||
|
Community examples are attributed to their original authors.
|
||||||
|
|
@ -549,6 +549,10 @@ chips:
|
||||||
🏛️ {{ state_attr('sensor.<home_name>_price_today', 'tax_mean') | round(1) }} ct
|
🏛️ {{ state_attr('sensor.<home_name>_price_today', 'tax_mean') | round(1) }} ct
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Country-Specific Calculations
|
||||||
|
|
||||||
|
The composition of the `tax` field varies by country (Norway, Sweden, Germany, Netherlands each have different fee structures). For detailed examples of how to build country-specific calculations using `input_number` helpers and template sensors — including **Dutch solar feed-in compensation (saldering)** — see the **[Community Examples](community-examples.md#country-specific-price-calculations)** page.
|
||||||
|
|
||||||
### In Chart Data Actions
|
### In Chart Data Actions
|
||||||
|
|
||||||
The `energy_price` and `tax` fields are also available in the `get_chartdata` action. See [Actions — Energy & Tax Fields](./actions.md#energy--tax-fields-in-get_chartdata) for details.
|
The `energy_price` and `tax` fields are also available in the `get_chartdata` action. See [Actions — Energy & Tax Fields](./actions.md#energy--tax-fields-in-get_chartdata) for details.
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,14 @@ const sidebars: SidebarsConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: '🔧 Help & Support',
|
label: '<27> Community',
|
||||||
|
items: ['community-examples'],
|
||||||
|
collapsible: true,
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: '<27>🔧 Help & Support',
|
||||||
items: ['faq', 'troubleshooting'],
|
items: ['faq', 'troubleshooting'],
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue