Compare commits

...

3 commits

Author SHA1 Message Date
Julian Pawlowski
5314454a26 docs(user): add community examples page with NL solar feed-in templates
Some checks are pending
Deploy Docusaurus Documentation (Dual Sites) / Build and Deploy Documentation Sites (push) Waiting to run
Lint / Ruff (push) Waiting to run
Validate / Hassfest validation (push) Waiting to run
Validate / HACS validation (push) Waiting to run
New dedicated page for community-contributed examples, starting with
Dutch solar feed-in compensation (saldering) patterns adapted from
GitHub Discussion #105 (OdynBrouwer). Includes input_number helpers
for country-specific tax rates, template sensors for feed-in with
and without saldering, smart export automation, and dashboard card.
German spot price share template as starting point. Norwegian/Swedish
sections as placeholders for future contributions.

YAML code blocks use collapsible details elements to keep the page
scannable. Page is framed generically to accommodate any type of
community example in the future (not just country-specific).

Added cross-reference from sensors.md Energy Price section and new
"Community" sidebar category.

Impact: Users (especially Netherlands) can find ready-to-adapt template
examples for country-specific price calculations using the energy_price
and tax attributes. Framework ready for additional community examples.
2026-04-10 14:57:02 +00:00
Julian Pawlowski
6e7b7b3ceb docs(agents): document entity migration & repairs pattern
Added Entity Migration & Breaking Change Repairs section (Common Pitfalls #6)
documenting the separation between migrations.py, coordinator/repairs.py, and
_migrate_config_options().

Additional changes:
- migrations.py added to allowed root files list and component structure tree
- Common Tasks "Add a new sensor": step 6 for ENTITY_KEY_RENAMES registration
- Duration sensor pattern (native=MINUTES, suggested=HOURS) documented

Impact: Future sessions generate correct migration code on first try without
rediscovering the pattern.
2026-04-10 12:21:55 +00:00
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
11 changed files with 471 additions and 2 deletions

View file

@ -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):**

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

View 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.

View file

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

View file

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