Compare commits

...

13 commits

Author SHA1 Message Date
Julian Pawlowski
6937068a7a chore(release): bump version to 0.32.0b1
Some checks failed
Deploy Docusaurus Documentation (Dual Sites) / Build and Deploy Documentation Sites (push) Has been cancelled
Lint / Ruff (push) Has been cancelled
Auto-Tag on Version Bump / Check and create version tag (push) Has been cancelled
Validate / Hassfest validation (push) Has been cancelled
Validate / HACS validation (push) Has been cancelled
2026-05-30 14:48:01 +00:00
Julian Pawlowski
b69be7e87a docs(actions): document success flag and outage-vs-no-data contract
Explain the top-level success flag and the price_data_unavailable reason for
get_price, find_cheapest_*, and plan_charging, and clarify that an empty
result with success=true (e.g. tomorrow's prices not yet published) is not an
error. Note that day-ahead prices usually arrive around 13:00 but can be
delayed into the afternoon or evening.

Impact: Automation authors can reliably tell a temporary API outage apart from
normally missing data.
2026-05-30 14:46:40 +00:00
Julian Pawlowski
ada4e33448 feat(coordinator): surface prolonged API outages and cache-fallback state
Track API failures over time in the repair manager and raise an api_outage
repair issue once fresh data has been missing for longer than two hours,
independent of the update interval. A genuine successful fetch clears it;
serving cached data as a fallback keeps the outage active. Expose the degraded
cache-fallback state via the connection binary sensor (using_cached_data,
last_successful_update) so users can tell when sensors run on locally cached
prices. Add the api_outage repair strings to all supported languages.

Impact: Users get a clear repair notice during prolonged Tibber outages and
can see when the integration is running on cached data.
2026-05-30 14:46:32 +00:00
Julian Pawlowski
feaf748cb6 feat(services): isolate price-fetch failures and add success flag to responses
Add a track_degraded parameter to IntervalPool.get_intervals so service calls
(track_degraded=False) never flip the coordinator's sensor-health flags or use
the covers-current-interval cache fallback; authentication errors still
propagate. All price-fetching services now go through
async_fetch_service_intervals and return a structured response with a
top-level success flag: success=false with reason="price_data_unavailable" on
a real API outage, and success=true (possibly empty) otherwise.

Impact: Action calls (get_price, find_cheapest_*, plan_charging) always return
the expected shape, never impair sensors during a Tibber outage, and let
automations distinguish an outage from "no data yet".
2026-05-30 14:46:23 +00:00
Julian Pawlowski
8da3083fb2 fix(api): accept priceInfoRange-only responses and retry transient empty data
Some markets/accounts (e.g. NL hourly) return an empty
priceInfo(QUARTER_HOURLY).today while priceInfoRange still delivers the full
set of intervals. _check_price_info_empty required today's data and wrongly
classified such complete responses as empty, which blocked setup. It now
treats a response as valid when today OR yesterday data is present, since
priceInfoRange is the authoritative source the interval pool uses.

Additionally, "Empty data received" is now retried a few times with
exponential backoff, smoothing brief Tibber outages that transiently return
empty data within a single update cycle. Long outages are still absorbed by
the IntervalPool cache fallback, so the in-request retry count stays small.

Reported in #141.

Impact: Setup and updates no longer fail with "Empty data received" for NL
hourly accounts (and similar), and short Tibber blips are smoothed over.
2026-05-30 14:46:13 +00:00
github-actions[bot]
a4a1243a36 docs: add version snapshot v0.31.0 and cleanup old versions [skip ci] 2026-05-30 13:07:40 +00:00
Julian Pawlowski
b28d30f3fb chore(release): bump version to 0.31.0 2026-05-30 13:05:45 +00:00
Julian Pawlowski
a27d04d1c8 Merge branch 'main' of https://github.com/jpawlowski/hass.tibber_prices 2026-05-30 13:05:09 +00:00
Julian Pawlowski
d45fe0b14c docs(sensor-reference): update translations for price phase sensors
Updated the translations for various price phase sensors in the sensor reference documentation to ensure consistency and accuracy across multiple languages.

Impact: Users will see improved clarity in the documentation for price phase sensors in their preferred language.
2026-05-30 13:04:55 +00:00
Julian Pawlowski
6ec3318941 docs(actions): mark scheduling actions and plan_charging as experimental
Add warnings to indicate that scheduling actions and the plan_charging action are experimental and subject to change. This informs users to use them with caution and report any issues.

Impact: Users are aware of the experimental nature of these features, promoting careful usage.
2026-05-30 12:41:48 +00:00
github-actions[bot]
f7bb6dbfe8 chore(release): sync manifest.json with tag v0.31.0b2 2026-05-30 12:33:40 +00:00
Julian Pawlowski
82a5290fe7 chore(deps): update dependency versions and add update script for Home Assistant
Updated the pytest-homeassistant-custom-component version to match the Home Assistant versioning scheme. Added a script to automate the update process for Home Assistant versions across the development environment.

Impact: Developers can now easily update Home Assistant versions and corresponding dependencies with a single command.
2026-05-30 12:19:04 +00:00
dependabot[bot]
f215bdb248
chore(deps): update pytest-homeassistant-custom-component requirement (#149)
Updates the requirements on [pytest-homeassistant-custom-component](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component) to permit the latest version.
- [Release notes](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/releases)
- [Changelog](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/compare/0.13.326...0.13.334)

---
updated-dependencies:
- dependency-name: pytest-homeassistant-custom-component
  dependency-version: 0.13.334
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-30 13:46:01 +02:00
96 changed files with 17087 additions and 853 deletions

View file

@ -0,0 +1,34 @@
{
"features": {
"ghcr.io/devcontainer-community/devcontainer-features/yq:1": {
"version": "1.0.3",
"resolved": "ghcr.io/devcontainer-community/devcontainer-features/yq@sha256:9e73b838a7d821f89c8020b51c2605aa4174811d152c70b43f2e98a0f8413d35",
"integrity": "sha256:9e73b838a7d821f89c8020b51c2605aa4174811d152c70b43f2e98a0f8413d35"
},
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"version": "1.0.6",
"resolved": "ghcr.io/devcontainers-extra/features/apt-packages@sha256:55c54412112da81b9381e470cdbbe55278564950d1ff536ce925b1e8e096babd",
"integrity": "sha256:55c54412112da81b9381e470cdbbe55278564950d1ff536ce925b1e8e096babd"
},
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "1.1.0",
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
},
"ghcr.io/devcontainers/features/node:2": {
"version": "2.0.0",
"resolved": "ghcr.io/devcontainers/features/node@sha256:fedd4c11f7adfb64283b578dddc7da906728daa25fa293351c9d913231acf12f",
"integrity": "sha256:fedd4c11f7adfb64283b578dddc7da906728daa25fa293351c9d913231acf12f"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.5.0",
"resolved": "ghcr.io/devcontainers/features/rust@sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c",
"integrity": "sha256:0c55e65f2e3df736e478f26ee4d5ed41bae6b54dac1318c443e31444c8ed283c"
},
"ghcr.io/flexwie/devcontainer-features/op:1": {
"version": "1.0.0",
"resolved": "ghcr.io/flexwie/devcontainer-features/op@sha256:68098ff0ac2447bcf0eeee7ac19123bd0375fd43330ee3c9ca4dee510b74c6d3",
"integrity": "sha256:68098ff0ac2447bcf0eeee7ac19123bd0375fd43330ee3c9ca4dee510b74c6d3"
}
}
}

View file

@ -44,3 +44,8 @@ updates:
ignore:
# Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json
- dependency-name: "homeassistant"
# Each phac version pins exactly one HA version (1:1 mapping), so it must be
# updated manually together with HA_VERSION in scripts/setup/bootstrap
- dependency-name: "pytest-homeassistant-custom-component"
# pytest version is pinned by phac; updating it independently causes conflicts
- dependency-name: "pytest"

View file

@ -51,6 +51,19 @@ class TibberPricesApiClient:
self._max_retries = 5
self._retry_delay = 2 # Base retry delay in seconds
# Empty-data responses are usually permanent (no data for the requested
# range), but during a Tibber outage the API can return empty data
# transiently before recovering. Retry a small number of times with backoff
# so a brief blip is smoothed out within a single update cycle.
#
# NOTE: This is NOT the mechanism that bridges multi-hour outages. Long
# outages are absorbed by the IntervalPool cache fallback (see
# interval_pool/manager.py): as long as cached intervals cover the current
# interval, the integration keeps serving cached prices and retries on the
# NEXT update cycle. That bridge is unbounded in time, so this in-request
# retry count must stay small to avoid blocking the event loop.
self._max_empty_data_retries = 2
# Timeout configuration - more granular control
self._connect_timeout = 10 # Connection timeout in seconds
self._request_timeout = 25 # Total request timeout in seconds
@ -834,9 +847,17 @@ class TibberPricesApiClient:
"""Handle retry logic for API-specific errors."""
error_msg = str(error)
# Non-retryable: Invalid queries, bad requests, empty data
# Empty data means API has no data for the requested range - retrying won't help
if "Invalid GraphQL query" in error_msg or "Bad request" in error_msg or "Empty data received" in error_msg:
# Non-retryable: Invalid queries and bad requests are permanent client errors.
if "Invalid GraphQL query" in error_msg or "Bad request" in error_msg:
return False, 0
# Empty data is usually permanent (API has no data for the requested range),
# but can be returned transiently during a Tibber outage. Retry a limited
# number of times with exponential backoff before giving up.
if "Empty data received" in error_msg:
if retry < self._max_empty_data_retries:
delay = min(self._retry_delay * (2**retry), 30)
return True, delay
return False, 0
# Rate limits - only retry if server explicitly says so

View file

@ -219,9 +219,13 @@ def _check_price_info_empty(data: dict) -> bool:
and len(subscription["priceInfo"]["today"]) > 0
)
# Only require today's data - yesterday is optional
# If subscription exists but no today data, that's a structural problem
is_empty = not has_today
# Consider the response valid if EITHER today's intervals OR the priceInfoRange
# edges are present. Some markets/accounts (e.g. NL hourly accounts) return an
# empty priceInfo(QUARTER_HOURLY).today while priceInfoRange still delivers the
# full set of intervals. Requiring today's data alone would wrongly classify
# such a complete response as "empty" and block setup. priceInfoRange is the
# authoritative source the interval pool uses, so its presence means we have data.
is_empty = not has_today and not has_yesterday
_LOGGER_DETAILS.debug(
"Price info check - priceInfoRange: %s, today: %s, is_empty: %s",

View file

@ -50,6 +50,8 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
# Frequently Changing Diagnostics
"icon_color",
"data_status",
# Connection diagnostics (time-bound, not useful in long-term history)
"last_successful_update",
# Static/Rarely Changing
"level_value",
"rating_value",
@ -317,11 +319,28 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
if key == "tomorrow_data_available":
return self._get_tomorrow_data_available_attributes()
if key == "connection":
return self._get_connection_attributes()
if key in ("in_rising_price_phase", "in_falling_price_phase", "in_flat_price_phase"):
return get_phase_attributes(self.coordinator.data, time=self.coordinator.time)
return None
def _get_connection_attributes(self) -> dict | None:
"""
Build attributes for the connection sensor.
Distinguishes a healthy connection from degraded cache-fallback operation,
so users can tell when the integration is running on locally cached data
during a temporary Tibber API outage.
"""
last_update = self.coordinator.last_successful_update
return {
"using_cached_data": self.coordinator.using_cached_fallback,
"last_successful_update": last_update.isoformat() if last_update else None,
}
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""

View file

@ -246,6 +246,11 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._is_fetching: bool = False # Flag to track active API fetch (read by lifecycle sensor)
self._last_coordinator_update: datetime | None = None # When Timer #1 last ran (_async_update_data)
# Degraded-mode tracking (cache fallback during API outage).
# True when the last update served cached data because the API fetch failed
# but cached data still covered the current interval. Exposed via connection sensor.
self._using_cached_fallback: bool = False
# Runtime config overrides from config entities (number/switch)
# Structure: {"section_name": {"config_key": value, ...}, ...}
# When set, these override the corresponding options from config_entry.options
@ -260,6 +265,16 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
prefixed_message = f"{self._log_prefix} {message}"
getattr(_LOGGER, level)(prefixed_message, *args, **kwargs)
@property
def using_cached_fallback(self) -> bool:
"""Return True if the last update served cached data due to an API failure."""
return self._using_cached_fallback
@property
def last_successful_update(self) -> datetime | None:
"""Return when price data was last successfully fetched from the API."""
return self._last_price_update
async def _handle_options_update(self, _hass: HomeAssistant, _config_entry: ConfigEntry) -> None:
"""Handle options update by invalidating config caches and re-transforming data."""
self._log("debug", "Options update triggered, re-transforming data")
@ -774,6 +789,11 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# Track rate limit errors for repair system
await self._track_rate_limit_error(err)
# Track the API outage for the time-based outage repair.
# This branch means the IntervalPool could NOT serve cached data either
# (no fallback possible), so this is a genuine, user-impacting failure.
await self._repair_manager.track_api_failure(current_time)
# Handle API error - will re-raise as ConfigEntryAuthFailed or UpdateFailed
# Note: With IntervalPool, there's no local cache fallback here.
# The Pool has its own persistence for offline recovery.
@ -830,6 +850,18 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# 3. Clear rate limit tracking on successful API call
await self._repair_manager.clear_rate_limit_tracking()
# 4. Update degraded-mode flag and outage tracking.
# The IntervalPool flags itself as degraded when it served cached data because
# a fetch failed. In that case the outage is still ongoing (we just cushioned
# it with cache), so we keep tracking it for the time-based outage repair.
# Only a genuine fresh fetch clears the outage.
degraded = self.interval_pool.last_fetch_degraded
self._using_cached_fallback = degraded
if degraded:
await self._repair_manager.track_api_failure(current_time)
else:
await self._repair_manager.clear_api_failure_tracking()
async def load_cache(self) -> None:
"""Load cached user data from storage (price data is in IntervalPool)."""
await self._price_data_manager.load_cache()

View file

@ -8,10 +8,12 @@ Repair Types:
1. Tomorrow Data Missing - Warns when tomorrow's price data is unavailable after 18:00
2. Persistent Rate Limits - Warns when API rate limiting persists after multiple errors
3. Home Not Found - Warns when a home no longer exists in the Tibber account
4. API Outage - Warns when the API has been unreachable for a prolonged period (time-based)
"""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
@ -29,6 +31,14 @@ _LOGGER = logging.getLogger(__name__)
TOMORROW_DATA_WARNING_HOUR = 18 # Warn after 18:00 if tomorrow data missing
RATE_LIMIT_WARNING_THRESHOLD = 3 # Warn after 3 consecutive rate limit errors
# How long the integration silently cushions an API outage (serving cached data
# or hard-failing) before surfacing a repair issue. The cushioning itself is
# unbounded: as long as cached data covers the current interval, sensors keep
# working across every update cycle. This delay only governs WHEN we inform the
# user about a prolonged outage, independent of the update interval or how often
# the API was retried.
OUTAGE_REPAIR_DELAY = timedelta(hours=2)
class TibberPricesRepairManager:
"""Manage repair issues for Tibber Prices integration."""
@ -50,10 +60,15 @@ class TibberPricesRepairManager:
# Track consecutive rate limit errors
self._rate_limit_error_count = 0
# Track when an ongoing API outage started (None = no active outage).
# Set on the first failed/degraded update, cleared on a genuine success.
self._outage_since: datetime | None = None
# Track if repairs are currently active
self._tomorrow_data_repair_active = False
self._rate_limit_repair_active = False
self._home_not_found_repair_active = False
self._outage_repair_active = False
async def check_tomorrow_data_availability(
self,
@ -105,6 +120,41 @@ class TibberPricesRepairManager:
if self._rate_limit_repair_active:
await self._clear_rate_limit_repair()
async def track_api_failure(self, current_time: datetime) -> None:
"""
Track an ongoing API outage and surface a repair after a prolonged period.
Call this on every update cycle where the integration could NOT fetch fresh
data - whether it served cached data as a fallback (degraded) or failed
outright (no cache). The first such call records the outage start time; once
the outage has lasted longer than ``OUTAGE_REPAIR_DELAY`` a repair issue is
created. This is time-based (not retry/cycle-count based) so it reflects the
real outage duration regardless of the update interval.
Args:
current_time: Current time of this update cycle.
"""
if self._outage_since is None:
self._outage_since = current_time
outage_duration = current_time - self._outage_since
if outage_duration >= OUTAGE_REPAIR_DELAY and not self._outage_repair_active:
await self._create_outage_repair()
async def clear_api_failure_tracking(self) -> None:
"""
Clear outage tracking after a genuinely successful API fetch.
Resets the outage start time and clears any active outage repair. Call this
only when fresh data was actually received (NOT when serving cached data as
a fallback, which still counts as an ongoing outage).
"""
self._outage_since = None
if self._outage_repair_active:
await self._clear_outage_repair()
async def create_home_not_found_repair(self) -> None:
"""
Create repair for home no longer found in Tibber account.
@ -160,6 +210,8 @@ class TibberPricesRepairManager:
await self._clear_rate_limit_repair()
if self._home_not_found_repair_active:
await self.clear_home_not_found_repair()
if self._outage_repair_active:
await self._clear_outage_repair()
async def _create_tomorrow_data_repair(self) -> None:
"""Create repair issue for missing tomorrow data."""
@ -226,3 +278,37 @@ class TibberPricesRepairManager:
f"rate_limit_exceeded_{self._entry_id}",
)
self._rate_limit_repair_active = False
async def _create_outage_repair(self) -> None:
"""Create repair issue for a prolonged API outage."""
since = self._outage_since.isoformat(timespec="minutes") if self._outage_since else "unknown"
_LOGGER.warning(
"Prolonged Tibber API outage for home '%s' (no fresh data since %s) - creating repair issue",
self._home_name,
since,
)
ir.async_create_issue(
self._hass,
DOMAIN,
f"api_outage_{self._entry_id}",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="api_outage",
translation_placeholders={
"home_name": self._home_name,
"since": since,
},
)
self._outage_repair_active = True
async def _clear_outage_repair(self) -> None:
"""Clear API outage repair issue."""
_LOGGER.debug("Tibber API reachable again for '%s' - clearing outage repair issue", self._home_name)
ir.async_delete_issue(
self._hass,
DOMAIN,
f"api_outage_{self._entry_id}",
)
self._outage_repair_active = False

View file

@ -274,32 +274,32 @@
},
"price_trajectory_3h": {
"description": "Preisverlauf innerhalb des nächsten 3-Stunden-Fensters",
"long_description": "Compares the average of the first 1.5 hours with the average of the second 1.5 hours within the next 3-hour window. 'rising' = prices are climbing over the 3h window; 'falling' = prices are dropping. Reveals the direction of price movement independent of the current price level. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"long_description": "Vergleicht den Durchschnitt der ersten 1,5 Stunden mit dem Durchschnitt der zweiten 1,5 Stunden innerhalb des nächsten 3-Stunden-Fensters. 'rising' = die Preise steigen über das 3-Stunden-Fenster; 'falling' = die Preise fallen. Zeigt die Richtung der Preisbewegung unabhängig vom aktuellen Preisniveau. Steigend/fallend bei ±3 %, stark bei ±9 % (konfigurierbar, volatilitätsabhängig).",
"usage_tips": "Appliance timing: 'outlook: strongly_falling + trajectory: rising' = you're at or past the minimum, the window average is low but prices are already recovering — start now."
},
"price_trajectory_4h": {
"description": "Preisverlauf innerhalb des nächsten 4-Stunden-Fensters",
"long_description": "Compares the average of the first 2 hours with the average of the second 2 hours within the next 4-hour window. 'rising' = prices are climbing over the 4h window; 'falling' = prices are dropping. Complementary to price_outlook_4h: outlook answers 'is NOW cheap vs the window average?', trajectory answers 'are prices rising or falling within the window?'. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"long_description": "Vergleicht den Durchschnitt der ersten 2 Stunden mit dem Durchschnitt der zweiten 2 Stunden innerhalb des nächsten 4-Stunden-Fensters. 'rising' = die Preise steigen über das 4-Stunden-Fenster; 'falling' = die Preise fallen. Ergänzend zu price_outlook_4h: outlook beantwortet 'ist JETZT günstig gegenüber dem Fensterdurchschnitt?', trajectory beantwortet 'steigen oder fallen die Preise innerhalb des Fensters?'. Steigend/fallend bei ±3 %, stark bei ±9 % (konfigurierbar, volatilitätsabhängig).",
"usage_tips": "Heat pump charging: 'rising' = first half window is cheaper, charge now before prices climb. 'falling' = second half window is cheaper, wait if you can. Combine with outlook: if both rising, very strong signal to act now."
},
"price_trajectory_5h": {
"description": "Preisverlauf innerhalb des nächsten 5-Stunden-Fensters",
"long_description": "Compares the average of the first 2.5 hours with the average of the second 2.5 hours within the next 5-hour window. 'rising' = prices are climbing over the 5h window; 'falling' = prices are dropping. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"long_description": "Vergleicht den Durchschnitt der ersten 2,5 Stunden mit dem Durchschnitt der zweiten 2,5 Stunden innerhalb des nächsten 5-Stunden-Fensters. 'rising' = die Preise steigen über das 5-Stunden-Fenster; 'falling' = die Preise fallen. Steigend/fallend bei ±3 %, stark bei ±9 % (konfigurierbar, volatilitätsabhängig).",
"usage_tips": "Eco/long cycles: 'outlook: rising + trajectory: rising' = clear signal to start now. 'outlook: falling + trajectory: rising' = you're near the bottom, good time to start before costs climb."
},
"price_trajectory_6h": {
"description": "Preisverlauf innerhalb des nächsten 6-Stunden-Fensters",
"long_description": "Compares the average of the first 3 hours with the average of the second 3 hours within the next 6-hour window. 'rising' = prices are climbing over the 6h window; 'falling' = prices are dropping. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"long_description": "Vergleicht den Durchschnitt der ersten 3 Stunden mit dem Durchschnitt der zweiten 3 Stunden innerhalb des nächsten 6-Stunden-Fensters. 'rising' = die Preise steigen über das 6-Stunden-Fenster; 'falling' = die Preise fallen. Steigend/fallend bei ±3 %, stark bei ±9 % (konfigurierbar, volatilitätsabhängig).",
"usage_tips": "Evening/overnight planning: 'falling' at evening peak = overnight will be cheaper, postpone. 'rising' in the morning = current morning prices are the day's low — good time for high consumption."
},
"price_trajectory_8h": {
"description": "Preisverlauf innerhalb des nächsten 8-Stunden-Fensters",
"long_description": "Compares the average of the first 4 hours with the average of the second 4 hours within the next 8-hour window. 'rising' = prices are climbing over the 8h window; 'falling' = prices are dropping. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"long_description": "Vergleicht den Durchschnitt der ersten 4 Stunden mit dem Durchschnitt der zweiten 4 Stunden innerhalb des nächsten 8-Stunden-Fensters. 'rising' = die Preise steigen über das 8-Stunden-Fenster; 'falling' = die Preise fallen. Steigend/fallend bei ±3 %, stark bei ±9 % (konfigurierbar, volatilitätsabhängig).",
"usage_tips": "Overnight charging: 'rising' during evening = first half of night is cheapest, start charging earlier. 'falling' = second half of night will be cheapest, delay start."
},
"price_trajectory_12h": {
"description": "Preisverlauf innerhalb des nächsten 12-Stunden-Fensters",
"long_description": "Compares the average of the first 6 hours with the average of the second 6 hours within the next 12-hour window. 'rising' = prices are climbing over the 12h window; 'falling' = prices are dropping. Rising/falling at ±3%, strongly at ±9% (configurable, volatility-adaptive).",
"long_description": "Vergleicht den Durchschnitt der ersten 6 Stunden mit dem Durchschnitt der zweiten 6 Stunden innerhalb des nächsten 12-Stunden-Fensters. 'rising' = die Preise steigen über das 12-Stunden-Fenster; 'falling' = die Preise fallen. Steigend/fallend bei ±3 %, stark bei ±9 % (konfigurierbar, volatilitätsabhängig).",
"usage_tips": "Day-ahead planning: 'rising' at midnight = first half of day is cheaper, schedule morning loads. 'falling' = prices drop in the second part of the day, afternoon/evening scheduling is better."
},
"current_price_trend": {
@ -512,54 +512,54 @@
"usage_tips": "Nutzen in Automationen: 'Wenn next_price_phase = steigend und next_price_phase.start in weniger als 1 Stunde, Waschmaschine jetzt starten'. Oder kombiniere mit current_price_phase: 'Wenn current_price_phase = fallend und next_price_phase = flach, nähern wir uns dem Tagestiefpunkt guter Zeitpunkt für flexible Lasten'. Das Attribut start ist besonders wertvoll: Automationen können exakt dann ausgelöst werden, wenn die nächste Phase beginnt."
},
"current_price_phase_end_time": {
"description": "When the current intra-day price phase ends",
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
"description": "Wann die aktuelle Preisphase endet",
"long_description": "Zeigt den genauen Zeitstempel, wann die aktuell aktive steigende, fallende oder flache Preisphase endet und in die nächste Phase übergeht. Wird nicht verfügbar, wenn keine Segmentdaten vorliegen. Aktualisiert sich an Intervallgrenzen (alle 15 Minuten). Kombiniere mit current_price_phase, um sowohl die aktuelle Phase als auch deren Ende zu kennen.",
"usage_tips": "Nutze es in Automationen, um Aufgaben zu planen, die vor einer Preisänderung abgeschlossen sein müssen: 'Starte die Waschmaschine, wenn current_price_phase = falling und current_price_phase_end_time mehr als 2 Stunden entfernt ist'. Oder um zu warnen, wenn eine günstige Phase bald endet."
},
"current_price_phase_remaining_minutes": {
"description": "Minutes remaining in the current price phase",
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
"description": "Verbleibende Minuten in der aktuellen Preisphase",
"long_description": "Zeigt, wie viele Minuten in der aktuellen Preisphase (steigend, fallend oder flach) noch übrig sind. Aktualisiert sich jede Minute für präzise Countdowns. Gibt 0 zurück, wenn keine Segmentdaten vorliegen. Das Attribut remaining_minutes spiegelt den Sensorwert in ganzen Minuten für einfachere Automations-Templates wider.",
"usage_tips": "Nutze es in Automationen: 'Wenn current_price_phase = falling und current_price_phase_remaining_minutes < 30, starte jetzt den Geschirrspüler, bevor sich die Preise stabilisieren'. Auch nützlich für Dashboard-Karten mit einem Countdown-Balken bis zum Phasenwechsel."
},
"current_price_phase_duration": {
"description": "Total duration of the current price phase",
"long_description": "Shows the total length of the currently active price phase in hours.",
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
"description": "Gesamtdauer der aktuellen Preisphase",
"long_description": "Zeigt die Gesamtlänge der aktuell aktiven Preisphase in Stunden (intern in Minuten ausgedrückt). Aktualisiert sich an Intervallgrenzen. Das zeigt dir, wie lange das aktuelle Trendsegment insgesamt dauert — nützlich, um zu verstehen, ob es eine kurze Schwankung oder eine längere Phase steigender oder fallender Preise ist.",
"usage_tips": "Kombiniere mit current_price_phase_remaining_minutes, um abzuschätzen, wie weit du in der Phase bist, oder vergleiche mit current_price_phase_progress, um das Zeitprofil des aktuellen Trends zu verstehen."
},
"current_price_phase_progress": {
"description": "How far through the current price phase we are",
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0100%). Updates every minute.",
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
"description": "Wie weit die aktuelle Preisphase fortgeschritten ist",
"long_description": "Zeigt den Prozentsatz der aktuellen Preisphase, der bereits verstrichen ist (0100 %). Aktualisiert sich jede Minute. Ein Wert nahe 0 % bedeutet, dass die Phase gerade begonnen hat; nahe 100 % bedeutet, dass sie bald endet. Gibt 0 zurück, wenn keine Segmentdaten vorliegen.",
"usage_tips": "Nutze es in Dashboard-Karten, um einen visuellen Fortschrittsbalken für die aktuelle Preisphase anzuzeigen. In Automationen: 'Wenn current_price_phase = falling und current_price_phase_progress > 80, sind die günstigsten Preise nah — bereite jetzt flexible Verbraucher vor'."
},
"next_rising_phase_start_time": {
"description": "When the next rising price phase begins",
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to schedule loads before prices start rising again."
"description": "Wann die nächste steigende Preisphase beginnt",
"long_description": "Zeigt den Zeitstempel des nächsten anstehenden steigenden Preissegments über die verbleibenden Phasen von heute und die Phasen von morgen. Gibt 'nicht verfügbar' zurück, wenn in den Daten von heute oder morgen keine steigenden Phasen mehr existieren. Aktualisiert sich an Intervallgrenzen (alle 15 Minuten).",
"usage_tips": "Nutze in_falling_price_phase und next_rising_phase_start_time, um flexible Verbraucher zu planen: Lass sie jetzt laufen, solange die Preise fallen, und beende sie vor dem nächsten Anstieg. 'Wenn in_falling_price_phase ON ist und next_rising_phase_start_time weniger als 1 Stunde entfernt ist, starte die Waschmaschine sofort'."
},
"next_falling_phase_start_time": {
"description": "When the next falling price phase begins",
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to delay flexible loads until the next price drop starts."
"description": "Wann die nächste fallende Preisphase beginnt",
"long_description": "Zeigt den Zeitstempel des nächsten anstehenden fallenden Preissegments über die verbleibenden Phasen von heute und die Phasen von morgen. Gibt 'nicht verfügbar' zurück, wenn in den Daten von heute oder morgen keine fallenden Phasen mehr existieren. Aktualisiert sich an Intervallgrenzen (alle 15 Minuten).",
"usage_tips": "Nutze es, um flexible Verbraucher bis zum nächsten Preisrückgang zu verzögern. 'Wenn next_falling_phase_start_time innerhalb von 2 Stunden liegt, warte ggf., bevor du den Geschirrspüler oder die Wärmepumpe startest'."
},
"next_flat_phase_start_time": {
"description": "When the next flat (stable) price phase begins",
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use for scheduling loads that need predictable costs over time."
"description": "Wann die nächste flache (stabile) Preisphase beginnt",
"long_description": "Zeigt den Zeitstempel des nächsten anstehenden flachen Preissegments (in dem die Preise kaum variieren) über die verbleibenden Phasen von heute und die Phasen von morgen. Gibt 'nicht verfügbar' zurück, wenn in den verfügbaren Daten keine flachen Phasen mehr existieren. Aktualisiert sich an Intervallgrenzen (alle 15 Minuten).",
"usage_tips": "Flache Phasen deuten auf Preisstabilität hin — nützlich, um Verbraucher mit über die Zeit vorhersehbaren Kosten zu planen. 'Wenn next_flat_phase_start_time innerhalb von 30 Minuten liegt und current_price_phase = rising, ist die Preisspitze fast vorbei'."
},
"next_rising_phase_in_minutes": {
"description": "Minutes until the next rising price phase begins",
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
"usage_tips": "Use in countdown automations to alert before the next price rise."
"description": "Minuten bis die nächste steigende Preisphase beginnt",
"long_description": "Zeigt, wie viele Minuten bis zum Beginn der nächsten steigenden Preisphase verbleiben. Aktualisiert sich jede Minute. Gibt 'nicht verfügbar' zurück, wenn in den Daten von heute oder morgen keine steigenden Phasen mehr existieren. Das Attribut next_in_minutes spiegelt den Sensorwert in ganzen Minuten für Automations-Templates wider.",
"usage_tips": "Nutze es in Countdown-Automationen: 'Benachrichtige mich 15 Minuten vor dem nächsten Preisanstieg, damit ich flexible Verbraucher verzögern kann'. Kombiniere mit in_falling_price_phase: Wenn die Preise gerade fallen und ein Anstieg bevorsteht, handle, bevor die Preise zu klettern beginnen."
},
"next_falling_phase_in_minutes": {
"description": "Minutes until the next falling price phase begins",
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
"description": "Minuten bis die nächste fallende Preisphase beginnt",
"long_description": "Zeigt, wie viele Minuten bis zum Beginn der nächsten fallenden Preisphase verbleiben. Aktualisiert sich jede Minute. Gibt 'nicht verfügbar' zurück, wenn in den Daten von heute oder morgen keine fallenden Phasen mehr existieren. Das Attribut next_in_minutes spiegelt den Sensorwert in ganzen Minuten für Automations-Templates wider.",
"usage_tips": "Nutze es, um flexible Verbraucher optimal zu timen: 'Wenn next_falling_phase_in_minutes < 60, verzögere den Start der Waschmaschine, um vom kommenden Preisrückgang zu profitieren'."
},
"next_flat_phase_in_minutes": {
"description": "Minutes until the next flat (stable) price phase begins",
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
"description": "Minuten bis die nächste flache (stabile) Preisphase beginnt",
"long_description": "Zeigt, wie viele Minuten bis zum Beginn der nächsten flachen Preisphase verbleiben. Aktualisiert sich jede Minute. Gibt 'nicht verfügbar' zurück, wenn in den Daten von heute oder morgen keine flachen Phasen mehr existieren. Das Attribut next_in_minutes spiegelt den Sensorwert in ganzen Minuten wider.",
"usage_tips": "Nutze es, um eine Preisstabilisierung nach einer volatilen Phase vorauszusehen. 'Wenn next_flat_phase_in_minutes < 30 und current_price_phase = rising, wird die Preisspitze bald abflachen — erwäge, Verbraucher bis dahin zu verzögern'."
},
"chart_data_export": {
"description": "Datenexport für Dashboard-Integrationen",
@ -644,19 +644,19 @@
"usage_tips": "Nutze dies, um Geräte mit hohem Verbrauch während der günstigsten Intervalle zu betreiben"
},
"in_rising_price_phase": {
"description": "Whether prices are currently in a rising phase",
"long_description": "Turns ON when the current intra-day price phase is rising.",
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
"description": "Ob die Preise aktuell in einer steigenden Phase sind",
"long_description": "Schaltet auf ON, wenn die aktuelle Preisphase steigend ist — d. h. die Preise steigen seit dem letzten Phasenwechsel. Genau eines von in_rising_price_phase, in_falling_price_phase und in_flat_price_phase ist zu jedem Zeitpunkt ON. Wird nicht verfügbar, wenn keine Segmentdaten vorliegen.",
"usage_tips": "Nutze es in Automationen, um flexible Verbraucher zu verzögern oder zu vermeiden: 'Wenn in_rising_price_phase ON ist, verschiebe den Geschirrspüler'. Kombiniere mit next_falling_phase_start_time, um zu wissen, wann die Preise wieder zu fallen beginnen."
},
"in_falling_price_phase": {
"description": "Whether prices are currently in a falling phase",
"long_description": "Turns ON when the current intra-day price phase is falling.",
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
"description": "Ob die Preise aktuell in einer fallenden Phase sind",
"long_description": "Schaltet auf ON, wenn die aktuelle Preisphase fallend ist — d. h. die Preise fallen seit dem letzten Phasenwechsel. Das ist oft ein gutes Zeitfenster, um flexible Verbraucher zu starten. Genau eines von in_rising_price_phase, in_falling_price_phase und in_flat_price_phase ist zu jedem Zeitpunkt ON.",
"usage_tips": "Nutze es in Automationen, um fallende Preise auszunutzen: 'Wenn in_falling_price_phase ON ist und current_price_phase_remaining_minutes > 60, starte die Waschmaschine'. Kombiniere mit next_rising_phase_start_time, um keinen Verbraucher zu starten, der vor dem Preisanstieg nicht fertig wird."
},
"in_flat_price_phase": {
"description": "Whether prices are currently in a flat (stable) phase",
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
"description": "Ob die Preise aktuell in einer flachen (stabilen) Phase sind",
"long_description": "Schaltet auf ON, wenn die aktuelle Preisphase flach ist — d. h. die Preise sind relativ stabil ohne nennenswerten Anstieg oder Rückgang. Flache Phasen bedeuten vorhersehbare Kosten und eignen sich für Verbraucher mit unsicherer oder variabler Dauer. Genau eines von in_rising_price_phase, in_falling_price_phase und in_flat_price_phase ist zu jedem Zeitpunkt ON.",
"usage_tips": "Nutze es für Verbraucher, denen die Preisrichtung egal ist, die aber Stabilität bevorzugen: Wärmepumpen im stationären Betrieb, Hintergrundladen oder Nachheizen des Warmwasserspeichers. Kombiniere mit current_price_phase_end_time, um zu wissen, wie lange das stabile Fenster anhält."
},
"connection": {
"description": "Ob die Verbindung zur Tibber API funktioniert",

View file

@ -512,54 +512,54 @@
"usage_tips": "Bruk i automasjonar: 'Hvis next_price_phase = stigende og next_price_phase.start er innen 1 time, start vaskemaskin nå'. Eller kombiner med current_price_phase: 'Hvis current_price_phase = fallende og next_price_phase = flat, nærmer vi oss daglig lavpunkt godt tidspunkt for fleksible laster'. Attributtet start er spesielt verdifullt: utløs automasjonar nøyaktig når neste fase begynner."
},
"current_price_phase_end_time": {
"description": "When the current intra-day price phase ends",
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
"description": "Når den gjeldende prisfasen slutter",
"long_description": "Viser nøyaktig tidsstempel for når den aktive stigende, fallende eller flate prisfasen slutter og går over til neste fase. Blir utilgjengelig når det ikke finnes segmentdata. Oppdateres ved intervallgrenser (hvert 15. minutt). Kombiner med current_price_phase for å vite både hvilken fase du er i og når den slutter.",
"usage_tips": "Bruk i automasjoner for å planlegge oppgaver som må fullføres før prisene endres: 'Start vaskemaskinen hvis current_price_phase = falling og current_price_phase_end_time er mer enn 2 timer unna'. Eller for å varsle når en billig fase snart er over."
},
"current_price_phase_remaining_minutes": {
"description": "Minutes remaining in the current price phase",
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
"description": "Gjenværende minutter i den gjeldende prisfasen",
"long_description": "Viser hvor mange minutter som er igjen av den gjeldende prisfasen (stigende, fallende eller flat). Oppdateres hvert minutt for presis nedtelling. Returnerer 0 når det ikke finnes segmentdata. Attributtet remaining_minutes speiler sensorverdien i hele minutter for enklere automasjonsmaler.",
"usage_tips": "Bruk i automasjoner: 'Hvis current_price_phase = falling og current_price_phase_remaining_minutes < 30, start oppvaskmaskinen nå før prisene jevner seg ut'. Også nyttig for dashbordkort som viser en nedtellingslinje til fasen skifter."
},
"current_price_phase_duration": {
"description": "Total duration of the current price phase",
"long_description": "Shows the total length of the currently active price phase in hours.",
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
"description": "Total varighet for den gjeldende prisfasen",
"long_description": "Viser den totale lengden på den aktive prisfasen i timer (uttrykt internt i minutter). Oppdateres ved intervallgrenser. Dette forteller hvor lenge det gjeldende trendsegmentet varer totalt — nyttig for å forstå om det er en kort svingning eller en lengre periode med stigende eller fallende priser.",
"usage_tips": "Kombiner med current_price_phase_remaining_minutes for å anslå hvor langt du har kommet i fasen, eller sammenlign med current_price_phase_progress for å forstå tidsprofilen til den gjeldende trenden."
},
"current_price_phase_progress": {
"description": "How far through the current price phase we are",
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0100%). Updates every minute.",
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
"description": "Hvor langt den gjeldende prisfasen har kommet",
"long_description": "Viser hvor stor andel av den gjeldende prisfasen som har gått (0100 %). Oppdateres hvert minutt. En verdi nær 0 % betyr at fasen nettopp har startet; nær 100 % betyr at den snart slutter. Returnerer 0 når det ikke finnes segmentdata.",
"usage_tips": "Bruk i dashbordkort for å vise en visuell fremdriftslinje for den gjeldende prisfasen. I automasjoner: 'Hvis current_price_phase = falling og current_price_phase_progress > 80, er de billigste prisene nær — gjør klar fleksible laster nå'."
},
"next_rising_phase_start_time": {
"description": "When the next rising price phase begins",
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to schedule loads before prices start rising again."
"description": "Når den neste stigende prisfasen begynner",
"long_description": "Viser tidsstempelet for det neste kommende stigende prissegmentet på tvers av dagens gjenværende faser og morgendagens faser. Returnerer utilgjengelig når det ikke finnes flere stigende faser i dagens eller morgendagens data. Oppdateres ved intervallgrenser (hvert 15. minutt).",
"usage_tips": "Bruk in_falling_price_phase og next_rising_phase_start_time for å planlegge fleksible laster: kjør dem nå mens prisene faller, og avslutt før neste oppgang starter. 'Hvis in_falling_price_phase er ON og next_rising_phase_start_time er mindre enn 1 time unna, start vaskemaskinen umiddelbart'."
},
"next_falling_phase_start_time": {
"description": "When the next falling price phase begins",
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to delay flexible loads until the next price drop starts."
"description": "Når den neste fallende prisfasen begynner",
"long_description": "Viser tidsstempelet for det neste kommende fallende prissegmentet på tvers av dagens gjenværende faser og morgendagens faser. Returnerer utilgjengelig når det ikke finnes flere fallende faser i dagens eller morgendagens data. Oppdateres ved intervallgrenser (hvert 15. minutt).",
"usage_tips": "Bruk det til å utsette fleksible laster til neste prisfall starter. 'Hvis next_falling_phase_start_time er innen 2 timer, vurder å vente før du starter oppvaskmaskinen eller varmepumpen'."
},
"next_flat_phase_start_time": {
"description": "When the next flat (stable) price phase begins",
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use for scheduling loads that need predictable costs over time."
"description": "Når den neste flate (stabile) prisfasen begynner",
"long_description": "Viser tidsstempelet for det neste kommende flate prissegmentet (der prisene varierer lite) på tvers av dagens gjenværende faser og morgendagens faser. Returnerer utilgjengelig når det ikke finnes flere flate faser i tilgjengelige data. Oppdateres ved intervallgrenser (hvert 15. minutt).",
"usage_tips": "Flate faser indikerer prisstabilitet — nyttig for å planlegge laster som trenger forutsigbar kostnad over tid. 'Hvis next_flat_phase_start_time er innen 30 minutter og current_price_phase = rising, er pristoppen nesten over'."
},
"next_rising_phase_in_minutes": {
"description": "Minutes until the next rising price phase begins",
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
"usage_tips": "Use in countdown automations to alert before the next price rise."
"description": "Minutter til den neste stigende prisfasen begynner",
"long_description": "Viser hvor mange minutter til den neste stigende prisfasen starter. Oppdateres hvert minutt. Returnerer utilgjengelig hvis det ikke finnes flere stigende faser i dagens eller morgendagens data. Attributtet next_in_minutes speiler sensorverdien i hele minutter for automasjonsmaler.",
"usage_tips": "Bruk i nedtellingsautomasjoner: 'Varsle meg 15 minutter før neste prisoppgang så jeg kan utsette fleksible laster'. Kombiner med in_falling_price_phase: hvis prisene faller nå og en oppgang er nært forestående, handle før prisene begynner å stige."
},
"next_falling_phase_in_minutes": {
"description": "Minutes until the next falling price phase begins",
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
"description": "Minutter til den neste fallende prisfasen begynner",
"long_description": "Viser hvor mange minutter til den neste fallende prisfasen starter. Oppdateres hvert minutt. Returnerer utilgjengelig hvis det ikke finnes flere fallende faser i dagens eller morgendagens data. Attributtet next_in_minutes speiler sensorverdien i hele minutter for automasjonsmaler.",
"usage_tips": "Bruk det til å time fleksible laster optimalt: 'Hvis next_falling_phase_in_minutes < 60, utsett starten av vaskemaskinen for å dra nytte av det kommende prisfallet'."
},
"next_flat_phase_in_minutes": {
"description": "Minutes until the next flat (stable) price phase begins",
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
"description": "Minutter til den neste flate (stabile) prisfasen begynner",
"long_description": "Viser hvor mange minutter til den neste flate prisfasen starter. Oppdateres hvert minutt. Returnerer utilgjengelig hvis det ikke finnes flere flate faser i dagens eller morgendagens data. Attributtet next_in_minutes speiler sensorverdien i hele minutter.",
"usage_tips": "Bruk det til å forutse prisstabilisering etter en volatil fase. 'Hvis next_flat_phase_in_minutes < 30 og current_price_phase = rising, vil pristoppen snart flate ut — vurder å utsette laster til da'."
},
"chart_data_export": {
"description": "Dataeksport for dashboardintegrasjoner",
@ -644,19 +644,19 @@
"usage_tips": "Bruk dette til å kjøre høyforbruksapparater i de billigste intervallene"
},
"in_rising_price_phase": {
"description": "Whether prices are currently in a rising phase",
"long_description": "Turns ON when the current intra-day price phase is rising.",
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
"description": "Om prisene for øyeblikket er i en stigende fase",
"long_description": "Slår seg PÅ når den gjeldende prisfasen er stigende — dvs. prisene har steget siden forrige faseovergang. Nøyaktig én av in_rising_price_phase, in_falling_price_phase og in_flat_price_phase er PÅ til enhver tid. Blir utilgjengelig når det ikke finnes segmentdata.",
"usage_tips": "Bruk i automasjoner for å utsette eller unngå å kjøre fleksible laster: 'Hvis in_rising_price_phase er PÅ, utsett oppvaskmaskinen'. Kombiner med next_falling_phase_start_time for å vite når prisene begynner å falle igjen."
},
"in_falling_price_phase": {
"description": "Whether prices are currently in a falling phase",
"long_description": "Turns ON when the current intra-day price phase is falling.",
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
"description": "Om prisene for øyeblikket er i en fallende fase",
"long_description": "Slår seg PÅ når den gjeldende prisfasen er fallende — dvs. prisene har falt siden forrige faseovergang. Dette er ofte et godt tidsvindu for å starte fleksible laster. Nøyaktig én av in_rising_price_phase, in_falling_price_phase og in_flat_price_phase er PÅ til enhver tid.",
"usage_tips": "Bruk i automasjoner for å utnytte fallende priser: 'Hvis in_falling_price_phase er PÅ og current_price_phase_remaining_minutes > 60, start vaskemaskinen'. Kombiner med next_rising_phase_start_time for å unngå å starte en last som ikke blir ferdig før prisene stiger."
},
"in_flat_price_phase": {
"description": "Whether prices are currently in a flat (stable) phase",
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
"description": "Om prisene for øyeblikket er i en flat (stabil) fase",
"long_description": "Slår seg PÅ når den gjeldende prisfasen er flat — dvs. prisene er relativt stabile uten betydelig oppgang eller nedgang. Flate faser betyr forutsigbare kostnader og passer for laster med usikker eller variabel varighet. Nøyaktig én av in_rising_price_phase, in_falling_price_phase og in_flat_price_phase er PÅ til enhver tid.",
"usage_tips": "Bruk det for laster som er likegyldige til prisretning, men foretrekker stabilitet: varmepumper i stabil drift, bakgrunnslading eller etterfylling av varmtvannsbereder. Kombiner med current_price_phase_end_time for å vite hvor lenge det stabile vinduet varer."
},
"connection": {
"description": "Om tilkoblingen til Tibber API fungerer",

View file

@ -512,54 +512,54 @@
"usage_tips": "Gebruik in automations: 'Als next_price_phase = stijgend en next_price_phase.start binnen 1 uur is, start de wasmachine nu'. Of combineer met current_price_phase: 'Als current_price_phase = dalend en next_price_phase = vlak, naderen we het dagdieptepunt goed moment voor flexibele lasten'. Het attribuut start is bijzonder waardevol: activeer automations precies wanneer de volgende fase begint."
},
"current_price_phase_end_time": {
"description": "When the current intra-day price phase ends",
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
"description": "Wanneer de huidige prijsfase eindigt",
"long_description": "Toont de exacte tijdstempel waarop de momenteel actieve stijgende, dalende of vlakke prijsfase eindigt en overgaat naar de volgende fase. Wordt niet beschikbaar als er geen segmentgegevens zijn. Werkt bij op intervalgrenzen (elke 15 minuten). Combineer met current_price_phase om zowel te weten in welke fase je zit als wanneer die eindigt.",
"usage_tips": "Gebruik in automatiseringen om taken te plannen die klaar moeten zijn voordat de prijzen veranderen: 'Start de wasmachine als current_price_phase = falling en current_price_phase_end_time meer dan 2 uur weg is'. Of om te waarschuwen wanneer een goedkope fase bijna voorbij is."
},
"current_price_phase_remaining_minutes": {
"description": "Minutes remaining in the current price phase",
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
"description": "Resterende minuten in de huidige prijsfase",
"long_description": "Toont hoeveel minuten er nog over zijn in de huidige prijsfase (stijgend, dalend of vlak). Werkt elke minuut bij voor nauwkeurige aftelling. Geeft 0 terug als er geen segmentgegevens zijn. Het attribuut remaining_minutes weerspiegelt de sensorwaarde in hele minuten voor eenvoudigere automatiseringssjablonen.",
"usage_tips": "Gebruik in automatiseringen: 'Als current_price_phase = falling en current_price_phase_remaining_minutes < 30, start nu de vaatwasser voordat de prijzen afvlakken'. Ook handig voor dashboardkaarten met een afteltbalk tot de faseovergang."
},
"current_price_phase_duration": {
"description": "Total duration of the current price phase",
"long_description": "Shows the total length of the currently active price phase in hours.",
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
"description": "Totale duur van de huidige prijsfase",
"long_description": "Toont de totale lengte van de momenteel actieve prijsfase in uren (intern uitgedrukt in minuten). Werkt bij op intervalgrenzen. Dit vertelt je hoe lang het huidige trendsegment in totaal duurt — handig om te begrijpen of het een korte schommeling is of een langere periode van stijgende of dalende prijzen.",
"usage_tips": "Combineer met current_price_phase_remaining_minutes om in te schatten hoe ver je in de fase bent, of vergelijk met current_price_phase_progress om het tijdsprofiel van de huidige trend te begrijpen."
},
"current_price_phase_progress": {
"description": "How far through the current price phase we are",
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0100%). Updates every minute.",
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
"description": "Hoe ver de huidige prijsfase gevorderd is",
"long_description": "Toont het percentage van de huidige prijsfase dat al verstreken is (0100%). Werkt elke minuut bij. Een waarde rond 0% betekent dat de fase net is begonnen; rond 100% betekent dat die bijna eindigt. Geeft 0 terug als er geen segmentgegevens zijn.",
"usage_tips": "Gebruik in dashboardkaarten om een visuele voortgangsbalk voor de huidige prijsfase te tonen. In automatiseringen: 'Als current_price_phase = falling en current_price_phase_progress > 80, zijn de goedkoopste prijzen dichtbij — maak nu flexibele verbruikers klaar'."
},
"next_rising_phase_start_time": {
"description": "When the next rising price phase begins",
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to schedule loads before prices start rising again."
"description": "Wanneer de volgende stijgende prijsfase begint",
"long_description": "Toont de tijdstempel van het volgende komende stijgende prijssegment over de resterende fasen van vandaag en de fasen van morgen. Geeft niet-beschikbaar terug als er geen stijgende fasen meer zijn in de gegevens van vandaag of morgen. Werkt bij op intervalgrenzen (elke 15 minuten).",
"usage_tips": "Gebruik in_falling_price_phase en next_rising_phase_start_time om flexibele verbruikers te plannen: laat ze nu draaien terwijl de prijzen dalen en rond af voordat de volgende stijging begint. 'Als in_falling_price_phase ON is en next_rising_phase_start_time minder dan 1 uur weg is, start de wasmachine direct'."
},
"next_falling_phase_start_time": {
"description": "When the next falling price phase begins",
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to delay flexible loads until the next price drop starts."
"description": "Wanneer de volgende dalende prijsfase begint",
"long_description": "Toont de tijdstempel van het volgende komende dalende prijssegment over de resterende fasen van vandaag en de fasen van morgen. Geeft niet-beschikbaar terug als er geen dalende fasen meer zijn in de gegevens van vandaag of morgen. Werkt bij op intervalgrenzen (elke 15 minuten).",
"usage_tips": "Gebruik het om flexibele verbruikers uit te stellen tot de volgende prijsdaling begint. 'Als next_falling_phase_start_time binnen 2 uur is, overweeg dan te wachten voordat je de vaatwasser of warmtepomp start'."
},
"next_flat_phase_start_time": {
"description": "When the next flat (stable) price phase begins",
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use for scheduling loads that need predictable costs over time."
"description": "Wanneer de volgende vlakke (stabiele) prijsfase begint",
"long_description": "Toont de tijdstempel van het volgende komende vlakke prijssegment (waar de prijzen weinig variatie tonen) over de resterende fasen van vandaag en de fasen van morgen. Geeft niet-beschikbaar terug als er geen vlakke fasen meer zijn in de beschikbare gegevens. Werkt bij op intervalgrenzen (elke 15 minuten).",
"usage_tips": "Vlakke fasen wijzen op prijsstabiliteit — handig voor het plannen van verbruikers die in de tijd voorspelbare kosten nodig hebben. 'Als next_flat_phase_start_time binnen 30 minuten is en current_price_phase = rising, is de prijspiek bijna voorbij'."
},
"next_rising_phase_in_minutes": {
"description": "Minutes until the next rising price phase begins",
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
"usage_tips": "Use in countdown automations to alert before the next price rise."
"description": "Minuten tot de volgende stijgende prijsfase begint",
"long_description": "Toont hoeveel minuten tot de volgende stijgende prijsfase begint. Werkt elke minuut bij. Geeft niet-beschikbaar terug als er geen stijgende fasen meer zijn in de gegevens van vandaag of morgen. Het attribuut next_in_minutes weerspiegelt de sensorwaarde in hele minuten voor automatiseringssjablonen.",
"usage_tips": "Gebruik in aftelautomatiseringen: 'Waarschuw me 15 minuten voor de volgende prijsstijging zodat ik flexibele verbruikers kan uitstellen'. Combineer met in_falling_price_phase: als de prijzen nu dalen en een stijging ophanden is, handel dan voordat de prijzen beginnen te klimmen."
},
"next_falling_phase_in_minutes": {
"description": "Minutes until the next falling price phase begins",
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
"description": "Minuten tot de volgende dalende prijsfase begint",
"long_description": "Toont hoeveel minuten tot de volgende dalende prijsfase begint. Werkt elke minuut bij. Geeft niet-beschikbaar terug als er geen dalende fasen meer zijn in de gegevens van vandaag of morgen. Het attribuut next_in_minutes weerspiegelt de sensorwaarde in hele minuten voor automatiseringssjablonen.",
"usage_tips": "Gebruik het om flexibele verbruikers optimaal te timen: 'Als next_falling_phase_in_minutes < 60, stel de start van de wasmachine uit om te profiteren van de komende prijsdaling'."
},
"next_flat_phase_in_minutes": {
"description": "Minutes until the next flat (stable) price phase begins",
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
"description": "Minuten tot de volgende vlakke (stabiele) prijsfase begint",
"long_description": "Toont hoeveel minuten tot de volgende vlakke prijsfase begint. Werkt elke minuut bij. Geeft niet-beschikbaar terug als er geen vlakke fasen meer zijn in de gegevens van vandaag of morgen. Het attribuut next_in_minutes weerspiegelt de sensorwaarde in hele minuten.",
"usage_tips": "Gebruik het om prijsstabilisatie na een volatiele fase te anticiperen. 'Als next_flat_phase_in_minutes < 30 en current_price_phase = rising, zal de prijspiek binnenkort afvlakken — overweeg verbruikers tot dan uit te stellen'."
},
"chart_data_export": {
"description": "Data-export voor dashboard-integraties",
@ -644,19 +644,19 @@
"usage_tips": "Gebruik dit om apparaten met hoog verbruik te laten draaien tijdens de goedkoopste intervallen"
},
"in_rising_price_phase": {
"description": "Whether prices are currently in a rising phase",
"long_description": "Turns ON when the current intra-day price phase is rising.",
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
"description": "Of de prijzen zich momenteel in een stijgende fase bevinden",
"long_description": "Gaat AAN wanneer de huidige prijsfase stijgend is — d.w.z. de prijzen stijgen sinds de laatste faseovergang. Precies één van in_rising_price_phase, in_falling_price_phase en in_flat_price_phase is op elk moment AAN. Wordt niet beschikbaar als er geen segmentgegevens zijn.",
"usage_tips": "Gebruik in automatiseringen om flexibele verbruikers uit te stellen of te vermijden: 'Als in_rising_price_phase AAN is, stel de vaatwasser uit'. Combineer met next_falling_phase_start_time om te weten wanneer de prijzen weer beginnen te dalen."
},
"in_falling_price_phase": {
"description": "Whether prices are currently in a falling phase",
"long_description": "Turns ON when the current intra-day price phase is falling.",
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
"description": "Of de prijzen zich momenteel in een dalende fase bevinden",
"long_description": "Gaat AAN wanneer de huidige prijsfase dalend is — d.w.z. de prijzen dalen sinds de laatste faseovergang. Dit is vaak een goed moment om flexibele verbruikers te starten. Precies één van in_rising_price_phase, in_falling_price_phase en in_flat_price_phase is op elk moment AAN.",
"usage_tips": "Gebruik in automatiseringen om dalende prijzen te benutten: 'Als in_falling_price_phase AAN is en current_price_phase_remaining_minutes > 60, start de wasmachine'. Combineer met next_rising_phase_start_time om te voorkomen dat je een verbruiker start die niet klaar is voordat de prijzen stijgen."
},
"in_flat_price_phase": {
"description": "Whether prices are currently in a flat (stable) phase",
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
"description": "Of de prijzen zich momenteel in een vlakke (stabiele) fase bevinden",
"long_description": "Gaat AAN wanneer de huidige prijsfase vlak is — d.w.z. de prijzen zijn relatief stabiel zonder noemenswaardige stijging of daling. Vlakke fasen betekenen voorspelbare kosten en zijn geschikt voor verbruikers met een onzekere of variabele duur. Precies één van in_rising_price_phase, in_falling_price_phase en in_flat_price_phase is op elk moment AAN.",
"usage_tips": "Gebruik het voor verbruikers die onverschillig zijn voor de prijsrichting maar stabiliteit verkiezen: warmtepompen in stabiele modus, achtergrondladen of het bijverwarmen van de boiler. Combineer met current_price_phase_end_time om te weten hoe lang het stabiele venster duurt."
},
"connection": {
"description": "Of de verbinding met de Tibber API werkt",

View file

@ -512,54 +512,54 @@
"usage_tips": "Använd i automationer: 'Om next_price_phase = stigande och next_price_phase.start är inom 1 timme, starta tvättmaskinen nu'. Eller kombinera med current_price_phase: 'Om current_price_phase = fallande och next_price_phase = flat, närmar vi oss dagens lågpunkt bra tid för flexibla laster'. Attributet start är särskilt värdefull: utlös automationer precis när nästa fas börjar."
},
"current_price_phase_end_time": {
"description": "When the current intra-day price phase ends",
"long_description": "Shows the exact timestamp when the currently active rising, falling, or flat price phase will end and transition to the next phase.",
"usage_tips": "Use in automations to schedule tasks that must finish before prices change."
"description": "När den aktuella prisfasen slutar",
"long_description": "Visar den exakta tidsstämpeln för när den aktiva stigande, fallande eller flata prisfasen slutar och övergår till nästa fas. Blir otillgänglig när det inte finns segmentdata. Uppdateras vid intervallgränser (var 15:e minut). Kombinera med current_price_phase för att veta både vilken fas du är i och när den slutar.",
"usage_tips": "Använd i automationer för att schemalägga uppgifter som måste vara klara innan priserna ändras: 'Starta tvättmaskinen om current_price_phase = falling och current_price_phase_end_time är mer än 2 timmar bort'. Eller för att varna när en billig fas snart är slut."
},
"current_price_phase_remaining_minutes": {
"description": "Minutes remaining in the current price phase",
"long_description": "Shows how many minutes are left in the current intra-day price phase. Updates every minute.",
"usage_tips": "Use in automations: 'If current_price_phase = falling and remaining < 30, start the dishwasher now'."
"description": "Återstående minuter i den aktuella prisfasen",
"long_description": "Visar hur många minuter som är kvar av den aktuella prisfasen (stigande, fallande eller flat). Uppdateras varje minut för exakt nedräkning. Returnerar 0 när det inte finns segmentdata. Attributet remaining_minutes speglar sensorvärdet i hela minuter för enklare automationsmallar.",
"usage_tips": "Använd i automationer: 'Om current_price_phase = falling och current_price_phase_remaining_minutes < 30, starta diskmaskinen nu innan priserna planar ut'. Även användbart för instrumentpanelskort som visar en nedräkningsstapel till fasövergången."
},
"current_price_phase_duration": {
"description": "Total duration of the current price phase",
"long_description": "Shows the total length of the currently active price phase in hours.",
"usage_tips": "Combine with remaining minutes to understand how far through the phase you are."
"description": "Total varaktighet för den aktuella prisfasen",
"long_description": "Visar den totala längden på den aktiva prisfasen i timmar (uttryckt internt i minuter). Uppdateras vid intervallgränser. Detta talar om hur länge det aktuella trendsegmentet varar totalt — användbart för att förstå om det är en kort fluktuation eller en längre period av stigande eller fallande priser.",
"usage_tips": "Kombinera med current_price_phase_remaining_minutes för att uppskatta hur långt du har kommit i fasen, eller jämför med current_price_phase_progress för att förstå tidsprofilen för den aktuella trenden."
},
"current_price_phase_progress": {
"description": "How far through the current price phase we are",
"long_description": "Shows the percentage of the current intra-day price phase that has elapsed (0100%). Updates every minute.",
"usage_tips": "Use in dashboard cards to display a visual progress bar for the current price phase."
"description": "Hur långt den aktuella prisfasen har kommit",
"long_description": "Visar hur stor andel av den aktuella prisfasen som har förflutit (0100 %). Uppdateras varje minut. Ett värde nära 0 % betyder att fasen just har börjat; nära 100 % betyder att den snart slutar. Returnerar 0 när det inte finns segmentdata.",
"usage_tips": "Använd i instrumentpanelskort för att visa en visuell förloppsindikator för den aktuella prisfasen. I automationer: 'Om current_price_phase = falling och current_price_phase_progress > 80, är de billigaste priserna nära — förbered flexibla laster nu'."
},
"next_rising_phase_start_time": {
"description": "When the next rising price phase begins",
"long_description": "Shows the timestamp of the next upcoming rising price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to schedule loads before prices start rising again."
"description": "När nästa stigande prisfas börjar",
"long_description": "Visar tidsstämpeln för nästa kommande stigande prissegment över dagens återstående faser och morgondagens faser. Returnerar otillgänglig när det inte finns fler stigande faser i dagens eller morgondagens data. Uppdateras vid intervallgränser (var 15:e minut).",
"usage_tips": "Använd in_falling_price_phase och next_rising_phase_start_time för att schemalägga flexibla laster: kör dem nu medan priserna faller och avsluta innan nästa uppgång börjar. 'Om in_falling_price_phase är ON och next_rising_phase_start_time är mindre än 1 timme bort, starta tvättmaskinen omedelbart'."
},
"next_falling_phase_start_time": {
"description": "When the next falling price phase begins",
"long_description": "Shows the timestamp of the next upcoming falling price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use to delay flexible loads until the next price drop starts."
"description": "När nästa fallande prisfas börjar",
"long_description": "Visar tidsstämpeln för nästa kommande fallande prissegment över dagens återstående faser och morgondagens faser. Returnerar otillgänglig när det inte finns fler fallande faser i dagens eller morgondagens data. Uppdateras vid intervallgränser (var 15:e minut).",
"usage_tips": "Använd det för att skjuta upp flexibla laster tills nästa prisfall börjar. 'Om next_falling_phase_start_time är inom 2 timmar, överväg att vänta innan du startar diskmaskinen eller värmepumpen'."
},
"next_flat_phase_start_time": {
"description": "When the next flat (stable) price phase begins",
"long_description": "Shows the timestamp of the next upcoming flat price segment across today's remaining phases and tomorrow's phases.",
"usage_tips": "Use for scheduling loads that need predictable costs over time."
"description": "När nästa flata (stabila) prisfas börjar",
"long_description": "Visar tidsstämpeln för nästa kommande flata prissegment (där priserna varierar lite) över dagens återstående faser och morgondagens faser. Returnerar otillgänglig när det inte finns fler flata faser i tillgängliga data. Uppdateras vid intervallgränser (var 15:e minut).",
"usage_tips": "Flata faser indikerar prisstabilitet — användbart för att schemalägga laster som behöver en förutsägbar kostnad över tid. 'Om next_flat_phase_start_time är inom 30 minuter och current_price_phase = rising, är pristoppen nästan över'."
},
"next_rising_phase_in_minutes": {
"description": "Minutes until the next rising price phase begins",
"long_description": "Shows how many minutes until the next rising price phase starts. Updates every minute.",
"usage_tips": "Use in countdown automations to alert before the next price rise."
"description": "Minuter tills nästa stigande prisfas börjar",
"long_description": "Visar hur många minuter tills nästa stigande prisfas börjar. Uppdateras varje minut. Returnerar otillgänglig om det inte finns fler stigande faser i dagens eller morgondagens data. Attributet next_in_minutes speglar sensorvärdet i hela minuter för automationsmallar.",
"usage_tips": "Använd i nedräkningsautomationer: 'Meddela mig 15 minuter före nästa prisuppgång så att jag kan skjuta upp flexibla laster'. Kombinera med in_falling_price_phase: om priserna faller nu och en uppgång är nära förestående, agera innan priserna börjar klättra."
},
"next_falling_phase_in_minutes": {
"description": "Minutes until the next falling price phase begins",
"long_description": "Shows how many minutes until the next falling price phase starts. Updates every minute.",
"usage_tips": "Use to time flexible loads: delay until the upcoming price drop."
"description": "Minuter tills nästa fallande prisfas börjar",
"long_description": "Visar hur många minuter tills nästa fallande prisfas börjar. Uppdateras varje minut. Returnerar otillgänglig om det inte finns fler fallande faser i dagens eller morgondagens data. Attributet next_in_minutes speglar sensorvärdet i hela minuter för automationsmallar.",
"usage_tips": "Använd det för att tajma flexibla laster optimalt: 'Om next_falling_phase_in_minutes < 60, skjut upp starten av tvättmaskinen för att dra nytta av det kommande prisfallet'."
},
"next_flat_phase_in_minutes": {
"description": "Minutes until the next flat (stable) price phase begins",
"long_description": "Shows how many minutes until the next flat price phase starts. Updates every minute.",
"usage_tips": "Use to anticipate price stabilisation after a volatile phase."
"description": "Minuter tills nästa flata (stabila) prisfas börjar",
"long_description": "Visar hur många minuter tills nästa flata prisfas börjar. Uppdateras varje minut. Returnerar otillgänglig om det inte finns fler flata faser i dagens eller morgondagens data. Attributet next_in_minutes speglar sensorvärdet i hela minuter.",
"usage_tips": "Använd det för att förutse prisstabilisering efter en volatil fas. 'Om next_flat_phase_in_minutes < 30 och current_price_phase = rising, kommer pristoppen snart att plana ut — överväg att skjuta upp laster till dess'."
},
"chart_data_export": {
"description": "Dataexport för dashboard-integrationer",
@ -644,19 +644,19 @@
"usage_tips": "Använd detta för att köra högkonsumtionsapparater under de billigaste intervallerna"
},
"in_rising_price_phase": {
"description": "Whether prices are currently in a rising phase",
"long_description": "Turns ON when the current intra-day price phase is rising.",
"usage_tips": "Use in automations to delay or avoid running flexible loads during rising prices."
"description": "Om priserna för närvarande är i en stigande fas",
"long_description": "Slås PÅ när den aktuella prisfasen är stigande — dvs. priserna har stigit sedan den senaste fasövergången. Exakt en av in_rising_price_phase, in_falling_price_phase och in_flat_price_phase är PÅ vid varje tidpunkt. Blir otillgänglig när det inte finns segmentdata.",
"usage_tips": "Använd i automationer för att skjuta upp eller undvika att köra flexibla laster: 'Om in_rising_price_phase är PÅ, skjut upp diskmaskinen'. Kombinera med next_falling_phase_start_time för att veta när priserna börjar falla igen."
},
"in_falling_price_phase": {
"description": "Whether prices are currently in a falling phase",
"long_description": "Turns ON when the current intra-day price phase is falling.",
"usage_tips": "Use in automations to take advantage of falling prices for flexible loads."
"description": "Om priserna för närvarande är i en fallande fas",
"long_description": "Slås PÅ när den aktuella prisfasen är fallande — dvs. priserna har fallit sedan den senaste fasövergången. Detta är ofta ett bra tidsfönster för att starta flexibla laster. Exakt en av in_rising_price_phase, in_falling_price_phase och in_flat_price_phase är PÅ vid varje tidpunkt.",
"usage_tips": "Använd i automationer för att utnyttja fallande priser: 'Om in_falling_price_phase är PÅ och current_price_phase_remaining_minutes > 60, starta tvättmaskinen'. Kombinera med next_rising_phase_start_time för att undvika att starta en last som inte hinner bli klar innan priserna stiger."
},
"in_flat_price_phase": {
"description": "Whether prices are currently in a flat (stable) phase",
"long_description": "Turns ON when the current intra-day price phase is flat and prices are relatively stable.",
"usage_tips": "Use for loads that prefer price stability rather than the lowest price."
"description": "Om priserna för närvarande är i en flat (stabil) fas",
"long_description": "Slås PÅ när den aktuella prisfasen är flat — dvs. priserna är relativt stabila utan betydande uppgång eller nedgång. Flata faser innebär förutsägbara kostnader och passar för laster med osäker eller variabel varaktighet. Exakt en av in_rising_price_phase, in_falling_price_phase och in_flat_price_phase är PÅ vid varje tidpunkt.",
"usage_tips": "Använd det för laster som är likgiltiga för prisriktningen men föredrar stabilitet: värmepumpar i stabilt läge, bakgrundsladdning eller påfyllning av varmvattenberedaren. Kombinera med current_price_phase_end_time för att veta hur länge det stabila fönstret varar."
},
"connection": {
"description": "Om anslutningen till Tibber API fungerar",

View file

@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any
from zoneinfo import ZoneInfo
from custom_components.tibber_prices.api.exceptions import (
TibberPricesApiClientCommunicationError,
TibberPricesApiClientAuthenticationError,
TibberPricesApiClientError,
)
from homeassistant.util import dt as dt_util
@ -118,6 +118,13 @@ class TibberPricesIntervalPool:
self._save_debounce_task: asyncio.Task | None = None
self._save_lock = asyncio.Lock()
# Degraded-mode tracking (cache fallback during API outage).
# When a fetch fails but cached data still covers the current interval,
# the pool keeps serving cached data and flags itself as degraded so the
# coordinator can expose this on the connection sensor. Reset on success.
self._last_fetch_degraded = False
self._last_fetch_error: str | None = None
# DST fall-back extra intervals.
# On DST fall-back nights (e.g. last Sunday October in EU), wall-clock
# 02:00-02:45 occurs twice: once in CEST (+02:00) and once in CET (+01:00).
@ -127,12 +134,24 @@ class TibberPricesIntervalPool:
# Structure: {"2026-10-25T02:00:00": [{"startsAt": "...+01:00", ...}], ...}
self._dst_extras: dict[str, list[dict[str, Any]]] = {}
@property
def last_fetch_degraded(self) -> bool:
"""Return True if the most recent API fetch fell back to cached data."""
return self._last_fetch_degraded
@property
def last_fetch_error(self) -> str | None:
"""Return the error message of the most recent degraded fetch, if any."""
return self._last_fetch_error
async def get_intervals(
self,
api_client: TibberPricesApiClient,
user_data: dict[str, Any],
start_time: datetime,
end_time: datetime,
*,
track_degraded: bool = True,
) -> tuple[list[dict[str, Any]], bool]:
"""
Get price intervals for time range (cached + fetch missing).
@ -152,6 +171,14 @@ class TibberPricesIntervalPool:
user_data: User data dict containing home metadata.
start_time: Start of range (inclusive, timezone-aware).
end_time: End of range (exclusive, timezone-aware).
track_degraded: When True (default), API failures update the pool's
degraded-state flags (last_fetch_degraded/last_fetch_error) which
govern sensor availability, and the cache-fallback path is used to
keep sensors alive. This is the SENSOR path (get_sensor_data).
When False, the call is a SERVICE request: it must never touch
sensor-health state. Any API error is propagated unchanged so the
service handler can return a structured "data unavailable" response
without impairing sensors.
Returns:
Tuple of (intervals, api_called):
@ -203,6 +230,7 @@ class TibberPricesIntervalPool:
# Fetch missing ranges from API
api_fetch_failed = False
fallback_intervals: list[dict[str, Any]] | None = None
if missing_ranges:
fetch_time_iso = dt_util.now().isoformat()
@ -214,25 +242,71 @@ class TibberPricesIntervalPool:
missing_ranges=missing_ranges,
on_intervals_fetched=lambda intervals, _: self._add_intervals(intervals, fetch_time_iso),
)
except TibberPricesApiClientCommunicationError as err:
if cached_intervals:
# Transient API error (e.g. 503) but we have cached data - use it as
# fallback so the coordinator can finish initializing. The next regular
# update cycle will retry the API automatically.
except TibberPricesApiClientAuthenticationError:
# Authentication errors must always propagate so the coordinator can
# trigger the reauth flow. Cached data is never a valid fallback here -
# the user must provide a new token.
raise
except TibberPricesApiClientError as err:
# Any other API error (transient communication failure like 503/timeout,
# rate limiting, or an "empty data" response during a Tibber outage).
if not track_degraded:
# Service request: never mutate sensor-health degraded state and never
# silently serve a partial cache. Propagate so the service handler can
# return a structured "data unavailable" response. Sensor availability
# is governed solely by the regular coordinator update (get_sensor_data).
raise
#
# Resilience strategy: As long as the cache still covers the CURRENT
# interval, keep serving locally cached data so the integration keeps
# working benevolently through temporary outages. The next regular update
# cycle will retry the API automatically.
#
# Re-read the cache because the fetcher may have partially cached some
# ranges before failing.
available_intervals = self._get_cached_intervals(start_time_iso, end_time_iso)
if self._covers_current_interval(available_intervals):
_LOGGER.warning(
"API temporarily unavailable for home %s (%s) - using %d cached intervals as fallback",
"Tibber API temporarily unavailable for home %s (%s) - continuing with "
"%d cached intervals that still cover the current interval. "
"Will retry on next update cycle",
self._home_id,
err,
len(cached_intervals),
len(available_intervals),
)
api_fetch_failed = True
fallback_intervals = available_intervals
# Flag degraded mode so the coordinator/connection sensor can surface it.
self._last_fetch_degraded = True
self._last_fetch_error = str(err)
else:
# No cached data at all - re-raise so the caller can decide
# No cached data for the current interval - we cannot serve valid
# prices. Re-raise so the caller can mark the entry as not-ready
# (fresh setup) or raise UpdateFailed (running integration).
_LOGGER.error(
"Tibber API unavailable for home %s (%s) and no cached data covers the "
"current interval - cannot provide price data",
self._home_id,
err,
)
raise
else:
# Fetch succeeded - clear any previous degraded state (sensor path only).
# Service requests (track_degraded=False) must not clear a real sensor
# degradation, otherwise a successful service fetch could mask it.
if track_degraded:
self._last_fetch_degraded = False
self._last_fetch_error = None
# After caching all API responses, read from cache again to get final result
# This ensures we return exactly what user requested, filtering out extra intervals
final_result = self._get_cached_intervals(start_time_iso, end_time_iso)
# After caching all API responses, read from cache again to get final result.
# This ensures we return exactly what user requested, filtering out extra intervals.
# When we fell back to cache during an outage, reuse the already-read intervals
# instead of querying the cache a second time.
final_result = (
fallback_intervals
if fallback_intervals is not None
else self._get_cached_intervals(start_time_iso, end_time_iso)
)
# Track if API was called (True if any missing ranges were attempted)
# If fetch failed but we fell back to cache, treat as "no API call succeeded"
@ -551,6 +625,58 @@ class TibberPricesIntervalPool:
return None
def _covers_current_interval(self, intervals: list[dict[str, Any]]) -> bool:
"""
Return True if the cached intervals cover the current moment.
This is the resilience threshold used during Tibber API outages: as long
as we still have a price interval for "now", the integration can keep
serving sensors from locally cached data instead of failing to load.
Only when the current interval is missing do we treat the cache as
unusable and let the caller raise (alarm).
Args:
intervals: Cached interval dicts (each with a "startsAt" field).
Returns:
True if an interval covering the current time exists, False otherwise.
"""
if not intervals:
return False
now = self._time_service.now() if self._time_service else dt_util.now()
# Collect interval start datetimes (startsAt may be ISO string or datetime)
starts: list[datetime] = []
for interval in intervals:
starts_at = interval.get("startsAt")
if starts_at is None:
continue
start_dt = starts_at if isinstance(starts_at, datetime) else dt_util.parse_datetime(starts_at)
if start_dt is not None:
starts.append(start_dt)
if not starts:
return False
starts.sort()
# An interval covers "now" if start <= now < start + length. The length is
# derived from the gap to the next interval, capped at one hour so a large
# gap (missing data) cannot falsely report coverage. This handles both
# quarter-hourly (15 min) and hourly (60 min) resolutions.
max_length = timedelta(minutes=INTERVAL_HOURLY)
for index, start_dt in enumerate(starts):
if index + 1 < len(starts):
length = min(starts[index + 1] - start_dt, max_length)
else:
length = max_length
if start_dt <= now < start_dt + length:
return True
return False
def _get_cached_intervals(
self,
start_time_iso: str,

View file

@ -11,5 +11,5 @@
"requirements": [
"aiofiles>=23.2.1"
],
"version": "0.31.0b4"
"version": "0.32.0b1"
}

View file

@ -30,6 +30,7 @@ from .helpers import (
PRICE_LEVEL_ORDER,
VALID_SEARCH_SCOPES,
apply_must_finish_by,
async_fetch_service_intervals,
build_rating_lookup,
build_response_interval,
calculate_search_range_avg,
@ -310,25 +311,42 @@ async def _handle_find_block(
user_data = coordinator._cached_user_data # noqa: SLF001
pool = entry.runtime_data.interval_pool
try:
price_info, _api_called = await pool.get_intervals(
price_info, fetch_ok = await async_fetch_service_intervals(
pool,
api_client=api_client,
user_data=user_data,
start_time=search_start,
end_time=search_end,
service_label=service_label,
)
except Exception as error:
_LOGGER.exception("Error fetching price data for %s", service_label)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="price_fetch_failed",
) from error
# Determine currency and unit
currency = entry.data.get("currency", "EUR")
unit_factor = 1 if use_base_unit else get_display_unit_factor(entry)
price_unit = f"{currency}/kWh" if use_base_unit else get_display_unit_string(entry, currency)
if not fetch_ok:
# Price data unavailable (API outage on uncached range). Return a well-formed
# empty response with success=False so automations can detect this directly.
unavailable: dict[str, Any] = {
"success": False,
"reason": "price_data_unavailable",
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),
"must_finish_by": must_finish_by_dt.isoformat() if must_finish_by_dt else None,
"duration_minutes_requested": duration_minutes_requested,
"duration_minutes": duration_minutes_requested,
"currency": currency,
"price_unit": price_unit,
"window_found": False,
"relaxation_applied": False,
"window": None,
}
if resolved_refs:
unavailable["_resolved"] = resolved_refs
return unavailable
# --- Attempt with original parameters ---
effective_duration = duration_intervals
result, reason = _attempt_find_block(
@ -393,6 +411,7 @@ async def _handle_find_block(
len(price_info),
)
response: dict[str, Any] = {
"success": True,
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),
@ -452,6 +471,7 @@ async def _handle_find_block(
seconds_until_end = max(0, int((end_time_dt - now).total_seconds()))
response = {
"success": True,
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),

View file

@ -27,6 +27,7 @@ from .helpers import (
PRICE_LEVEL_ORDER,
VALID_SEARCH_SCOPES,
apply_must_finish_by,
async_fetch_service_intervals,
build_rating_lookup,
build_response_interval,
calculate_search_range_avg,
@ -267,6 +268,7 @@ def _build_found_response(
)
return {
"success": True,
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),
@ -382,25 +384,44 @@ async def _handle_find_hours(
user_data = coordinator._cached_user_data # noqa: SLF001
pool = entry.runtime_data.interval_pool
try:
price_info, _api_called = await pool.get_intervals(
price_info, fetch_ok = await async_fetch_service_intervals(
pool,
api_client=api_client,
user_data=user_data,
start_time=search_start,
end_time=search_end,
service_label=service_label,
)
except Exception as error:
_LOGGER.exception("Error fetching price data for %s", service_label)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="price_fetch_failed",
) from error
# Determine currency and unit
currency = entry.data.get("currency", "EUR")
unit_factor = 1 if use_base_unit else get_display_unit_factor(entry)
price_unit = f"{currency}/kWh" if use_base_unit else get_display_unit_string(entry, currency)
if not fetch_ok:
# Price data unavailable (API outage on uncached range). Return a well-formed
# empty response with success=False so automations can detect this directly.
unavailable: dict[str, Any] = {
"success": False,
"reason": "price_data_unavailable",
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),
"must_finish_by": must_finish_by_dt.isoformat() if must_finish_by_dt else None,
"total_minutes_requested": total_minutes_requested,
"total_minutes": total_minutes_requested,
"min_segment_minutes_requested": min_segment_minutes_requested,
"min_segment_minutes": min_segment_minutes,
"currency": currency,
"price_unit": price_unit,
"intervals_found": False,
"relaxation_applied": False,
"schedule": None,
}
if resolved_refs:
unavailable["_resolved"] = resolved_refs
return unavailable
# --- Attempt with original parameters ---
effective_total = total_intervals
result, reason = _attempt_find_hours(
@ -467,6 +488,7 @@ async def _handle_find_hours(
len(price_info),
)
response: dict[str, Any] = {
"success": True,
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),

View file

@ -30,6 +30,7 @@ from .helpers import (
PRICE_LEVEL_ORDER,
VALID_SEARCH_SCOPES,
apply_must_finish_by,
async_fetch_service_intervals,
build_rating_lookup,
build_response_interval,
filter_intervals_by_price_level,
@ -479,24 +480,42 @@ async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
user_data = coordinator._cached_user_data # noqa: SLF001
pool = entry.runtime_data.interval_pool
try:
price_info, _api_called = await pool.get_intervals(
price_info, fetch_ok = await async_fetch_service_intervals(
pool,
api_client=api_client,
user_data=user_data,
start_time=search_start,
end_time=search_end,
service_label=service_label,
)
except Exception as error:
_LOGGER.exception("Error fetching price data for %s", service_label)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="price_fetch_failed",
) from error
currency = entry.data.get("currency", "EUR")
unit_factor = 1 if use_base_unit else get_display_unit_factor(entry)
price_unit = f"{currency}/kWh" if use_base_unit else get_display_unit_string(entry, currency)
if not fetch_ok:
# Price data unavailable (API outage on uncached range). Return a well-formed
# empty response with success=False so automations can detect this directly.
unavailable: dict[str, Any] = {
"success": False,
"reason": "price_data_unavailable",
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),
"must_finish_by": must_finish_by_dt.isoformat() if must_finish_by_dt else None,
"currency": currency,
"price_unit": price_unit,
"sequential": sequential,
"all_tasks_scheduled": False,
"relaxation_applied": False,
"unscheduled_tasks": None,
"tasks": [],
"total_estimated_cost": None,
}
if resolved_refs:
unavailable["_resolved"] = resolved_refs
return unavailable
# --- Attempt with original parameters ---
raw_assignments, unscheduled, filtered = _attempt_schedule(
price_info,
@ -676,6 +695,7 @@ async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
task_entry["seconds_until_end"] = max(0, int((task_end_dt - now).total_seconds()))
result: dict[str, Any] = {
"success": True,
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),

View file

@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
from .entity_resolver import or_entity_ref, resolve_entity_references
from .helpers import get_entry_and_data
from .helpers import async_fetch_service_intervals, get_entry_and_data
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse
@ -150,31 +150,41 @@ async def handle_get_price(call: ServiceCall) -> ServiceResponse:
end_time,
)
try:
# Get interval pool from entry runtime_data (one pool per config entry)
pool = entry.runtime_data.interval_pool
# Call the interval pool to get intervals (with intelligent caching)
# Single-home architecture: pool knows its home_id, no parameter needed
price_info, _api_called = await pool.get_intervals(
# Resilient fetch: never impairs sensors, returns empty result on API failure
# instead of raising. Single-home architecture: pool knows its home_id.
price_info, fetch_ok = await async_fetch_service_intervals(
pool,
api_client=api_client,
user_data=user_data,
start_time=start_time,
end_time=end_time,
service_label="get_price",
)
# Note: We ignore api_called flag here - service always returns requested data
# regardless of whether it came from cache or was fetched fresh from API
except Exception as error:
_LOGGER.exception("Error fetching price data")
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="price_fetch_failed",
) from error
if not fetch_ok:
# Price data could not be fetched (API outage on an uncached range). Return a
# well-formed empty response with success=False so automations can detect this
# without inspecting the data fields.
response = {
"success": False,
"reason": "price_data_unavailable",
"home_id": home_id,
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),
"interval_count": 0,
"price_info": [],
}
if resolved_refs:
response["_resolved"] = resolved_refs
_LOGGER.info("get_price service completed: price data unavailable")
return response
else:
# Add metadata to response
response = {
"success": True,
"home_id": home_id,
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),

View file

@ -31,8 +31,13 @@ Used by:
from __future__ import annotations
from datetime import datetime, time as dt_time, timedelta
import logging
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.api.exceptions import (
TibberPricesApiClientAuthenticationError,
TibberPricesApiClientError,
)
from custom_components.tibber_prices.const import DOMAIN
from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets
from homeassistant.exceptions import ServiceValidationError
@ -42,8 +47,11 @@ if TYPE_CHECKING:
from zoneinfo import ZoneInfo
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.interval_pool import TibberPricesIntervalPool
from homeassistant.core import HomeAssistant
_LOGGER = logging.getLogger(__name__)
# Interval duration in minutes (quarter-hourly resolution)
INTERVAL_MINUTES = 15
@ -259,6 +267,69 @@ def get_entry_and_data(hass: HomeAssistant, entry_id: str) -> tuple[Any, Any, di
return entry, coordinator, data
async def async_fetch_service_intervals(
pool: TibberPricesIntervalPool,
*,
api_client: Any,
user_data: dict[str, Any] | None,
start_time: datetime,
end_time: datetime,
service_label: str,
) -> tuple[list[dict[str, Any]], bool]:
"""
Fetch price intervals for a service request resiliently.
Shared, resilient wrapper around the interval pool used by every price-query
service. It guarantees two things the raw pool call does not:
1. Sensor isolation: passes track_degraded=False so a service request can never
flip the pool's degraded-state flags (which govern sensor availability). A
failing or succeeding service fetch therefore has zero effect on sensor health.
2. Graceful failure: on a transient API/data error the helper returns
([], False) instead of raising, so the service can still return a well-formed
response (possibly with empty contents) for automations to consume.
Authentication errors are re-raised unchanged so the coordinator can trigger the
reauth flow.
Args:
pool: The config entry's interval pool.
api_client: TibberPricesApiClient instance.
user_data: Cached user data dict (may be None during early setup).
start_time: Start of range (inclusive, timezone-aware).
end_time: End of range (exclusive, timezone-aware).
service_label: Human-readable service name for logging.
Returns:
Tuple of (intervals, fetch_ok):
- intervals: Price intervals for the range. Empty list on failure.
- fetch_ok: True if the data was available, False if an API error
prevented fetching the requested (uncached) range.
Raises:
TibberPricesApiClientAuthenticationError: If authentication failed.
"""
try:
intervals, _api_called = await pool.get_intervals(
api_client=api_client,
user_data=user_data or {},
start_time=start_time,
end_time=end_time,
track_degraded=False,
)
except TibberPricesApiClientAuthenticationError:
raise
except TibberPricesApiClientError as err:
_LOGGER.warning(
"%s: price data unavailable (%s) - returning empty result so sensors stay unaffected",
service_label,
err,
)
return [], False
return intervals, True
def has_tomorrow_data(coordinator: TibberPricesDataUpdateCoordinator) -> bool:
"""
Check if tomorrow's price data is available in coordinator.

View file

@ -39,6 +39,7 @@ from .helpers import (
PRICE_LEVEL_ORDER,
VALID_SEARCH_SCOPES,
apply_must_finish_by,
async_fetch_service_intervals,
build_rating_lookup,
build_response_interval,
calculate_search_range_avg,
@ -660,6 +661,7 @@ async def handle_plan_charging(call: ServiceCall) -> ServiceResponse:
currency = entry.data.get("currency", "EUR")
price_unit = f"{currency}/kWh" if use_base_unit else get_display_unit_string(entry, currency)
response: dict[str, Any] = {
"success": True,
"home_id": entry.data.get("home_id", ""),
"intervals_found": False,
"reason": "already_at_target",
@ -742,19 +744,45 @@ async def handle_plan_charging(call: ServiceCall) -> ServiceResponse:
api_client = coordinator.api
user_data = coordinator._cached_user_data # noqa: SLF001
pool = entry.runtime_data.interval_pool
try:
price_info, _api_called = await pool.get_intervals(
price_info, fetch_ok = await async_fetch_service_intervals(
pool,
api_client=api_client,
user_data=user_data,
start_time=search_start,
end_time=search_end,
service_label=PLAN_CHARGING_SERVICE_NAME,
)
except Exception as error:
_LOGGER.exception("Error fetching price data for %s", PLAN_CHARGING_SERVICE_NAME)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="price_fetch_failed",
) from error
if not fetch_ok:
# Price data unavailable (API outage on uncached range). Return a well-formed
# empty response with success=False so automations can detect this directly.
unavailable: dict[str, Any] = {
"success": False,
"reason": "price_data_unavailable",
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),
"must_finish_by": must_finish_by_dt.isoformat() if must_finish_by_dt else None,
"intervals_found": False,
"battery": _build_battery_info(
current_soc_kwh=current_soc_kwh,
target_soc_kwh=target_soc_kwh,
capacity_kwh=capacity_kwh,
requested_energy_needed_kwh=requested_energy_needed_grid_kwh,
charging_efficiency=charging_efficiency,
achieved_soc_kwh=current_soc_kwh,
must_reach_soc_kwh=must_reach_soc_kwh,
),
"charging": None,
"deadline": {"must_reach_by": deadline.isoformat(), "source": deadline_source} if deadline else None,
"economics": None,
"currency": currency,
"price_unit": price_unit,
"relaxation_applied": False,
}
if resolved_refs:
unavailable["_resolved"] = resolved_refs
return unavailable
plan_ctx = _PlanContext(
price_info=price_info,
@ -824,6 +852,7 @@ async def handle_plan_charging(call: ServiceCall) -> ServiceResponse:
if planning_result is None:
response: dict[str, Any] = {
"success": True,
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),
@ -930,6 +959,7 @@ async def handle_plan_charging(call: ServiceCall) -> ServiceResponse:
)
response: dict[str, Any] = {
"success": True,
"home_id": home_id,
"search_start": search_start.isoformat(),
"search_end": search_end.isoformat(),

View file

@ -35,7 +35,7 @@
"finish": {
"description": "Wähle ein Zuhause, um Preisinformationen und Bewertungen abzurufen.",
"data": {
"home_id": "Home ID"
"home_id": "Zuhause-ID"
},
"title": "Wähle ein Zuhause",
"submit": "Zuhause auswählen"
@ -375,7 +375,7 @@
"submit": "↩ Speichern & Zurück"
},
"chart_data_export": {
"title": "📊 Chart Data Export Sensor",
"title": "📊 Chart-Daten-Export-Sensor",
"description": "Der Chart Data Export Sensor stellt Preisdaten als Sensor-Attribute zur Verfügung.\n\n⚠ **Hinweis:** Dieser Sensor ist ein Legacy-Feature für Kompatibilität mit älteren Tools.\n\n**Für neue Setups empfohlen:** Nutze den `tibber_prices.get_chartdata` **Service direkt** - er ist flexibler, effizienter und der moderne Home Assistant-Ansatz.\n\n**Wann dieser Sensor sinnvoll ist:**\n\n✅ Dein Dashboard-Tool kann **nur** Attribute lesen (keine Service-Aufrufe)\n✅ Du brauchst statische Daten, die automatisch aktualisiert werden\n❌ **Nicht für Automationen:** Nutze dort direkt `tibber_prices.get_chartdata` - flexibler und effizienter!\n\n---\n\n{sensor_status_info}",
"submit": "↩ Ok & Zurück"
},
@ -1029,34 +1029,34 @@
}
},
"current_price_phase_end_time": {
"name": "Current Phase End Time"
"name": "Aktuelle Phase Endzeit"
},
"current_price_phase_remaining_minutes": {
"name": "Current Phase Remaining"
"name": "Aktuelle Phase verbleibend"
},
"current_price_phase_duration": {
"name": "Current Phase Duration"
"name": "Aktuelle Phase Dauer"
},
"current_price_phase_progress": {
"name": "Current Phase Progress"
"name": "Aktuelle Phase Fortschritt"
},
"next_rising_phase_start_time": {
"name": "Next Rising Phase Start"
"name": "Nächste steigende Phase Start"
},
"next_falling_phase_start_time": {
"name": "Next Falling Phase Start"
"name": "Nächste fallende Phase Start"
},
"next_flat_phase_start_time": {
"name": "Next Flat Phase Start"
"name": "Nächste flache Phase Start"
},
"next_rising_phase_in_minutes": {
"name": "Time to Next Rising Phase"
"name": "Zeit bis nächste steigende Phase"
},
"next_falling_phase_in_minutes": {
"name": "Time to Next Falling Phase"
"name": "Zeit bis nächste fallende Phase"
},
"next_flat_phase_in_minutes": {
"name": "Time to Next Flat Phase"
"name": "Zeit bis nächste flache Phase"
},
"chart_data_export": {
"name": "Diagramm-Datenexport",
@ -1116,13 +1116,13 @@
"name": "Bestpreis-Zeitraum"
},
"in_rising_price_phase": {
"name": "In Rising Price Phase"
"name": "In steigender Preisphase"
},
"in_falling_price_phase": {
"name": "In Falling Price Phase"
"name": "In fallender Preisphase"
},
"in_flat_price_phase": {
"name": "In Flat Price Phase"
"name": "In flacher Preisphase"
},
"connection": {
"name": "Tibber-API-Verbindung"
@ -1201,6 +1201,10 @@
"title": "API-Ratenlimit erreicht für {home_name}",
"description": "Die Tibber-API hat diese Integration nach {error_count} aufeinanderfolgenden Fehlern ratenlimitiert. Das bedeutet, dass Anfragen zu häufig gestellt werden.\n\nDie Integration wird automatisch mit zunehmenden Verzögerungen erneut versuchen. Dieses Problem löst sich, sobald das Ratenlimit abläuft.\n\nFalls dies mehrere Stunden anhält, überprüfe:\n- Ob mehrere Home Assistant Instanzen denselben API-Token verwenden\n- Ob andere Anwendungen deinen Tibber-API-Token stark nutzen\n- Die Update-Frequenz reduzieren, falls du sie angepasst hast"
},
"api_outage": {
"title": "Tibber-API nicht erreichbar für {home_name}",
"description": "Seit {since} wurden keine aktuellen Preisdaten mehr von Tibber empfangen. Die Integration verwendet weiterhin lokal zwischengespeicherte Preise, sofern verfügbar, sodass deine Sensoren möglicherweise weiterarbeiten - sie können aber unverfügbar werden, wenn der Ausfall andauert (z. B. könnten die Preise für morgen fehlen).\n\nUrsache ist meist ein vorübergehender Tibber-Ausfall oder ein Netzwerkproblem. Die Integration versucht es bei jedem Update-Zyklus automatisch erneut und löst dieses Problem, sobald wieder frische Daten eintreffen.\n\nFalls dies anhält, überprüfe den Tibber-Dienststatus, deine Internetverbindung und ob dein API-Token noch gültig ist (ggf. neu anmelden)."
},
"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}."
@ -1237,7 +1241,7 @@
"message": "Der Endzeitpunkt ({search_end}) muss nach dem Startzeitpunkt ({search_start}) liegen. Überprüfe die Zeit-Parameter und eventuelle Day-Offsets."
},
"price_fetch_failed": {
"message": "Unable to fetch price data for the requested search range."
"message": "Preisdaten für den angeforderten Suchbereich konnten nicht abgerufen werden."
},
"invalid_search_scope": {
"message": "Ungültiger Suchbereich. Gültige Werte sind: today, tomorrow, remaining_today, next_24h, next_48h."
@ -1309,49 +1313,49 @@
"message": "Wert '{raw_value}' von '{entity_id}' ({attribute}) kann nicht in {expected_type} konvertiert werden. Überprüfe, ob die Entity einen kompatiblen Wert liefert."
},
"capacity_required_for_percent": {
"message": "battery_capacity_kwh is required when current or target SoC is provided as a percentage."
"message": "battery_capacity_kwh ist erforderlich, wenn der aktuelle oder Ziel-SoC in Prozent angegeben wird."
},
"ambiguous_soc_input": {
"message": "The field {field} was provided both as percent and as kWh. Use only one representation."
"message": "Das Feld {field} wurde sowohl in Prozent als auch in kWh angegeben. Verwende nur eine Darstellung."
},
"already_at_target": {
"message": "Current state of charge is already at or above the target. No charging schedule is needed."
"message": "Der aktuelle Ladezustand liegt bereits auf oder über dem Ziel. Es ist kein Ladeplan erforderlich."
},
"target_exceeds_capacity": {
"message": "Target SoC ({target_soc_kwh} kWh) exceeds battery capacity ({capacity_kwh} kWh)."
"message": "Der Ziel-SoC ({target_soc_kwh} kWh) übersteigt die Batteriekapazität ({capacity_kwh} kWh)."
},
"energy_unreachable": {
"message": "The requested charging target cannot be reached within the available search window and charging constraints."
"message": "Das angeforderte Ladeziel kann innerhalb des verfügbaren Suchfensters und der Ladebeschränkungen nicht erreicht werden."
},
"missing_current_soc": {
"message": "Provide either current_soc_percent or current_soc_kwh."
"message": "Gib entweder current_soc_percent oder current_soc_kwh an."
},
"missing_target_soc": {
"message": "Provide either target_soc_percent or target_soc_kwh."
"message": "Gib entweder target_soc_percent oder target_soc_kwh an."
},
"missing_deadline_for_must_reach": {
"message": "Provide must_reach_by or must_reach_by_event when using must_reach_soc_percent or must_reach_soc_kwh."
"message": "Gib must_reach_by oder must_reach_by_event an, wenn du must_reach_soc_percent oder must_reach_soc_kwh verwendest."
},
"missing_must_reach_soc": {
"message": "Provide must_reach_soc_percent or must_reach_soc_kwh when using a must_reach deadline."
"message": "Gib must_reach_soc_percent oder must_reach_soc_kwh an, wenn du einen must_reach-Stichtag verwendest."
},
"invalid_must_reach_soc": {
"message": "The minimum state of charge by deadline must be between current and target state of charge."
"message": "Der Mindestladezustand bis zum Stichtag muss zwischen dem aktuellen und dem Ziel-Ladezustand liegen."
},
"power_strategy_conflict": {
"message": "Use either min_charge_power_w or charge_power_steps_w, not both at the same time."
"message": "Verwende entweder min_charge_power_w oder charge_power_steps_w, nicht beide gleichzeitig."
},
"grid_limit_too_low": {
"message": "grid_import_limit_w is lower than the minimum required charging power."
"message": "grid_import_limit_w ist niedriger als die minimal erforderliche Ladeleistung."
},
"deadline_conflict": {
"message": "Use either must_reach_by or must_reach_by_event, not both at the same time."
"message": "Verwende entweder must_reach_by oder must_reach_by_event, nicht beide gleichzeitig."
},
"deadline_event_not_available": {
"message": "The selected deadline event is not available in the current coordinator data."
"message": "Das ausgewählte Stichtagsereignis ist in den aktuellen Koordinatordaten nicht verfügbar."
},
"deadline_outside_search_range": {
"message": "The resolved deadline must be inside the selected search range."
"message": "Der aufgelöste Stichtag muss innerhalb des ausgewählten Suchbereichs liegen."
}
},
"services": {
@ -1570,7 +1574,7 @@
}
},
"find_cheapest_block": {
"name": "Günstigsten Block finden",
"name": "Günstigsten Block finden (Experimentell)",
"description": "Findet das günstigste zusammenhängende Zeitfenster einer bestimmten Dauer. Gedacht für Geräteplanung: Spülmaschine, Waschmaschine, Trockner usw. Gibt das günstigste Fenster mit Start-/Endzeiten und Preisstatistiken zurück.",
"sections": {
"search_range": {
@ -1690,7 +1694,7 @@
}
},
"find_most_expensive_block": {
"name": "Teuersten Block finden",
"name": "Teuersten Block finden (Experimentell)",
"description": "Findet das teuerste zusammenhängende Zeitfenster einer bestimmten Dauer. Nützlich zur Erkennung von Spitzenpreiszeiträumen, die vermieden werden sollten. Gibt das teuerste Fenster mit Start-/Endzeiten und Preisstatistiken zurück.",
"sections": {
"search_range": {
@ -1810,7 +1814,7 @@
}
},
"find_cheapest_hours": {
"name": "Günstigste Stunden finden",
"name": "Günstigste Stunden finden (Experimentell)",
"description": "Findet die günstigsten Intervalle für eine bestimmte Gesamtdauer, nicht unbedingt zusammenhängend. Gedacht für flexible Lasten: Batterieladung, E-Auto, Warmwasserspeicher. Gibt einen Zeitplan mit Intervallen gruppiert in zusammenhängende Segmente zurück.",
"sections": {
"search_range": {
@ -1934,7 +1938,7 @@
}
},
"find_most_expensive_hours": {
"name": "Teuerste Stunden finden",
"name": "Teuerste Stunden finden (Experimentell)",
"description": "Findet die teuersten Intervalle für eine bestimmte Gesamtdauer, nicht unbedingt zusammenhängend. Nützlich zur Erkennung von Spitzenpreiszeiträumen, die vermieden werden sollten. Gibt einen Zeitplan mit Intervallen gruppiert in zusammenhängende Segmente zurück.",
"sections": {
"search_range": {
@ -2058,7 +2062,7 @@
}
},
"find_cheapest_schedule": {
"name": "Günstigstes Programm planen",
"name": "Günstigstes Programm planen (Experimentell)",
"description": "Plant mehrere Geräte optimal ohne Zeitüberschneidung. Jede Aufgabe erhält das günstigste verfügbare zusammenhängende Zeitfenster.",
"sections": {
"search_range": {
@ -2174,206 +2178,206 @@
}
},
"plan_charging": {
"name": "Plan Charging",
"description": "Creates a lowest-cost charging schedule from battery parameters instead of a fixed duration. Supports fixed, continuous, or stepped charging power, optional deadline-aware minimum SoC planning, and economic filtering for later discharge use cases. Returns a per-interval charging plan with power, energy, SoC progression, segment grouping, total cost, and profitability details. If no schedule is found, the response includes a stable reason code in the reason field (for example: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"name": "Laden planen (experimentell)",
"description": "Erstellt einen kostenoptimierten Ladeplan aus Batterieparametern statt einer festen Dauer. Unterstützt festes, kontinuierliches oder gestuftes Laden, optionale Mindest-SoC-Planung mit Stichtag sowie wirtschaftliche Filterung für spätere Entlade-Szenarien. Gibt einen Ladeplan pro Intervall zurück mit Leistung, Energie, SoC-Verlauf, Segmentgruppierung, Gesamtkosten und Wirtschaftlichkeitsdetails. Wird kein Plan gefunden, enthält die Antwort einen stabilen Ursachencode im Feld „reason“ (zum Beispiel: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"sections": {
"battery": {
"name": "Battery Parameters",
"description": "Describe the battery: usable capacity, current and target SoC, and charging efficiency. You can provide SoC in percent (requires capacity) or directly in kWh."
"name": "Batterieparameter",
"description": "Beschreibe die Batterie: nutzbare Kapazität, aktueller und gewünschter SoC sowie Ladewirkungsgrad. Du kannst den SoC in Prozent (erfordert Kapazität) oder direkt in kWh angeben."
},
"charging": {
"name": "Charging Strategy",
"description": "Configure variable-power charging behavior, grid limits, and minimum run-time constraints."
"name": "Ladestrategie",
"description": "Konfiguriere das Laden mit variabler Leistung, Netzgrenzen und Mindestlaufzeit-Vorgaben."
},
"deadline": {
"name": "Deadline Planning",
"description": "Require a minimum state of charge by a specific moment (for example before a peak period). Set both the Minimum SoC by Deadline and when it must be reached — either as an absolute time (Must Reach By) or from a known event (Deadline Event)."
"name": "Stichtagsplanung",
"description": "Verlange einen Mindestladezustand bis zu einem bestimmten Zeitpunkt (zum Beispiel vor einer Hochpreisphase). Lege sowohl den Mindest-SoC bis Stichtag als auch den Zeitpunkt fest entweder als absolute Zeit (Erreichen bis) oder über ein bekanntes Ereignis (Stichtagsereignis)."
},
"search_range": {
"name": "Custom Search Range",
"description": "Define precise start and end times for the search. Overrides Search Scope when set."
"name": "Eigener Suchbereich",
"description": "Lege präzise Start- und Endzeiten für die Suche fest. Überschreibt den Suchumfang, wenn gesetzt."
},
"time_alternatives": {
"name": "Advanced Time Options",
"description": "Alternative ways to define the search range using time-of-day and minute offsets."
"name": "Erweiterte Zeitoptionen",
"description": "Alternative Möglichkeiten, den Suchbereich über Tageszeit und Minutenversätze festzulegen."
},
"price_filter": {
"name": "Price Level Filter",
"description": "Restrict search to intervals within the specified Tibber price level range."
"name": "Preisniveau-Filter",
"description": "Beschränke die Suche auf Intervalle innerhalb des angegebenen Tibber-Preisniveaus."
},
"search_tuning": {
"name": "Search Algorithm Tuning",
"description": "Fine-tune how the search handles outliers, minimum quality thresholds, and fallback behavior."
"name": "Suchalgorithmus-Feinabstimmung",
"description": "Stelle fein ein, wie die Suche mit Ausreißern, Mindestqualitäts-Schwellen und Rückfallverhalten umgeht."
},
"economics": {
"name": "Economic Filters",
"description": "Filter charging intervals by maximum cost or expected later discharge value."
"name": "Wirtschaftliche Filter",
"description": "Filtere Ladeintervalle nach maximalen Kosten oder erwartetem späteren Entladewert."
},
"output": {
"name": "Output Options",
"description": "Control output format: comparison details and currency unit."
"name": "Ausgabeoptionen",
"description": "Steuere das Ausgabeformat: Vergleichsdetails und Währungseinheit."
}
},
"fields": {
"entry_id": {
"name": "Entry ID",
"description": "The config entry ID for the Tibber integration."
"name": "Eintrags-ID",
"description": "Die Konfigurationseintrags-ID der Tibber-Integration."
},
"max_charge_power_w": {
"name": "Maximum Charge Power",
"description": "Maximum charging power in watts. This defines the upper bound for fixed, continuous, or stepped charging schedules."
"name": "Maximale Ladeleistung",
"description": "Maximale Ladeleistung in Watt. Sie definiert die Obergrenze für festes, kontinuierliches oder gestuftes Laden."
},
"battery_capacity_kwh": {
"name": "Battery Capacity",
"description": "Usable battery capacity in kWh. Required when current or target SoC is provided as a percentage."
"name": "Batteriekapazität",
"description": "Nutzbare Batteriekapazität in kWh. Erforderlich, wenn aktueller oder Ziel-SoC in Prozent angegeben wird."
},
"current_soc_percent": {
"name": "Current SoC (%)",
"description": "Current battery state of charge in percent. Cannot be combined with Current SoC (kWh)."
"name": "Aktueller SoC (%)",
"description": "Aktueller Ladezustand der Batterie in Prozent. Kann nicht mit Aktueller SoC (kWh) kombiniert werden."
},
"current_soc_kwh": {
"name": "Current SoC (kWh)",
"description": "Current battery state of charge in kWh. Cannot be combined with Current SoC (%)."
"name": "Aktueller SoC (kWh)",
"description": "Aktueller Ladezustand der Batterie in kWh. Kann nicht mit Aktueller SoC (%) kombiniert werden."
},
"target_soc_percent": {
"name": "Target SoC (%)",
"description": "Desired battery state of charge in percent. Cannot be combined with Target SoC (kWh)."
"name": "Ziel-SoC (%)",
"description": "Gewünschter Ladezustand der Batterie in Prozent. Kann nicht mit Ziel-SoC (kWh) kombiniert werden."
},
"target_soc_kwh": {
"name": "Target SoC (kWh)",
"description": "Desired battery state of charge in kWh. Cannot be combined with Target SoC (%)."
"name": "Ziel-SoC (kWh)",
"description": "Gewünschter Ladezustand der Batterie in kWh. Kann nicht mit Ziel-SoC (%) kombiniert werden."
},
"charging_efficiency": {
"name": "Charging Efficiency",
"description": "Fraction of grid energy that is stored in the battery. Example: 0.92 means 8% charging losses."
"name": "Ladewirkungsgrad",
"description": "Anteil der Netzenergie, der in der Batterie gespeichert wird. Beispiel: 0,92 bedeutet 8 % Ladeverluste."
},
"search_scope": {
"name": "Search Scope",
"description": "Shorthand for common search ranges. Overrides all other time range options. today / tomorrow = full calendar day, remaining_today = now until midnight, next_24h / next_48h = rolling window from now."
"name": "Suchumfang",
"description": "Kurzform für gängige Suchbereiche. Überschreibt alle anderen Zeitbereichsoptionen. today / tomorrow = ganzer Kalendertag, remaining_today = jetzt bis Mitternacht, next_24h / next_48h = gleitendes Fenster ab jetzt."
},
"include_current_interval": {
"name": "Include Current Interval",
"description": "Include the currently running 15-minute interval in the search. When enabled, charging may begin in the current interval if it is part of the cheapest result."
"name": "Aktuelles Intervall einbeziehen",
"description": "Bezieht das aktuell laufende 15-Minuten-Intervall in die Suche ein. Wenn aktiviert, kann das Laden im aktuellen Intervall beginnen, sofern es Teil des günstigsten Ergebnisses ist."
},
"search_start": {
"name": "Search Start",
"description": "Start of the search range as exact date and time. Highest priority — overrides all other start options. Defaults to now if not specified."
"name": "Suchbeginn",
"description": "Beginn des Suchbereichs als genaues Datum und Uhrzeit. Höchste Priorität überschreibt alle anderen Startoptionen. Standard ist jetzt, falls nicht angegeben."
},
"search_end": {
"name": "Search End",
"description": "End of the search range as exact date and time. Highest priority — overrides all other end options. Defaults to end of tomorrow if not specified."
"name": "Suchende",
"description": "Ende des Suchbereichs als genaues Datum und Uhrzeit. Höchste Priorität überschreibt alle anderen Endoptionen. Standard ist Ende morgen, falls nicht angegeben."
},
"must_finish_by": {
"name": "Must Finish By",
"description": "Deadline: charging must be finished by this time. The search range ends at this deadline — the service finds the cheapest intervals that complete before it. Cannot be combined with Search End, Search End Time, or Search End Offset."
"name": "Fertig spätestens bis",
"description": "Stichtag: Das Laden muss bis zu diesem Zeitpunkt abgeschlossen sein. Der Suchbereich endet an diesem Stichtag der Dienst findet die günstigsten Intervalle, die vorher abschließen. Kann nicht mit Suchende, Suchende-Uhrzeit oder Suchende-Versatz kombiniert werden."
},
"search_start_time": {
"name": "Search Start Time",
"description": "Alternative: start searching at this time of day. Combine with day offset. Ignored if Search Start (datetime) is set."
"name": "Suchbeginn-Uhrzeit",
"description": "Alternative: Beginne die Suche zu dieser Tageszeit. Mit Tagesversatz kombinieren. Wird ignoriert, wenn Suchbeginn (Datum/Zeit) gesetzt ist."
},
"search_start_day_offset": {
"name": "Search Start Day Offset",
"description": "Day offset for Search Start Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search Start Time."
"name": "Suchbeginn-Tagesversatz",
"description": "Tagesversatz für die Suchbeginn-Uhrzeit. -7 bis 2: -1 = gestern, 0 = heute, 1 = morgen. Nur mit Suchbeginn-Uhrzeit verwendet."
},
"search_end_time": {
"name": "Search End Time",
"description": "Alternative: stop searching at this time of day. Combine with day offset. Ignored if Search End (datetime) is set."
"name": "Suchende-Uhrzeit",
"description": "Alternative: Beende die Suche zu dieser Tageszeit. Mit Tagesversatz kombinieren. Wird ignoriert, wenn Suchende (Datum/Zeit) gesetzt ist."
},
"search_end_day_offset": {
"name": "Search End Day Offset",
"description": "Day offset for Search End Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search End Time."
"name": "Suchende-Tagesversatz",
"description": "Tagesversatz für die Suchende-Uhrzeit. -7 bis 2: -1 = gestern, 0 = heute, 1 = morgen. Nur mit Suchende-Uhrzeit verwendet."
},
"search_start_offset_minutes": {
"name": "Search Start Offset (minutes)",
"description": "Alternative: start searching this many minutes from now. Positive = future, negative = past. Ignored if Search Start or Search Start Time is set."
"name": "Suchbeginn-Versatz (Minuten)",
"description": "Alternative: Beginne die Suche so viele Minuten ab jetzt. Positiv = Zukunft, negativ = Vergangenheit. Wird ignoriert, wenn Suchbeginn oder Suchbeginn-Uhrzeit gesetzt ist."
},
"search_end_offset_minutes": {
"name": "Search End Offset (minutes)",
"description": "Alternative: stop searching this many minutes from now. Positive = future, negative = past. Ignored if Search End or Search End Time is set."
"name": "Suchende-Versatz (Minuten)",
"description": "Alternative: Beende die Suche so viele Minuten ab jetzt. Positiv = Zukunft, negativ = Vergangenheit. Wird ignoriert, wenn Suchende oder Suchende-Uhrzeit gesetzt ist."
},
"max_price_level": {
"name": "Maximum Price Level",
"description": "Only consider intervals at or below this Tibber price level. very_cheap = most restrictive, very_expensive = no restriction."
"name": "Maximales Preisniveau",
"description": "Berücksichtige nur Intervalle bei oder unter diesem Tibber-Preisniveau. very_cheap = am restriktivsten, very_expensive = keine Einschränkung."
},
"min_price_level": {
"name": "Minimum Price Level",
"description": "Only consider intervals at or above this Tibber price level. Useful to exclude unrealistically cheap negative-price-only windows from mixed searches."
"name": "Minimales Preisniveau",
"description": "Berücksichtige nur Intervalle bei oder über diesem Tibber-Preisniveau. Nützlich, um unrealistisch günstige Fenster mit ausschließlich negativen Preisen aus gemischten Suchen auszuschließen."
},
"include_comparison_details": {
"name": "Include Comparison Details",
"description": "Enrich the price_comparison result with additional fields: comparison_price_min and comparison_price_max."
"name": "Vergleichsdetails einbeziehen",
"description": "Reichert das Ergebnis price_comparison um zusätzliche Felder an: comparison_price_min und comparison_price_max."
},
"use_base_unit": {
"name": "Use Base Currency Unit",
"description": "Force prices in base currency (EUR, NOK) instead of the configured display unit (ct, øre). Useful for calculations."
"name": "Basiswährungseinheit verwenden",
"description": "Erzwingt Preise in der Basiswährung (EUR, NOK) statt in der konfigurierten Anzeigeeinheit (ct, øre). Nützlich für Berechnungen."
},
"smooth_outliers": {
"name": "Smooth Outliers",
"description": "Smooth price outliers before searching. Outlier intervals are temporarily replaced by the average of their neighbors, so a single spike or dip does not dominate the result. The response always shows the original (unsmoothed) prices. Default: enabled."
"name": "Ausreißer glätten",
"description": "Glättet Preisausreißer vor der Suche. Ausreißer-Intervalle werden vorübergehend durch den Durchschnitt ihrer Nachbarn ersetzt, sodass eine einzelne Spitze oder ein Tief das Ergebnis nicht dominiert. Die Antwort zeigt immer die ursprünglichen (ungeglätteten) Preise. Standard: aktiviert."
},
"min_distance_from_avg": {
"name": "Min. Distance from Average",
"description": "Require the selected charging intervals to be at least this percentage below the average price of the full search range. Leave empty to disable."
"name": "Mindestabstand zum Durchschnitt",
"description": "Verlange, dass die ausgewählten Ladeintervalle mindestens diesen Prozentsatz unter dem Durchschnittspreis des gesamten Suchbereichs liegen. Leer lassen zum Deaktivieren."
},
"allow_relaxation": {
"name": "Allow relaxation",
"description": "Progressively relax filters to guarantee a result when possible. Phases: 1) Reduce/remove distance threshold 2) Expand level filters 3) Reduce duration. Default: enabled."
"name": "Lockerung zulassen",
"description": "Lockert die Filter schrittweise, um wenn möglich ein Ergebnis zu garantieren. Phasen: 1) Abstandsschwelle reduzieren/entfernen 2) Preisniveau-Filter erweitern 3) Dauer verkürzen. Standard: aktiviert."
},
"duration_flexibility_minutes": {
"name": "Duration flexibility",
"description": "Maximum minutes the automatically calculated charging duration may be shortened during relaxation (0120, step 15). Leave empty for automatic calculation."
"name": "Dauer-Flexibilität",
"description": "Maximale Minuten, um die die automatisch berechnete Ladedauer während der Lockerung verkürzt werden darf (0120, Schritt 15). Leer lassen für automatische Berechnung."
},
"must_reach_soc_percent": {
"name": "Minimum SoC by Deadline (%)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (kWh)."
"name": "Mindest-SoC bis Stichtag (%)",
"description": "Mindestladezustand der Batterie, der bis zum Stichtag erreicht werden muss. Kann nicht mit Mindest-SoC bis Stichtag (kWh) kombiniert werden."
},
"must_reach_soc_kwh": {
"name": "Minimum SoC by Deadline (kWh)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (%)."
"name": "Mindest-SoC bis Stichtag (kWh)",
"description": "Mindestladezustand der Batterie, der bis zum Stichtag erreicht werden muss. Kann nicht mit Mindest-SoC bis Stichtag (%) kombiniert werden."
},
"min_charge_power_w": {
"name": "Minimum Charge Power",
"description": "Enable continuous charging mode. The planner may reduce the last interval down to this minimum power instead of always using the maximum."
"name": "Minimale Ladeleistung",
"description": "Aktiviert den kontinuierlichen Lademodus. Der Planer darf das letzte Intervall bis zu dieser Mindestleistung reduzieren, statt immer die maximale Leistung zu nutzen."
},
"charge_power_steps_w": {
"name": "Charge Power Steps",
"description": "Enable stepped charging mode. Provide the allowed power steps in watts as a list, for example [1380, 4140, 11000]. The planner picks the smallest step that still covers the remaining energy in the final interval. Mutually exclusive with Minimum Charge Power."
"name": "Ladeleistungsstufen",
"description": "Aktiviert den gestuften Lademodus. Gib die erlaubten Leistungsstufen in Watt als Liste an, zum Beispiel [1380, 4140, 11000]. Der Planer wählt die kleinste Stufe, die die verbleibende Energie im letzten Intervall noch deckt. Schließt sich mit Minimale Ladeleistung gegenseitig aus."
},
"grid_import_limit_w": {
"name": "Grid Import Limit",
"description": "Upper limit for charging power drawn from the grid in watts. Useful when the charger must share available power with other loads."
"name": "Netzbezugsgrenze",
"description": "Obergrenze für die aus dem Netz bezogene Ladeleistung in Watt. Nützlich, wenn der Lader die verfügbare Leistung mit anderen Verbrauchern teilen muss."
},
"must_reach_by": {
"name": "Must Reach By",
"description": "Absolute deadline for must_reach_soc_*. The planner first guarantees the minimum SoC before this moment, then continues with the remaining target if possible."
"name": "Erreichen bis",
"description": "Absoluter Stichtag für must_reach_soc_*. Der Planer garantiert zuerst den Mindest-SoC vor diesem Zeitpunkt und fährt dann wenn möglich mit dem verbleibenden Ziel fort."
},
"must_reach_by_event": {
"name": "Deadline Event",
"description": "Alternative deadline derived from coordinator data. Use this instead of Must Reach By to plan around midnight, the next peak period, or the next best-price period end."
"name": "Stichtagsereignis",
"description": "Alternativer Stichtag, abgeleitet aus Koordinatordaten. Verwende dies statt Erreichen bis, um um Mitternacht, die nächste Hochpreisphase oder das Ende der nächsten Bestpreisphase herum zu planen."
},
"discharging_efficiency": {
"name": "Discharging Efficiency",
"description": "Fraction of stored battery energy that is still usable when later discharged. Used for profitability calculations."
"name": "Entladewirkungsgrad",
"description": "Anteil der gespeicherten Batterieenergie, der beim späteren Entladen noch nutzbar ist. Wird für Wirtschaftlichkeitsberechnungen verwendet."
},
"expected_discharge_price": {
"name": "Expected Discharge Price",
"description": "Expected value of each discharged kWh. Intervals above the break-even price can be filtered when Reserve For Discharge is enabled."
"name": "Erwarteter Entladepreis",
"description": "Erwarteter Wert jeder entladenen kWh. Intervalle über dem Break-even-Preis können gefiltert werden, wenn Für Entladung reservieren aktiviert ist."
},
"reserve_for_discharge": {
"name": "Reserve For Discharge",
"description": "Keep only intervals that are economically sensible for a later discharge based on expected discharge price and round-trip efficiency."
"name": "Für Entladung reservieren",
"description": "Behält nur Intervalle, die für eine spätere Entladung wirtschaftlich sinnvoll sind basierend auf erwartetem Entladepreis und Roundtrip-Wirkungsgrad."
},
"max_cost_per_kwh": {
"name": "Maximum Charge Price",
"description": "Discard candidate intervals above this price per kWh before scheduling. Uses the selected price unit."
"name": "Maximaler Ladepreis",
"description": "Verwirft Kandidaten-Intervalle über diesem Preis pro kWh vor der Planung. Verwendet die gewählte Preiseinheit."
},
"min_charge_duration_minutes": {
"name": "Minimum Charge Duration",
"description": "Merge short isolated intervals into longer charging blocks where possible. Useful for chargers that should avoid very short runs."
"name": "Minimale Ladedauer",
"description": "Führt kurze isolierte Intervalle nach Möglichkeit zu längeren Ladeblöcken zusammen. Nützlich für Lader, die sehr kurze Läufe vermeiden sollten."
},
"max_cycles_per_day": {
"name": "Maximum Charge Cycles Per Day",
"description": "Limit how many separate charging segments may be used per day. The planner keeps the cheapest segments within this limit."
"name": "Maximale Ladezyklen pro Tag",
"description": "Begrenzt, wie viele separate Ladesegmente pro Tag genutzt werden dürfen. Der Planer behält die günstigsten Segmente innerhalb dieser Grenze."
}
}
},
@ -2505,9 +2509,9 @@
},
"charging_deadline_event": {
"options": {
"midnight": "Midnight",
"next_peak_period": "Next Peak Period",
"next_best_period_end": "End of Next Best-Price Period"
"midnight": "Mitternacht",
"next_peak_period": "Nächste Hochpreisphase",
"next_best_period_end": "Ende der nächsten Bestpreisphase"
}
}
}

View file

@ -1201,6 +1201,10 @@
"title": "API rate limit exceeded for {home_name}",
"description": "The Tibber API has rate-limited this integration after {error_count} consecutive errors. This means requests are being made too frequently.\n\nThe integration will automatically retry with increasing delays. This issue will resolve once the rate limit expires.\n\nIf this persists for several hours, consider:\n- Checking if multiple Home Assistant instances are using the same API token\n- Verifying no other applications are heavily using your Tibber API token\n- Reducing the update frequency if you've customized it"
},
"api_outage": {
"title": "Tibber API unavailable for {home_name}",
"description": "No fresh price data has been received from Tibber since {since}. The integration keeps using locally cached prices where available, so your sensors may continue to work, but they can become unavailable if the outage continues (for example, tomorrow's prices may be missing).\n\nThis is usually caused by a temporary Tibber outage or a network problem. The integration retries automatically on every update cycle and clears this issue as soon as fresh data arrives.\n\nIf this persists, check the Tibber service status, your internet connection, and that your API token is still valid (re-authenticate if prompted)."
},
"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."
@ -1570,7 +1574,7 @@
}
},
"find_cheapest_block": {
"name": "Find Cheapest Block",
"name": "Find Cheapest Block (Experimental)",
"description": "Finds the cheapest contiguous time window of a given duration. Designed for appliance scheduling: dishwasher, washing machine, dryer, etc. Returns the single cheapest window with start/end times and price statistics. If no window is found, the response includes a stable reason code in the reason field (for example: no_data_in_range, no_intervals_matching_level_filter, insufficient_intervals_after_filter, insufficient_contiguous_window).",
"sections": {
"search_range": {
@ -1690,7 +1694,7 @@
}
},
"find_most_expensive_block": {
"name": "Find Most Expensive Block",
"name": "Find Most Expensive Block (Experimental)",
"description": "Finds the most expensive contiguous time window of a given duration. Useful for identifying peak price periods to avoid. Returns the single most expensive window with start/end times and price statistics.",
"sections": {
"search_range": {
@ -1810,7 +1814,7 @@
}
},
"find_cheapest_hours": {
"name": "Find Cheapest Hours",
"name": "Find Cheapest Hours (Experimental)",
"description": "Finds the cheapest intervals totaling a given duration, not necessarily contiguous. Designed for flexible loads: battery charging, EV, water heater. Returns a schedule of intervals grouped into contiguous segments. If no schedule is found, the response includes a stable reason code in the reason field (for example: no_data_in_range, no_intervals_matching_level_filter, insufficient_intervals_after_filter, insufficient_intervals_for_constraints).",
"sections": {
"search_range": {
@ -1934,7 +1938,7 @@
}
},
"find_most_expensive_hours": {
"name": "Find Most Expensive Hours",
"name": "Find Most Expensive Hours (Experimental)",
"description": "Finds the most expensive intervals totaling a given duration, not necessarily contiguous. Useful for identifying peak price periods to avoid. Returns a schedule of intervals grouped into contiguous segments.",
"sections": {
"search_range": {
@ -2058,7 +2062,7 @@
}
},
"find_cheapest_schedule": {
"name": "Find Cheapest Schedule",
"name": "Find Cheapest Schedule (Experimental)",
"description": "Schedules multiple appliances optimally without time overlap. Each task gets the cheapest available contiguous window; tasks are placed longest-first for efficient packing unless sequential ordering is enabled. Returns a per-task schedule with start/end times and price stats. If scheduling is incomplete, the response includes a stable reason code in the reason field (for example: no_data_in_range, no_intervals_matching_level_filter, insufficient_contiguous_window, insufficient_contiguous_window_for_some_tasks).",
"sections": {
"search_range": {
@ -2174,7 +2178,7 @@
}
},
"plan_charging": {
"name": "Plan Charging",
"name": "Plan Charging (Experimental)",
"description": "Creates a lowest-cost charging schedule from battery parameters instead of a fixed duration. Supports fixed, continuous, or stepped charging power, optional deadline-aware minimum SoC planning, and economic filtering for later discharge use cases. Returns a per-interval charging plan with power, energy, SoC progression, segment grouping, total cost, and profitability details. If no schedule is found, the response includes a stable reason code in the reason field (for example: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"sections": {
"battery": {

View file

@ -1029,34 +1029,34 @@
}
},
"current_price_phase_end_time": {
"name": "Current Phase End Time"
"name": "Gjeldende fase sluttid"
},
"current_price_phase_remaining_minutes": {
"name": "Current Phase Remaining"
"name": "Gjeldende fase gjenværende"
},
"current_price_phase_duration": {
"name": "Current Phase Duration"
"name": "Gjeldende fase varighet"
},
"current_price_phase_progress": {
"name": "Current Phase Progress"
"name": "Gjeldende fase fremgang"
},
"next_rising_phase_start_time": {
"name": "Next Rising Phase Start"
"name": "Neste stigende fase start"
},
"next_falling_phase_start_time": {
"name": "Next Falling Phase Start"
"name": "Neste fallende fase start"
},
"next_flat_phase_start_time": {
"name": "Next Flat Phase Start"
"name": "Neste flate fase start"
},
"next_rising_phase_in_minutes": {
"name": "Time to Next Rising Phase"
"name": "Tid til neste stigende fase"
},
"next_falling_phase_in_minutes": {
"name": "Time to Next Falling Phase"
"name": "Tid til neste fallende fase"
},
"next_flat_phase_in_minutes": {
"name": "Time to Next Flat Phase"
"name": "Tid til neste flate fase"
},
"chart_data_export": {
"name": "Diagramdataeksport",
@ -1116,13 +1116,13 @@
"name": "Lavpris-periode"
},
"in_rising_price_phase": {
"name": "In Rising Price Phase"
"name": "I stigende prisfase"
},
"in_falling_price_phase": {
"name": "In Falling Price Phase"
"name": "I fallende prisfase"
},
"in_flat_price_phase": {
"name": "In Flat Price Phase"
"name": "I flat prisfase"
},
"connection": {
"name": "Tibber API-tilkobling"
@ -1201,6 +1201,10 @@
"title": "API-hastighetsbegrensning overskredet for {home_name}",
"description": "Tibber-APIet har hastighetsbegrenset denne integrasjonen etter {error_count} påfølgende feil. Dette betyr at forespørsler blir gjort for hyppig.\n\nIntegrasjonen vil automatisk prøve på nytt med økende forsinkelser. Dette problemet vil løse seg når hastighetsbegrensningen utløper.\n\nHvis dette vedvarer i flere timer, vurder:\n- Å sjekke om flere Home Assistant-instanser bruker samme API-token\n- Å verifisere at ingen andre applikasjoner bruker Tibber-API-tokenet ditt mye\n- Å redusere oppdateringsfrekvensen hvis du har tilpasset den"
},
"api_outage": {
"title": "Tibber-API utilgjengelig for {home_name}",
"description": "Ingen ferske prisdata har blitt mottatt fra Tibber siden {since}. Integrasjonen fortsetter å bruke lokalt mellomlagrede priser der det er mulig, så sensorene dine kan fortsette å fungere - men de kan bli utilgjengelige hvis avbruddet vedvarer (for eksempel kan morgendagens priser mangle).\n\nDette skyldes vanligvis et midlertidig Tibber-avbrudd eller et nettverksproblem. Integrasjonen prøver automatisk på nytt ved hver oppdateringssyklus og fjerner dette problemet så snart ferske data ankommer.\n\nHvis dette vedvarer, sjekk Tibber-tjenestestatusen, internettforbindelsen din og at API-tokenet ditt fortsatt er gyldig (autentiser på nytt hvis du blir bedt om det)."
},
"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."
@ -1237,7 +1241,7 @@
"message": "Sluttidspunktet ({search_end}) må være etter starttidspunktet ({search_start}). Sjekk tid-parameterne og eventuelle day-offsets."
},
"price_fetch_failed": {
"message": "Unable to fetch price data for the requested search range."
"message": "Kunne ikke hente prisdata for det forespurte søkeområdet."
},
"invalid_search_scope": {
"message": "Ugyldig søkeområde. Gyldige verdier er: today, tomorrow, remaining_today, next_24h, next_48h."
@ -1309,49 +1313,49 @@
"message": "Kan ikke konvertere verdi '{raw_value}' fra '{entity_id}' ({attribute}) til {expected_type}. Kontroller at entiteten gir en kompatibel verdi."
},
"capacity_required_for_percent": {
"message": "battery_capacity_kwh is required when current or target SoC is provided as a percentage."
"message": "battery_capacity_kwh er påkrevd når nåværende eller mål-SoC oppgis i prosent."
},
"ambiguous_soc_input": {
"message": "The field {field} was provided both as percent and as kWh. Use only one representation."
"message": "Feltet {field} ble oppgitt både i prosent og i kWh. Bruk kun én representasjon."
},
"already_at_target": {
"message": "Current state of charge is already at or above the target. No charging schedule is needed."
"message": "Nåværende ladetilstand er allerede på eller over målet. Ingen ladeplan er nødvendig."
},
"target_exceeds_capacity": {
"message": "Target SoC ({target_soc_kwh} kWh) exceeds battery capacity ({capacity_kwh} kWh)."
"message": "Mål-SoC ({target_soc_kwh} kWh) overstiger batterikapasiteten ({capacity_kwh} kWh)."
},
"energy_unreachable": {
"message": "The requested charging target cannot be reached within the available search window and charging constraints."
"message": "Det forespurte ladenmålet kan ikke nås innenfor det tilgjengelige søkevinduet og ladebegrensningene."
},
"missing_current_soc": {
"message": "Provide either current_soc_percent or current_soc_kwh."
"message": "Oppgi enten current_soc_percent eller current_soc_kwh."
},
"missing_target_soc": {
"message": "Provide either target_soc_percent or target_soc_kwh."
"message": "Oppgi enten target_soc_percent eller target_soc_kwh."
},
"missing_deadline_for_must_reach": {
"message": "Provide must_reach_by or must_reach_by_event when using must_reach_soc_percent or must_reach_soc_kwh."
"message": "Oppgi must_reach_by eller must_reach_by_event når du bruker must_reach_soc_percent eller must_reach_soc_kwh."
},
"missing_must_reach_soc": {
"message": "Provide must_reach_soc_percent or must_reach_soc_kwh when using a must_reach deadline."
"message": "Oppgi must_reach_soc_percent eller must_reach_soc_kwh når du bruker en must_reach-frist."
},
"invalid_must_reach_soc": {
"message": "The minimum state of charge by deadline must be between current and target state of charge."
"message": "Minimums ladetilstand innen frist må være mellom nåværende og mål-ladetilstand."
},
"power_strategy_conflict": {
"message": "Use either min_charge_power_w or charge_power_steps_w, not both at the same time."
"message": "Bruk enten min_charge_power_w eller charge_power_steps_w, ikke begge samtidig."
},
"grid_limit_too_low": {
"message": "grid_import_limit_w is lower than the minimum required charging power."
"message": "grid_import_limit_w er lavere enn den minimalt påkrevde ladeeffekten."
},
"deadline_conflict": {
"message": "Use either must_reach_by or must_reach_by_event, not both at the same time."
"message": "Bruk enten must_reach_by eller must_reach_by_event, ikke begge samtidig."
},
"deadline_event_not_available": {
"message": "The selected deadline event is not available in the current coordinator data."
"message": "Den valgte fristhendelsen er ikke tilgjengelig i de gjeldende koordinatordataene."
},
"deadline_outside_search_range": {
"message": "The resolved deadline must be inside the selected search range."
"message": "Den oppløste fristen må være innenfor det valgte søkeområdet."
}
},
"services": {
@ -1570,7 +1574,7 @@
}
},
"find_cheapest_block": {
"name": "Finn billigste blokk",
"name": "Finn billigste blokk (Eksperimentell)",
"description": "Finner det billigste sammenhengende tidsvinduet med en gitt varighet. Designet for apparatplanlegging: oppvaskmaskin, vaskemaskin, tørketrommel osv. Returnerer det billigste vinduet med start-/sluttider og prisstatistikk.",
"sections": {
"search_range": {
@ -1690,7 +1694,7 @@
}
},
"find_most_expensive_block": {
"name": "Finn dyreste blokk",
"name": "Finn dyreste blokk (Eksperimentell)",
"description": "Finner det dyreste sammenhengende tidsvinduet med en gitt varighet. Nyttig for å identifisere topprisperioder som bør unngås. Returnerer det dyreste vinduet med start-/sluttider og prisstatistikk.",
"sections": {
"search_range": {
@ -1810,7 +1814,7 @@
}
},
"find_cheapest_hours": {
"name": "Finn billigste timer",
"name": "Finn billigste timer (Eksperimentell)",
"description": "Finner de billigste intervallene for en gitt total varighet, ikke nødvendigvis sammenhengende. Designet for fleksible laster: batterilading, elbil, varmtvannsbereder. Returnerer en tidsplan med intervaller gruppert i sammenhengende segmenter.",
"sections": {
"search_range": {
@ -1934,7 +1938,7 @@
}
},
"find_most_expensive_hours": {
"name": "Finn dyreste timer",
"name": "Finn dyreste timer (Eksperimentell)",
"description": "Finner de dyreste intervallene for en gitt total varighet, ikke nødvendigvis sammenhengende. Nyttig for å identifisere topprisperioder som bør unngås. Returnerer en tidsplan med intervaller gruppert i sammenhengende segmenter.",
"sections": {
"search_range": {
@ -2058,7 +2062,7 @@
}
},
"find_cheapest_schedule": {
"name": "Finn billigste tidsplan",
"name": "Finn billigste tidsplan (Eksperimentell)",
"description": "Planlegger flere apparater optimalt uten tidsoverlapp. Hver oppgave tildeles det billigste tilgjengelige sammenhengende tidsvinduet.",
"sections": {
"search_range": {
@ -2174,206 +2178,206 @@
}
},
"plan_charging": {
"name": "Plan Charging",
"description": "Creates a lowest-cost charging schedule from battery parameters instead of a fixed duration. Supports fixed, continuous, or stepped charging power, optional deadline-aware minimum SoC planning, and economic filtering for later discharge use cases. Returns a per-interval charging plan with power, energy, SoC progression, segment grouping, total cost, and profitability details. If no schedule is found, the response includes a stable reason code in the reason field (for example: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"name": "Planlegg lading (eksperimentell)",
"description": "Lager en lavkost-ladeplan ut fra batteriparametere i stedet for en fast varighet. Støtter fast, kontinuerlig eller trinnvis ladeeffekt, valgfri planlegging med minimums-SoC innen en frist, og økonomisk filtrering for senere utladingsscenarier. Returnerer en ladeplan per intervall med effekt, energi, SoC-utvikling, segmentgruppering, totalkostnad og lønnsomhetsdetaljer. Hvis ingen plan finnes, inneholder svaret en stabil årsakskode i feltet «reason» (for eksempel: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"sections": {
"battery": {
"name": "Battery Parameters",
"description": "Describe the battery: usable capacity, current and target SoC, and charging efficiency. You can provide SoC in percent (requires capacity) or directly in kWh."
"name": "Batteriparametere",
"description": "Beskriv batteriet: brukbar kapasitet, nåværende og ønsket SoC, og ladeeffektivitet. Du kan oppgi SoC i prosent (krever kapasitet) eller direkte i kWh."
},
"charging": {
"name": "Charging Strategy",
"description": "Configure variable-power charging behavior, grid limits, and minimum run-time constraints."
"name": "Ladestrategi",
"description": "Konfigurer lading med variabel effekt, nettgrenser og minimums kjøretidsbegrensninger."
},
"deadline": {
"name": "Deadline Planning",
"description": "Require a minimum state of charge by a specific moment (for example before a peak period). Set both the Minimum SoC by Deadline and when it must be reached — either as an absolute time (Must Reach By) or from a known event (Deadline Event)."
"name": "Fristplanlegging",
"description": "Krev en minimums ladetilstand innen et bestemt tidspunkt (for eksempel før en topprisperiode). Sett både Minimums-SoC innen frist og når den må nås enten som et absolutt tidspunkt (Må nås innen) eller fra en kjent hendelse (Fristhendelse)."
},
"search_range": {
"name": "Custom Search Range",
"description": "Define precise start and end times for the search. Overrides Search Scope when set."
"name": "Egendefinert søkeområde",
"description": "Definer presise start- og sluttidspunkt for søket. Overstyrer Søkeomfang når det er satt."
},
"time_alternatives": {
"name": "Advanced Time Options",
"description": "Alternative ways to define the search range using time-of-day and minute offsets."
"name": "Avanserte tidsalternativer",
"description": "Alternative måter å definere søkeområdet på ved hjelp av tid på døgnet og minuttforskyvninger."
},
"price_filter": {
"name": "Price Level Filter",
"description": "Restrict search to intervals within the specified Tibber price level range."
"name": "Prisnivåfilter",
"description": "Begrens søket til intervaller innenfor det angitte Tibber-prisnivået."
},
"search_tuning": {
"name": "Search Algorithm Tuning",
"description": "Fine-tune how the search handles outliers, minimum quality thresholds, and fallback behavior."
"name": "Finjustering av søkealgoritme",
"description": "Finjuster hvordan søket håndterer avvik, minimums kvalitetsterskler og reserveatferd."
},
"economics": {
"name": "Economic Filters",
"description": "Filter charging intervals by maximum cost or expected later discharge value."
"name": "Økonomiske filtre",
"description": "Filtrer ladeintervaller etter maksimal kostnad eller forventet senere utladingsverdi."
},
"output": {
"name": "Output Options",
"description": "Control output format: comparison details and currency unit."
"name": "Utdataalternativer",
"description": "Styr utdataformatet: sammenligningsdetaljer og valutaenhet."
}
},
"fields": {
"entry_id": {
"name": "Entry ID",
"description": "The config entry ID for the Tibber integration."
"name": "Oppførings-ID",
"description": "Konfigurasjonsoppførings-ID-en for Tibber-integrasjonen."
},
"max_charge_power_w": {
"name": "Maximum Charge Power",
"description": "Maximum charging power in watts. This defines the upper bound for fixed, continuous, or stepped charging schedules."
"name": "Maksimal ladeeffekt",
"description": "Maksimal ladeeffekt i watt. Dette definerer øvre grense for fast, kontinuerlig eller trinnvis lading."
},
"battery_capacity_kwh": {
"name": "Battery Capacity",
"description": "Usable battery capacity in kWh. Required when current or target SoC is provided as a percentage."
"name": "Batterikapasitet",
"description": "Brukbar batterikapasitet i kWh. Påkrevd når nåværende eller mål-SoC oppgis i prosent."
},
"current_soc_percent": {
"name": "Current SoC (%)",
"description": "Current battery state of charge in percent. Cannot be combined with Current SoC (kWh)."
"name": "Nåværende SoC (%)",
"description": "Nåværende ladetilstand for batteriet i prosent. Kan ikke kombineres med Nåværende SoC (kWh)."
},
"current_soc_kwh": {
"name": "Current SoC (kWh)",
"description": "Current battery state of charge in kWh. Cannot be combined with Current SoC (%)."
"name": "Nåværende SoC (kWh)",
"description": "Nåværende ladetilstand for batteriet i kWh. Kan ikke kombineres med Nåværende SoC (%)."
},
"target_soc_percent": {
"name": "Target SoC (%)",
"description": "Desired battery state of charge in percent. Cannot be combined with Target SoC (kWh)."
"name": "Mål-SoC (%)",
"description": "Ønsket ladetilstand for batteriet i prosent. Kan ikke kombineres med Mål-SoC (kWh)."
},
"target_soc_kwh": {
"name": "Target SoC (kWh)",
"description": "Desired battery state of charge in kWh. Cannot be combined with Target SoC (%)."
"name": "Mål-SoC (kWh)",
"description": "Ønsket ladetilstand for batteriet i kWh. Kan ikke kombineres med Mål-SoC (%)."
},
"charging_efficiency": {
"name": "Charging Efficiency",
"description": "Fraction of grid energy that is stored in the battery. Example: 0.92 means 8% charging losses."
"name": "Ladeeffektivitet",
"description": "Andelen nettenergi som lagres i batteriet. Eksempel: 0,92 betyr 8 % ladetap."
},
"search_scope": {
"name": "Search Scope",
"description": "Shorthand for common search ranges. Overrides all other time range options. today / tomorrow = full calendar day, remaining_today = now until midnight, next_24h / next_48h = rolling window from now."
"name": "Søkeomfang",
"description": "Snarvei for vanlige søkeområder. Overstyrer alle andre tidsområdealternativer. today / tomorrow = hele kalenderdagen, remaining_today = nå til midnatt, next_24h / next_48h = rullerende vindu fra nå."
},
"include_current_interval": {
"name": "Include Current Interval",
"description": "Include the currently running 15-minute interval in the search. When enabled, charging may begin in the current interval if it is part of the cheapest result."
"name": "Inkluder gjeldende intervall",
"description": "Inkluder det 15-minutters intervallet som kjører nå i søket. Når aktivert, kan lading begynne i gjeldende intervall hvis det er en del av det billigste resultatet."
},
"search_start": {
"name": "Search Start",
"description": "Start of the search range as exact date and time. Highest priority — overrides all other start options. Defaults to now if not specified."
"name": "Søkestart",
"description": "Start på søkeområdet som nøyaktig dato og tid. Høyest prioritet overstyrer alle andre startalternativer. Standard er nå hvis ikke angitt."
},
"search_end": {
"name": "Search End",
"description": "End of the search range as exact date and time. Highest priority — overrides all other end options. Defaults to end of tomorrow if not specified."
"name": "Søkeslutt",
"description": "Slutt på søkeområdet som nøyaktig dato og tid. Høyest prioritet overstyrer alle andre sluttalternativer. Standard er slutten av morgendagen hvis ikke angitt."
},
"must_finish_by": {
"name": "Must Finish By",
"description": "Deadline: charging must be finished by this time. The search range ends at this deadline — the service finds the cheapest intervals that complete before it. Cannot be combined with Search End, Search End Time, or Search End Offset."
"name": "Må fullføres innen",
"description": "Frist: lading må være fullført innen dette tidspunktet. Søkeområdet slutter ved denne fristen tjenesten finner de billigste intervallene som fullfører før den. Kan ikke kombineres med Søkeslutt, Søkesluttid eller Søkesluttforskyvning."
},
"search_start_time": {
"name": "Search Start Time",
"description": "Alternative: start searching at this time of day. Combine with day offset. Ignored if Search Start (datetime) is set."
"name": "Søkestarttidspunkt",
"description": "Alternativ: start søk på dette tidspunktet på døgnet. Kombiner med dagforskyvning. Ignoreres hvis Søkestart (dato/tid) er satt."
},
"search_start_day_offset": {
"name": "Search Start Day Offset",
"description": "Day offset for Search Start Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search Start Time."
"name": "Dagforskyvning for søkestart",
"description": "Dagforskyvning for søkestarttidspunktet. -7 til 2: -1 = i går, 0 = i dag, 1 = i morgen. Brukes kun med Søkestarttidspunkt."
},
"search_end_time": {
"name": "Search End Time",
"description": "Alternative: stop searching at this time of day. Combine with day offset. Ignored if Search End (datetime) is set."
"name": "Søkesluttidspunkt",
"description": "Alternativ: stopp søk på dette tidspunktet på døgnet. Kombiner med dagforskyvning. Ignoreres hvis Søkeslutt (dato/tid) er satt."
},
"search_end_day_offset": {
"name": "Search End Day Offset",
"description": "Day offset for Search End Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search End Time."
"name": "Dagforskyvning for søkeslutt",
"description": "Dagforskyvning for søkesluttidspunktet. -7 til 2: -1 = i går, 0 = i dag, 1 = i morgen. Brukes kun med Søkesluttidspunkt."
},
"search_start_offset_minutes": {
"name": "Search Start Offset (minutes)",
"description": "Alternative: start searching this many minutes from now. Positive = future, negative = past. Ignored if Search Start or Search Start Time is set."
"name": "Søkestartforskyvning (minutter)",
"description": "Alternativ: start søk så mange minutter fra nå. Positiv = fremtid, negativ = fortid. Ignoreres hvis Søkestart eller Søkestarttidspunkt er satt."
},
"search_end_offset_minutes": {
"name": "Search End Offset (minutes)",
"description": "Alternative: stop searching this many minutes from now. Positive = future, negative = past. Ignored if Search End or Search End Time is set."
"name": "Søkesluttforskyvning (minutter)",
"description": "Alternativ: stopp søk så mange minutter fra nå. Positiv = fremtid, negativ = fortid. Ignoreres hvis Søkeslutt eller Søkesluttidspunkt er satt."
},
"max_price_level": {
"name": "Maximum Price Level",
"description": "Only consider intervals at or below this Tibber price level. very_cheap = most restrictive, very_expensive = no restriction."
"name": "Maksimalt prisnivå",
"description": "Vurder kun intervaller på eller under dette Tibber-prisnivået. very_cheap = mest restriktiv, very_expensive = ingen begrensning."
},
"min_price_level": {
"name": "Minimum Price Level",
"description": "Only consider intervals at or above this Tibber price level. Useful to exclude unrealistically cheap negative-price-only windows from mixed searches."
"name": "Minimum prisnivå",
"description": "Vurder kun intervaller på eller over dette Tibber-prisnivået. Nyttig for å utelukke urealistisk billige vinduer med kun negative priser fra blandede søk."
},
"include_comparison_details": {
"name": "Include Comparison Details",
"description": "Enrich the price_comparison result with additional fields: comparison_price_min and comparison_price_max."
"name": "Inkluder sammenligningsdetaljer",
"description": "Berik price_comparison-resultatet med flere felt: comparison_price_min og comparison_price_max."
},
"use_base_unit": {
"name": "Use Base Currency Unit",
"description": "Force prices in base currency (EUR, NOK) instead of the configured display unit (ct, øre). Useful for calculations."
"name": "Bruk basisvalutaenhet",
"description": "Tving priser i basisvaluta (EUR, NOK) i stedet for den konfigurerte visningsenheten (ct, øre). Nyttig for beregninger."
},
"smooth_outliers": {
"name": "Smooth Outliers",
"description": "Smooth price outliers before searching. Outlier intervals are temporarily replaced by the average of their neighbors, so a single spike or dip does not dominate the result. The response always shows the original (unsmoothed) prices. Default: enabled."
"name": "Jevn ut avvik",
"description": "Jevn ut prisavvik før søk. Avviksintervaller erstattes midlertidig med gjennomsnittet av naboene sine, slik at en enkelt topp eller bunn ikke dominerer resultatet. Svaret viser alltid de opprinnelige (ujevnede) prisene. Standard: aktivert."
},
"min_distance_from_avg": {
"name": "Min. Distance from Average",
"description": "Require the selected charging intervals to be at least this percentage below the average price of the full search range. Leave empty to disable."
"name": "Min. avstand fra gjennomsnitt",
"description": "Krev at de valgte ladeintervallene ligger minst denne prosenten under gjennomsnittsprisen for hele søkeområdet. La stå tom for å deaktivere."
},
"allow_relaxation": {
"name": "Allow relaxation",
"description": "Progressively relax filters to guarantee a result when possible. Phases: 1) Reduce/remove distance threshold 2) Expand level filters 3) Reduce duration. Default: enabled."
"name": "Tillat oppmykning",
"description": "Myk gradvis opp filtre for å garantere et resultat når det er mulig. Faser: 1) Reduser/fjern avstandsterskel 2) Utvid nivåfiltre 3) Reduser varighet. Standard: aktivert."
},
"duration_flexibility_minutes": {
"name": "Duration flexibility",
"description": "Maximum minutes the automatically calculated charging duration may be shortened during relaxation (0120, step 15). Leave empty for automatic calculation."
"name": "Varighetsfleksibilitet",
"description": "Maksimalt antall minutter den automatisk beregnede ladevarigheten kan forkortes under oppmykning (0120, steg 15). La stå tom for automatisk beregning."
},
"must_reach_soc_percent": {
"name": "Minimum SoC by Deadline (%)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (kWh)."
"name": "Minimums-SoC innen frist (%)",
"description": "Minimums ladetilstand for batteriet som må nås innen fristen. Kan ikke kombineres med Minimums-SoC innen frist (kWh)."
},
"must_reach_soc_kwh": {
"name": "Minimum SoC by Deadline (kWh)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (%)."
"name": "Minimums-SoC innen frist (kWh)",
"description": "Minimums ladetilstand for batteriet som må nås innen fristen. Kan ikke kombineres med Minimums-SoC innen frist (%)."
},
"min_charge_power_w": {
"name": "Minimum Charge Power",
"description": "Enable continuous charging mode. The planner may reduce the last interval down to this minimum power instead of always using the maximum."
"name": "Minimum ladeeffekt",
"description": "Aktiver kontinuerlig lademodus. Planleggeren kan redusere det siste intervallet ned til denne minimumseffekten i stedet for alltid å bruke maksimum."
},
"charge_power_steps_w": {
"name": "Charge Power Steps",
"description": "Enable stepped charging mode. Provide the allowed power steps in watts as a list, for example [1380, 4140, 11000]. The planner picks the smallest step that still covers the remaining energy in the final interval. Mutually exclusive with Minimum Charge Power."
"name": "Ladeeffekttrinn",
"description": "Aktiver trinnvis lademodus. Oppgi de tillatte effekttrinnene i watt som en liste, for eksempel [1380, 4140, 11000]. Planleggeren velger det minste trinnet som fortsatt dekker gjenværende energi i det siste intervallet. Gjensidig utelukkende med Minimum ladeeffekt."
},
"grid_import_limit_w": {
"name": "Grid Import Limit",
"description": "Upper limit for charging power drawn from the grid in watts. Useful when the charger must share available power with other loads."
"name": "Nettimportgrense",
"description": "Øvre grense for ladeeffekt hentet fra nettet i watt. Nyttig når laderen må dele tilgjengelig effekt med andre laster."
},
"must_reach_by": {
"name": "Must Reach By",
"description": "Absolute deadline for must_reach_soc_*. The planner first guarantees the minimum SoC before this moment, then continues with the remaining target if possible."
"name": "Må nås innen",
"description": "Absolutt frist for must_reach_soc_*. Planleggeren garanterer først minimums-SoC før dette tidspunktet, og fortsetter deretter med det gjenværende målet hvis mulig."
},
"must_reach_by_event": {
"name": "Deadline Event",
"description": "Alternative deadline derived from coordinator data. Use this instead of Must Reach By to plan around midnight, the next peak period, or the next best-price period end."
"name": "Fristhendelse",
"description": "Alternativ frist avledet fra koordinatordata. Bruk dette i stedet for Må nås innen for å planlegge rundt midnatt, neste topprisperiode eller slutten av neste bestprisperiode."
},
"discharging_efficiency": {
"name": "Discharging Efficiency",
"description": "Fraction of stored battery energy that is still usable when later discharged. Used for profitability calculations."
"name": "Utladingseffektivitet",
"description": "Andelen lagret batterienergi som fortsatt er brukbar ved senere utlading. Brukes til lønnsomhetsberegninger."
},
"expected_discharge_price": {
"name": "Expected Discharge Price",
"description": "Expected value of each discharged kWh. Intervals above the break-even price can be filtered when Reserve For Discharge is enabled."
"name": "Forventet utladingspris",
"description": "Forventet verdi av hver utladede kWh. Intervaller over balansprisen kan filtreres når Reserver for utlading er aktivert."
},
"reserve_for_discharge": {
"name": "Reserve For Discharge",
"description": "Keep only intervals that are economically sensible for a later discharge based on expected discharge price and round-trip efficiency."
"name": "Reserver for utlading",
"description": "Behold kun intervaller som er økonomisk fornuftige for en senere utlading basert på forventet utladingspris og rundturseffektivitet."
},
"max_cost_per_kwh": {
"name": "Maximum Charge Price",
"description": "Discard candidate intervals above this price per kWh before scheduling. Uses the selected price unit."
"name": "Maksimal ladepris",
"description": "Forkast kandidatintervaller over denne prisen per kWh før planlegging. Bruker den valgte prisenheten."
},
"min_charge_duration_minutes": {
"name": "Minimum Charge Duration",
"description": "Merge short isolated intervals into longer charging blocks where possible. Useful for chargers that should avoid very short runs."
"name": "Minimum ladevarighet",
"description": "Slå sammen korte isolerte intervaller til lengre ladeblokker der det er mulig. Nyttig for ladere som bør unngå svært korte kjøringer."
},
"max_cycles_per_day": {
"name": "Maximum Charge Cycles Per Day",
"description": "Limit how many separate charging segments may be used per day. The planner keeps the cheapest segments within this limit."
"name": "Maksimalt antall ladesykluser per dag",
"description": "Begrens hvor mange separate ladesegmenter som kan brukes per dag. Planleggeren beholder de billigste segmentene innenfor denne grensen."
}
}
},
@ -2505,9 +2509,9 @@
},
"charging_deadline_event": {
"options": {
"midnight": "Midnight",
"next_peak_period": "Next Peak Period",
"next_best_period_end": "End of Next Best-Price Period"
"midnight": "Midnatt",
"next_peak_period": "Neste topprisperiode",
"next_best_period_end": "Slutten på neste bestprisperiode"
}
}
}

View file

@ -1029,34 +1029,34 @@
}
},
"current_price_phase_end_time": {
"name": "Current Phase End Time"
"name": "Huidige Fase Eindtijd"
},
"current_price_phase_remaining_minutes": {
"name": "Current Phase Remaining"
"name": "Huidige Fase Resterend"
},
"current_price_phase_duration": {
"name": "Current Phase Duration"
"name": "Huidige Fase Duur"
},
"current_price_phase_progress": {
"name": "Current Phase Progress"
"name": "Huidige Fase Voortgang"
},
"next_rising_phase_start_time": {
"name": "Next Rising Phase Start"
"name": "Volgende Stijgende Fase Start"
},
"next_falling_phase_start_time": {
"name": "Next Falling Phase Start"
"name": "Volgende Dalende Fase Start"
},
"next_flat_phase_start_time": {
"name": "Next Flat Phase Start"
"name": "Volgende Vlakke Fase Start"
},
"next_rising_phase_in_minutes": {
"name": "Time to Next Rising Phase"
"name": "Tijd tot Volgende Stijgende Fase"
},
"next_falling_phase_in_minutes": {
"name": "Time to Next Falling Phase"
"name": "Tijd tot Volgende Dalende Fase"
},
"next_flat_phase_in_minutes": {
"name": "Time to Next Flat Phase"
"name": "Tijd tot Volgende Vlakke Fase"
},
"chart_data_export": {
"name": "Grafiekdata Export",
@ -1116,13 +1116,13 @@
"name": "Beste Prijs Periode"
},
"in_rising_price_phase": {
"name": "In Rising Price Phase"
"name": "In Stijgende Prijsfase"
},
"in_falling_price_phase": {
"name": "In Falling Price Phase"
"name": "In Dalende Prijsfase"
},
"in_flat_price_phase": {
"name": "In Flat Price Phase"
"name": "In Vlakke Prijsfase"
},
"connection": {
"name": "Tibber API Verbinding"
@ -1201,6 +1201,10 @@
"title": "API rate limiet overschreden voor {home_name}",
"description": "De Tibber API heeft deze integratie beperkt na {error_count} opeenvolgende fouten. Dit betekent dat verzoeken te frequent worden gedaan.\n\nDe integratie zal automatisch opnieuw proberen met toenemende vertragingen. Dit probleem lost zich op zodra de rate limiet verloopt.\n\nAls dit meerdere uren aanhoudt, overweeg dan:\n- Te controleren of meerdere Home Assistant instanties hetzelfde API-token gebruiken\n- Te verifiëren dat geen andere applicaties intensief je Tibber API-token gebruiken\n- De update frequentie te verlagen als je deze hebt aangepast"
},
"api_outage": {
"title": "Tibber API niet beschikbaar voor {home_name}",
"description": "Sinds {since} zijn er geen verse prijsgegevens meer van Tibber ontvangen. De integratie blijft lokaal gecachete prijzen gebruiken waar beschikbaar, dus je sensoren kunnen blijven werken - maar ze kunnen onbeschikbaar worden als de storing aanhoudt (de prijzen van morgen kunnen bijvoorbeeld ontbreken).\n\nDit wordt meestal veroorzaakt door een tijdelijke Tibber-storing of een netwerkprobleem. De integratie probeert bij elke updatecyclus automatisch opnieuw en lost dit probleem op zodra er weer verse gegevens binnenkomen.\n\nAls dit aanhoudt, controleer de Tibber servicestatus, je internetverbinding en of je API-token nog geldig is (authenticeer opnieuw indien gevraagd)."
},
"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."
@ -1237,7 +1241,7 @@
"message": "Het eindtijdstip ({search_end}) moet na het starttijdstip ({search_start}) liggen. Controleer je tijdparameters en eventuele day-offsets."
},
"price_fetch_failed": {
"message": "Unable to fetch price data for the requested search range."
"message": "Kan geen prijsgegevens ophalen voor het gevraagde zoekbereik."
},
"invalid_search_scope": {
"message": "Ongeldig zoekbereik. Geldige waarden zijn: today, tomorrow, remaining_today, next_24h, next_48h."
@ -1309,49 +1313,49 @@
"message": "Kan waarde '{raw_value}' van '{entity_id}' ({attribute}) niet converteren naar {expected_type}. Controleer of de entiteit een compatibele waarde biedt."
},
"capacity_required_for_percent": {
"message": "battery_capacity_kwh is required when current or target SoC is provided as a percentage."
"message": "battery_capacity_kwh is vereist wanneer huidige of doel-SoC in procenten wordt opgegeven."
},
"ambiguous_soc_input": {
"message": "The field {field} was provided both as percent and as kWh. Use only one representation."
"message": "Het veld {field} is zowel in procenten als in kWh opgegeven. Gebruik slechts één weergave."
},
"already_at_target": {
"message": "Current state of charge is already at or above the target. No charging schedule is needed."
"message": "De huidige laadtoestand is al op of boven het doel. Er is geen laadschema nodig."
},
"target_exceeds_capacity": {
"message": "Target SoC ({target_soc_kwh} kWh) exceeds battery capacity ({capacity_kwh} kWh)."
"message": "Doel-SoC ({target_soc_kwh} kWh) overschrijdt de batterijcapaciteit ({capacity_kwh} kWh)."
},
"energy_unreachable": {
"message": "The requested charging target cannot be reached within the available search window and charging constraints."
"message": "Het gevraagde laaddoel kan niet worden bereikt binnen het beschikbare zoekvenster en de laadbeperkingen."
},
"missing_current_soc": {
"message": "Provide either current_soc_percent or current_soc_kwh."
"message": "Geef current_soc_percent of current_soc_kwh op."
},
"missing_target_soc": {
"message": "Provide either target_soc_percent or target_soc_kwh."
"message": "Geef target_soc_percent of target_soc_kwh op."
},
"missing_deadline_for_must_reach": {
"message": "Provide must_reach_by or must_reach_by_event when using must_reach_soc_percent or must_reach_soc_kwh."
"message": "Geef must_reach_by of must_reach_by_event op wanneer je must_reach_soc_percent of must_reach_soc_kwh gebruikt."
},
"missing_must_reach_soc": {
"message": "Provide must_reach_soc_percent or must_reach_soc_kwh when using a must_reach deadline."
"message": "Geef must_reach_soc_percent of must_reach_soc_kwh op wanneer je een must_reach-deadline gebruikt."
},
"invalid_must_reach_soc": {
"message": "The minimum state of charge by deadline must be between current and target state of charge."
"message": "De minimale laadtoestand vóór de deadline moet tussen de huidige en de doel-laadtoestand liggen."
},
"power_strategy_conflict": {
"message": "Use either min_charge_power_w or charge_power_steps_w, not both at the same time."
"message": "Gebruik min_charge_power_w of charge_power_steps_w, niet beide tegelijk."
},
"grid_limit_too_low": {
"message": "grid_import_limit_w is lower than the minimum required charging power."
"message": "grid_import_limit_w is lager dan het minimaal vereiste laadvermogen."
},
"deadline_conflict": {
"message": "Use either must_reach_by or must_reach_by_event, not both at the same time."
"message": "Gebruik must_reach_by of must_reach_by_event, niet beide tegelijk."
},
"deadline_event_not_available": {
"message": "The selected deadline event is not available in the current coordinator data."
"message": "De geselecteerde deadlinegebeurtenis is niet beschikbaar in de huidige coördinatorgegevens."
},
"deadline_outside_search_range": {
"message": "The resolved deadline must be inside the selected search range."
"message": "De afgeleide deadline moet binnen het geselecteerde zoekbereik liggen."
}
},
"services": {
@ -1360,7 +1364,7 @@
"description": "Haal prijsgegevens op voor een specifiek tijdsbereik met automatische routing. Ontwikkelings- en testservice voor de price_info_for_range API-functie. Gebruikt automatisch PRICE_INFO, PRICE_INFO_RANGE of beide op basis van de tijdsbereikgrens.",
"fields": {
"entry_id": {
"name": "Entry ID",
"name": "Item-ID",
"description": "De configuratie entry ID voor de Tibber integratie."
},
"start_time": {
@ -1570,7 +1574,7 @@
}
},
"find_cheapest_block": {
"name": "Goedkoopste blok vinden",
"name": "Goedkoopste blok vinden (Experimenteel)",
"description": "Vindt het goedkoopste aaneengesloten tijdvenster van een bepaalde duur. Ontworpen voor apparaatplanning: vaatwasser, wasmachine, droger enz. Retourneert het goedkoopste venster met start-/eindtijden en prijsstatistieken.",
"sections": {
"search_range": {
@ -1690,7 +1694,7 @@
}
},
"find_most_expensive_block": {
"name": "Duurste blok vinden",
"name": "Duurste blok vinden (Experimenteel)",
"description": "Vindt het duurste aaneengesloten tijdvenster van een bepaalde duur. Nuttig voor het identificeren van piekprijsperioden die vermeden moeten worden. Retourneert het duurste venster met start-/eindtijden en prijsstatistieken.",
"sections": {
"search_range": {
@ -1810,7 +1814,7 @@
}
},
"find_cheapest_hours": {
"name": "Goedkoopste uren vinden",
"name": "Goedkoopste uren vinden (Experimenteel)",
"description": "Vindt de goedkoopste intervallen voor een bepaalde totale duur, niet noodzakelijk aaneengesloten. Ontworpen voor flexibele belastingen: batterijladen, elektrisch voertuig, warmwaterboiler. Retourneert een schema van intervallen gegroepeerd in aaneengesloten segmenten.",
"sections": {
"search_range": {
@ -1934,7 +1938,7 @@
}
},
"find_most_expensive_hours": {
"name": "Duurste uren vinden",
"name": "Duurste uren vinden (Experimenteel)",
"description": "Vindt de duurste intervallen voor een bepaalde totale duur, niet noodzakelijk aaneengesloten. Nuttig voor het identificeren van piekprijsperioden die vermeden moeten worden. Retourneert een schema van intervallen gegroepeerd in aaneengesloten segmenten.",
"sections": {
"search_range": {
@ -2058,7 +2062,7 @@
}
},
"find_cheapest_schedule": {
"name": "Goedkoopste schema vinden",
"name": "Goedkoopste schema vinden (Experimenteel)",
"description": "Plant meerdere apparaten optimaal zonder tijdoverlap. Elke taak krijgt het goedkoopste beschikbare aaneengesloten tijdvenster.",
"sections": {
"search_range": {
@ -2174,206 +2178,206 @@
}
},
"plan_charging": {
"name": "Plan Charging",
"description": "Creates a lowest-cost charging schedule from battery parameters instead of a fixed duration. Supports fixed, continuous, or stepped charging power, optional deadline-aware minimum SoC planning, and economic filtering for later discharge use cases. Returns a per-interval charging plan with power, energy, SoC progression, segment grouping, total cost, and profitability details. If no schedule is found, the response includes a stable reason code in the reason field (for example: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"name": "Laden plannen (experimenteel)",
"description": "Maakt een schema voor laden tegen de laagste kosten op basis van batterijparameters in plaats van een vaste duur. Ondersteunt vast, continu of getrapt laadvermogen, optionele planning met een minimum-SoC vóór een deadline, en economische filtering voor latere ontladingsscenario's. Retourneert een laadschema per interval met vermogen, energie, SoC-verloop, segmentgroepering, totale kosten en winstgevendheidsdetails. Als er geen schema wordt gevonden, bevat het antwoord een stabiele redencode in het veld 'reason' (bijvoorbeeld: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"sections": {
"battery": {
"name": "Battery Parameters",
"description": "Describe the battery: usable capacity, current and target SoC, and charging efficiency. You can provide SoC in percent (requires capacity) or directly in kWh."
"name": "Batterijparameters",
"description": "Beschrijf de batterij: bruikbare capaciteit, huidige en gewenste SoC, en laadrendement. Je kunt de SoC opgeven in procenten (vereist capaciteit) of direct in kWh."
},
"charging": {
"name": "Charging Strategy",
"description": "Configure variable-power charging behavior, grid limits, and minimum run-time constraints."
"name": "Laadstrategie",
"description": "Configureer laden met variabel vermogen, netlimieten en minimale looptijdbeperkingen."
},
"deadline": {
"name": "Deadline Planning",
"description": "Require a minimum state of charge by a specific moment (for example before a peak period). Set both the Minimum SoC by Deadline and when it must be reached — either as an absolute time (Must Reach By) or from a known event (Deadline Event)."
"name": "Deadlineplanning",
"description": "Eis een minimale laadtoestand vóór een bepaald moment (bijvoorbeeld vóór een piekperiode). Stel zowel de Minimum-SoC vóór deadline in als wanneer deze bereikt moet zijn als absolute tijd (Bereiken vóór) of vanuit een bekende gebeurtenis (Deadlinegebeurtenis)."
},
"search_range": {
"name": "Custom Search Range",
"description": "Define precise start and end times for the search. Overrides Search Scope when set."
"name": "Aangepast zoekbereik",
"description": "Definieer precieze start- en eindtijden voor het zoeken. Overschrijft Zoekbereik wanneer ingesteld."
},
"time_alternatives": {
"name": "Advanced Time Options",
"description": "Alternative ways to define the search range using time-of-day and minute offsets."
"name": "Geavanceerde tijdopties",
"description": "Alternatieve manieren om het zoekbereik te definiëren met tijdstip van de dag en minuutverschuivingen."
},
"price_filter": {
"name": "Price Level Filter",
"description": "Restrict search to intervals within the specified Tibber price level range."
"name": "Prijsniveaufilter",
"description": "Beperk het zoeken tot intervallen binnen het opgegeven Tibber-prijsniveau."
},
"search_tuning": {
"name": "Search Algorithm Tuning",
"description": "Fine-tune how the search handles outliers, minimum quality thresholds, and fallback behavior."
"name": "Zoekalgoritme-afstemming",
"description": "Stem fijn af hoe het zoeken omgaat met uitschieters, minimale kwaliteitsdrempels en terugvalgedrag."
},
"economics": {
"name": "Economic Filters",
"description": "Filter charging intervals by maximum cost or expected later discharge value."
"name": "Economische filters",
"description": "Filter laadintervallen op maximale kosten of verwachte latere ontladingswaarde."
},
"output": {
"name": "Output Options",
"description": "Control output format: comparison details and currency unit."
"name": "Uitvoeropties",
"description": "Bepaal het uitvoerformaat: vergelijkingsdetails en valuta-eenheid."
}
},
"fields": {
"entry_id": {
"name": "Entry ID",
"description": "The config entry ID for the Tibber integration."
"name": "Item-ID",
"description": "De configuratie-item-ID voor de Tibber-integratie."
},
"max_charge_power_w": {
"name": "Maximum Charge Power",
"description": "Maximum charging power in watts. This defines the upper bound for fixed, continuous, or stepped charging schedules."
"name": "Maximaal laadvermogen",
"description": "Maximaal laadvermogen in watt. Dit bepaalt de bovengrens voor vaste, continue of getrapte laadschema's."
},
"battery_capacity_kwh": {
"name": "Battery Capacity",
"description": "Usable battery capacity in kWh. Required when current or target SoC is provided as a percentage."
"name": "Batterijcapaciteit",
"description": "Bruikbare batterijcapaciteit in kWh. Vereist wanneer huidige of doel-SoC in procenten wordt opgegeven."
},
"current_soc_percent": {
"name": "Current SoC (%)",
"description": "Current battery state of charge in percent. Cannot be combined with Current SoC (kWh)."
"name": "Huidige SoC (%)",
"description": "Huidige laadtoestand van de batterij in procenten. Kan niet worden gecombineerd met Huidige SoC (kWh)."
},
"current_soc_kwh": {
"name": "Current SoC (kWh)",
"description": "Current battery state of charge in kWh. Cannot be combined with Current SoC (%)."
"name": "Huidige SoC (kWh)",
"description": "Huidige laadtoestand van de batterij in kWh. Kan niet worden gecombineerd met Huidige SoC (%)."
},
"target_soc_percent": {
"name": "Target SoC (%)",
"description": "Desired battery state of charge in percent. Cannot be combined with Target SoC (kWh)."
"name": "Doel-SoC (%)",
"description": "Gewenste laadtoestand van de batterij in procenten. Kan niet worden gecombineerd met Doel-SoC (kWh)."
},
"target_soc_kwh": {
"name": "Target SoC (kWh)",
"description": "Desired battery state of charge in kWh. Cannot be combined with Target SoC (%)."
"name": "Doel-SoC (kWh)",
"description": "Gewenste laadtoestand van de batterij in kWh. Kan niet worden gecombineerd met Doel-SoC (%)."
},
"charging_efficiency": {
"name": "Charging Efficiency",
"description": "Fraction of grid energy that is stored in the battery. Example: 0.92 means 8% charging losses."
"name": "Laadrendement",
"description": "Deel van de netenergie dat in de batterij wordt opgeslagen. Voorbeeld: 0,92 betekent 8% laadverlies."
},
"search_scope": {
"name": "Search Scope",
"description": "Shorthand for common search ranges. Overrides all other time range options. today / tomorrow = full calendar day, remaining_today = now until midnight, next_24h / next_48h = rolling window from now."
"name": "Zoekbereik",
"description": "Snelkoppeling voor veelgebruikte zoekbereiken. Overschrijft alle andere tijdbereikopties. today / tomorrow = volledige kalenderdag, remaining_today = nu tot middernacht, next_24h / next_48h = voortschrijdend venster vanaf nu."
},
"include_current_interval": {
"name": "Include Current Interval",
"description": "Include the currently running 15-minute interval in the search. When enabled, charging may begin in the current interval if it is part of the cheapest result."
"name": "Huidig interval opnemen",
"description": "Neem het momenteel lopende interval van 15 minuten op in het zoeken. Indien ingeschakeld, kan laden in het huidige interval beginnen als het deel uitmaakt van het goedkoopste resultaat."
},
"search_start": {
"name": "Search Start",
"description": "Start of the search range as exact date and time. Highest priority — overrides all other start options. Defaults to now if not specified."
"name": "Zoekstart",
"description": "Start van het zoekbereik als exacte datum en tijd. Hoogste prioriteit overschrijft alle andere startopties. Standaard nu indien niet opgegeven."
},
"search_end": {
"name": "Search End",
"description": "End of the search range as exact date and time. Highest priority — overrides all other end options. Defaults to end of tomorrow if not specified."
"name": "Zoekeinde",
"description": "Einde van het zoekbereik als exacte datum en tijd. Hoogste prioriteit overschrijft alle andere eindopties. Standaard einde van morgen indien niet opgegeven."
},
"must_finish_by": {
"name": "Must Finish By",
"description": "Deadline: charging must be finished by this time. The search range ends at this deadline — the service finds the cheapest intervals that complete before it. Cannot be combined with Search End, Search End Time, or Search End Offset."
"name": "Moet klaar zijn vóór",
"description": "Deadline: laden moet vóór dit tijdstip voltooid zijn. Het zoekbereik eindigt op deze deadline de service vindt de goedkoopste intervallen die ervoor voltooien. Kan niet worden gecombineerd met Zoekeinde, Zoekeindtijd of Zoekeindverschuiving."
},
"search_start_time": {
"name": "Search Start Time",
"description": "Alternative: start searching at this time of day. Combine with day offset. Ignored if Search Start (datetime) is set."
"name": "Zoekstarttijd",
"description": "Alternatief: begin met zoeken op dit tijdstip van de dag. Combineer met dagverschuiving. Genegeerd als Zoekstart (datum/tijd) is ingesteld."
},
"search_start_day_offset": {
"name": "Search Start Day Offset",
"description": "Day offset for Search Start Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search Start Time."
"name": "Zoekstart-dagverschuiving",
"description": "Dagverschuiving voor de Zoekstarttijd. -7 tot 2: -1 = gisteren, 0 = vandaag, 1 = morgen. Alleen gebruikt met Zoekstarttijd."
},
"search_end_time": {
"name": "Search End Time",
"description": "Alternative: stop searching at this time of day. Combine with day offset. Ignored if Search End (datetime) is set."
"name": "Zoekeindtijd",
"description": "Alternatief: stop met zoeken op dit tijdstip van de dag. Combineer met dagverschuiving. Genegeerd als Zoekeinde (datum/tijd) is ingesteld."
},
"search_end_day_offset": {
"name": "Search End Day Offset",
"description": "Day offset for Search End Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search End Time."
"name": "Zoekeinde-dagverschuiving",
"description": "Dagverschuiving voor de Zoekeindtijd. -7 tot 2: -1 = gisteren, 0 = vandaag, 1 = morgen. Alleen gebruikt met Zoekeindtijd."
},
"search_start_offset_minutes": {
"name": "Search Start Offset (minutes)",
"description": "Alternative: start searching this many minutes from now. Positive = future, negative = past. Ignored if Search Start or Search Start Time is set."
"name": "Zoekstartverschuiving (minuten)",
"description": "Alternatief: begin met zoeken zoveel minuten vanaf nu. Positief = toekomst, negatief = verleden. Genegeerd als Zoekstart of Zoekstarttijd is ingesteld."
},
"search_end_offset_minutes": {
"name": "Search End Offset (minutes)",
"description": "Alternative: stop searching this many minutes from now. Positive = future, negative = past. Ignored if Search End or Search End Time is set."
"name": "Zoekeindeverschuiving (minuten)",
"description": "Alternatief: stop met zoeken zoveel minuten vanaf nu. Positief = toekomst, negatief = verleden. Genegeerd als Zoekeinde of Zoekeindtijd is ingesteld."
},
"max_price_level": {
"name": "Maximum Price Level",
"description": "Only consider intervals at or below this Tibber price level. very_cheap = most restrictive, very_expensive = no restriction."
"name": "Maximaal prijsniveau",
"description": "Overweeg alleen intervallen op of onder dit Tibber-prijsniveau. very_cheap = meest beperkend, very_expensive = geen beperking."
},
"min_price_level": {
"name": "Minimum Price Level",
"description": "Only consider intervals at or above this Tibber price level. Useful to exclude unrealistically cheap negative-price-only windows from mixed searches."
"name": "Minimaal prijsniveau",
"description": "Overweeg alleen intervallen op of boven dit Tibber-prijsniveau. Handig om onrealistisch goedkope vensters met alleen negatieve prijzen uit te sluiten bij gemengde zoekopdrachten."
},
"include_comparison_details": {
"name": "Include Comparison Details",
"description": "Enrich the price_comparison result with additional fields: comparison_price_min and comparison_price_max."
"name": "Vergelijkingsdetails opnemen",
"description": "Verrijk het price_comparison-resultaat met extra velden: comparison_price_min en comparison_price_max."
},
"use_base_unit": {
"name": "Use Base Currency Unit",
"description": "Force prices in base currency (EUR, NOK) instead of the configured display unit (ct, øre). Useful for calculations."
"name": "Basisvaluta-eenheid gebruiken",
"description": "Forceer prijzen in basisvaluta (EUR, NOK) in plaats van de geconfigureerde weergave-eenheid (ct, øre). Handig voor berekeningen."
},
"smooth_outliers": {
"name": "Smooth Outliers",
"description": "Smooth price outliers before searching. Outlier intervals are temporarily replaced by the average of their neighbors, so a single spike or dip does not dominate the result. The response always shows the original (unsmoothed) prices. Default: enabled."
"name": "Uitschieters afvlakken",
"description": "Vlak prijsuitschieters af vóór het zoeken. Uitschieterintervallen worden tijdelijk vervangen door het gemiddelde van hun buren, zodat een enkele piek of dal het resultaat niet domineert. Het antwoord toont altijd de oorspronkelijke (niet-afgevlakte) prijzen. Standaard: ingeschakeld."
},
"min_distance_from_avg": {
"name": "Min. Distance from Average",
"description": "Require the selected charging intervals to be at least this percentage below the average price of the full search range. Leave empty to disable."
"name": "Min. afstand tot gemiddelde",
"description": "Eis dat de geselecteerde laadintervallen minstens dit percentage onder de gemiddelde prijs van het volledige zoekbereik liggen. Laat leeg om uit te schakelen."
},
"allow_relaxation": {
"name": "Allow relaxation",
"description": "Progressively relax filters to guarantee a result when possible. Phases: 1) Reduce/remove distance threshold 2) Expand level filters 3) Reduce duration. Default: enabled."
"name": "Versoepeling toestaan",
"description": "Versoepel filters geleidelijk om indien mogelijk een resultaat te garanderen. Fasen: 1) Afstandsdrempel verlagen/verwijderen 2) Niveaufilters uitbreiden 3) Duur verkorten. Standaard: ingeschakeld."
},
"duration_flexibility_minutes": {
"name": "Duration flexibility",
"description": "Maximum minutes the automatically calculated charging duration may be shortened during relaxation (0120, step 15). Leave empty for automatic calculation."
"name": "Duurflexibiliteit",
"description": "Maximaal aantal minuten dat de automatisch berekende laadduur tijdens versoepeling mag worden ingekort (0120, stap 15). Laat leeg voor automatische berekening."
},
"must_reach_soc_percent": {
"name": "Minimum SoC by Deadline (%)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (kWh)."
"name": "Minimum-SoC vóór deadline (%)",
"description": "Minimale laadtoestand van de batterij die vóór de deadline moet worden bereikt. Kan niet worden gecombineerd met Minimum-SoC vóór deadline (kWh)."
},
"must_reach_soc_kwh": {
"name": "Minimum SoC by Deadline (kWh)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (%)."
"name": "Minimum-SoC vóór deadline (kWh)",
"description": "Minimale laadtoestand van de batterij die vóór de deadline moet worden bereikt. Kan niet worden gecombineerd met Minimum-SoC vóór deadline (%)."
},
"min_charge_power_w": {
"name": "Minimum Charge Power",
"description": "Enable continuous charging mode. The planner may reduce the last interval down to this minimum power instead of always using the maximum."
"name": "Minimaal laadvermogen",
"description": "Schakel continue laadmodus in. De planner mag het laatste interval verlagen tot dit minimumvermogen in plaats van altijd het maximum te gebruiken."
},
"charge_power_steps_w": {
"name": "Charge Power Steps",
"description": "Enable stepped charging mode. Provide the allowed power steps in watts as a list, for example [1380, 4140, 11000]. The planner picks the smallest step that still covers the remaining energy in the final interval. Mutually exclusive with Minimum Charge Power."
"name": "Laadvermogensstappen",
"description": "Schakel getrapte laadmodus in. Geef de toegestane vermogensstappen in watt op als lijst, bijvoorbeeld [1380, 4140, 11000]. De planner kiest de kleinste stap die de resterende energie in het laatste interval nog dekt. Sluit elkaar uit met Minimaal laadvermogen."
},
"grid_import_limit_w": {
"name": "Grid Import Limit",
"description": "Upper limit for charging power drawn from the grid in watts. Useful when the charger must share available power with other loads."
"name": "Netimportlimiet",
"description": "Bovengrens voor laadvermogen dat uit het net wordt onttrokken in watt. Handig wanneer de lader het beschikbare vermogen moet delen met andere belastingen."
},
"must_reach_by": {
"name": "Must Reach By",
"description": "Absolute deadline for must_reach_soc_*. The planner first guarantees the minimum SoC before this moment, then continues with the remaining target if possible."
"name": "Bereiken vóór",
"description": "Absolute deadline voor must_reach_soc_*. De planner garandeert eerst de minimum-SoC vóór dit moment en gaat daarna indien mogelijk verder met het resterende doel."
},
"must_reach_by_event": {
"name": "Deadline Event",
"description": "Alternative deadline derived from coordinator data. Use this instead of Must Reach By to plan around midnight, the next peak period, or the next best-price period end."
"name": "Deadlinegebeurtenis",
"description": "Alternatieve deadline afgeleid uit coördinatorgegevens. Gebruik dit in plaats van Bereiken vóór om rond middernacht, de volgende piekperiode of het einde van de volgende beste-prijsperiode te plannen."
},
"discharging_efficiency": {
"name": "Discharging Efficiency",
"description": "Fraction of stored battery energy that is still usable when later discharged. Used for profitability calculations."
"name": "Ontladingsrendement",
"description": "Deel van de opgeslagen batterijenergie dat bij later ontladen nog bruikbaar is. Gebruikt voor winstgevendheidsberekeningen."
},
"expected_discharge_price": {
"name": "Expected Discharge Price",
"description": "Expected value of each discharged kWh. Intervals above the break-even price can be filtered when Reserve For Discharge is enabled."
"name": "Verwachte ontladingsprijs",
"description": "Verwachte waarde van elke ontladen kWh. Intervallen boven de break-evenprijs kunnen worden gefilterd wanneer Reserveren voor ontladen is ingeschakeld."
},
"reserve_for_discharge": {
"name": "Reserve For Discharge",
"description": "Keep only intervals that are economically sensible for a later discharge based on expected discharge price and round-trip efficiency."
"name": "Reserveren voor ontladen",
"description": "Behoud alleen intervallen die economisch zinvol zijn voor een latere ontlading op basis van de verwachte ontladingsprijs en het rendement van de volledige cyclus."
},
"max_cost_per_kwh": {
"name": "Maximum Charge Price",
"description": "Discard candidate intervals above this price per kWh before scheduling. Uses the selected price unit."
"name": "Maximale laadprijs",
"description": "Verwerp kandidaat-intervallen boven deze prijs per kWh vóór de planning. Gebruikt de geselecteerde prijseenheid."
},
"min_charge_duration_minutes": {
"name": "Minimum Charge Duration",
"description": "Merge short isolated intervals into longer charging blocks where possible. Useful for chargers that should avoid very short runs."
"name": "Minimale laadduur",
"description": "Voeg korte geïsoleerde intervallen waar mogelijk samen tot langere laadblokken. Handig voor laders die zeer korte runs moeten vermijden."
},
"max_cycles_per_day": {
"name": "Maximum Charge Cycles Per Day",
"description": "Limit how many separate charging segments may be used per day. The planner keeps the cheapest segments within this limit."
"name": "Maximaal aantal laadcycli per dag",
"description": "Beperk hoeveel afzonderlijke laadsegmenten per dag mogen worden gebruikt. De planner behoudt de goedkoopste segmenten binnen deze limiet."
}
}
},
@ -2505,9 +2509,9 @@
},
"charging_deadline_event": {
"options": {
"midnight": "Midnight",
"next_peak_period": "Next Peak Period",
"next_best_period_end": "End of Next Best-Price Period"
"midnight": "Middernacht",
"next_peak_period": "Volgende piekperiode",
"next_best_period_end": "Einde van de volgende beste-prijsperiode"
}
}
}

View file

@ -545,7 +545,7 @@
"name": "Gårdagens prisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -553,7 +553,7 @@
"name": "Dagens prisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -561,7 +561,7 @@
"name": "Morgondagens prisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -587,7 +587,7 @@
"name": "Aktuellt prisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -595,7 +595,7 @@
"name": "Nästa prisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -603,7 +603,7 @@
"name": "Föregående prisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -611,7 +611,7 @@
"name": "Aktuellt timprisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -619,7 +619,7 @@
"name": "Nästa timprisbetyg",
"state": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -1029,34 +1029,34 @@
}
},
"current_price_phase_end_time": {
"name": "Current Phase End Time"
"name": "Aktuell fas sluttid"
},
"current_price_phase_remaining_minutes": {
"name": "Current Phase Remaining"
"name": "Aktuell fas återstående"
},
"current_price_phase_duration": {
"name": "Current Phase Duration"
"name": "Aktuell fas varaktighet"
},
"current_price_phase_progress": {
"name": "Current Phase Progress"
"name": "Aktuell fas framsteg"
},
"next_rising_phase_start_time": {
"name": "Next Rising Phase Start"
"name": "Nästa stigande fas start"
},
"next_falling_phase_start_time": {
"name": "Next Falling Phase Start"
"name": "Nästa fallande fas start"
},
"next_flat_phase_start_time": {
"name": "Next Flat Phase Start"
"name": "Nästa flata fas start"
},
"next_rising_phase_in_minutes": {
"name": "Time to Next Rising Phase"
"name": "Tid till nästa stigande fas"
},
"next_falling_phase_in_minutes": {
"name": "Time to Next Falling Phase"
"name": "Tid till nästa fallande fas"
},
"next_flat_phase_in_minutes": {
"name": "Time to Next Flat Phase"
"name": "Tid till nästa flata fas"
},
"chart_data_export": {
"name": "Diagramdataexport",
@ -1116,13 +1116,13 @@
"name": "Bästa Prisperiod"
},
"in_rising_price_phase": {
"name": "In Rising Price Phase"
"name": "I stigande prisfas"
},
"in_falling_price_phase": {
"name": "In Falling Price Phase"
"name": "I fallande prisfas"
},
"in_flat_price_phase": {
"name": "In Flat Price Phase"
"name": "I flat prisfas"
},
"connection": {
"name": "Tibber API-anslutning"
@ -1201,6 +1201,10 @@
"title": "API-hastighetsgräns överskriden för {home_name}",
"description": "Tibber API har hastighetsbegränsat denna integration efter {error_count} konsekutiva fel. Detta betyder att förfrågningar görs för ofta.\n\nIntegrationen kommer automatiskt att försöka igen med ökande fördröjningar. Detta problem löser sig när hastighetsgränsen löper ut.\n\nOm detta kvarstår i flera timmar, överväg:\n- Kontrollera om flera Home Assistant-instanser använder samma API-token\n- Verifiera att inga andra applikationer använder din Tibber API-token kraftigt\n- Minska uppdateringsfrekvensen om du har anpassat den"
},
"api_outage": {
"title": "Tibber API otillgänglig för {home_name}",
"description": "Inga färska prisdata har tagits emot från Tibber sedan {since}. Integrationen fortsätter att använda lokalt cachade priser där det är möjligt, så dina sensorer kan fortsätta fungera - men de kan bli otillgängliga om avbrottet fortsätter (till exempel kan morgondagens priser saknas).\n\nDetta orsakas vanligtvis av ett tillfälligt Tibber-avbrott eller ett nätverksproblem. Integrationen försöker automatiskt igen vid varje uppdateringscykel och rensar detta problem så snart färska data anländer.\n\nOm detta kvarstår, kontrollera Tibbers tjänststatus, din internetanslutning och att din API-token fortfarande är giltig (autentisera igen om du uppmanas)."
},
"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."
@ -1237,7 +1241,7 @@
"message": "Sluttidpunkten ({search_end}) måste vara efter starttidpunkten ({search_start}). Kontrollera tidsparametrarna och eventuella day-offsets."
},
"price_fetch_failed": {
"message": "Unable to fetch price data for the requested search range."
"message": "Det gick inte att hämta prisdata för det begärda sökintervallet."
},
"invalid_search_scope": {
"message": "Ogiltigt sökområde. Giltiga värden är: today, tomorrow, remaining_today, next_24h, next_48h."
@ -1309,49 +1313,49 @@
"message": "Kan inte konvertera värdet '{raw_value}' från '{entity_id}' ({attribute}) till {expected_type}. Kontrollera att entiteten ger ett kompatibelt värde."
},
"capacity_required_for_percent": {
"message": "battery_capacity_kwh is required when current or target SoC is provided as a percentage."
"message": "battery_capacity_kwh krävs när nuvarande eller mål-SoC anges i procent."
},
"ambiguous_soc_input": {
"message": "The field {field} was provided both as percent and as kWh. Use only one representation."
"message": "Fältet {field} angavs både i procent och i kWh. Använd endast en representation."
},
"already_at_target": {
"message": "Current state of charge is already at or above the target. No charging schedule is needed."
"message": "Den nuvarande laddningsnivån är redan på eller över målet. Inget laddningsschema behövs."
},
"target_exceeds_capacity": {
"message": "Target SoC ({target_soc_kwh} kWh) exceeds battery capacity ({capacity_kwh} kWh)."
"message": "Mål-SoC ({target_soc_kwh} kWh) överstiger batterikapaciteten ({capacity_kwh} kWh)."
},
"energy_unreachable": {
"message": "The requested charging target cannot be reached within the available search window and charging constraints."
"message": "Det begärda laddmålet kan inte nås inom det tillgängliga sökfönstret och laddbegränsningarna."
},
"missing_current_soc": {
"message": "Provide either current_soc_percent or current_soc_kwh."
"message": "Ange antingen current_soc_percent eller current_soc_kwh."
},
"missing_target_soc": {
"message": "Provide either target_soc_percent or target_soc_kwh."
"message": "Ange antingen target_soc_percent eller target_soc_kwh."
},
"missing_deadline_for_must_reach": {
"message": "Provide must_reach_by or must_reach_by_event when using must_reach_soc_percent or must_reach_soc_kwh."
"message": "Ange must_reach_by eller must_reach_by_event när du använder must_reach_soc_percent eller must_reach_soc_kwh."
},
"missing_must_reach_soc": {
"message": "Provide must_reach_soc_percent or must_reach_soc_kwh when using a must_reach deadline."
"message": "Ange must_reach_soc_percent eller must_reach_soc_kwh när du använder en must_reach-deadline."
},
"invalid_must_reach_soc": {
"message": "The minimum state of charge by deadline must be between current and target state of charge."
"message": "Den lägsta laddningsnivån före deadline måste ligga mellan nuvarande och mål-laddningsnivå."
},
"power_strategy_conflict": {
"message": "Use either min_charge_power_w or charge_power_steps_w, not both at the same time."
"message": "Använd antingen min_charge_power_w eller charge_power_steps_w, inte båda samtidigt."
},
"grid_limit_too_low": {
"message": "grid_import_limit_w is lower than the minimum required charging power."
"message": "grid_import_limit_w är lägre än den minsta laddeffekt som krävs."
},
"deadline_conflict": {
"message": "Use either must_reach_by or must_reach_by_event, not both at the same time."
"message": "Använd antingen must_reach_by eller must_reach_by_event, inte båda samtidigt."
},
"deadline_event_not_available": {
"message": "The selected deadline event is not available in the current coordinator data."
"message": "Den valda deadlinehändelsen är inte tillgänglig i de aktuella koordinatordata."
},
"deadline_outside_search_range": {
"message": "The resolved deadline must be inside the selected search range."
"message": "Den upplösta deadlinen måste ligga inom det valda sökintervallet."
}
},
"services": {
@ -1570,7 +1574,7 @@
}
},
"find_cheapest_block": {
"name": "Hitta billigaste blocket",
"name": "Hitta billigaste blocket (Experimentell)",
"description": "Hittar det billigaste sammanhängande tidsfönstret med en given varaktighet. Designat för apparatschemaläggning: diskmaskin, tvättmaskin, torktumlare osv. Returnerar det billigaste fönstret med start-/sluttider och prisstatistik.",
"sections": {
"search_range": {
@ -1690,7 +1694,7 @@
}
},
"find_most_expensive_block": {
"name": "Hitta dyraste blocket",
"name": "Hitta dyraste blocket (Experimentell)",
"description": "Hittar det dyraste sammanhängande tidsfönstret med en given varaktighet. Användbart för att identifiera topprisperioder som bör undvikas. Returnerar det dyraste fönstret med start-/sluttider och prisstatistik.",
"sections": {
"search_range": {
@ -1810,7 +1814,7 @@
}
},
"find_cheapest_hours": {
"name": "Hitta billigaste timmarna",
"name": "Hitta billigaste timmarna (Experimentell)",
"description": "Hittar de billigaste intervallen för en given total varaktighet, inte nödvändigtvis sammanhängande. Designat för flexibla laster: batteriladdning, elbil, varmvattenberedare. Returnerar ett schema av intervaller grupperade i sammanhängande segment.",
"sections": {
"search_range": {
@ -1934,7 +1938,7 @@
}
},
"find_most_expensive_hours": {
"name": "Hitta dyraste timmarna",
"name": "Hitta dyraste timmarna (Experimentell)",
"description": "Hittar de dyraste intervallen för en given total varaktighet, inte nödvändigtvis sammanhängande. Användbart för att identifiera topprisperioder som bör undvikas. Returnerar ett schema av intervaller grupperade i sammanhängande segment.",
"sections": {
"search_range": {
@ -2058,7 +2062,7 @@
}
},
"find_cheapest_schedule": {
"name": "Hitta billigaste schema",
"name": "Hitta billigaste schema (Experimentell)",
"description": "Schemalaggar flera apparater optimalt utan tidsoeverlapp. Varje uppgift tilldelas det billigaste tillgaengliga sammanhangande tidsfoenster.",
"sections": {
"search_range": {
@ -2174,206 +2178,206 @@
}
},
"plan_charging": {
"name": "Plan Charging",
"description": "Creates a lowest-cost charging schedule from battery parameters instead of a fixed duration. Supports fixed, continuous, or stepped charging power, optional deadline-aware minimum SoC planning, and economic filtering for later discharge use cases. Returns a per-interval charging plan with power, energy, SoC progression, segment grouping, total cost, and profitability details. If no schedule is found, the response includes a stable reason code in the reason field (for example: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"name": "Planera laddning (experimentell)",
"description": "Skapar ett laddningsschema till lägsta kostnad utifrån batteriparametrar i stället för en fast varaktighet. Stöder fast, kontinuerlig eller stegvis laddeffekt, valfri planering med lägsta SoC före en deadline, samt ekonomisk filtrering för senare urladdningsscenarier. Returnerar ett laddningsschema per intervall med effekt, energi, SoC-förlopp, segmentgruppering, totalkostnad och lönsamhetsdetaljer. Om inget schema hittas innehåller svaret en stabil orsakskod i fältet ”reason” (till exempel: already_at_target, no_data_in_range, no_intervals_matching_level_filter, no_intervals_after_economic_filter, energy_unreachable, energy_unreachable_by_deadline, selection_above_distance_threshold).",
"sections": {
"battery": {
"name": "Battery Parameters",
"description": "Describe the battery: usable capacity, current and target SoC, and charging efficiency. You can provide SoC in percent (requires capacity) or directly in kWh."
"name": "Batteriparametrar",
"description": "Beskriv batteriet: användbar kapacitet, nuvarande och önskad SoC, samt laddeffektivitet. Du kan ange SoC i procent (kräver kapacitet) eller direkt i kWh."
},
"charging": {
"name": "Charging Strategy",
"description": "Configure variable-power charging behavior, grid limits, and minimum run-time constraints."
"name": "Laddningsstrategi",
"description": "Konfigurera laddning med variabel effekt, nätgränser och minsta körtidsbegränsningar."
},
"deadline": {
"name": "Deadline Planning",
"description": "Require a minimum state of charge by a specific moment (for example before a peak period). Set both the Minimum SoC by Deadline and when it must be reached — either as an absolute time (Must Reach By) or from a known event (Deadline Event)."
"name": "Deadlineplanering",
"description": "Kräv en lägsta laddningsnivå vid en viss tidpunkt (till exempel före en topprisperiod). Ange både Lägsta SoC före deadline och när den måste nås antingen som en absolut tid (Måste nås senast) eller från en känd händelse (Deadlinehändelse)."
},
"search_range": {
"name": "Custom Search Range",
"description": "Define precise start and end times for the search. Overrides Search Scope when set."
"name": "Anpassat sökintervall",
"description": "Definiera exakta start- och sluttider för sökningen. Åsidosätter Sökomfång när det är angivet."
},
"time_alternatives": {
"name": "Advanced Time Options",
"description": "Alternative ways to define the search range using time-of-day and minute offsets."
"name": "Avancerade tidsalternativ",
"description": "Alternativa sätt att definiera sökintervallet med tid på dygnet och minutförskjutningar."
},
"price_filter": {
"name": "Price Level Filter",
"description": "Restrict search to intervals within the specified Tibber price level range."
"name": "Prisnivåfilter",
"description": "Begränsa sökningen till intervall inom den angivna Tibber-prisnivån."
},
"search_tuning": {
"name": "Search Algorithm Tuning",
"description": "Fine-tune how the search handles outliers, minimum quality thresholds, and fallback behavior."
"name": "Finjustering av sökalgoritm",
"description": "Finjustera hur sökningen hanterar avvikare, lägsta kvalitetströsklar och reservbeteende."
},
"economics": {
"name": "Economic Filters",
"description": "Filter charging intervals by maximum cost or expected later discharge value."
"name": "Ekonomiska filter",
"description": "Filtrera laddintervall efter maximal kostnad eller förväntat senare urladdningsvärde."
},
"output": {
"name": "Output Options",
"description": "Control output format: comparison details and currency unit."
"name": "Utdataalternativ",
"description": "Styr utdataformatet: jämförelsedetaljer och valutaenhet."
}
},
"fields": {
"entry_id": {
"name": "Entry ID",
"description": "The config entry ID for the Tibber integration."
"name": "Post-ID",
"description": "Konfigurationspost-ID för Tibber-integrationen."
},
"max_charge_power_w": {
"name": "Maximum Charge Power",
"description": "Maximum charging power in watts. This defines the upper bound for fixed, continuous, or stepped charging schedules."
"name": "Maximal laddeffekt",
"description": "Maximal laddeffekt i watt. Detta definierar den övre gränsen för fast, kontinuerlig eller stegvis laddning."
},
"battery_capacity_kwh": {
"name": "Battery Capacity",
"description": "Usable battery capacity in kWh. Required when current or target SoC is provided as a percentage."
"name": "Batterikapacitet",
"description": "Användbar batterikapacitet i kWh. Krävs när nuvarande eller mål-SoC anges i procent."
},
"current_soc_percent": {
"name": "Current SoC (%)",
"description": "Current battery state of charge in percent. Cannot be combined with Current SoC (kWh)."
"name": "Nuvarande SoC (%)",
"description": "Nuvarande laddningsnivå för batteriet i procent. Kan inte kombineras med Nuvarande SoC (kWh)."
},
"current_soc_kwh": {
"name": "Current SoC (kWh)",
"description": "Current battery state of charge in kWh. Cannot be combined with Current SoC (%)."
"name": "Nuvarande SoC (kWh)",
"description": "Nuvarande laddningsnivå för batteriet i kWh. Kan inte kombineras med Nuvarande SoC (%)."
},
"target_soc_percent": {
"name": "Target SoC (%)",
"description": "Desired battery state of charge in percent. Cannot be combined with Target SoC (kWh)."
"name": "Mål-SoC (%)",
"description": "Önskad laddningsnivå för batteriet i procent. Kan inte kombineras med Mål-SoC (kWh)."
},
"target_soc_kwh": {
"name": "Target SoC (kWh)",
"description": "Desired battery state of charge in kWh. Cannot be combined with Target SoC (%)."
"name": "Mål-SoC (kWh)",
"description": "Önskad laddningsnivå för batteriet i kWh. Kan inte kombineras med Mål-SoC (%)."
},
"charging_efficiency": {
"name": "Charging Efficiency",
"description": "Fraction of grid energy that is stored in the battery. Example: 0.92 means 8% charging losses."
"name": "Laddeffektivitet",
"description": "Andel av nätenergin som lagras i batteriet. Exempel: 0,92 betyder 8 % laddförluster."
},
"search_scope": {
"name": "Search Scope",
"description": "Shorthand for common search ranges. Overrides all other time range options. today / tomorrow = full calendar day, remaining_today = now until midnight, next_24h / next_48h = rolling window from now."
"name": "Sökomfång",
"description": "Genväg för vanliga sökintervall. Åsidosätter alla andra tidsintervallalternativ. today / tomorrow = hela kalenderdagen, remaining_today = nu till midnatt, next_24h / next_48h = rullande fönster från nu."
},
"include_current_interval": {
"name": "Include Current Interval",
"description": "Include the currently running 15-minute interval in the search. When enabled, charging may begin in the current interval if it is part of the cheapest result."
"name": "Inkludera nuvarande intervall",
"description": "Inkludera det 15-minutersintervall som körs nu i sökningen. När det är aktiverat kan laddning börja i det nuvarande intervallet om det är en del av det billigaste resultatet."
},
"search_start": {
"name": "Search Start",
"description": "Start of the search range as exact date and time. Highest priority — overrides all other start options. Defaults to now if not specified."
"name": "Sökstart",
"description": "Start på sökintervallet som exakt datum och tid. Högsta prioritet åsidosätter alla andra startalternativ. Standard är nu om inget anges."
},
"search_end": {
"name": "Search End",
"description": "End of the search range as exact date and time. Highest priority — overrides all other end options. Defaults to end of tomorrow if not specified."
"name": "Sökslut",
"description": "Slut på sökintervallet som exakt datum och tid. Högsta prioritet åsidosätter alla andra slutalternativ. Standard är slutet på morgondagen om inget anges."
},
"must_finish_by": {
"name": "Must Finish By",
"description": "Deadline: charging must be finished by this time. The search range ends at this deadline — the service finds the cheapest intervals that complete before it. Cannot be combined with Search End, Search End Time, or Search End Offset."
"name": "Måste vara klart senast",
"description": "Deadline: laddningen måste vara klar senast vid denna tidpunkt. Sökintervallet slutar vid denna deadline tjänsten hittar de billigaste intervallen som blir klara dessförinnan. Kan inte kombineras med Sökslut, Söksluttid eller Sökslutförskjutning."
},
"search_start_time": {
"name": "Search Start Time",
"description": "Alternative: start searching at this time of day. Combine with day offset. Ignored if Search Start (datetime) is set."
"name": "Sökstarttid",
"description": "Alternativ: börja söka vid denna tid på dygnet. Kombinera med dagförskjutning. Ignoreras om Sökstart (datum/tid) är angiven."
},
"search_start_day_offset": {
"name": "Search Start Day Offset",
"description": "Day offset for Search Start Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search Start Time."
"name": "Dagförskjutning för sökstart",
"description": "Dagförskjutning för Sökstarttiden. -7 till 2: -1 = i går, 0 = i dag, 1 = i morgon. Används endast med Sökstarttid."
},
"search_end_time": {
"name": "Search End Time",
"description": "Alternative: stop searching at this time of day. Combine with day offset. Ignored if Search End (datetime) is set."
"name": "Söksluttid",
"description": "Alternativ: sluta söka vid denna tid på dygnet. Kombinera med dagförskjutning. Ignoreras om Sökslut (datum/tid) är angiven."
},
"search_end_day_offset": {
"name": "Search End Day Offset",
"description": "Day offset for Search End Time. -7 to 2: -1 = yesterday, 0 = today, 1 = tomorrow. Only used with Search End Time."
"name": "Dagförskjutning för sökslut",
"description": "Dagförskjutning för Söksluttiden. -7 till 2: -1 = i går, 0 = i dag, 1 = i morgon. Används endast med Söksluttid."
},
"search_start_offset_minutes": {
"name": "Search Start Offset (minutes)",
"description": "Alternative: start searching this many minutes from now. Positive = future, negative = past. Ignored if Search Start or Search Start Time is set."
"name": "Sökstartförskjutning (minuter)",
"description": "Alternativ: börja söka så här många minuter från nu. Positivt = framtid, negativt = dåtid. Ignoreras om Sökstart eller Sökstarttid är angiven."
},
"search_end_offset_minutes": {
"name": "Search End Offset (minutes)",
"description": "Alternative: stop searching this many minutes from now. Positive = future, negative = past. Ignored if Search End or Search End Time is set."
"name": "Sökslutförskjutning (minuter)",
"description": "Alternativ: sluta söka så här många minuter från nu. Positivt = framtid, negativt = dåtid. Ignoreras om Sökslut eller Söksluttid är angiven."
},
"max_price_level": {
"name": "Maximum Price Level",
"description": "Only consider intervals at or below this Tibber price level. very_cheap = most restrictive, very_expensive = no restriction."
"name": "Maximal prisnivå",
"description": "Beakta endast intervall på eller under denna Tibber-prisnivå. very_cheap = mest restriktiv, very_expensive = ingen begränsning."
},
"min_price_level": {
"name": "Minimum Price Level",
"description": "Only consider intervals at or above this Tibber price level. Useful to exclude unrealistically cheap negative-price-only windows from mixed searches."
"name": "Minsta prisnivå",
"description": "Beakta endast intervall på eller över denna Tibber-prisnivå. Användbart för att utesluta orealistiskt billiga fönster med enbart negativa priser från blandade sökningar."
},
"include_comparison_details": {
"name": "Include Comparison Details",
"description": "Enrich the price_comparison result with additional fields: comparison_price_min and comparison_price_max."
"name": "Inkludera jämförelsedetaljer",
"description": "Berika resultatet price_comparison med ytterligare fält: comparison_price_min och comparison_price_max."
},
"use_base_unit": {
"name": "Use Base Currency Unit",
"description": "Force prices in base currency (EUR, NOK) instead of the configured display unit (ct, øre). Useful for calculations."
"name": "Använd basvalutaenhet",
"description": "Tvinga priser i basvaluta (EUR, NOK) i stället för den konfigurerade visningsenheten (ct, öre). Användbart för beräkningar."
},
"smooth_outliers": {
"name": "Smooth Outliers",
"description": "Smooth price outliers before searching. Outlier intervals are temporarily replaced by the average of their neighbors, so a single spike or dip does not dominate the result. The response always shows the original (unsmoothed) prices. Default: enabled."
"name": "Jämna ut avvikare",
"description": "Jämna ut prisavvikare före sökningen. Avvikande intervall ersätts tillfälligt med medelvärdet av sina grannar, så att en enstaka topp eller dal inte dominerar resultatet. Svaret visar alltid de ursprungliga (ojämnade) priserna. Standard: aktiverat."
},
"min_distance_from_avg": {
"name": "Min. Distance from Average",
"description": "Require the selected charging intervals to be at least this percentage below the average price of the full search range. Leave empty to disable."
"name": "Min. avstånd från medel",
"description": "Kräv att de valda laddintervallen ligger minst denna procent under medelpriset för hela sökintervallet. Lämna tomt för att inaktivera."
},
"allow_relaxation": {
"name": "Allow relaxation",
"description": "Progressively relax filters to guarantee a result when possible. Phases: 1) Reduce/remove distance threshold 2) Expand level filters 3) Reduce duration. Default: enabled."
"name": "Tillåt uppmjukning",
"description": "Mjuka gradvis upp filter för att garantera ett resultat när det är möjligt. Faser: 1) Minska/ta bort avståndströskel 2) Utöka nivåfilter 3) Minska varaktighet. Standard: aktiverat."
},
"duration_flexibility_minutes": {
"name": "Duration flexibility",
"description": "Maximum minutes the automatically calculated charging duration may be shortened during relaxation (0120, step 15). Leave empty for automatic calculation."
"name": "Varaktighetsflexibilitet",
"description": "Maximalt antal minuter som den automatiskt beräknade laddtiden får förkortas under uppmjukning (0120, steg 15). Lämna tomt för automatisk beräkning."
},
"must_reach_soc_percent": {
"name": "Minimum SoC by Deadline (%)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (kWh)."
"name": "Lägsta SoC före deadline (%)",
"description": "Lägsta laddningsnivå för batteriet som måste nås före deadline. Kan inte kombineras med Lägsta SoC före deadline (kWh)."
},
"must_reach_soc_kwh": {
"name": "Minimum SoC by Deadline (kWh)",
"description": "Minimum battery state of charge that must be reached by the deadline. Cannot be combined with Minimum SoC by Deadline (%)."
"name": "Lägsta SoC före deadline (kWh)",
"description": "Lägsta laddningsnivå för batteriet som måste nås före deadline. Kan inte kombineras med Lägsta SoC före deadline (%)."
},
"min_charge_power_w": {
"name": "Minimum Charge Power",
"description": "Enable continuous charging mode. The planner may reduce the last interval down to this minimum power instead of always using the maximum."
"name": "Minsta laddeffekt",
"description": "Aktivera kontinuerligt laddläge. Planeraren får minska det sista intervallet ned till denna minsta effekt i stället för att alltid använda maximum."
},
"charge_power_steps_w": {
"name": "Charge Power Steps",
"description": "Enable stepped charging mode. Provide the allowed power steps in watts as a list, for example [1380, 4140, 11000]. The planner picks the smallest step that still covers the remaining energy in the final interval. Mutually exclusive with Minimum Charge Power."
"name": "Laddeffektsteg",
"description": "Aktivera stegvis laddläge. Ange de tillåtna effektstegen i watt som en lista, till exempel [1380, 4140, 11000]. Planeraren väljer det minsta steget som fortfarande täcker den återstående energin i det sista intervallet. Ömsesidigt uteslutande med Minsta laddeffekt."
},
"grid_import_limit_w": {
"name": "Grid Import Limit",
"description": "Upper limit for charging power drawn from the grid in watts. Useful when the charger must share available power with other loads."
"name": "Nätimportgräns",
"description": "Övre gräns för laddeffekt som tas från nätet i watt. Användbart när laddaren måste dela tillgänglig effekt med andra laster."
},
"must_reach_by": {
"name": "Must Reach By",
"description": "Absolute deadline for must_reach_soc_*. The planner first guarantees the minimum SoC before this moment, then continues with the remaining target if possible."
"name": "Måste nås senast",
"description": "Absolut deadline för must_reach_soc_*. Planeraren garanterar först lägsta SoC före denna tidpunkt och fortsätter sedan med det återstående målet om möjligt."
},
"must_reach_by_event": {
"name": "Deadline Event",
"description": "Alternative deadline derived from coordinator data. Use this instead of Must Reach By to plan around midnight, the next peak period, or the next best-price period end."
"name": "Deadlinehändelse",
"description": "Alternativ deadline härledd från koordinatordata. Använd detta i stället för Måste nås senast för att planera kring midnatt, nästa topprisperiod eller slutet på nästa bästprisperiod."
},
"discharging_efficiency": {
"name": "Discharging Efficiency",
"description": "Fraction of stored battery energy that is still usable when later discharged. Used for profitability calculations."
"name": "Urladdningseffektivitet",
"description": "Andel av den lagrade batterienergin som fortfarande är användbar vid senare urladdning. Används för lönsamhetsberäkningar."
},
"expected_discharge_price": {
"name": "Expected Discharge Price",
"description": "Expected value of each discharged kWh. Intervals above the break-even price can be filtered when Reserve For Discharge is enabled."
"name": "Förväntat urladdningspris",
"description": "Förväntat värde av varje urladdad kWh. Intervall över balanspriset kan filtreras när Reservera för urladdning är aktiverat."
},
"reserve_for_discharge": {
"name": "Reserve For Discharge",
"description": "Keep only intervals that are economically sensible for a later discharge based on expected discharge price and round-trip efficiency."
"name": "Reservera för urladdning",
"description": "Behåll endast intervall som är ekonomiskt rimliga för en senare urladdning baserat på förväntat urladdningspris och rundgångseffektivitet."
},
"max_cost_per_kwh": {
"name": "Maximum Charge Price",
"description": "Discard candidate intervals above this price per kWh before scheduling. Uses the selected price unit."
"name": "Maximalt laddpris",
"description": "Förkasta kandidatintervall över detta pris per kWh före planering. Använder den valda prisenheten."
},
"min_charge_duration_minutes": {
"name": "Minimum Charge Duration",
"description": "Merge short isolated intervals into longer charging blocks where possible. Useful for chargers that should avoid very short runs."
"name": "Minsta laddtid",
"description": "Slå samman korta isolerade intervall till längre laddblock där det är möjligt. Användbart för laddare som bör undvika mycket korta körningar."
},
"max_cycles_per_day": {
"name": "Maximum Charge Cycles Per Day",
"description": "Limit how many separate charging segments may be used per day. The planner keeps the cheapest segments within this limit."
"name": "Maximalt antal laddcykler per dag",
"description": "Begränsa hur många separata laddsegment som får användas per dag. Planeraren behåller de billigaste segmenten inom denna gräns."
}
}
},
@ -2433,7 +2437,7 @@
"rating_level_filter": {
"options": {
"low": "Låg",
"normal": "Normal",
"normal": "Normalt",
"high": "Hög"
}
},
@ -2505,9 +2509,9 @@
},
"charging_deadline_event": {
"options": {
"midnight": "Midnight",
"next_peak_period": "Next Peak Period",
"next_best_period_end": "End of Next Best-Price Period"
"midnight": "Midnatt",
"next_peak_period": "Nästa topprisperiod",
"next_best_period_end": "Slutet på nästa bästprisperiod"
}
}
}

View file

@ -0,0 +1,203 @@
---
comments: false
---
# API Reference
Documentation of the Tibber GraphQL API used by this integration.
## GraphQL Endpoint
```
https://api.tibber.com/v1-beta/gql
```
**Authentication:** Bearer token in `Authorization` header
## Queries Used
### User Data Query
Fetches home information and metadata:
```graphql
query {
viewer {
homes {
id
appNickname
address {
address1
postalCode
city
country
}
timeZone
currentSubscription {
priceInfo {
current {
currency
}
}
}
meteringPointData {
consumptionEan
gridAreaCode
}
}
}
}
```
**Cached for:** 24 hours
### Price Data Query
Fetches quarter-hourly prices:
```graphql
query ($homeId: ID!) {
viewer {
home(id: $homeId) {
currentSubscription {
priceInfo {
range(resolution: QUARTER_HOURLY, first: 384) {
nodes {
total
startsAt
level
}
}
}
}
}
}
}
```
**Parameters:**
- `homeId`: Tibber home identifier
- `resolution`: Always `QUARTER_HOURLY`
- `first`: 384 intervals (4 days of data)
**Cached until:** Midnight local time
## Rate Limits
Tibber API rate limits (as of 2024):
- **5000 requests per hour** per token
- **Burst limit:** 100 requests per minute
Integration stays well below these limits:
- Polls every 15 minutes = 96 requests/day
- User data cached for 24h = 1 request/day
- **Total:** ~100 requests/day per home
## Response Format
### Price Node Structure
```json
{
"total": 0.2456,
"startsAt": "2024-12-06T14:00:00.000+01:00",
"level": "NORMAL"
}
```
**Fields:**
- `total`: Price including VAT and fees (currency's major unit, e.g., EUR)
- `startsAt`: ISO 8601 timestamp with timezone
- `level`: Tibber's own classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE)
### Currency Information
```json
{
"currency": "EUR"
}
```
Supported currencies:
- `EUR` (Euro) - displayed as ct/kWh
- `NOK` (Norwegian Krone) - displayed as øre/kWh
- `SEK` (Swedish Krona) - displayed as öre/kWh
## Error Handling
### Common Error Responses
**Invalid Token:**
```json
{
"errors": [
{
"message": "Unauthorized",
"extensions": {
"code": "UNAUTHENTICATED"
}
}
]
}
```
**Rate Limit Exceeded:**
```json
{
"errors": [
{
"message": "Too Many Requests",
"extensions": {
"code": "RATE_LIMIT_EXCEEDED"
}
}
]
}
```
**Home Not Found:**
```json
{
"errors": [
{
"message": "Home not found",
"extensions": {
"code": "NOT_FOUND"
}
}
]
}
```
Integration handles these with:
- Exponential backoff retry (3 attempts)
- ConfigEntryAuthFailed for auth errors
- ConfigEntryNotReady for temporary failures
## Data Transformation
Raw API data is enriched with:
- **Trailing 24h average** - Calculated from previous intervals
- **Leading 24h average** - Calculated from future intervals
- **Price difference %** - Deviation from average
- **Custom rating** - Based on user thresholds (different from Tibber's `level`)
See `utils/price.py` for enrichment logic.
---
💡 **External Resources:**
- [Tibber API Documentation](https://developer.tibber.com/docs/overview)
- [GraphQL Explorer](https://developer.tibber.com/explorer)
- [Get API Token](https://developer.tibber.com/settings/access-token)

View file

@ -0,0 +1,365 @@
---
comments: false
---
# Architecture
This document provides a visual overview of the integration's architecture, focusing on end-to-end data flow and caching layers.
For detailed implementation patterns, see [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md).
---
## End-to-End Data Flow
```mermaid
flowchart TB
%% External Systems
TIBBER[("🌐 Tibber GraphQL API<br/>api.tibber.com")]
HA[("🏠 Home Assistant<br/>Core")]
%% Entry Point
SETUP["__init__.py<br/>async_setup_entry()"]
%% Core Components
API["api.py<br/>TibberPricesApiClient<br/><br/>GraphQL queries"]
COORD["coordinator.py<br/>TibberPricesDataUpdateCoordinator<br/><br/>Orchestrates updates every 15min"]
%% Caching Layers
CACHE_API["💾 API Cache<br/>coordinator/cache.py<br/><br/>HA Storage (persistent)<br/>User: 24h | Prices: until midnight"]
CACHE_TRANS["💾 Transformation Cache<br/>coordinator/data_transformation.py<br/><br/>Memory (enriched prices)<br/>Until config change or midnight"]
CACHE_PERIOD["💾 Period Cache<br/>coordinator/periods.py<br/><br/>Memory (calculated periods)<br/>Hash-based invalidation"]
CACHE_CONFIG["💾 Config Cache<br/>coordinator/*<br/><br/>Memory (parsed options)<br/>Until config change"]
CACHE_TRANS_TEXT["💾 Translation Cache<br/>const.py<br/><br/>Memory (UI strings)<br/>Until HA restart"]
%% Processing Components
TRANSFORM["coordinator/data_transformation.py<br/>DataTransformer<br/><br/>Enrich prices with statistics"]
PERIODS["coordinator/periods.py<br/>PeriodCalculator<br/><br/>Calculate best/peak periods"]
ENRICH["price_utils.py + average_utils.py<br/><br/>Calculate trailing/leading averages<br/>rating_level, differences"]
%% Output Components
SENSORS["sensor/<br/>TibberPricesSensor<br/><br/>120+ price/level/rating sensors"]
BINARY["binary_sensor/<br/>TibberPricesBinarySensor<br/><br/>Period indicators"]
SERVICES["services/<br/><br/>Custom service endpoints<br/>(get_chartdata, ApexCharts)"]
%% Flow Connections
TIBBER -->|"Query user data<br/>Query prices<br/>(yesterday/today/tomorrow)"| API
API -->|"Raw GraphQL response"| COORD
COORD -->|"Check cache first"| CACHE_API
CACHE_API -.->|"Cache hit:<br/>Return cached"| COORD
CACHE_API -.->|"Cache miss:<br/>Fetch from API"| API
COORD -->|"Raw price data"| TRANSFORM
TRANSFORM -->|"Check cache"| CACHE_TRANS
CACHE_TRANS -.->|"Cache hit"| TRANSFORM
CACHE_TRANS -.->|"Cache miss"| ENRICH
ENRICH -->|"Enriched data"| TRANSFORM
TRANSFORM -->|"Enriched price data"| COORD
COORD -->|"Enriched data"| PERIODS
PERIODS -->|"Check cache"| CACHE_PERIOD
CACHE_PERIOD -.->|"Hash match:<br/>Return cached"| PERIODS
CACHE_PERIOD -.->|"Hash mismatch:<br/>Recalculate"| PERIODS
PERIODS -->|"Calculated periods"| COORD
COORD -->|"Complete data<br/>(prices + periods)"| SENSORS
COORD -->|"Complete data"| BINARY
COORD -->|"Data access"| SERVICES
SENSORS -->|"Entity states"| HA
BINARY -->|"Entity states"| HA
SERVICES -->|"Service responses"| HA
%% Config access
CACHE_CONFIG -.->|"Parsed options"| TRANSFORM
CACHE_CONFIG -.->|"Parsed options"| PERIODS
CACHE_TRANS_TEXT -.->|"UI strings"| SENSORS
CACHE_TRANS_TEXT -.->|"UI strings"| BINARY
SETUP -->|"Initialize"| COORD
SETUP -->|"Register"| SENSORS
SETUP -->|"Register"| BINARY
SETUP -->|"Register"| SERVICES
%% Styling
classDef external fill:#e1f5ff,stroke:#0288d1,stroke-width:3px
classDef cache fill:#fff3e0,stroke:#f57c00,stroke-width:2px
classDef processing fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef output fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
class TIBBER,HA external
class CACHE_API,CACHE_TRANS,CACHE_PERIOD,CACHE_CONFIG,CACHE_TRANS_TEXT cache
class TRANSFORM,PERIODS,ENRICH processing
class SENSORS,BINARY,SERVICES output
```
### Flow Description
1. **Setup** (`__init__.py`)
- Integration loads, creates coordinator instance
- Registers entity platforms (sensor, binary_sensor)
- Sets up custom services
2. **Data Fetch** (every 15 minutes)
- Coordinator triggers update via `api.py`
- API client checks **persistent cache** first (`coordinator/cache.py`)
- If cache valid → return cached data
- If cache stale → query Tibber GraphQL API
- Store fresh data in persistent cache (survives HA restart)
3. **Price Enrichment**
- Coordinator passes raw prices to `DataTransformer`
- Transformer checks **transformation cache** (memory)
- If cache valid → return enriched data
- If cache invalid → enrich via `price_utils.py` + `average_utils.py`
- Calculate 24h trailing/leading averages
- Calculate price differences (% from average)
- Assign rating levels (LOW/NORMAL/HIGH)
- Store enriched data in transformation cache
4. **Period Calculation**
- Coordinator passes enriched data to `PeriodCalculator`
- Calculator computes **hash** from prices + config
- If hash matches cache → return cached periods
- If hash differs → recalculate best/peak price periods
- Store periods with new hash
5. **Entity Updates**
- Coordinator provides complete data (prices + periods)
- Sensors read values via unified handlers
- Binary sensors evaluate period states
- Entities update on quarter-hour boundaries (00/15/30/45)
6. **Service Calls**
- Custom services access coordinator data directly
- Return formatted responses (JSON, ApexCharts format)
---
## Caching Architecture
### Overview
The integration uses **5 independent caching layers** for optimal performance:
| Layer | Location | Lifetime | Invalidation | Memory |
| ------------------------ | ------------------------------------ | -------------------------------------- | ------------ | ------ |
| **API Cache** | `coordinator/cache.py` | 24h (user)<br/>Until midnight (prices) | Automatic | 50KB |
| **Translation Cache** | `const.py` | Until HA restart | Never | 5KB |
| **Config Cache** | `coordinator/*` | Until config change | Explicit | 1KB |
| **Period Cache** | `coordinator/periods.py` | Until data/config change | Hash-based | 10KB |
| **Transformation Cache** | `coordinator/data_transformation.py` | Until midnight/config | Automatic | 60KB |
**Total cache overhead:** ~126KB per coordinator instance (main entry + subentries)
### Cache Coordination
```mermaid
flowchart LR
USER[("User changes options")]
MIDNIGHT[("Midnight turnover")]
NEWDATA[("Tomorrow data arrives")]
USER -->|"Explicit invalidation"| CONFIG["Config Cache<br/>❌ Clear"]
USER -->|"Explicit invalidation"| PERIOD["Period Cache<br/>❌ Clear"]
USER -->|"Explicit invalidation"| TRANS["Transformation Cache<br/>❌ Clear"]
MIDNIGHT -->|"Date validation"| API["API Cache<br/>❌ Clear prices"]
MIDNIGHT -->|"Date check"| TRANS
NEWDATA -->|"Hash mismatch"| PERIOD
CONFIG -.->|"Next access"| CONFIG_NEW["Reparse options"]
PERIOD -.->|"Next access"| PERIOD_NEW["Recalculate"]
TRANS -.->|"Next access"| TRANS_NEW["Re-enrich"]
API -.->|"Next access"| API_NEW["Fetch from API"]
classDef invalid fill:#ffebee,stroke:#c62828,stroke-width:2px
classDef rebuild fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
class CONFIG,PERIOD,TRANS,API invalid
class CONFIG_NEW,PERIOD_NEW,TRANS_NEW,API_NEW rebuild
```
**Key insight:** No cascading invalidations - each cache is independent and rebuilds on-demand.
For detailed cache behavior, see [Caching Strategy](./caching-strategy.md).
---
## Component Responsibilities
### Core Components
| Component | File | Responsibility |
| --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------- |
| **API Client** | `api.py` | GraphQL queries to Tibber, retry logic, error handling |
| **Coordinator** | `coordinator.py` | Update orchestration, cache management, absolute-time scheduling with boundary tolerance |
| **Data Transformer** | `coordinator/data_transformation.py` | Price enrichment (averages, ratings, differences) |
| **Period Calculator** | `coordinator/periods.py` | Best/peak price period calculation with relaxation |
| **Sensors** | `sensor/` | 80+ entities for prices, levels, ratings, statistics |
| **Binary Sensors** | `binary_sensor/` | Period indicators (best/peak price active) |
| **Services** | `services/` | Custom service endpoints (get_chartdata, get_apexcharts_yaml, refresh_user_data) |
### Sensor Architecture (Calculator Pattern)
The sensor platform uses **Calculator Pattern** for clean separation of concerns (refactored Nov 2025):
| Component | Files | Lines | Responsibility |
| ---------------- | ------------------------- | ----- | ------------------------------------------------------- |
| **Entity Class** | `sensor/core.py` | 909 | Entity lifecycle, coordinator, delegates to calculators |
| **Calculators** | `sensor/calculators/` | 1,838 | Business logic (8 specialized calculators) |
| **Attributes** | `sensor/attributes/` | 1,209 | State presentation (8 specialized modules) |
| **Routing** | `sensor/value_getters.py` | 276 | Centralized sensor → calculator mapping |
| **Chart Export** | `sensor/chart_data.py` | 144 | Service call handling, YAML parsing |
| **Helpers** | `sensor/helpers.py` | 188 | Aggregation functions, utilities |
**Calculator Package** (`sensor/calculators/`):
- `base.py` - Abstract BaseCalculator with coordinator access
- `interval.py` - Single interval calculations (current/next/previous)
- `rolling_hour.py` - 5-interval rolling windows
- `daily_stat.py` - Calendar day min/max/avg statistics
- `window_24h.py` - Trailing/leading 24h windows
- `volatility.py` - Price volatility analysis
- `trend.py` - Complex trend analysis with caching
- `timing.py` - Best/peak price period timing
- `metadata.py` - Home/metering metadata
**Benefits:**
- 58% reduction in core.py (2,170 → 909 lines)
- Clear separation: Calculators (logic) vs Attributes (presentation)
- Independent testability for each calculator
- Easy to add sensors: Choose calculation pattern, add to routing
### Helper Utilities
| Utility | File | Purpose |
| ----------------- | ------------------ | ------------------------------------------------- |
| **Price Utils** | `utils/price.py` | Rating calculation, enrichment, level aggregation |
| **Average Utils** | `utils/average.py` | Trailing/leading 24h average calculations |
| **Entity Utils** | `entity_utils/` | Shared icon/color/attribute logic |
| **Translations** | `const.py` | Translation loading and caching |
---
## Key Patterns
### 1. Dual Translation System
- **Standard translations** (`/translations/*.json`): HA-compliant schema for entity names
- **Custom translations** (`/custom_translations/*.json`): Extended descriptions, usage tips
- Both loaded at integration setup, cached in memory
- Access via `get_translation()` helper function
### 2. Price Data Enrichment
All quarter-hourly price intervals get augmented via `utils/price.py`:
```python
# Original from Tibber API
{
"startsAt": "2025-11-03T14:00:00+01:00",
"total": 0.2534,
"level": "NORMAL"
}
# After enrichment (utils/price.py)
{
"startsAt": "2025-11-03T14:00:00+01:00",
"total": 0.2534,
"level": "NORMAL",
"trailing_avg_24h": 0.2312, # ← Added: 24h trailing average
"difference": 9.6, # ← Added: % diff from trailing avg
"rating_level": "NORMAL" # ← Added: LOW/NORMAL/HIGH based on thresholds
}
```
### 3. Quarter-Hour Precision
- **API polling**: Every 15 minutes (coordinator fetch cycle)
- **Entity updates**: On 00/15/30/45-minute boundaries via `coordinator/listeners.py`
- **Timer scheduling**: Uses `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)`
- HA may trigger ±few milliseconds before/after exact boundary
- Smart boundary tolerance (±2 seconds) handles scheduling jitter in `sensor/helpers.py`
- If HA schedules at 14:59:58 → rounds to 15:00:00 (shows new interval data)
- If HA restarts at 14:59:30 → stays at 14:45:00 (shows current interval data)
- **Absolute time tracking**: Timer plans for **all future boundaries** (not relative delays)
- Prevents double-updates (if triggered at 14:59:58, next trigger is 15:15:00, not 15:00:00)
- **Result**: Current price sensors update without waiting for next API poll
### 4. Calculator Pattern (Sensor Platform)
Sensors organized by **calculation method** (refactored Nov 2025):
**Unified Handler Methods** (`sensor/core.py`):
- `_get_interval_value(offset, type)` - current/next/previous intervals
- `_get_rolling_hour_value(offset, type)` - 5-interval rolling windows
- `_get_daily_stat_value(day, stat_func)` - calendar day min/max/avg
- `_get_24h_window_value(stat_func)` - trailing/leading statistics
**Routing** (`sensor/value_getters.py`):
- Single source of truth mapping 80+ entity keys to calculator methods
- Organized by calculation type (Interval, Rolling Hour, Daily Stats, etc.)
**Calculators** (`sensor/calculators/`):
- Each calculator inherits from `BaseCalculator` with coordinator access
- Focused responsibility: `IntervalCalculator`, `TrendCalculator`, etc.
- Complex logic isolated (e.g., `TrendCalculator` has internal caching)
**Attributes** (`sensor/attributes/`):
- Separate from business logic, handles state presentation
- Builds extra_state_attributes dicts for entity classes
- Unified builders: `build_sensor_attributes()`, `build_extra_state_attributes()`
**Benefits:**
- Minimal code duplication across 80+ sensors
- Clear separation of concerns (calculation vs presentation)
- Easy to extend: Add sensor → choose pattern → add to routing
- Independent testability for each component
---
## Performance Characteristics
### API Call Reduction
- **Without caching:** 96 API calls/day (every 15 min)
- **With caching:** ~1-2 API calls/day (only when cache expires)
- **Reduction:** ~98%
### CPU Optimization
| Optimization | Location | Savings |
| ------------------- | ------------------------ | ---------------------------- |
| Config caching | `coordinator/*` | ~50% on config checks |
| Period caching | `coordinator/periods.py` | ~70% on period recalculation |
| Lazy logging | Throughout | ~15% on log-heavy operations |
| Import optimization | Module structure | ~20% faster loading |
### Memory Usage
- **Per coordinator instance:** ~126KB cache overhead
- **Typical setup:** 1 main + 2 subentries = ~378KB total
- **Redundancy eliminated:** 14% reduction (10KB saved per coordinator)
---
## Related Documentation
- **[Timer Architecture](./timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers)
- **[Caching Strategy](./caching-strategy.md)** - Detailed cache behavior, invalidation, debugging
- **[Setup Guide](./setup.md)** - Development environment setup
- **[Testing Guide](./testing.md)** - How to test changes
- **[Release Management](./release-management.md)** - Release workflow and versioning
- **[AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md)** - Complete reference for AI development

View file

@ -0,0 +1,487 @@
---
comments: false
---
# Caching Strategy
This document explains all caching mechanisms in the Tibber Prices integration, their purpose, invalidation logic, and lifetime.
For timer coordination and scheduling details, see [Timer Architecture](./timer-architecture.md).
## Overview
The integration uses **4 distinct caching layers** with different purposes and lifetimes:
1. **Persistent API Data Cache** (HA Storage) - Hours to days
2. **Translation Cache** (Memory) - Forever (until HA restart)
3. **Config Dictionary Cache** (Memory) - Until config changes
4. **Period Calculation Cache** (Memory) - Until price data or config changes
## 1. Persistent API Data Cache
**Location:** `coordinator/cache.py` → HA Storage (`.storage/tibber_prices.<entry_id>`)
**Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts.
**What is cached:**
- **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total)
- **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query
- **Timestamps**: Last update times for validation
**Lifetime:**
- **Price data**: Until midnight turnover (cleared daily at 00:00 local time)
- **User data**: 24 hours (refreshed daily)
- **Survives**: HA restarts via persistent Storage
**Invalidation triggers:**
1. **Midnight turnover** (Timer #2 in coordinator):
```python
# coordinator/day_transitions.py
def _handle_midnight_turnover() -> None:
self._cached_price_data = None # Force fresh fetch for new day
self._last_price_update = None
await self.store_cache()
```
2. **Cache validation on load**:
```python
# coordinator/cache.py
def is_cache_valid(cache_data: CacheData) -> bool:
# Checks if price data is from a previous day
if today_date < local_now.date(): # Yesterday's data
return False
```
3. **Tomorrow data check** (after 13:00):
```python
# coordinator/data_fetching.py
if tomorrow_missing or tomorrow_invalid:
return "tomorrow_check" # Update needed
```
**Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires.
---
## 2. Translation Cache
**Location:** `const.py``_TRANSLATIONS_CACHE` and `_STANDARD_TRANSLATIONS_CACHE` (in-memory dicts)
**Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc.
**What is cached:**
- **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names
- **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions
**Lifetime:**
- **Forever** (until HA restart)
- No invalidation during runtime
**When populated:**
- At integration setup: `async_load_translations(hass, "en")` in `__init__.py`
- Lazy loading: If translation missing, attempts file load once
**Access pattern:**
```python
# Non-blocking synchronous access from cached data
description = get_translation("binary_sensor.best_price_period.description", "en")
```
**Why this cache matters:** Entity attributes are accessed on every state update (~15 times per hour per entity). File I/O would block the event loop. Cache enables synchronous, non-blocking attribute generation.
---
## 3. Config Dictionary Cache
**Location:** `coordinator/data_transformation.py` and `coordinator/periods.py` (per-instance fields)
**Purpose:** Avoid ~30-40 `options.get()` calls on every coordinator update (every 15 minutes).
**What is cached:**
### DataTransformer Config Cache
```python
{
"thresholds": {"low": 15, "high": 35},
"volatility_thresholds": {"moderate": 15.0, "high": 25.0, "very_high": 40.0},
# ... 20+ more config fields
}
```
### PeriodCalculator Config Cache
```python
{
"best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60},
"peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}
}
```
**Lifetime:**
- Until `invalidate_config_cache()` is called
- Built once on first use per coordinator update cycle
**Invalidation trigger:**
- **Options change** (user reconfigures integration):
```python
# coordinator/core.py
async def _handle_options_update(...) -> None:
self._data_transformer.invalidate_config_cache()
self._period_calculator.invalidate_config_cache()
await self.async_request_refresh()
```
**Performance impact:**
- **Before:** ~30 dict lookups + type conversions per update = ~50μs
- **After:** 1 cache check = ~1μs
- **Savings:** ~98% (50μs → 1μs per update)
**Why this cache matters:** Config is read multiple times per update (transformation + period calculation + validation). Caching eliminates redundant lookups without changing behavior.
---
## 4. Period Calculation Cache
**Location:** `coordinator/periods.py``PeriodCalculator._cached_periods`
**Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed.
**What is cached:**
```python
{
"best_price": {
"periods": [...], # Calculated period objects
"intervals": [...], # All intervals in periods
"metadata": {...} # Config snapshot
},
"best_price_relaxation": {"relaxation_active": bool, ...},
"peak_price": {...},
"peak_price_relaxation": {...}
}
```
**Cache key:** Hash of relevant inputs
```python
hash_data = (
today_signature, # (startsAt, rating_level) for each interval
tuple(best_config.items()), # Best price config
tuple(peak_config.items()), # Peak price config
best_level_filter, # Level filter overrides
peak_level_filter
)
```
**Lifetime:**
- Until price data changes (today's intervals modified)
- Until config changes (flex, thresholds, filters)
- Recalculated at midnight (new today data)
**Invalidation triggers:**
1. **Config change** (explicit):
```python
def invalidate_config_cache() -> None:
self._cached_periods = None
self._last_periods_hash = None
```
2. **Price data change** (automatic via hash mismatch):
```python
current_hash = self._compute_periods_hash(price_info)
if self._last_periods_hash != current_hash:
# Cache miss - recalculate
```
**Cache hit rate:**
- **High:** During normal operation (coordinator updates every 15min, price data unchanged)
- **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00)
**Performance impact:**
- **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts)
- **Cache hit:** `<`1ms (hash comparison + dict lookup)
- **Savings:** ~70% of calculation time (most updates hit cache)
**Why this cache matters:** Period calculation is CPU-intensive (filtering, gap tolerance, relaxation). Caching avoids recalculating unchanged periods 3-4 times per hour.
---
## 5. Transformation Cache (Price Enrichment Only)
**Location:** `coordinator/data_transformation.py``_cached_transformed_data`
**Status:** ✅ **Clean separation** - enrichment only, no redundancy
**What is cached:**
```python
{
"timestamp": ...,
"homes": {...},
"priceInfo": {...}, # Enriched price data (trailing_avg_24h, difference, rating_level)
# NO periods - periods are exclusively managed by PeriodCalculator
}
```
**Purpose:** Avoid re-enriching price data when config unchanged between midnight checks.
**Current behavior:**
- Caches **only enriched price data** (price + statistics)
- **Does NOT cache periods** (handled by Period Calculation Cache)
- Invalidated when:
- Config changes (thresholds affect enrichment)
- Midnight turnover detected
- New update cycle begins
**Architecture:**
- DataTransformer: Handles price enrichment only
- PeriodCalculator: Handles period calculation only (with hash-based cache)
- Coordinator: Assembles final data on-demand from both caches
**Memory savings:** Eliminating redundant period storage saves ~10KB per coordinator (14% reduction).
---
## Cache Invalidation Flow
### User Changes Options (Config Flow)
```
User saves options
config_entry.add_update_listener() triggers
coordinator._handle_options_update()
├─> DataTransformer.invalidate_config_cache()
│ └─> _config_cache = None
│ _config_cache_valid = False
│ _cached_transformed_data = None
└─> PeriodCalculator.invalidate_config_cache()
└─> _config_cache = None
_config_cache_valid = False
_cached_periods = None
_last_periods_hash = None
coordinator.async_request_refresh()
Fresh data fetch with new config
```
### Midnight Turnover (Day Transition)
```
Timer #2 fires at 00:00
coordinator._handle_midnight_turnover()
├─> Clear persistent cache
│ └─> _cached_price_data = None
│ _last_price_update = None
└─> Clear transformation cache
└─> _cached_transformed_data = None
_last_transformation_config = None
Period cache auto-invalidates (hash mismatch on new "today")
Fresh API fetch for new day
```
### Tomorrow Data Arrives (~13:00)
```
Coordinator update cycle
should_update_price_data() checks tomorrow
Tomorrow data missing/invalid
API fetch with new tomorrow data
Price data hash changes (new intervals)
Period cache auto-invalidates (hash mismatch)
Periods recalculated with tomorrow included
```
---
## Cache Coordination
**All caches work together:**
```
Persistent Storage (HA restart)
API Data Cache (price_data, user_data)
├─> Enrichment (add rating_level, difference, etc.)
│ ↓
│ Transformation Cache (_cached_transformed_data)
└─> Period Calculation
Period Cache (_cached_periods)
Config Cache (avoid re-reading options)
Translation Cache (entity descriptions)
```
**No cache invalidation cascades:**
- Config cache invalidation is **explicit** (on options update)
- Period cache invalidation is **automatic** (via hash mismatch)
- Transformation cache invalidation is **automatic** (on midnight/config change)
- Translation cache is **never invalidated** (read-only after load)
**Thread safety:**
- All caches are accessed from `MainThread` only (Home Assistant event loop)
- No locking needed (single-threaded execution model)
---
## Performance Characteristics
### Typical Operation (No Changes)
```
Coordinator Update (every 15 min)
├─> API fetch: SKIP (cache valid)
├─> Config dict build: ~1μs (cached)
├─> Period calculation: ~1ms (cached, hash match)
├─> Transformation: ~10ms (enrichment only, periods cached)
└─> Entity updates: ~5ms (translation cache hit)
Total: ~16ms (down from ~600ms without caching)
```
### After Midnight Turnover
```
Coordinator Update (00:00)
├─> API fetch: ~500ms (cache cleared, fetch new day)
├─> Config dict build: ~50μs (rebuild, no cache)
├─> Period calculation: ~200ms (cache miss, recalculate)
├─> Transformation: ~50ms (re-enrich, rebuild)
└─> Entity updates: ~5ms (translation cache still valid)
Total: ~755ms (expected once per day)
```
### After Config Change
```
Options Update
├─> Cache invalidation: `<`1ms
├─> Coordinator refresh: ~600ms
│ ├─> API fetch: SKIP (data unchanged)
│ ├─> Config rebuild: ~50μs
│ ├─> Period recalculation: ~200ms (new thresholds)
│ ├─> Re-enrichment: ~50ms
│ └─> Entity updates: ~5ms
└─> Total: ~600ms (expected on manual reconfiguration)
```
---
## Summary Table
| Cache Type | Lifetime | Size | Invalidation | Purpose |
| ---------------------- | ---------------------------- | ------ | ------------------------- | ------------------------------- |
| **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls |
| **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O |
| **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups |
| **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation |
| **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment |
**Total memory overhead:** ~116KB per coordinator instance (main + subentries)
**Benefits:**
- 97% reduction in API calls (from every 15min to once per day)
- 70% reduction in period calculation time (cache hits during normal operation)
- 98% reduction in config access time (30+ lookups → 1 cache check)
- Zero file I/O during runtime (translations cached at startup)
**Trade-offs:**
- Memory usage: ~116KB per home (negligible for modern systems)
- Code complexity: 5 cache invalidation points (well-tested, documented)
- Debugging: Must understand cache lifetime when investigating stale data issues
---
## Debugging Cache Issues
### Symptom: Stale data after config change
**Check:**
1. Is `_handle_options_update()` called? (should see "Options updated" log)
2. Are `invalidate_config_cache()` methods executed?
3. Does `async_request_refresh()` trigger?
**Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init.
### Symptom: Period calculation not updating
**Check:**
1. Verify hash changes when data changes: `_compute_periods_hash()`
2. Check `_last_periods_hash` vs `current_hash`
3. Look for "Using cached period calculation" vs "Calculating periods" logs
**Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs.
### Symptom: Yesterday's prices shown as today
**Check:**
1. `is_cache_valid()` logic in `coordinator/cache.py`
2. Midnight turnover execution (Timer #2)
3. Cache clear confirmation in logs
**Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration.
### Symptom: Missing translations
**Check:**
1. `async_load_translations()` called at startup?
2. Translation files exist in `/translations/` and `/custom_translations/`?
3. Cache population: `_TRANSLATIONS_CACHE` keys
**Fix:** Re-install integration or restart HA to reload translation files.
---
## Related Documentation
- **[Timer Architecture](./timer-architecture.md)** - Timer system, scheduling, midnight coordination
- **[Architecture](./architecture.md)** - Overall system design, data flow
- **[AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md)** - Complete reference for AI development

View file

@ -0,0 +1,124 @@
---
comments: false
---
# Coding Guidelines
> **Note:** For complete coding standards, see [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md).
## Code Style
- **Formatter/Linter**: Ruff (replaces Black, Flake8, isort)
- **Max line length**: 120 characters
- **Max complexity**: 25 (McCabe)
- **Target**: Python 3.13
Run before committing:
```bash
./scripts/lint # Auto-fix issues
./scripts/release/hassfest # Validate integration structure
```
## Naming Conventions
### Class Names
**All public classes MUST use the integration name as prefix.**
This is a Home Assistant standard to avoid naming conflicts between integrations.
```python
# ✅ CORRECT
class TibberPricesApiClient:
class TibberPricesDataUpdateCoordinator:
class TibberPricesSensor:
# ❌ WRONG - Missing prefix
class ApiClient:
class DataFetcher:
class TimeService:
```
**When prefix is required:**
- Public classes used across multiple modules
- All exception classes
- All coordinator and entity classes
- Data classes (dataclasses, NamedTuples) used as public APIs
**When prefix can be omitted:**
- Private helper classes within a single module (prefix with `_` underscore)
- Type aliases and callbacks (e.g., `TimeServiceCallback`)
- Small internal NamedTuples for function returns
**Private Classes:**
If a helper class is ONLY used within a single module file, prefix it with underscore:
```python
# ✅ Private class - used only in this file
class _InternalHelper:
"""Helper used only within this module."""
pass
# ❌ Wrong - no prefix but used across modules
class DataFetcher: # Should be TibberPricesDataFetcher
pass
```
**Note:** Currently (Nov 2025), this project has **NO private classes** - all classes are used across module boundaries.
**Current Technical Debt:**
Many existing classes lack the `TibberPrices` prefix. Before refactoring:
1. Document the plan in `/planning/class-naming-refactoring.md`
2. Use `multi_replace_string_in_file` for bulk renames
3. Test thoroughly after each module
See [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md) for complete list of classes needing rename.
## Import Order
1. Python stdlib (specific types only)
2. Third-party (`homeassistant.*`, `aiohttp`)
3. Local (`.api`, `.const`)
## Critical Patterns
### Time Handling
Always use `dt_util` from `homeassistant.util`:
```python
from homeassistant.util import dt as dt_util
price_time = dt_util.parse_datetime(starts_at)
price_time = dt_util.as_local(price_time) # Convert to HA timezone
now = dt_util.now()
```
### Translation Loading
```python
# In __init__.py async_setup_entry:
await async_load_translations(hass, "en")
await async_load_standard_translations(hass, "en")
```
### Price Data Enrichment
Always enrich raw API data:
```python
from .price_utils import enrich_price_info_with_differences
enriched = enrich_price_info_with_differences(
price_info_data,
thresholds,
)
```
See [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md) for complete guidelines.

View file

@ -0,0 +1,233 @@
# Contributing Guide
Welcome! This guide helps you contribute to the Tibber Prices integration.
## Getting Started
### Prerequisites
- Git
- VS Code with Remote Containers extension
- Docker Desktop
### Fork and Clone
1. Fork the repository on GitHub
2. Clone your fork:
```bash
git clone https://github.com/YOUR_USERNAME/hass.tibber_prices.git
cd hass.tibber_prices
```
3. Open in VS Code
4. Click "Reopen in Container" when prompted
The DevContainer will set up everything automatically.
## Development Workflow
### 1. Create a Branch
```bash
git checkout -b feature/your-feature-name
# or
git checkout -b fix/issue-123-description
```
**Branch naming:**
- `feature/` - New features
- `fix/` - Bug fixes
- `docs/` - Documentation only
- `refactor/` - Code restructuring
- `test/` - Test improvements
### 2. Make Changes
Edit code, following [Coding Guidelines](coding-guidelines.md).
**Run checks frequently:**
```bash
./scripts/type-check # Pyright type checking
./scripts/lint # Ruff linting (auto-fix)
./scripts/test # Run tests
```
### 3. Test Locally
```bash
./scripts/develop # Start HA with integration loaded
```
Access at http://localhost:8123
### 4. Write Tests
Add tests in `/tests/` for new features:
```python
@pytest.mark.unit
async def test_your_feature(hass, coordinator):
"""Test your new feature."""
# Arrange
coordinator.data = {...}
# Act
result = your_function(coordinator.data)
# Assert
assert result == expected_value
```
Run your test:
```bash
./scripts/test tests/test_your_feature.py -v
```
### 5. Commit Changes
Follow [Conventional Commits](https://www.conventionalcommits.org/):
```bash
git add .
git commit -m "feat(sensors): add volatility trend sensor
Add new sensor showing 3-hour volatility trend direction.
Includes attributes with historical volatility data.
Impact: Users can predict when prices will stabilize or continue fluctuating."
```
**Commit types:**
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation
- `refactor:` - Code restructuring
- `test:` - Test changes
- `chore:` - Maintenance
**Add scope when relevant:**
- `feat(sensors):` - Sensor platform
- `fix(coordinator):` - Data coordinator
- `docs(user):` - User documentation
### 6. Push and Create PR
```bash
git push origin your-branch-name
```
Then open Pull Request on GitHub.
## Pull Request Guidelines
### PR Template
Title: Short, descriptive (50 chars max)
Description should include:
```markdown
## What
Brief description of changes
## Why
Problem being solved or feature rationale
## How
Implementation approach
## Testing
- [ ] Manual testing in Home Assistant
- [ ] Unit tests added/updated
- [ ] Type checking passes
- [ ] Linting passes
## Breaking Changes
(If any - describe migration path)
## Related Issues
Closes #123
```
### PR Checklist
Before submitting:
- [ ] Code follows [Coding Guidelines](coding-guidelines.md)
- [ ] All tests pass (`./scripts/test`)
- [ ] Type checking passes (`./scripts/type-check`)
- [ ] Linting passes (`./scripts/lint-check`)
- [ ] Documentation updated (if needed)
- [ ] AGENTS.md updated (if patterns changed)
- [ ] Commit messages follow Conventional Commits
### Review Process
1. **Automated checks** run (CI/CD)
2. **Maintainer review** (usually within 3 days)
3. **Address feedback** if requested
4. **Approval** → Maintainer merges
## Code Review Tips
### What Reviewers Look For
✅ **Good:**
- Clear, self-explanatory code
- Appropriate comments for complex logic
- Tests covering edge cases
- Type hints on all functions
- Follows existing patterns
❌ **Avoid:**
- Large PRs (>500 lines) - split into smaller ones
- Mixing unrelated changes
- Missing tests for new features
- Breaking changes without migration path
- Copy-pasted code (refactor into shared functions)
### Responding to Feedback
- Don't take it personally - we're improving code together
- Ask questions if feedback unclear
- Push additional commits to address comments
- Mark conversations as resolved when fixed
## Finding Issues to Work On
Good first issues are labeled:
- `good first issue` - Beginner-friendly
- `help wanted` - Maintainers welcome contributions
- `documentation` - Docs improvements
Comment on issue before starting work to avoid duplicates.
## Communication
- **GitHub Issues** - Bug reports, feature requests
- **Pull Requests** - Code discussion
- **Discussions** - General questions, ideas
Be respectful, constructive, and patient. We're all volunteers! 🙏
---
💡 **Related:**
- [Setup Guide](setup.md) - DevContainer setup
- [Coding Guidelines](coding-guidelines.md) - Style guide
- [Testing](testing.md) - Writing tests
- [Release Management](release-management.md) - How releases work

View file

@ -0,0 +1,315 @@
---
comments: false
---
# Critical Behavior Patterns - Testing Guide
**Purpose:** This documentation lists essential behavior patterns that must be tested to ensure production-quality code and prevent resource leaks.
**Last Updated:** 2025-11-22
**Test Coverage:** 41 tests implemented (100% of critical patterns)
## 🎯 Why Are These Tests Critical?
Home Assistant integrations run **continuously** in the background. Resource leaks lead to:
- **Memory Leaks**: RAM usage grows over days/weeks until HA becomes unstable
- **Callback Leaks**: Listeners remain registered after entity removal → CPU load increases
- **Timer Leaks**: Timers continue running after unload → unnecessary background tasks
- **File Handle Leaks**: Storage files remain open → system resources exhausted
## ✅ Test Categories
### 1. Resource Cleanup (Memory Leak Prevention)
**File:** `tests/test_resource_cleanup.py`
#### 1.1 Listener Cleanup ✅
**What is tested:**
- Time-sensitive listeners are correctly removed (`async_add_time_sensitive_listener()`)
- Minute-update listeners are correctly removed (`async_add_minute_update_listener()`)
- Lifecycle callbacks are correctly unregistered (`register_lifecycle_callback()`)
- Sensor cleanup removes ALL registered listeners
- Binary sensor cleanup removes ALL registered listeners
**Why critical:**
- Each registered listener holds references to Entity + Coordinator
- Without cleanup: Entities are not freed by GC → Memory Leak
- With 80+ sensors × 3 listener types = 240+ callbacks that must be cleanly removed
**Code Locations:**
- `coordinator/listeners.py``async_add_time_sensitive_listener()`, `async_add_minute_update_listener()`
- `coordinator/core.py``register_lifecycle_callback()`
- `sensor/core.py``async_will_remove_from_hass()`
- `binary_sensor/core.py``async_will_remove_from_hass()`
#### 1.2 Timer Cleanup ✅
**What is tested:**
- Quarter-hour timer is cancelled and reference cleared
- Minute timer is cancelled and reference cleared
- Both timers are cancelled together
- Cleanup works even when timers are `None`
**Why critical:**
- Uncancelled timers continue running after integration unload
- HA's `async_track_utc_time_change()` creates persistent callbacks
- Without cleanup: Timers keep firing → CPU load + unnecessary coordinator updates
**Code Locations:**
- `coordinator/listeners.py``cancel_timers()`
- `coordinator/core.py``async_shutdown()`
#### 1.3 Config Entry Cleanup ✅
**What is tested:**
- Options update listener is registered via `async_on_unload()`
- Cleanup function is correctly passed to `async_on_unload()`
**Why critical:**
- `entry.add_update_listener()` registers permanent callback
- Without `async_on_unload()`: Listener remains active after reload → duplicate updates
- Pattern: `entry.async_on_unload(entry.add_update_listener(handler))`
**Code Locations:**
- `coordinator/core.py``__init__()` (listener registration)
- `__init__.py``async_unload_entry()`
### 2. Cache Invalidation ✅
**File:** `tests/test_resource_cleanup.py`
#### 2.1 Config Cache Invalidation
**What is tested:**
- DataTransformer config cache is invalidated on options change
- PeriodCalculator config + period cache is invalidated
- Trend calculator cache is cleared on coordinator update
**Why critical:**
- Stale config → Sensors use old user settings
- Stale period cache → Incorrect best/peak price periods
- Stale trend cache → Outdated trend analysis
**Code Locations:**
- `coordinator/data_transformation.py``invalidate_config_cache()`
- `coordinator/periods.py``invalidate_config_cache()`
- `sensor/calculators/trend.py``clear_trend_cache()`
### 3. Storage Cleanup ✅
**File:** `tests/test_resource_cleanup.py` + `tests/test_coordinator_shutdown.py`
#### 3.1 Persistent Storage Removal
**What is tested:**
- Storage file is deleted on config entry removal
- Cache is saved on shutdown (no data loss)
**Why critical:**
- Without storage removal: Old files remain after uninstallation
- Without cache save on shutdown: Data loss on HA restart
- Storage path: `.storage/tibber_prices.{entry_id}`
**Code Locations:**
- `__init__.py``async_remove_entry()`
- `coordinator/core.py``async_shutdown()`
### 4. Timer Scheduling ✅
**File:** `tests/test_timer_scheduling.py`
**What is tested:**
- Quarter-hour timer is registered with correct parameters
- Minute timer is registered with correct parameters
- Timers can be re-scheduled (override old timer)
- Midnight turnover detection works correctly
**Why critical:**
- Wrong timer parameters → Entities update at wrong times
- Without timer override on re-schedule → Multiple parallel timers → Performance problem
### 5. Sensor-to-Timer Assignment ✅
**File:** `tests/test_sensor_timer_assignment.py`
**What is tested:**
- All `TIME_SENSITIVE_ENTITY_KEYS` are valid entity keys
- All `MINUTE_UPDATE_ENTITY_KEYS` are valid entity keys
- Both lists are disjoint (no overlap)
- Sensor and binary sensor platforms are checked
**Why critical:**
- Wrong timer assignment → Sensors update at wrong times
- Overlap → Duplicate updates → Performance problem
## 🚨 Additional Analysis (Nice-to-Have Patterns)
These patterns were analyzed and classified as **not critical**:
### 6. Async Task Management
**Current Status:** Fire-and-forget pattern for short tasks
- `sensor/core.py` → Chart data refresh (short-lived, max 1-2 seconds)
- `coordinator/core.py` → Cache storage (short-lived, max 100ms)
**Why no tests needed:**
- No long-running tasks (all < 2 seconds)
- HA's event loop handles short tasks automatically
- Task exceptions are already logged
**If needed:** `_chart_refresh_task` tracking + cancel in `async_will_remove_from_hass()`
### 7. API Session Cleanup
**Current Status:** ✅ Correctly implemented
- `async_get_clientsession(hass)` is used (shared session)
- No new sessions are created
- HA manages session lifecycle automatically
**Code:** `api/client.py` + `__init__.py`
### 8. Translation Cache Memory
**Current Status:** ✅ Bounded cache
- Max ~5-10 languages × 5KB = 50KB total
- Module-level cache without re-loading
- Practically no memory issue
**Code:** `const.py``_TRANSLATIONS_CACHE`, `_STANDARD_TRANSLATIONS_CACHE`
### 9. Coordinator Data Structure Integrity
**Current Status:** Manually tested via `./scripts/develop`
- Midnight turnover works correctly (observed over several days)
- Missing keys are handled via `.get()` with defaults
- 80+ sensors access `coordinator.data` without errors
**Structure:**
```python
coordinator.data = {
"user_data": {...},
"priceInfo": [...], # Flat list of all enriched intervals
"currency": "EUR" # Top-level for easy access
}
```
### 10. Service Response Memory
**Current Status:** HA's response lifecycle
- HA automatically frees service responses after return
- ApexCharts ~20KB response is one-time per call
- No response accumulation in integration code
**Code:** `services/apexcharts.py`
## 📊 Test Coverage Status
### ✅ Implemented Tests (41 total)
| Category | Status | Tests | File | Coverage |
| ----------------------- | ------ | ------ | --------------------------------- | ------------------- |
| Listener Cleanup | ✅ | 5 | `test_resource_cleanup.py` | 100% |
| Timer Cleanup | ✅ | 4 | `test_resource_cleanup.py` | 100% |
| Config Entry Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% |
| Cache Invalidation | ✅ | 3 | `test_resource_cleanup.py` | 100% |
| Storage Cleanup | ✅ | 1 | `test_resource_cleanup.py` | 100% |
| Storage Persistence | ✅ | 2 | `test_coordinator_shutdown.py` | 100% |
| Timer Scheduling | ✅ | 8 | `test_timer_scheduling.py` | 100% |
| Sensor-Timer Assignment | ✅ | 17 | `test_sensor_timer_assignment.py` | 100% |
| **TOTAL** | **✅** | **41** | | **100% (critical)** |
### 📋 Analyzed but Not Implemented (Nice-to-Have)
| Category | Status | Rationale |
| ------------------------ | ------ | ---------------------------------------------------- |
| Async Task Management | 📋 | Fire-and-forget pattern used (no long-running tasks) |
| API Session Cleanup | ✅ | Pattern correct (`async_get_clientsession` used) |
| Translation Cache | ✅ | Cache size bounded (~50KB max for 10 languages) |
| Data Structure Integrity | 📋 | Would add test time without finding real issues |
| Service Response Memory | 📋 | HA automatically frees service responses |
**Legend:**
- ✅ = Fully tested or pattern verified correct
- 📋 = Analyzed, low priority for testing (no known issues)
## 🎯 Development Status
### ✅ All Critical Patterns Tested
All essential memory leak prevention patterns are covered by 41 tests:
- ✅ Listeners are correctly removed (no callback leaks)
- ✅ Timers are cancelled (no background task leaks)
- ✅ Config entry cleanup works (no dangling listeners)
- ✅ Caches are invalidated (no stale data issues)
- ✅ Storage is saved and cleaned up (no data loss)
- ✅ Timer scheduling works correctly (no update issues)
- ✅ Sensor-timer assignment is correct (no wrong updates)
### 📋 Nice-to-Have Tests (Optional)
If problems arise in the future, these tests can be added:
1. **Async Task Management** - Pattern analyzed (fire-and-forget for short tasks)
2. **Data Structure Integrity** - Midnight rotation manually tested
3. **Service Response Memory** - HA's response lifecycle automatic
**Conclusion:** The integration has production-quality test coverage for all critical resource leak patterns.
## 🔍 How to Run Tests
```bash
# Run all resource cleanup tests (14 tests)
./scripts/test tests/test_resource_cleanup.py -v
# Run all critical pattern tests (41 tests)
./scripts/test tests/test_resource_cleanup.py tests/test_coordinator_shutdown.py \
tests/test_timer_scheduling.py tests/test_sensor_timer_assignment.py -v
# Run all tests with coverage
./scripts/test --cov=custom_components.tibber_prices --cov-report=html
# Type checking and linting
./scripts/check
# Manual memory leak test
# 1. Start HA: ./scripts/develop
# 2. Monitor RAM: watch -n 1 'ps aux | grep home-assistant'
# 3. Reload integration multiple times (HA UI: Settings → Devices → Tibber Prices → Reload)
# 4. RAM should stabilize (not grow continuously)
```
## 📚 References
- **Home Assistant Cleanup Patterns**: https://developers.home-assistant.io/docs/integration_setup_failures/#cleanup
- **Async Best Practices**: https://developers.home-assistant.io/docs/asyncio_101/
- **Memory Profiling**: https://docs.python.org/3/library/tracemalloc.html

View file

@ -0,0 +1,248 @@
# Debugging Guide
Tips and techniques for debugging the Tibber Prices integration during development.
## Logging
### Enable Debug Logging
Add to `configuration.yaml`:
```yaml
logger:
default: info
logs:
custom_components.tibber_prices: debug
```
Restart Home Assistant to apply.
### Key Log Messages
**Coordinator Updates:**
```
[custom_components.tibber_prices.coordinator] Successfully fetched price data
[custom_components.tibber_prices.coordinator] Cache valid, using cached data
[custom_components.tibber_prices.coordinator] Midnight turnover detected, clearing cache
```
**Period Calculation:**
```
[custom_components.tibber_prices.coordinator.periods] Calculating BEST PRICE periods: flex=15.0%
[custom_components.tibber_prices.coordinator.periods] Day 2024-12-06: Found 2 periods
[custom_components.tibber_prices.coordinator.periods] Period 1: 02:00-05:00 (12 intervals)
```
**API Errors:**
```
[custom_components.tibber_prices.api] API request failed: Unauthorized
[custom_components.tibber_prices.api] Retrying (attempt 2/3) after 2.0s
```
## VS Code Debugging
### Launch Configuration
`.vscode/launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Home Assistant",
"type": "debugpy",
"request": "launch",
"module": "homeassistant",
"args": ["-c", "config", "--debug"],
"justMyCode": false,
"env": {
"PYTHONPATH": "${workspaceFolder}/.venv/lib/python3.13/site-packages"
}
}
]
}
```
### Set Breakpoints
**Coordinator update:**
```python
# coordinator/core.py
async def _async_update_data(self) -> dict:
"""Fetch data from API."""
breakpoint() # Or set VS Code breakpoint
```
**Period calculation:**
```python
# coordinator/period_handlers/core.py
def calculate_periods(...) -> list[dict]:
"""Calculate best/peak price periods."""
breakpoint()
```
## pytest Debugging
### Run Single Test with Output
```bash
.venv/bin/python -m pytest tests/test_period_calculation.py::test_midnight_crossing -v -s
```
**Flags:**
- `-v` - Verbose output
- `-s` - Show print statements
- `-k pattern` - Run tests matching pattern
### Debug Test in VS Code
Set breakpoint in test file, use "Debug Test" CodeLens.
### Useful Test Patterns
**Print coordinator data:**
```python
def test_something(coordinator):
print(f"Coordinator data: {coordinator.data}")
print(f"Price info count: {len(coordinator.data['priceInfo'])}")
```
**Inspect period attributes:**
```python
def test_periods(hass, coordinator):
periods = coordinator.data.get('best_price_periods', [])
for period in periods:
print(f"Period: {period['start']} to {period['end']}")
print(f" Intervals: {len(period['intervals'])}")
```
## Common Issues
### Integration Not Loading
**Check:**
```bash
grep "tibber_prices" config/home-assistant.log
```
**Common causes:**
- Syntax error in Python code → Check logs for traceback
- Missing dependency → Run `uv sync`
- Wrong file permissions → `chmod +x scripts/*`
### Sensors Not Updating
**Check coordinator state:**
```python
# In Developer Tools > Template
{{ states.sensor.tibber_home_current_interval_price.last_updated }}
```
**Debug in code:**
```python
# Add logging in sensor/core.py
_LOGGER.debug("Updating sensor %s: old=%s new=%s",
self.entity_id, self._attr_native_value, new_value)
```
### Period Calculation Wrong
**Enable detailed period logs:**
```python
# coordinator/period_handlers/period_building.py
_LOGGER.debug("Candidate intervals: %s",
[(i['startsAt'], i['total']) for i in candidates])
```
**Check filter statistics:**
```
[period_building] Flex filter blocked: 45 intervals
[period_building] Min distance blocked: 12 intervals
[period_building] Level filter blocked: 8 intervals
```
## Performance Profiling
### Time Execution
```python
import time
start = time.perf_counter()
result = expensive_function()
duration = time.perf_counter() - start
_LOGGER.debug("Function took %.3fs", duration)
```
### Memory Usage
```python
import tracemalloc
tracemalloc.start()
# ... your code ...
current, peak = tracemalloc.get_traced_memory()
_LOGGER.debug("Memory: current=%d peak=%d", current, peak)
tracemalloc.stop()
```
### Profile with cProfile
```bash
python -m cProfile -o profile.stats -m homeassistant -c config
python -m pstats profile.stats
# Then: sort cumtime, stats 20
```
## Live Debugging in Running HA
### Remote Debugging with debugpy
Add to coordinator code:
```python
import debugpy
debugpy.listen(5678)
_LOGGER.info("Waiting for debugger attach on port 5678")
debugpy.wait_for_client()
```
Connect from VS Code with remote attach configuration.
### IPython REPL
Install in container:
```bash
uv pip install ipython
```
Add breakpoint:
```python
from IPython import embed
embed() # Drops into interactive shell
```
---
💡 **Related:**
- [Testing Guide](testing.md) - Writing and running tests
- [Setup Guide](setup.md) - Development environment
- [Architecture](architecture.md) - Code structure

View file

@ -0,0 +1,186 @@
# Developer Documentation
This section contains documentation for contributors and maintainers of the **Tibber Prices custom integration**.
:::info Community Project
This is an independent, community-maintained custom integration for Home Assistant. It is **not** an official Tibber product and is **not** affiliated with Tibber AS.
:::
## 📚 Developer Guides
- **[Setup](setup.md)** - DevContainer, environment setup, and dependencies
- **[Architecture](architecture.md)** - Code structure, patterns, and conventions
- **[Period Calculation Theory](period-calculation-theory.md)** - Mathematical foundations, Flex/Distance interaction, Relaxation strategy
- **[Timer Architecture](timer-architecture.md)** - Timer system, scheduling, coordination (3 independent timers)
- **[Caching Strategy](caching-strategy.md)** - Cache layers, invalidation, debugging
- **[Testing](testing.md)** - How to run tests and write new test cases
- **[Release Management](release-management.md)** - Release workflow and versioning process
- **[Coding Guidelines](coding-guidelines.md)** - Style guide, linting, and best practices
- **[Refactoring Guide](refactoring-guide.md)** - How to plan and execute major refactorings
## 🤖 AI Documentation
The main AI/Copilot documentation is in [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md). This file serves as long-term memory for AI assistants and contains:
- Detailed architectural patterns
- Code quality rules and conventions
- Development workflow guidance
- Common pitfalls and anti-patterns
- Project-specific patterns and utilities
**Important:** When proposing changes to patterns or conventions, always update [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md) to keep AI guidance consistent.
### AI-Assisted Development
This integration is developed with extensive AI assistance (GitHub Copilot, Claude, and other AI tools). The AI handles:
- **Pattern Recognition**: Understanding and applying Home Assistant best practices
- **Code Generation**: Implementing features with proper type hints, error handling, and documentation
- **Refactoring**: Maintaining consistency across the codebase during structural changes
- **Translation Management**: Keeping 5 language files synchronized
- **Documentation**: Generating and maintaining comprehensive documentation
**Quality Assurance:**
- Automated linting with Ruff (120-char line length, max complexity 25)
- Home Assistant's type checking and validation
- Real-world testing in development environment
- Code review by maintainer before merging
**Benefits:**
- Rapid feature development while maintaining quality
- Consistent code patterns across all modules
- Comprehensive documentation maintained alongside code
- Quick bug fixes with proper understanding of context
**Limitations:**
- AI may occasionally miss edge cases or subtle bugs
- Some complex Home Assistant patterns may need human review
- Translation quality depends on AI's understanding of target language
- User feedback is crucial for discovering real-world issues
If you're working with AI tools on this project, the [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md) file provides the context and patterns that ensure consistency.
## 🚀 Quick Start for Contributors
1. **Fork and clone** the repository
2. **Open in DevContainer** (VS Code: "Reopen in Container")
3. **Run setup**: `./scripts/setup/setup` (happens automatically via `postCreateCommand`)
4. **Start development environment**: `./scripts/develop`
5. **Make your changes** following the [Coding Guidelines](coding-guidelines.md)
6. **Run linting**: `./scripts/lint`
7. **Validate integration**: `./scripts/release/hassfest`
8. **Test your changes** in the running Home Assistant instance
9. **Commit using Conventional Commits** format
10. **Open a Pull Request** with clear description
## 🛠️ Development Tools
The project includes several helper scripts in `./scripts/`:
- `bootstrap` - Initial setup of dependencies
- `develop` - Start Home Assistant in debug mode (auto-cleans .egg-info)
- `clean` - Remove build artifacts and caches
- `lint` - Auto-fix code issues with ruff
- `lint-check` - Check code without modifications (CI mode)
- `hassfest` - Validate integration structure (JSON, Python syntax, required files)
- `setup` - Install development tools (git-cliff, @github/copilot)
- `prepare-release` - Prepare a new release (bump version, create tag)
- `generate-release-notes` - Generate release notes from commits
## 📦 Project Structure
```
custom_components/tibber_prices/
├── __init__.py # Integration setup
├── coordinator.py # Data update coordinator with caching
├── api.py # Tibber GraphQL API client
├── price_utils.py # Price enrichment functions
├── average_utils.py # Average calculation utilities
├── sensor/ # Sensor platform (package)
│ ├── __init__.py # Platform setup
│ ├── core.py # TibberPricesSensor class
│ ├── definitions.py # Entity descriptions
│ ├── helpers.py # Pure helper functions
│ └── attributes.py # Attribute builders
├── binary_sensor.py # Binary sensor platform
├── entity_utils/ # Shared entity helpers
│ ├── icons.py # Icon mapping logic
│ ├── colors.py # Color mapping logic
│ └── attributes.py # Common attribute builders
├── services.py # Custom services
├── config_flow.py # UI configuration flow
├── const.py # Constants and helpers
├── translations/ # Standard HA translations
└── custom_translations/ # Extended translations (descriptions)
```
## 🔍 Key Concepts
**DataUpdateCoordinator Pattern:**
- Centralized data fetching and caching
- Automatic entity updates on data changes
- Persistent storage via `Store`
- Quarter-hour boundary refresh scheduling
**Price Data Enrichment:**
- Raw API data is enriched with statistical analysis
- Trailing/leading 24h averages calculated per interval
- Price differences and ratings added
- All via pure functions in `price_utils.py`
**Translation System:**
- Dual system: `/translations/` (HA schema) + `/custom_translations/` (extended)
- Both must stay in sync across all languages (de, en, nb, nl, sv)
- Async loading at integration setup
## 🧪 Testing
```bash
# Validate integration structure
./scripts/release/hassfest
# Run all tests
pytest tests/
# Run specific test file
pytest tests/test_coordinator.py
# Run with coverage
pytest --cov=custom_components.tibber_prices tests/
```
## 📝 Documentation Standards
Documentation is organized in two Docusaurus sites:
- **User docs** (`docs/user/`): Installation, configuration, usage guides
- Markdown files in `docs/user/docs/*.md`
- Navigation managed via `docs/user/sidebars.ts`
- **Developer docs** (`docs/developer/`): Architecture, patterns, contribution guides
- Markdown files in `docs/developer/docs/*.md`
- Navigation managed via `docs/developer/sidebars.ts`
- **AI guidance**: `AGENTS.md` (patterns, conventions, long-term memory)
**Best practices:**
- Use clear examples and code snippets
- Keep docs up-to-date with code changes
- Add new pages to appropriate `sidebars.ts` for navigation
## 🤝 Contributing
See [CONTRIBUTING.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/CONTRIBUTING.md) for detailed contribution guidelines, code of conduct, and pull request process.
## 📄 License
This project is licensed under the [MIT License](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/LICENSE).
---
**Note:** This documentation is for developers. End users should refer to the [User Documentation](https://jpawlowski.github.io/hass.tibber_prices/user/).

View file

@ -0,0 +1,339 @@
# Performance Optimization
Guidelines for maintaining and improving integration performance.
## Performance Goals
Target metrics:
- **Coordinator update**: &lt;500ms (typical: 200-300ms)
- **Sensor update**: &lt;10ms per sensor
- **Period calculation**: &lt;100ms (typical: 20-50ms)
- **Memory footprint**: &lt;10MB per home
- **API calls**: &lt;100 per day per home
## Profiling
### Timing Decorator
Use for performance-critical functions:
```python
import time
import functools
def timing(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
duration = time.perf_counter() - start
_LOGGER.debug("%s took %.3fms", func.__name__, duration * 1000)
return result
return wrapper
@timing
def expensive_calculation():
# Your code here
```
### Memory Profiling
```python
import tracemalloc
tracemalloc.start()
# Run your code
current, peak = tracemalloc.get_traced_memory()
_LOGGER.info("Memory: current=%.2fMB peak=%.2fMB",
current / 1024**2, peak / 1024**2)
tracemalloc.stop()
```
### Async Profiling
```bash
# Install aioprof
uv pip install aioprof
# Run with profiling
python -m aioprof homeassistant -c config
```
## Optimization Patterns
### Caching
**1. Persistent Cache** (API data):
```python
# Already implemented in coordinator/cache.py
store = Store(hass, STORAGE_VERSION, STORAGE_KEY)
data = await store.async_load()
```
**2. Translation Cache** (in-memory):
```python
# Already implemented in const.py
_TRANSLATION_CACHE: dict[str, dict] = {}
def get_translation(path: str, language: str) -> dict:
cache_key = f"{path}_{language}"
if cache_key not in _TRANSLATION_CACHE:
_TRANSLATION_CACHE[cache_key] = load_translation(path, language)
return _TRANSLATION_CACHE[cache_key]
```
**3. Config Cache** (invalidated on options change):
```python
class DataTransformer:
def __init__(self):
self._config_cache: dict | None = None
def get_config(self) -> dict:
if self._config_cache is None:
self._config_cache = self._build_config()
return self._config_cache
def invalidate_config_cache(self):
self._config_cache = None
```
### Lazy Loading
**Load data only when needed:**
```python
@property
def extra_state_attributes(self) -> dict | None:
"""Return attributes."""
# Calculate only when accessed
if self.entity_description.key == "complex_sensor":
return self._calculate_complex_attributes()
return None
```
### Bulk Operations
**Process multiple items at once:**
```python
# ❌ Slow - loop with individual operations
for interval in intervals:
enriched = enrich_single_interval(interval)
results.append(enriched)
# ✅ Fast - bulk processing
results = enrich_intervals_bulk(intervals)
```
### Async Best Practices
**1. Concurrent API calls:**
```python
# ❌ Sequential (slow)
user_data = await fetch_user_data()
price_data = await fetch_price_data()
# ✅ Concurrent (fast)
user_data, price_data = await asyncio.gather(
fetch_user_data(),
fetch_price_data()
)
```
**2. Don't block event loop:**
```python
# ❌ Blocking
result = heavy_computation() # Blocks for seconds
# ✅ Non-blocking
result = await hass.async_add_executor_job(heavy_computation)
```
## Memory Management
### Avoid Memory Leaks
**1. Clear references:**
```python
class Coordinator:
async def async_shutdown(self):
"""Clean up resources."""
self._listeners.clear()
self._data = None
self._cache = None
```
**2. Use weak references for callbacks:**
```python
import weakref
class Manager:
def __init__(self):
self._callbacks: list[weakref.ref] = []
def register(self, callback):
self._callbacks.append(weakref.ref(callback))
```
### Efficient Data Structures
**Use appropriate types:**
```python
# ❌ List for lookups (O(n))
if timestamp in timestamp_list:
...
# ✅ Set for lookups (O(1))
if timestamp in timestamp_set:
...
# ❌ List comprehension with filter
results = [x for x in items if condition(x)]
# ✅ Generator for large datasets
results = (x for x in items if condition(x))
```
## Coordinator Optimization
### Minimize API Calls
**Already implemented:**
- Cache valid until midnight
- User data cached for 24h
- Only poll when tomorrow data expected
**Monitor API usage:**
```python
_LOGGER.debug("API call: %s (cache_age=%s)",
endpoint, cache_age)
```
### Smart Updates
**Only update when needed:**
```python
async def _async_update_data(self) -> dict:
"""Fetch data from API."""
if self._is_cache_valid():
_LOGGER.debug("Using cached data")
return self.data
# Fetch new data
return await self._fetch_data()
```
## Database Impact
### State Class Selection
**Affects long-term statistics storage:**
```python
# ❌ MEASUREMENT for prices (stores every change)
state_class=SensorStateClass.MEASUREMENT # ~35K records/year
# ✅ None for prices (no long-term stats)
state_class=None # Only current state
# ✅ TOTAL for counters only
state_class=SensorStateClass.TOTAL # For cumulative values
```
### Attribute Size
**Keep attributes minimal:**
```python
# ❌ Large nested structures (KB per update)
attributes = {
"all_intervals": [...], # 384 intervals
"full_history": [...], # Days of data
}
# ✅ Essential data only (bytes per update)
attributes = {
"timestamp": "...",
"rating_level": "...",
"next_interval": "...",
}
```
## Testing Performance
### Benchmark Tests
```python
import pytest
import time
@pytest.mark.benchmark
def test_period_calculation_performance(coordinator):
"""Period calculation should complete in &lt;100ms."""
start = time.perf_counter()
periods = calculate_periods(coordinator.data)
duration = time.perf_counter() - start
assert duration < 0.1, f"Too slow: {duration:.3f}s"
```
### Load Testing
```python
@pytest.mark.integration
async def test_multiple_homes_performance(hass):
"""Test with 10 homes."""
coordinators = []
for i in range(10):
coordinator = create_coordinator(hass, home_id=f"home_{i}")
await coordinator.async_refresh()
coordinators.append(coordinator)
# Verify memory usage
# Verify update times
```
## Monitoring in Production
### Log Performance Metrics
```python
@timing
async def _async_update_data(self) -> dict:
"""Fetch data with timing."""
result = await self._fetch_data()
_LOGGER.info("Update completed in %.2fs", timing_duration)
return result
```
### Memory Tracking
```python
import psutil
import os
process = psutil.Process(os.getpid())
memory_mb = process.memory_info().rss / 1024**2
_LOGGER.debug("Current memory usage: %.2f MB", memory_mb)
```
---
💡 **Related:**
- [Caching Strategy](caching-strategy.md) - Cache layers
- [Architecture](architecture.md) - System design
- [Debugging](debugging.md) - Profiling tools

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,380 @@
# Recorder History Optimization
**Status**: ✅ IMPLEMENTED
**Last Updated**: 2025-12-07
## Overview
This document describes the implementation of `_unrecorded_attributes` for Tibber Prices entities to prevent Home Assistant Recorder database bloat by excluding non-essential attributes from historical data storage.
**Reference**: [HA Developer Docs - Excluding State Attributes](https://developers.home-assistant.io/docs/core/entity/#excluding-state-attributes-from-recorder-history)
## Implementation
Both `TibberPricesSensor` and `TibberPricesBinarySensor` implement `_unrecorded_attributes` as a class-level `frozenset` to exclude attributes that don't provide value in historical data analysis.
### Pattern
```python
class TibberPricesSensor(TibberPricesEntity, SensorEntity):
"""tibber_prices Sensor class."""
_unrecorded_attributes = frozenset(
{
"description",
"usage_tips",
# ... more attributes
}
)
```
**Key Points:**
- Must be a **class attribute** (not instance attribute)
- Use `frozenset` for immutability and performance
- Applied automatically by Home Assistant's Recorder component
## Categories of Excluded Attributes
### 1. Descriptions/Help Text
**Attributes:** `description`, `usage_tips`
**Reason:** Static, large text strings (100-500 chars each) that:
- Never change or change very rarely
- Don't provide analytical value in history
- Consume significant database space when recorded every state change
- Can be retrieved from translation files when needed
**Impact:** ~500-1000 bytes saved per state change
### 2. Large Nested Structures
**Attributes:**
- `periods` (binary_sensor) - Array of all period summaries
- `data` (chart_data_export) - Complete price data arrays
- `trend_attributes` - Detailed trend analysis
- `current_trend_attributes` - Current trend details
- `trend_change_attributes` - Trend change analysis
- `volatility_attributes` - Detailed volatility breakdown
**Reason:** Complex nested data structures that are:
- Serialized to JSON for storage (expensive)
- Create large database rows (2-20 KB each)
- Slow down history queries
- Provide limited value in historical analysis (current state usually sufficient)
**Impact:** ~10-30 KB saved per state change for affected sensors
**Example - periods array:**
```json
{
"periods": [
{
"start": "2025-12-07T06:00:00+01:00",
"end": "2025-12-07T08:00:00+01:00",
"duration_minutes": 120,
"price_mean": 18.5,
"price_median": 18.3,
"price_min": 17.2,
"price_max": 19.8
// ... 10+ more attributes × 10-20 periods
}
]
}
```
### 3. Frequently Changing Diagnostics
**Attributes:** `icon_color`, `cache_age`, `cache_validity`, `data_completeness`, `data_status`
**Reason:**
- Change every update cycle (every 15 minutes or more frequently)
- Don't provide long-term analytical value
- Create state changes even when core values haven't changed
- Clutter history with cosmetic changes
- Can be reconstructed from other attributes if needed
**Impact:** Prevents unnecessary state writes when only cosmetic attributes change
**Example:** `icon_color` changes from `#00ff00` to `#ffff00` but price hasn't changed → No state write needed
### 4. Static/Rarely Changing Configuration
**Attributes:** `tomorrow_expected_after`, `level_value`, `rating_value`, `level_id`, `rating_id`, `currency`, `resolution`, `yaxis_min`, `yaxis_max`
**Reason:**
- Configuration values that rarely change
- Wastes space when recorded repeatedly
- Can be derived from other attributes or from entity state
**Impact:** ~100-200 bytes saved per state change
### 5. Temporary/Time-Bound Data
**Attributes:** `timestamp`, `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error`
**Reason:**
- `timestamp` is the rounded-quarter reference time used at the moment of the state write — it's stale as soon as the next update fires and has no analytical value in history
- `next_api_poll`, `next_midnight_turnover` etc. are only relevant at the moment of reading; they're superseded by the next update
- Similar to `entity_picture` in HA core image entities
**Note:** The entity's `native_value` (the actual price/state) is always recorded by HA as the entity state itself — independently of `_unrecorded_attributes`. So excluding `timestamp` does not create a gap in the time-series; the state row already carries the recording timestamp.
**Impact:** ~200-400 bytes saved per state change
**Example:** `next_api_poll: "2025-12-07T14:30:00"` stored at 14:15 is useless when viewing history at 15:00
### 6. Relaxation Details
**Attributes:** `relaxation_level`, `relaxation_threshold_original_%`, `relaxation_threshold_applied_%`
**Reason:**
- Detailed technical information not needed for historical analysis
- Only useful for debugging during active development
- Boolean `relaxation_active` is kept for high-level analysis
**Impact:** ~50-100 bytes saved per state change
### 7. Redundant/Derived Data
**Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `period_count_total`, `period_count_remaining`
**Reason:**
- Can be calculated from other attributes
- Redundant information
- Doesn't add analytical value to history
**Impact:** ~100-200 bytes saved per state change
**Example:** `price_spread = price_max - price_min` (both are recorded, so spread can be calculated). `period_count_remaining = period_count_total - period_position` (both components are recorded).
## Attributes That ARE Recorded
These attributes **remain in history** because they provide essential analytical value:
### Time-Series Core
- All price values - Core sensor states (the entity's `native_value` is always recorded separately)
### Diagnostics & Tracking
- `cache_age_minutes` - Numeric value for diagnostics tracking over time
- `updates_today` - Tracking API usage patterns
### Data Completeness
- `interval_count`, `intervals_available` - Data completeness metrics
- `yesterday_available`, `today_available`, `tomorrow_available` - Boolean status
### Period Data
- `start`, `end`, `duration_minutes` - Core period timing
- `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics
- `period_position` - Position of current period in the day's sequence
- `period_count_today`, `period_count_tomorrow` - How many periods per day (useful in automations)
### High-Level Status
- `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation)
## Expected Database Impact
### Space Savings
**Per state change:**
- Before: ~3-8 KB average
- After: ~0.5-1.5 KB average
- **Reduction: 60-85%**
**Daily per sensor:**
| Sensor Type | Updates/Day | Before | After | Savings |
|------------|-------------|--------|-------|---------|
| High-frequency (15min) | 96 | ~290 KB | ~140 KB | 50% |
| Low-frequency (6h) | 4 | ~32 KB | ~6 KB | 80% |
### Most Impactful Exclusions
1. **`periods` array** (binary_sensor) - Saves 2-5 KB per state
2. **`data`** (chart_data_export) - Saves 5-20 KB per state
3. **`trend_attributes`** - Saves 1-2 KB per state
4. **`description`/`usage_tips`** - Saves 500-1000 bytes per state
5. **`icon_color`** - Prevents unnecessary state changes
### Real-World Impact
For a typical installation with:
- 80+ sensors
- Updates every 15 minutes
- ~10 sensors updating every minute
**Before:** ~1.5 GB per month
**After:** ~400-500 MB per month
**Savings:** ~1 GB per month (~66% reduction)
## Implementation Files
- **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py`
- Class: `TibberPricesSensor`
- 46 attributes excluded
- **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py`
- Class: `TibberPricesBinarySensor`
- 29 attributes excluded
## When to Update \_unrecorded_attributes
### Add to Exclusion List When:
✅ Adding new **description/help text** attributes
✅ Adding **large nested structures** (arrays, complex objects)
✅ Adding **frequently changing diagnostic info** (colors, formatted strings)
✅ Adding **temporary/time-bound data** (timestamps that become stale)
✅ Adding **redundant/derived calculations**
### Keep in History When:
**Core price/timing data** needed for analysis
**Boolean status flags** that show state transitions
**Numeric counters** useful for tracking patterns
**Data that helps understand system behavior** over time
## Decision Framework
When adding a new attribute, ask:
1. **Will this be useful in history queries 1 week from now?**
- No → Exclude
- Yes → Keep
2. **Can this be calculated from other recorded attributes?**
- Yes → Exclude
- No → Keep
3. **Is this primarily for current UI display?**
- Yes → Exclude
- No → Keep
4. **Does this change frequently without indicating state change?**
- Yes → Exclude
- No → Keep
5. **Is this larger than 100 bytes and not essential for analysis?**
- Yes → Exclude
- No → Keep
## Testing
After modifying `_unrecorded_attributes`:
1. **Restart Home Assistant** to apply changes
2. **Check Recorder database size** before/after
3. **Verify essential attributes** still appear in history
4. **Confirm excluded attributes** don't appear in new state writes
**SQL Query to check attribute presence:**
```sql
SELECT
state_id,
attributes
FROM states
WHERE entity_id = 'sensor.tibber_home_current_interval_price'
ORDER BY last_updated DESC
LIMIT 5;
```
## Maintenance Notes
- ✅ Must be a **class attribute** (instance attributes are ignored)
- ✅ Use `frozenset` for immutability
- ✅ Only affects **new** state writes (doesn't purge existing history)
- ✅ Attributes still available via `entity.attributes` in templates/automations
- ✅ Only prevents **storage** in Recorder, not runtime availability
## Long-Term Statistics Optimization (`state_class`)
**This is a second, independent mechanism** that controls writes to the HA `statistics` and `statistics_short_term` tables — distinct from `_unrecorded_attributes`, which only affects the `state_attributes` table.
### Why This Matters
- The `state_attributes` table (controlled by `_unrecorded_attributes`) is **auto-purged** after ~10 days
- The `statistics`/`statistics_short_term` tables (controlled by `state_class`) are **never auto-purged** — they grow unbounded
This makes `state_class=TOTAL` on many sensors the primary cause of long-term database bloat for users with long-running installations.
### HA Constraint: MONETARY Device Class
For sensors with `device_class=SensorDeviceClass.MONETARY`, only two `state_class` values are valid:
| `state_class` | Statistics written | Frontend effect |
| ------------- | ------------------------- | ------------------------------------------------- |
| `TOTAL` | ✅ Yes — unbounded growth | Statistics line-chart on entity detail page |
| `None` | ❌ No | States timeline only (History panel, "Show More") |
| `MEASUREMENT` | ❌ Blocked by hassfest | — |
`MEASUREMENT` causes a hassfest validation error for MONETARY sensors, leaving only `TOTAL` or `None`.
### Implementation Decision
Only 3 of 26 MONETARY sensors keep `state_class=TOTAL` — those where long-term history is genuinely useful:
| Sensor | Reason |
| ----------------------------- | ------------------------------------ |
| `current_interval_price` | Long-term price trend (weeks/months) |
| `current_interval_price_base` | Required for Energy Dashboard |
| `average_price_today` | Seasonal daily average tracking |
All other 23 MONETARY sensors use `state_class=None`:
- Forecast/future sensors (`next_avg_*h`)
- Daily snapshots (`lowest/highest_price_today/tomorrow`)
- Rolling windows (`trailing/leading_24h_*`)
- Next/previous interval sensors
**Effect of `state_class=None`:**
- ✅ Short-term state history (States timeline, ~10 days) still works normally
- ✅ Templates, automations, and attributes are unaffected
- ❌ Statistics line-chart removed from entity detail page for these sensors
- ❌ No writes to `statistics`/`statistics_short_term` tables
### Expected Impact
Going from 26 → 3 sensors writing to the statistics tables:
- **~88% reduction** in statistics table writes
- Prevents the primary cause of long-term database bloat
- Existing statistics data is retained (only new writes stop)
### Relationship to `_unrecorded_attributes`
These are two independent mechanisms targeting different tables:
| Mechanism | Table affected | Purged? | Controls |
| ------------------------ | ------------------------------------- | ----------- | ----------------------------------------------- |
| `_unrecorded_attributes` | `state_attributes` | ✅ ~10 days | Which attributes are stored per state write |
| `state_class=None` | `statistics`, `statistics_short_term` | ❌ Never | Whether long-term statistics are written at all |
Both optimizations work together. `_unrecorded_attributes` reduces the size of each state write; `state_class=None` eliminates an entire category of unbounded writes.
### Implementation File
- **`custom_components/tibber_prices/sensor/definitions.py`** — `state_class` set per-sensor in `SensorEntityDescription`
## References
- [HA Developer Docs - Excluding State Attributes](https://developers.home-assistant.io/docs/core/entity/#excluding-state-attributes-from-recorder-history)
- Implementation PR: [Link when merged]
- Related Issue: [Link if applicable]

View file

@ -0,0 +1,414 @@
# Refactoring Guide
This guide explains how to plan and execute major refactorings in this project.
## When to Plan a Refactoring
Not every code change needs a detailed plan. Create a refactoring plan when:
🔴 **Major changes requiring planning:**
- Splitting modules into packages (>5 files affected, >500 lines moved)
- Architectural changes (new packages, module restructuring)
- Breaking changes (API changes, config format migrations)
🟡 **Medium changes that might benefit from planning:**
- Complex features with multiple moving parts
- Changes affecting many files (>3 files, unclear best approach)
- Refactorings with unclear scope
🟢 **Small changes - no planning needed:**
- Bug fixes (straightforward, `<`100 lines)
- Small features (`<`3 files, clear approach)
- Documentation updates
- Cosmetic changes (formatting, renaming)
## The Planning Process
### 1. Create a Planning Document
Create a file in the `planning/` directory (git-ignored for free iteration):
```bash
# Example:
touch planning/my-feature-refactoring-plan.md
```
**Note:** The `planning/` directory is git-ignored, so you can iterate freely without polluting git history.
### 2. Use the Planning Template
Every planning document should include:
```markdown
# <Feature> Refactoring Plan
**Status**: 🔄 PLANNING | 🚧 IN PROGRESS | ✅ COMPLETED | ❌ CANCELLED
**Created**: YYYY-MM-DD
**Last Updated**: YYYY-MM-DD
## Problem Statement
- What's the issue?
- Why does it need fixing?
- Current pain points
## Proposed Solution
- High-level approach
- File structure (before/after)
- Module responsibilities
## Migration Strategy
- Phase-by-phase breakdown
- File lifecycle (CREATE/MODIFY/DELETE/RENAME)
- Dependencies between phases
- Testing checkpoints
## Risks & Mitigation
- What could go wrong?
- How to prevent it?
- Rollback strategy
## Success Criteria
- Measurable improvements
- Testing requirements
- Verification steps
```
See `planning/README.md` for detailed template explanation.
### 3. Iterate Freely
Since `planning/` is git-ignored:
- Draft multiple versions
- Get AI assistance without commit pressure
- Refine until the plan is solid
- No need to clean up intermediate versions
### 4. Implementation Phase
Once plan is approved:
- Follow the phases defined in the plan
- Test after each phase (don't skip!)
- Update plan if issues discovered
- Track progress through phase status
### 5. After Completion
**Option A: Archive in docs/development/**
If the plan has lasting value (successful pattern, reusable approach):
```bash
mv planning/my-feature-refactoring-plan.md docs/development/
git add docs/development/my-feature-refactoring-plan.md
git commit -m "docs: archive successful refactoring plan"
```
**Option B: Delete**
If the plan served its purpose and code is the source of truth:
```bash
rm planning/my-feature-refactoring-plan.md
```
**Option C: Keep locally (not committed)**
For "why we didn't do X" reference:
```bash
mkdir -p planning/archive
mv planning/my-feature-refactoring-plan.md planning/archive/
# Still git-ignored, just organized
```
## Real-World Example
The **sensor/ package refactoring** (Nov 2025) is a successful example:
**Before:**
- `sensor.py` - 2,574 lines, hard to navigate
**After:**
- `sensor/` package with 5 focused modules
- Each module `<`800 lines
- Clear separation of concerns
**Process:**
1. Created `planning/module-splitting-plan.md` (now in `docs/development/`)
2. Defined 6 phases with clear file lifecycle
3. Implemented phase by phase
4. Tested after each phase
5. Documented in AGENTS.md
6. Moved plan to `docs/development/` as reference
**Key learnings:**
- Temporary `_impl.py` files avoid Python package conflicts
- Test after EVERY phase (don't accumulate changes)
- Clear file lifecycle (CREATE/MODIFY/DELETE/RENAME)
- Phase-by-phase approach enables safe rollback
**Note:** The complete module splitting plan was documented during implementation but has been superseded by the actual code structure.
## Phase-by-Phase Implementation
### Why Phases Matter
Breaking refactorings into phases:
- ✅ Enables testing after each change (catch bugs early)
- ✅ Allows rollback to last good state
- ✅ Makes progress visible
- ✅ Reduces cognitive load (focus on one thing)
- ❌ Takes more time (but worth it!)
### Phase Structure
Each phase should:
1. **Have clear goal** - What's being changed?
2. **Document file lifecycle** - CREATE/MODIFY/DELETE/RENAME
3. **Define success criteria** - How to verify it worked?
4. **Include testing steps** - What to test?
5. **Estimate time** - Realistic time budget
### Example Phase Documentation
```markdown
### Phase 3: Extract Helper Functions (Session 3)
**Goal**: Move pure utility functions to helpers.py
**File Lifecycle**:
- ✨ CREATE `sensor/helpers.py` (utility functions)
- ✏️ MODIFY `sensor/core.py` (import from helpers.py)
**Steps**:
1. Create sensor/helpers.py
2. Move pure functions (no state, no self)
3. Add comprehensive docstrings
4. Update imports in core.py
**Estimated time**: 45 minutes
**Success criteria**:
- ✅ All pure functions moved
- ✅ `./scripts/lint-check` passes
- ✅ HA starts successfully
- ✅ All entities work correctly
```
## Testing Strategy
### After Each Phase
Minimum testing checklist:
```bash
# 1. Linting passes
./scripts/lint-check
# 2. Home Assistant starts
./scripts/develop
# Watch for startup errors in logs
# 3. Integration loads
# Check: Settings → Devices & Services → Tibber Prices
# Verify: All entities appear
# 4. Basic functionality
# Test: Data updates without errors
# Check: Entity states update correctly
```
### Comprehensive Testing (Final Phase)
After completing all phases:
- Test all entities (sensors, binary sensors)
- Test configuration flow (add/modify/remove)
- Test options flow (change settings)
- Test services (custom service calls)
- Test error handling (disconnect API, invalid data)
- Test caching (restart HA, verify cache loads)
- Test time-based updates (quarter-hour refresh)
## Common Pitfalls
### ❌ Skip Planning for Large Changes
**Problem:** "This seems straightforward, I'll just start coding..."
**Result:** Halfway through, realize the approach doesn't work. Wasted time.
**Solution:** If unsure, spend 30 minutes on a rough plan. Better to plan and discard than get stuck.
### ❌ Implement All Phases at Once
**Problem:** "I'll do all phases, then test everything..."
**Result:** 10+ files changed, 2000+ lines modified, hard to debug if something breaks.
**Solution:** Test after EVERY phase. Commit after each successful phase.
### ❌ Forget to Update Documentation
**Problem:** Code is refactored, but AGENTS.md and docs/ still reference old structure.
**Result:** AI/humans get confused by outdated documentation.
**Solution:** Include "Documentation Phase" at the end of every refactoring plan.
### ❌ Ignore the Planning Directory
**Problem:** "I'll just create the plan in docs/ directly..."
**Result:** Git history polluted with draft iterations, or pressure to "commit something" too early.
**Solution:** Always use `planning/` for work-in-progress. Move to `docs/` only when done.
## Integration with AI Development
This project uses AI heavily (GitHub Copilot, Claude). The planning process supports AI development:
**AI reads from:**
- `AGENTS.md` - Long-term memory, patterns, conventions (AI-focused)
- `docs/development/` - Human-readable guides (human-focused)
- `planning/` - Active refactoring plans (shared context)
**AI updates:**
- `AGENTS.md` - When patterns change
- `planning/*.md` - During refactoring implementation
- `docs/development/` - After successful completion
**Why separate AGENTS.md and docs/development/?**
- `AGENTS.md`: Technical, comprehensive, AI-optimized
- `docs/development/`: Practical, focused, human-optimized
- Both stay in sync but serve different audiences
See [AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md) section "Planning Major Refactorings" for AI-specific guidance.
## Tools and Resources
### Planning Directory
- `planning/` - Git-ignored workspace for drafts
- `planning/README.md` - Detailed planning documentation
- `planning/*.md` - Active refactoring plans
### Example Plans
- `docs/development/module-splitting-plan.md` - ✅ Completed, archived
- `planning/config-flow-refactoring-plan.md` - 🔄 Planned (1013 lines → 4 modules)
- `planning/binary-sensor-refactoring-plan.md` - 🔄 Planned (644 lines → 4 modules)
- `planning/coordinator-refactoring-plan.md` - 🔄 Planned (1446 lines, high complexity)
### Helper Scripts
```bash
./scripts/lint-check # Verify code quality
./scripts/develop # Start HA for testing
./scripts/lint # Auto-fix issues
```
## FAQ
### Q: When should I create a plan vs. just start coding?
**A:** If you're asking this question, you probably need a plan. 😊
Simple rule: If you can't describe the entire change in 3 sentences, create a plan.
### Q: How detailed should the plan be?
**A:** Detailed enough to execute without major surprises, but not a line-by-line script.
Good plan level:
- Lists all files affected (CREATE/MODIFY/DELETE)
- Defines phases with clear boundaries
- Includes testing strategy
- Estimates time per phase
Too detailed:
- Exact code snippets for every change
- Line-by-line instructions
Too vague:
- "Refactor sensor.py to be better"
- No phase breakdown
- No testing strategy
### Q: What if the plan changes during implementation?
**A:** Update the plan! Planning documents are living documents.
If you discover:
- Better approach → Update "Proposed Solution"
- More phases needed → Add to "Migration Strategy"
- New risks → Update "Risks & Mitigation"
Document WHY the plan changed (helps future refactorings).
### Q: Should every refactoring follow this process?
**A:** No! Use judgment:
- **Small changes (`<`100 lines, clear approach)**: Just do it, no plan needed
- **Medium changes (unclear scope)**: Write rough outline, refine if needed
- **Large changes (>500 lines, >5 files)**: Full planning process
### Q: How do I know when a refactoring is successful?
**A:** Check the "Success Criteria" from your plan:
Typical criteria:
- ✅ All linting checks pass
- ✅ HA starts without errors
- ✅ All entities functional
- ✅ No regressions (existing features work)
- ✅ Code easier to understand/modify
- ✅ Documentation updated
If you can't tick all boxes, the refactoring isn't done.
## Summary
**Key takeaways:**
1. **Plan when scope is unclear** (>500 lines, >5 files, breaking changes)
2. **Use planning/ directory** for free iteration (git-ignored)
3. **Work in phases** and test after each phase
4. **Document file lifecycle** (CREATE/MODIFY/DELETE/RENAME)
5. **Update documentation** after completion (AGENTS.md, docs/)
6. **Archive or delete** plan after implementation
**Remember:** Good planning prevents half-finished refactorings and makes rollback easier when things go wrong.
---
**Next steps:**
- Read `planning/README.md` for detailed template
- Check `docs/development/module-splitting-plan.md` for real example
- Browse `planning/` for active refactoring plans

View file

@ -0,0 +1,377 @@
---
comments: false
---
# Release Notes Generation
This project supports **three ways** to generate release notes from conventional commits, plus **automatic version management**.
## 🚀 Quick Start: Preparing a Release
**Recommended workflow (automatic & foolproof):**
```bash
# 1. Use the helper script to prepare release
./scripts/release/prepare 0.3.0
# This will:
# - Update manifest.json version to 0.3.0
# - Create commit: "chore(release): bump version to 0.3.0"
# - Create tag: v0.3.0
# - Show you what will be pushed
# 2. Review and push when ready
git push origin main v0.3.0
# 3. CI/CD automatically:
# - Detects the new tag
# - Generates release notes (excluding version bump commit)
# - Creates GitHub release
```
**If you forget to bump manifest.json:**
```bash
# Just edit manifest.json manually and commit
vim custom_components/tibber_prices/manifest.json # "version": "0.3.0"
git commit -am "chore(release): bump version to 0.3.0"
git push
# Auto-Tag workflow detects manifest.json change and creates tag automatically!
# Then Release workflow kicks in and creates the GitHub release
```
---
## 📋 Release Options
### 1. GitHub UI Button (Easiest)
Use GitHub's built-in release notes generator:
1. Go to [Releases](https://github.com/jpawlowski/hass.tibber_prices/releases)
2. Click "Draft a new release"
3. Select your tag
4. Click "Generate release notes" button
5. Edit if needed and publish
**Uses:** `.github/release.yml` configuration
**Best for:** Quick releases, works with PRs that have labels
**Note:** Direct commits appear in "Other Changes" category
---
### 2. Local Script (Intelligent)
Run `./scripts/release/generate-notes` to parse conventional commits locally.
**Automatic backend detection:**
```bash
# Generate from latest tag to HEAD
./scripts/release/generate-notes
# Generate between specific tags
./scripts/release/generate-notes v1.0.0 v1.1.0
# Generate from tag to HEAD
./scripts/release/generate-notes v1.0.0 HEAD
```
**Force specific backend:**
```bash
# Use AI (GitHub Copilot CLI)
RELEASE_NOTES_BACKEND=copilot ./scripts/release/generate-notes
# Use git-cliff (template-based)
RELEASE_NOTES_BACKEND=git-cliff ./scripts/release/generate-notes
# Use manual parsing (grep/awk fallback)
RELEASE_NOTES_BACKEND=manual ./scripts/release/generate-notes
```
**Disable AI** (useful for CI/CD):
```bash
USE_AI=false ./scripts/release/generate-notes
```
#### Backend Priority
The script automatically selects the best available backend:
1. **GitHub Copilot CLI** - AI-powered, context-aware (best quality)
2. **git-cliff** - Fast Rust tool with templates (reliable)
3. **Manual** - Simple grep/awk parsing (always works)
In CI/CD (`$CI` or `$GITHUB_ACTIONS`), AI is automatically disabled.
#### Installing Optional Backends
**In DevContainer (automatic):**
git-cliff is automatically installed when the DevContainer is built:
- **Rust toolchain**: Installed via `ghcr.io/devcontainers/features/rust:1` (minimal profile)
- **git-cliff**: Installed via cargo in `scripts/setup/setup`
Simply rebuild the container (VS Code: "Dev Containers: Rebuild Container") and git-cliff will be available.
**Manual installation (outside DevContainer):**
**git-cliff** (template-based):
```bash
# See: https://git-cliff.org/docs/installation
# macOS
brew install git-cliff
# Cargo (all platforms)
cargo install git-cliff
# Manual binary download
wget https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-x86_64-unknown-linux-gnu.tar.gz
tar -xzf git-cliff-*.tar.gz
sudo mv git-cliff-*/git-cliff /usr/local/bin/
```
---
### 3. CI/CD Automation
Automatic release notes on tag push.
**Workflow:** `.github/workflows/release.yml`
**Triggers:** Version tags (`v1.0.0`, `v2.1.3`, etc.)
```bash
# Create and push a tag to trigger automatic release
git tag v1.0.0
git push origin v1.0.0
# GitHub Actions will:
# 1. Detect the new tag
# 2. Generate release notes using git-cliff
# 3. Create a GitHub release automatically
```
**Backend:** Uses `git-cliff` (AI disabled in CI for reliability)
---
## 📝 Output Format
All methods produce GitHub-flavored Markdown with emoji categories:
```markdown
## 🎉 New Features
- **scope**: Description ([abc1234](link-to-commit))
## 🐛 Bug Fixes
- **scope**: Description ([def5678](link-to-commit))
## 📚 Documentation
- **scope**: Description ([ghi9012](link-to-commit))
## 🔧 Maintenance & Refactoring
- **scope**: Description ([jkl3456](link-to-commit))
## 🧪 Testing
- **scope**: Description ([mno7890](link-to-commit))
```
---
## 🎯 When to Use Which
| Method | Use Case | Pros | Cons |
| --------------------- | --------------------- | ----------------------------- | ------------------------ |
| **Helper Script** | Normal releases | Foolproof, automatic | Requires script |
| **Auto-Tag Workflow** | Forgot script | Safety net, automatic tagging | Still need manifest bump |
| **GitHub Button** | Manual quick release | Easy, no script | Limited categorization |
| **Local Script** | Testing release notes | Preview before release | Manual process |
| **CI/CD** | After tag push | Fully automatic | Needs tag first |
---
## 🔄 Complete Release Workflows
### Workflow A: Using Helper Script (Recommended)
```bash
# Step 1: Prepare release (all-in-one)
./scripts/release/prepare 0.3.0
# Step 2: Review changes
git log -1 --stat
git show v0.3.0
# Step 3: Push when ready
git push origin main v0.3.0
# Done! CI/CD creates the release automatically
```
**What happens:**
1. Script bumps manifest.json → commits → creates tag locally
2. You push commit + tag together
3. Release workflow sees tag → generates notes → creates release
---
### Workflow B: Manual (with Auto-Tag Safety Net)
```bash
# Step 1: Bump version manually
vim custom_components/tibber_prices/manifest.json
# Change: "version": "0.3.0"
# Step 2: Commit
git commit -am "chore(release): bump version to 0.3.0"
git push
# Step 3: Wait for Auto-Tag workflow
# GitHub Actions automatically creates v0.3.0 tag
# Then Release workflow creates the release
```
**What happens:**
1. You push manifest.json change
2. Auto-Tag workflow detects change → creates tag automatically
3. Release workflow sees new tag → creates release
---
### Workflow C: Manual Tag (Old Way)
```bash
# Step 1: Bump version
vim custom_components/tibber_prices/manifest.json
git commit -am "chore(release): bump version to 0.3.0"
# Step 2: Create tag manually
git tag v0.3.0
git push origin main v0.3.0
# Release workflow creates release
```
**What happens:**
1. You create and push tag manually
2. Release workflow creates release
3. Auto-Tag workflow skips (tag already exists)
---
## ⚙️ Configuration Files
- `scripts/release/prepare` - Helper script to bump version + create tag
- `.github/workflows/auto-tag.yml` - Automatic tag creation on manifest.json change
- `.github/workflows/release.yml` - Automatic release on tag push
- `.github/release.yml` - GitHub UI button configuration
- `cliff.toml` - git-cliff template (filters out version bumps)
---
## 🛡️ Safety Features
### 1. **Version Validation**
Both helper script and auto-tag workflow validate version format (X.Y.Z).
### 2. **No Duplicate Tags**
- Helper script checks if tag exists (local + remote)
- Auto-tag workflow checks if tag exists before creating
### 3. **Atomic Operations**
Helper script creates commit + tag locally. You decide when to push.
### 4. **Version Bumps Filtered**
Release notes automatically exclude `chore(release): bump version` commits.
### 5. **Rollback Instructions**
Helper script shows how to undo if you change your mind.
---
## 🐛 Troubleshooting
**"Tag already exists" error:**
```bash
# Local tag
git tag -d v0.3.0
# Remote tag (only if you need to recreate)
git push origin :refs/tags/v0.3.0
```
**Manifest version doesn't match tag:**
This shouldn't happen with the new workflows, but if it does:
```bash
# 1. Fix manifest.json
vim custom_components/tibber_prices/manifest.json
# 2. Amend the commit
git commit --amend -am "chore(release): bump version to 0.3.0"
# 3. Move the tag
git tag -f v0.3.0
git push -f origin main v0.3.0
```
**Auto-tag didn't create tag:**
Check workflow runs in GitHub Actions. Common causes:
- Tag already exists remotely
- Invalid version format in manifest.json
- manifest.json not in the commit that was pushed
---
## 🔍 Format Requirements
**HACS:** No specific format required, uses GitHub releases as-is
**Home Assistant:** No specific format required for custom integrations
**Markdown:** Standard GitHub-flavored Markdown supported
**HTML:** Can include `<ha-alert>` tags if needed
---
## 💡 Tips
1. **Conventional Commits:** Use proper commit format for best results:
```
feat(scope): Add new feature
Detailed description of what changed.
Impact: Users can now do X and Y.
```
2. **Impact Section:** Add `Impact:` in commit body for user-friendly descriptions
3. **Test Locally:** Run `./scripts/release/generate-notes` before creating release
4. **AI vs Template:** GitHub Copilot CLI provides better descriptions, git-cliff is faster and more reliable
5. **CI/CD:** Tag push triggers automatic release - no manual intervention needed

View file

@ -0,0 +1,352 @@
# Repairs System
The Tibber Prices integration includes a proactive repair notification system that alerts users to important issues requiring attention. This system leverages Home Assistant's built-in `issue_registry` to create user-facing notifications in the UI.
## Overview
The repairs system is implemented in `coordinator/repairs.py` via the `TibberPricesRepairManager` class, which is instantiated in the coordinator and integrated into the update cycle.
**Design Principles:**
- **Proactive**: Detect issues before they become critical
- **User-friendly**: Clear explanations with actionable guidance
- **Auto-clearing**: Repairs automatically disappear when conditions resolve
- **Non-blocking**: Integration continues to work even with active repairs
## Implemented Repair Types
### 1. Tomorrow Data Missing
**Issue ID:** `tomorrow_data_missing_{entry_id}`
**When triggered:**
- Current time is after 18:00 (configurable via `TOMORROW_DATA_WARNING_HOUR`)
- Tomorrow's electricity price data is still not available
**When cleared:**
- Tomorrow's data becomes available
- Automatically checks on every successful API update
**User impact:**
Users cannot plan ahead for tomorrow's electricity usage optimization. Automations relying on tomorrow's prices will not work.
**Implementation:**
```python
# In coordinator update cycle
has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"])
await self._repair_manager.check_tomorrow_data_availability(
has_tomorrow_data=has_tomorrow_data,
current_time=current_time,
)
```
**Translation placeholders:**
- `home_name`: Name of the affected home
- `warning_hour`: Hour after which warning appears (default: 18)
### 2. Rate Limit Exceeded
**Issue ID:** `rate_limit_exceeded_{entry_id}`
**When triggered:**
- Integration encounters 3 or more consecutive rate limit errors (HTTP 429)
- Threshold configurable via `RATE_LIMIT_WARNING_THRESHOLD`
**When cleared:**
- Successful API call completes (no rate limit error)
- Error counter resets to 0
**User impact:**
API requests are being throttled, causing stale data. Updates may be delayed until rate limit expires.
**Implementation:**
```python
# In error handler
is_rate_limit = (
"429" in error_str
or "rate limit" in error_str
or "too many requests" in error_str
)
if is_rate_limit:
await self._repair_manager.track_rate_limit_error()
# On successful update
await self._repair_manager.clear_rate_limit_tracking()
```
**Translation placeholders:**
- `home_name`: Name of the affected home
- `error_count`: Number of consecutive rate limit errors
### 3. Home Not Found
**Issue ID:** `home_not_found_{entry_id}`
**When triggered:**
- Home configured in this integration is no longer present in Tibber account
- Detected during user data refresh (daily check)
**When cleared:**
- Home reappears in Tibber account (unlikely - manual cleanup expected)
- Integration entry is removed (shutdown cleanup)
**User impact:**
Integration cannot fetch data for a non-existent home. User must remove the config entry and re-add if needed.
**Implementation:**
```python
# After user data update
home_exists = self._data_fetcher._check_home_exists(home_id)
if not home_exists:
await self._repair_manager.create_home_not_found_repair()
else:
await self._repair_manager.clear_home_not_found_repair()
```
**Translation placeholders:**
- `home_name`: Name of the missing home
- `entry_id`: Config entry ID for reference
## Configuration Constants
Defined in `coordinator/constants.py`:
```python
TOMORROW_DATA_WARNING_HOUR = 18 # Hour after which to warn about missing tomorrow data
RATE_LIMIT_WARNING_THRESHOLD = 3 # Number of consecutive errors before creating repair
```
## Architecture
### Class Structure
```python
class TibberPricesRepairManager:
"""Manages repair issues for a single Tibber home."""
def __init__(
self,
hass: HomeAssistant,
entry_id: str,
home_name: str,
) -> None:
"""Initialize repair manager."""
self._hass = hass
self._entry_id = entry_id
self._home_name = home_name
# State tracking
self._tomorrow_data_repair_active = False
self._rate_limit_error_count = 0
self._rate_limit_repair_active = False
self._home_not_found_repair_active = False
```
### State Tracking
Each repair type maintains internal state to avoid redundant operations:
- **`_tomorrow_data_repair_active`**: Boolean flag, prevents creating duplicate repairs
- **`_rate_limit_error_count`**: Integer counter, tracks consecutive errors
- **`_rate_limit_repair_active`**: Boolean flag, tracks repair status
- **`_home_not_found_repair_active`**: Boolean flag, one-time repair (manual cleanup)
### Lifecycle Integration
**Coordinator Initialization:**
```python
self._repair_manager = TibberPricesRepairManager(
hass=hass,
entry_id=self.config_entry.entry_id,
home_name=self._home_name,
)
```
**Update Cycle Integration:**
```python
# Success path - check conditions
if result and "priceInfo" in result:
has_tomorrow_data = self._data_fetcher.has_tomorrow_data(result["priceInfo"])
await self._repair_manager.check_tomorrow_data_availability(
has_tomorrow_data=has_tomorrow_data,
current_time=current_time,
)
await self._repair_manager.clear_rate_limit_tracking()
# Error path - track rate limits
if is_rate_limit:
await self._repair_manager.track_rate_limit_error()
```
**Shutdown Cleanup:**
```python
async def async_shutdown(self) -> None:
"""Shut down coordinator and clean up."""
await self._repair_manager.clear_all_repairs()
# ... other cleanup ...
```
## Translation System
Repairs use Home Assistant's standard translation system. Translations are defined in:
- `/translations/en.json`
- `/translations/de.json`
- `/translations/nb.json`
- `/translations/nl.json`
- `/translations/sv.json`
**Structure:**
```json
{
"issues": {
"tomorrow_data_missing": {
"title": "Tomorrow's price data missing for {home_name}",
"description": "Detailed explanation with multiple paragraphs...\n\nPossible causes:\n- Cause 1\n- Cause 2"
}
}
}
```
## Home Assistant Integration
Repairs appear in:
- **Settings → System → Repairs** (main repairs panel)
- **Notifications** (bell icon in UI shows repair count)
Repair properties:
- **`is_fixable=False`**: No automated fix available (user action required)
- **`severity=IssueSeverity.WARNING`**: Yellow warning level (not critical)
- **`translation_key`**: References `issues.{key}` in translation files
## Testing Repairs
### Tomorrow Data Missing
1. Wait until after 18:00 local time
2. Ensure integration has no tomorrow price data
3. Repair should appear in UI
4. When tomorrow data arrives (next API fetch), repair clears
**Manual trigger:**
```python
# Temporarily set warning hour to current hour for testing
TOMORROW_DATA_WARNING_HOUR = datetime.now().hour
```
### Rate Limit Exceeded
1. Simulate 3+ consecutive rate limit errors
2. Repair should appear after 3rd error
3. Successful API call clears the repair
**Manual test:**
- Reduce API polling interval to trigger rate limiting
- Or temporarily return HTTP 429 in API client
### Home Not Found
1. Remove home from Tibber account via app/web
2. Wait for user data refresh (daily check)
3. Repair appears indicating home is missing
4. Remove integration entry to clear repair
## Adding New Repair Types
To add a new repair type:
1. **Add constants** (if needed) in `coordinator/constants.py`
2. **Add state tracking** in `TibberPricesRepairManager.__init__`
3. **Implement check method** with create/clear logic
4. **Add translations** to all 5 language files
5. **Integrate into coordinator** update cycle or error handlers
6. **Add cleanup** to `clear_all_repairs()` method
7. **Document** in this file
**Example template:**
```python
async def check_new_condition(self, *, param: bool) -> None:
"""Check new condition and create/clear repair."""
should_warn = param # Your condition logic
if should_warn and not self._new_repair_active:
await self._create_new_repair()
elif not should_warn and self._new_repair_active:
await self._clear_new_repair()
async def _create_new_repair(self) -> None:
"""Create new repair issue."""
_LOGGER.warning("New issue detected - creating repair")
ir.async_create_issue(
self._hass,
DOMAIN,
f"new_issue_{self._entry_id}",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="new_issue",
translation_placeholders={
"home_name": self._home_name,
},
)
self._new_repair_active = True
async def _clear_new_repair(self) -> None:
"""Clear new repair issue."""
_LOGGER.debug("New issue resolved - clearing repair")
ir.async_delete_issue(
self._hass,
DOMAIN,
f"new_issue_{self._entry_id}",
)
self._new_repair_active = False
```
## Best Practices
1. **Always use state tracking** - Prevents duplicate repair creation
2. **Auto-clear when resolved** - Improves user experience
3. **Clear on shutdown** - Prevents orphaned repairs
4. **Use descriptive issue IDs** - Include entry_id for multi-home setups
5. **Provide actionable guidance** - Tell users what they can do
6. **Use appropriate severity** - WARNING for most cases, ERROR only for critical
7. **Test all language translations** - Ensure placeholders work correctly
8. **Document expected behavior** - What triggers, what clears, what user should do
## Future Enhancements
Potential additions to the repairs system:
- **Stale data warning**: Alert when cache is >24 hours old with no API updates
- **Missing permissions**: Detect insufficient API token scopes
- **Config migration needed**: Notify users of breaking changes requiring reconfiguration
- **Extreme price alert**: Warn when prices exceed historical thresholds (optional, user-configurable)
## References
- Home Assistant Repairs Documentation: https://developers.home-assistant.io/docs/core/platform/repairs
- Issue Registry API: `homeassistant.helpers.issue_registry`
- Integration Constants: `custom_components/tibber_prices/const.py`
- Repair Manager Implementation: `custom_components/tibber_prices/coordinator/repairs.py`

View file

@ -0,0 +1,57 @@
# Development Setup
> **Note:** This guide is under construction. For now, please refer to [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md) for detailed setup information.
## Prerequisites
- VS Code with Dev Container support
- Docker installed and running
- GitHub account (for Tibber API token)
## Quick Setup
```bash
# Clone the repository
git clone https://github.com/jpawlowski/hass.tibber_prices.git
cd hass.tibber_prices
# Open in VS Code
code .
# Reopen in DevContainer (VS Code will prompt)
# Or manually: Ctrl+Shift+P → "Dev Containers: Reopen in Container"
```
## Development Environment
The DevContainer includes:
- Python 3.13 with `.venv` at `/home/vscode/.venv/`
- `uv` package manager (fast, modern Python tooling)
- Home Assistant development dependencies
- Ruff linter/formatter
- Git, GitHub CLI, Node.js, Rust toolchain
## Running the Integration
```bash
# Start Home Assistant in debug mode
./scripts/develop
```
Visit http://localhost:8123
## Making Changes
```bash
# Lint and format code
./scripts/lint
# Check-only (CI mode)
./scripts/lint-check
# Validate integration structure
./scripts/release/hassfest
```
See [`AGENTS.md`](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md) for detailed patterns and conventions.

View file

@ -0,0 +1,52 @@
# Testing
> **Note:** This guide is under construction.
## Integration Validation
Before running tests or committing changes, validate the integration structure:
```bash
# Run local validation (JSON syntax, Python syntax, required files)
./scripts/release/hassfest
```
This lightweight script checks:
- ✓ `config_flow.py` exists
- ✓ `manifest.json` is valid JSON with required fields
- ✓ Translation files have valid JSON syntax
- ✓ All Python files compile without syntax errors
**Note:** Full hassfest validation runs in GitHub Actions on push.
## Running Tests
```bash
# Run all tests
pytest tests/
# Run specific test file
pytest tests/test_coordinator.py
# Run with coverage
pytest --cov=custom_components.tibber_prices tests/
```
## Manual Testing
```bash
# Start development environment
./scripts/develop
```
Then test in Home Assistant UI:
- Configuration flow
- Sensor states and attributes
- Services
- Translation strings
## Test Guidelines
Coming soon...

View file

@ -0,0 +1,455 @@
---
comments: false
---
# Timer Architecture
This document explains the timer/scheduler system in the Tibber Prices integration - what runs when, why, and how they coordinate.
## Overview
The integration uses **three independent timer mechanisms** for different purposes:
| Timer | Type | Interval | Purpose | Trigger Method |
| ------------ | ----------- | ------------------ | -------------------- | ------------------------------- |
| **Timer #1** | HA built-in | 15 minutes | API data updates | `DataUpdateCoordinator` |
| **Timer #2** | Custom | :00, :15, :30, :45 | Entity state refresh | `async_track_utc_time_change()` |
| **Timer #3** | Custom | Every minute | Countdown/progress | `async_track_utc_time_change()` |
**Key principle:** Timer #1 (HA) controls **data fetching**, Timer #2 controls **entity updates**, Timer #3 controls **timing displays**.
---
## Timer #1: DataUpdateCoordinator (HA Built-in)
**File:** `coordinator/core.py``TibberPricesDataUpdateCoordinator`
**Type:** Home Assistant's built-in `DataUpdateCoordinator` with `UPDATE_INTERVAL = 15 minutes`
**What it is:**
- HA provides this timer system automatically when you inherit from `DataUpdateCoordinator`
- Triggers `_async_update_data()` method every 15 minutes
- **Not** synchronized to clock boundaries (each installation has different start time)
**Purpose:** Check if fresh API data is needed, fetch if necessary
**What it does:**
```python
async def _async_update_data(self) -> TibberPricesData:
# Step 1: Check midnight turnover FIRST (prevents race with Timer #2)
if self._check_midnight_turnover_needed(dt_util.now()):
await self._perform_midnight_data_rotation(dt_util.now())
# Notify ALL entities after midnight turnover
return self.data # Early return
# Step 2: Check if we need tomorrow data (after 13:00)
if self._should_update_price_data() == "tomorrow_check":
await self._fetch_and_update_data() # Fetch from API
return self.data
# Step 3: Use cached data (fast path - most common)
return self.data
```
**Load Distribution:**
- Each HA installation starts Timer #1 at different times → natural distribution
- Tomorrow data check adds 0-30s random delay → prevents "thundering herd" on Tibber API
- Result: API load spread over ~30 minutes instead of all at once
**Midnight Coordination:**
- Atomic check: `_check_midnight_turnover_needed(now)` compares dates only (no side effects)
- If midnight turnover needed → performs it and returns early
- Timer #2 will see turnover already done and skip gracefully
**Why we use HA's timer:**
- Automatic restart after HA restart
- Built-in retry logic for temporary failures
- Standard HA integration pattern
- Handles backpressure (won't queue up if previous update still running)
---
## Timer #2: Quarter-Hour Refresh (Custom)
**File:** `coordinator/listeners.py``ListenerManager.schedule_quarter_hour_refresh()`
**Type:** Custom timer using `async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)`
**Purpose:** Update time-sensitive entity states at interval boundaries **without waiting for API poll**
**Problem it solves:**
- Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48)
- Current price changes at :00, :15, :30, :45 → entities would show stale data for up to 15 minutes
- Example: 14:00 new price, but Timer #1 ran at 13:58 → next update at 14:13 → users see old price until 14:13
**What it does:**
```python
async def _handle_quarter_hour_refresh(self, now: datetime) -> None:
# Step 1: Check midnight turnover (coordinates with Timer #1)
if self._check_midnight_turnover_needed(now):
# Timer #1 might have already done this → atomic check handles it
await self._perform_midnight_data_rotation(now)
# Notify ALL entities after midnight turnover
return
# Step 2: Normal quarter-hour refresh (most common path)
# Only notify time-sensitive entities (current_interval_price, etc.)
self._listener_manager.async_update_time_sensitive_listeners()
```
**Smart Boundary Tolerance:**
- Uses `round_to_nearest_quarter_hour()` with ±2 second tolerance
- HA may schedule timer at 14:59:58 → rounds to 15:00:00 (shows new interval)
- HA restart at 14:59:30 → stays at 14:45:00 (shows current interval)
- See [Architecture](./architecture.md#3-quarter-hour-precision) for details
**Absolute Time Scheduling:**
- `async_track_utc_time_change()` plans for **all future boundaries** (15:00, 15:15, 15:30, ...)
- NOT relative delays ("in 15 minutes")
- If triggered at 14:59:58 → next trigger is 15:15:00, NOT 15:00:00 (prevents double updates)
**Which entities listen:**
- All sensors that depend on "current interval" (e.g., `current_interval_price`, `next_interval_price`)
- Binary sensors that check "is now in period?" (e.g., `best_price_period_active`)
- ~50-60 entities out of 120+ total
**Why custom timer:**
- HA's built-in coordinator doesn't support exact boundary timing
- We need **absolute time** triggers, not periodic intervals
- Allows fast entity updates without expensive data transformation
---
## Timer #3: Minute Refresh (Custom)
**File:** `coordinator/listeners.py``ListenerManager.schedule_minute_refresh()`
**Type:** Custom timer using `async_track_utc_time_change(second=0)` (every minute)
**Purpose:** Update countdown and progress sensors for smooth UX
**What it does:**
```python
async def _handle_minute_refresh(self, now: datetime) -> None:
# Only notify minute-update entities
# No data fetching, no transformation, no midnight handling
self._listener_manager.async_update_minute_listeners()
```
**Which entities listen:**
- `best_price_remaining_minutes` - Countdown timer
- `peak_price_remaining_minutes` - Countdown timer
- `best_price_progress` - Progress bar (0-100%)
- `peak_price_progress` - Progress bar (0-100%)
- ~10 entities total
**Why custom timer:**
- Users want smooth countdowns (not jumping 15 minutes at a time)
- Progress bars need minute-by-minute updates
- Very lightweight (no data processing, just state recalculation)
**Why NOT every second:**
- Minute precision sufficient for countdown UX
- Reduces CPU load (60× fewer updates than seconds)
- Home Assistant best practice (avoid sub-minute updates)
---
## Listener Pattern (Python/HA Terminology)
**Your question:** "Sind Timer für dich eigentlich 'Listener'?"
**Answer:** In Home Assistant terminology:
- **Timer** = The mechanism that triggers at specific times (`async_track_utc_time_change`)
- **Listener** = A callback function that gets called when timer triggers
- **Observer Pattern** = Entities register callbacks, coordinator notifies them
**How it works:**
```python
# Entity registers a listener callback
class TibberPricesSensor(CoordinatorEntity):
async def async_added_to_hass(self):
# Register this entity's update callback
self._remove_listener = self.coordinator.async_add_time_sensitive_listener(
self._handle_coordinator_update
)
# Coordinator maintains list of listeners
class ListenerManager:
def __init__(self):
self._time_sensitive_listeners = [] # List of callbacks
def async_add_time_sensitive_listener(self, callback):
self._time_sensitive_listeners.append(callback)
def async_update_time_sensitive_listeners(self):
# Timer triggered → notify all listeners
for callback in self._time_sensitive_listeners:
callback() # Entity updates itself
```
**Why this pattern:**
- Decouples timer logic from entity logic
- One timer can notify many entities efficiently
- Entities can unregister when removed (cleanup)
- Standard HA pattern for coordinator-based integrations
---
## Timer Coordination Scenarios
### Scenario 1: Normal Operation (No Midnight)
```
14:00:00 → Timer #2 triggers
→ Update time-sensitive entities (current price changed)
→ 60 entities updated (~5ms)
14:03:12 → Timer #1 triggers (HA's 15-min cycle)
→ Check if tomorrow data needed (no, still cached)
→ Return cached data (fast path, ~2ms)
14:15:00 → Timer #2 triggers
→ Update time-sensitive entities
→ 60 entities updated (~5ms)
14:16:00 → Timer #3 triggers
→ Update countdown/progress entities
→ 10 entities updated (~1ms)
```
**Key observation:** Timer #1 and Timer #2 run **independently**, no conflicts.
### Scenario 2: Midnight Turnover
```
23:45:12 → Timer #1 triggers
→ Check midnight: current_date=2025-11-17, last_check=2025-11-17
→ No turnover needed
→ Return cached data
00:00:00 → Timer #2 triggers FIRST (synchronized to midnight)
→ Check midnight: current_date=2025-11-18, last_check=2025-11-17
→ Turnover needed! Perform rotation, save cache
→ _last_midnight_check = 2025-11-18
→ Notify ALL entities
00:03:12 → Timer #1 triggers (its regular cycle)
→ Check midnight: current_date=2025-11-18, last_check=2025-11-18
→ Turnover already done → skip
→ Return existing data (fast path)
```
**Key observation:** Atomic date comparison prevents double-turnover, whoever runs first wins.
### Scenario 3: Tomorrow Data Check (After 13:00)
```
13:00:00 → Timer #2 triggers
→ Normal quarter-hour refresh
→ Update time-sensitive entities
13:03:12 → Timer #1 triggers
→ Check tomorrow data: missing or invalid
→ Fetch from Tibber API (~300ms)
→ Transform data (~200ms)
→ Calculate periods (~100ms)
→ Notify ALL entities (new data available)
13:15:00 → Timer #2 triggers
→ Normal quarter-hour refresh (uses newly fetched data)
→ Update time-sensitive entities
```
**Key observation:** Timer #1 does expensive work (API + transform), Timer #2 does cheap work (entity notify).
---
## Why We Keep HA's Timer (Timer #1)
**Your question:** "warum wir den HA timer trotzdem weiter benutzen, da er ja für uns unkontrollierte aktualisierte änderungen triggert"
**Answer:** You're correct that it's not synchronized, but that's actually **intentional**:
### Reason 1: Load Distribution on Tibber API
If all installations used synchronized timers:
- ❌ Everyone fetches at 13:00:00 → Tibber API overload
- ❌ Everyone fetches at 14:00:00 → Tibber API overload
- ❌ "Thundering herd" problem
With HA's unsynchronized timer:
- ✅ Installation A: 13:03:12, 13:18:12, 13:33:12, ...
- ✅ Installation B: 13:07:45, 13:22:45, 13:37:45, ...
- ✅ Installation C: 13:11:28, 13:26:28, 13:41:28, ...
- ✅ Natural distribution over ~30 minutes
- ✅ Plus: Random 0-30s delay on tomorrow checks
**Result:** API load spread evenly, no spikes.
### Reason 2: What Timer #1 Actually Checks
Timer #1 does NOT blindly update. It checks:
```python
def _should_update_price_data(self) -> str:
# Check 1: Do we have tomorrow data? (only relevant after ~13:00)
if tomorrow_missing or tomorrow_invalid:
return "tomorrow_check" # Fetch needed
# Check 2: Is cache still valid?
if cache_valid:
return "cached" # No fetch needed (most common!)
# Check 3: Has enough time passed?
if time_since_last_update < threshold:
return "cached" # Too soon, skip fetch
return "update_needed" # Rare case
```
**Most Timer #1 cycles:** Fast path (~2ms), no API call, just returns cached data.
**API fetch only when:**
- Tomorrow data missing/invalid (after 13:00)
- Cache expired (midnight turnover)
- Explicit user refresh
### Reason 3: HA Integration Best Practices
- ✅ Standard HA pattern: `DataUpdateCoordinator` is recommended by HA docs
- ✅ Automatic retry logic for temporary API failures
- ✅ Backpressure handling (won't queue updates if previous still running)
- ✅ Developer tools integration (users can manually trigger refresh)
- ✅ Diagnostics integration (shows last update time, success/failure)
### What We DO Synchronize
- ✅ **Timer #2:** Entity state updates at exact boundaries (user-visible)
- ✅ **Timer #3:** Countdown/progress at exact minutes (user-visible)
- ❌ **Timer #1:** API fetch timing (invisible to user, distribution wanted)
---
## Performance Characteristics
### Timer #1 (DataUpdateCoordinator)
- **Triggers:** Every 15 minutes (unsynchronized)
- **Fast path:** ~2ms (cache check, return existing data)
- **Slow path:** ~600ms (API fetch + transform + calculate)
- **Frequency:** ~96 times/day
- **API calls:** ~1-2 times/day (cached otherwise)
### Timer #2 (Quarter-Hour Refresh)
- **Triggers:** 96 times/day (exact boundaries)
- **Processing:** ~5ms (notify 60 entities)
- **No API calls:** Uses cached/transformed data
- **No transformation:** Just entity state updates
### Timer #3 (Minute Refresh)
- **Triggers:** 1440 times/day (every minute)
- **Processing:** ~1ms (notify 10 entities)
- **No API calls:** No data processing at all
- **Lightweight:** Just countdown math
**Total CPU budget:** ~15 seconds/day for all timers combined.
---
## Debugging Timer Issues
### Check Timer #1 (HA Coordinator)
```python
# Enable debug logging
_LOGGER.setLevel(logging.DEBUG)
# Watch for these log messages:
"Fetching data from API (reason: tomorrow_check)" # API call
"Using cached data (no update needed)" # Fast path
"Midnight turnover detected (Timer #1)" # Turnover
```
### Check Timer #2 (Quarter-Hour)
```python
# Watch coordinator logs:
"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal
"Midnight turnover detected (Timer #2)" # Turnover
```
### Check Timer #3 (Minute)
```python
# Watch coordinator logs:
"Updated 10 minute-update entities" # Every minute
```
### Common Issues
1. **Timer #2 not triggering:**
- Check: `schedule_quarter_hour_refresh()` called in `__init__`?
- Check: `_quarter_hour_timer_cancel` properly stored?
2. **Double updates at midnight:**
- Should NOT happen (atomic coordination)
- Check: Both timers use same date comparison logic?
3. **API overload:**
- Check: Random delay working? (0-30s jitter on tomorrow check)
- Check: Cache validation logic correct?
---
## Related Documentation
- **[Architecture](./architecture.md)** - Overall system design, data flow
- **[Caching Strategy](./caching-strategy.md)** - Cache lifetimes, invalidation, midnight turnover
- **[AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.31.0/AGENTS.md)** - Complete reference for AI development
---
## Summary
**Three independent timers:**
1. **Timer #1** (HA built-in, 15 min, unsynchronized) → Data fetching (when needed)
2. **Timer #2** (Custom, :00/:15/:30/:45) → Entity state updates (always)
3. **Timer #3** (Custom, every minute) → Countdown/progress (always)
**Key insights:**
- Timer #1 unsynchronized = good (load distribution on API)
- Timer #2 synchronized = good (user sees correct data immediately)
- Timer #3 synchronized = good (smooth countdown UX)
- All three coordinate gracefully (atomic midnight checks, no conflicts)
**"Listener" terminology:**
- Timer = mechanism that triggers
- Listener = callback that gets called
- Observer pattern = entities register, coordinator notifies

View file

@ -0,0 +1,81 @@
{
"tutorialSidebar": [
"intro",
{
"type": "category",
"label": "🏗️ Architecture",
"link": {
"type": "doc",
"id": "architecture"
},
"items": [
"architecture",
"timer-architecture",
"caching-strategy",
"api-reference"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "💻 Development",
"link": {
"type": "doc",
"id": "setup"
},
"items": [
"setup",
"coding-guidelines",
"critical-patterns",
"repairs-system",
"debugging"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "📐 Advanced Topics",
"link": {
"type": "doc",
"id": "period-calculation-theory"
},
"items": [
"period-calculation-theory",
"refactoring-guide",
"performance",
"recorder-optimization"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "📝 Contributing",
"link": {
"type": "doc",
"id": "contributing"
},
"items": [
"contributing"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "🚀 Release",
"link": {
"type": "doc",
"id": "release-management"
},
"items": [
"release-management",
"testing"
],
"collapsible": true,
"collapsed": false
}
]
}

View file

@ -1,4 +1,5 @@
[
"v0.31.0",
"v0.30.0",
"v0.29.0",
"v0.28.0",

View file

@ -32,6 +32,10 @@ If you have configured more than one Tibber home, each home has its own config e
### Scheduling Actions
:::warning Experimental
The scheduling actions and `plan_charging` are **experimental** and still undergoing testing. Their parameters and response formats may change in future releases. Use them with care and [report issues](https://github.com/jpawlowski/hass.tibber_prices/issues).
:::
Find the cheapest (or most expensive) time windows for your appliances. Ideal for automating when to run devices based on real price data.
| Action | Description | Best For |

View file

@ -68,6 +68,7 @@ response_variable: chart_data
| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` |
| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` |
| `resolution` | `interval` (15-min) or `hourly` | `interval` |
| `price_source` | Primary price series: `total`, `energy`, or `tax` | `total` |
| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting |
| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) |
@ -135,6 +136,39 @@ data:
For detailed parameter descriptions, open **Developer Tools → Actions** and select `tibber_prices.get_chartdata`. The inline documentation is stored in `services.yaml` because actions are backed by services.
### Price Source (`price_source`)
By default, charts use the **total** price (energy + taxes + fees) as the main price series. With `price_source` you can switch the **primary** value to a single component instead:
| Value | Main price series |
|-------|-------------------|
| `total` (default) | Full price including energy, taxes, and fees |
| `energy` | Raw spot/energy price only (excluding taxes and fees) |
| `tax` | Tax and fee component only |
<details>
<summary>Show YAML: Chart based on the raw energy price</summary>
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: ["today", "tomorrow"]
price_source: energy # main price = spot price only
response_variable: chart_data
```
</details>
:::tip `price_source` vs. `include_energy` / `include_tax`
These solve different problems:
- **`price_source`** changes *which* component becomes the **main** price value (one series).
- **`include_energy` / `include_tax`** (see below) keep the total price as main value but **add extra fields** so you can show energy and tax **alongside** the total in the same chart.
:::
This parameter also applies to [`get_apexcharts_yaml`](#tibber_pricesget_apexcharts_yaml).
### Energy & Tax Fields
You can include the raw energy price (spot price) and/or tax component in chart data output. This is useful for visualizing how the total price is composed over time, or for feed-in calculations.
@ -291,6 +325,10 @@ card:
- **`rating_level`** (default): 3 series (LOW, NORMAL, HIGH) - based on your personal thresholds
- **`level`**: 5 series (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) - absolute price ranges
**Price Source Option:**
Like `get_chartdata`, this action accepts a `price_source` parameter (`total`, `energy`, or `tax`) to choose which price component the generated chart plots. Defaults to `total`. See [Price Source](#price-source-price_source) above for details.
**Best Price Period Highlights:**
When `highlight_best_price: true`:

View file

@ -97,7 +97,7 @@ Both shapes are reported as **`valley`** by the [Day Pattern sensor](sensors-pri
**Why this matters:** On these days, the Best Price Period may be short (12 hours, covering only the absolute minimum), but prices can remain favorable for 46 hours. By combining [trend sensors](sensors-trends.md) with [price levels](sensors-ratings-levels.md) in automations, you can ride the full cheap wave instead of only using the detected period.
See [Automation Examples → V-Shaped Days](automation-examples.md#understanding-v-shaped-price-days) for practical patterns.
See [Automation Examples → Heat Pump Smart Boost](automation-examples.md#heat-pump-smart-boost-with-trend-awareness) for practical patterns.
## Multi-Home Support

View file

@ -39,6 +39,7 @@ response_variable: price_data
```json
{
"success": true,
"home_id": "abc-123",
"start_time": "2025-11-01T00:00:00+01:00",
"end_time": "2025-11-02T00:00:00+01:00",
@ -63,6 +64,33 @@ response_variable: price_data
**Note:** Times are automatically converted to your Tibber home's timezone. The interval pool caches previously fetched intervals, so repeated calls for the same range are fast.
### Checking the Result
The response always has the expected shape — even when no data could be returned — so your automations never have to guard against missing fields. Use the `success` flag to tell apart two very different situations:
| Situation | `success` | `price_info` | What it means |
|-----------|-----------|--------------|---------------|
| Data returned | `true` | populated | Prices are available for the range. |
| No prices for the range *yet* | `true` | `[]` (`interval_count: 0`) | **Not an error.** Tomorrow's prices are usually published by Tibber around 13:00 local time, but the underlying day-ahead auction can be delayed — occasionally the data only appears later in the afternoon or evening (and on rare auction failures, not at all for that day). Before that, a range covering tomorrow legitimately returns empty. Retry later. |
| Tibber API unavailable | `false` (`reason: "price_data_unavailable"`) | `[]` | A temporary API outage prevented the fetch on an uncached range. Existing sensors keep working from cache; retry later. |
:::tip Distinguishing "no data yet" from "outage"
Check `success` first. `success: true` with an empty `price_info` means **the request worked, there simply are no prices for that range yet** (typically tomorrow before the day-ahead prices are published — usually around 13:00, but sometimes later in the afternoon or evening). `success: false` means **the API call itself failed** — treat it as a transient error and retry later.
:::
```yaml
# Example: only act when prices are actually available
- if: "{{ price_data.success and price_data.interval_count > 0 }}"
then:
- service: notify.mobile_app
data:
message: "Got {{ price_data.interval_count }} price intervals."
- if: "{{ not price_data.success }}"
then:
# Tibber API outage — retry later, sensors keep running from cache
- delay: "00:30:00"
```
---
## tibber_prices.refresh_user_data

View file

@ -96,7 +96,7 @@ A Best Price Period is the **cheapest contiguous block** of time — a stretch o
On a V-shaped day (prices drop sharply to a minimum, then rise again), the period therefore starts **at** the low point, not before it — because the intervals leading up to the minimum did not yet meet the price criteria. Only the intervals near the bottom of the V qualify as a coherent cheap block.
**For flexible loads** (e.g., heat pump, battery charging): you can easily ride the full cheap wave by combining the period sensor with the price level and trend sensors. See [V-Shaped Price Days in Automation Examples](./automation-examples.md#understanding-v-shaped-price-days).
**For flexible loads** (e.g., heat pump, battery charging): you can easily ride the full cheap wave by combining the period sensor with the price level and trend sensors. See [Heat Pump Smart Boost in Automation Examples](./automation-examples.md#heat-pump-smart-boost-with-trend-awareness).
:::
### Algorithm Overview
@ -586,7 +586,7 @@ As described in [Phase 6](#phase-6-relaxation-adaptive), relaxation automaticall
## Common Scenarios
:::tip V-shaped price days
On days with a sharp price dip (e.g. solar midday surplus), the Best Price Period covers only the absolute minimum. The surrounding cheap hours are intentional — the integration shows you the cheapest contiguous block, not a fixed-length window centered on the minimum. To run a device during the full cheap window, see [Understanding V-Shaped Price Days](./automation-examples.md#understanding-v-shaped-price-days) in the Automation Examples.
On days with a sharp price dip (e.g. solar midday surplus), the Best Price Period covers only the absolute minimum. The surrounding cheap hours are intentional — the integration shows you the cheapest contiguous block, not a fixed-length window centered on the minimum. To run a device during the full cheap window, see [Heat Pump Smart Boost](./automation-examples.md#heat-pump-smart-boost-with-trend-awareness) in the Automation Examples.
:::
### Scenario 1: Simple Best Price (Default)

View file

@ -2,6 +2,10 @@
The `plan_charging` action turns **battery parameters** into a complete **cost-minimized charging schedule**. Instead of manually computing energy, duration, and power, you describe the battery (capacity, current SoC, target SoC, max power) and the action returns a per-interval plan with SoC progression, cost totals, and segment grouping.
:::warning Experimental
The `plan_charging` action is **experimental** and still undergoing testing. Its parameters, response format, and behavior may change in future releases. Use it in automations with care, and please [report any issues](https://github.com/jpawlowski/hass.tibber_prices/issues).
:::
:::tip When to use this
If you already know the duration in minutes and just need the cheapest time window, use [`find_cheapest_hours`](scheduling-actions.md#find-cheapest-hours) or [`find_cheapest_block`](scheduling-actions.md#find-cheapest-block). Use `plan_charging` when you know your battery/EV parameters and want the integration to compute the duration, account for charging losses, and produce a SoC progression.
:::
@ -127,6 +131,7 @@ The response contains the following top-level keys:
| Key | Description |
|-----|-------------|
| `success` | `true` when the request itself worked (even if no schedule was produced); `false` only on a Tibber API outage (`reason: "price_data_unavailable"`). |
| `intervals_found` | `true` when a schedule was produced. |
| `battery` | Normalized SoC / capacity / efficiency / `achieved_soc_kwh` (what you actually reach with the returned schedule). |
| `charging` | Mode, total duration, total energy, total cost, and the `schedule` block. |
@ -153,8 +158,9 @@ When no schedule is found, `reason` contains one of:
| Code | Meaning |
|------|---------|
| `price_data_unavailable` | The Tibber API was temporarily unavailable for an uncached range. This is the **only** case where `success` is `false` — treat it as a transient error and retry later. Your sensors keep running from cache. |
| `already_at_target` | Current SoC is already at or above target — no charging needed. |
| `no_data_in_range` | The search range has no price data. |
| `no_data_in_range` | The search range has no price data. Often **not an error**: tomorrow's prices are usually published by Tibber around 13:00 local time, but the day-ahead auction can be delayed — sometimes the data only arrives later in the afternoon or evening. A range covering tomorrow returns this until then (with `success: true`). Retry later. |
| `no_intervals_matching_level_filter` | `min_price_level` / `max_price_level` filtered everything out. |
| `no_intervals_after_economic_filter` | `max_cost_per_kwh` or `reserve_for_discharge` filtered everything out. |
| `energy_unreachable` | The energy needed cannot be charged within the available intervals + power limits. |

View file

@ -2,6 +2,10 @@
Find the cheapest (or most expensive) time windows for your appliances — automatically. These actions analyze real Tibber price data and return optimal scheduling recommendations.
:::warning Experimental
The scheduling actions (`find_cheapest_block`, `find_cheapest_hours`, `find_cheapest_schedule`, `find_most_expensive_block`, `find_most_expensive_hours`) are **experimental** and still undergoing testing. Their parameters, response formats, and behavior may change in future releases. Use them in automations with care, and please [report any issues](https://github.com/jpawlowski/hass.tibber_prices/issues).
:::
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
@ -379,6 +383,7 @@ response_variable: result
```json
{
"success": true,
"home_id": "abc-123",
"search_start": "2026-04-11T14:00:00+02:00",
"search_end": "2026-04-12T14:00:00+02:00",
@ -530,6 +535,7 @@ response_variable: result
```json
{
"success": true,
"home_id": "abc-123",
"search_start": "2026-04-11T14:00:00+02:00",
"search_end": "2026-04-12T14:00:00+02:00",
@ -675,6 +681,7 @@ response_variable: result
```json
{
"success": true,
"home_id": "abc-123",
"search_start": "2026-04-11T22:00:00+02:00",
"search_end": "2026-04-12T07:00:00+02:00",
@ -1043,7 +1050,8 @@ The `reason` field contains a stable machine-readable code you can use in automa
| Reason Code | Meaning |
|-------------|---------|
| `no_data_in_range` | No price data available for the search range |
| `price_data_unavailable` | The Tibber API was temporarily unavailable for an uncached range (see [Distinguishing an outage from "no data yet"](#distinguishing-an-outage-from-no-data-yet)) |
| `no_data_in_range` | No price data available for the search range (e.g., tomorrow's prices not published yet) |
| `no_intervals_matching_level_filter` | Level filter excluded all intervals |
| `insufficient_intervals_after_filter` | Not enough intervals left after filtering |
| `insufficient_intervals_for_constraints` | Enough intervals, but constraints (min segment) can't be met |
@ -1056,3 +1064,18 @@ The `reason` field contains a stable machine-readable code you can use in automa
| `relaxation_exhausted` | All relaxation steps tried, still no result (only when `allow_relaxation: true`) |
Always check the failure fields in your automations before using the results.
### Distinguishing an outage from "no data yet"
Every response carries a top-level `success` flag so automations can react correctly **without inspecting the data fields**:
| Situation | `success` | `reason` | What it means |
|-----------|-----------|----------|---------------|
| Result found | `true` | — | Use the schedule/window. |
| No match for your criteria | `true` | `no_data_in_range`, `no_intervals_matching_level_filter`, … | **Not an error.** The request worked; there is simply no result. A common case: tomorrow's prices are usually published by Tibber around 13:00 local time, but the day-ahead auction can be delayed — sometimes the data only arrives later in the afternoon or evening. A search covering tomorrow returns `no_data_in_range` until then. Retry later. |
| Tibber API unavailable | `false` | `price_data_unavailable` | A temporary API outage prevented the fetch on an uncached range. Existing sensors keep working from cache; retry later. |
:::tip Key distinction
`success: true` means **the request itself worked** — even if no window was found (e.g., tomorrow's prices aren't out yet). `success: false` with `reason: "price_data_unavailable"` means **the API call failed** — treat it as a transient error and retry. These service calls never impair your sensors: a failed fetch for an uncached range leaves cached sensor data untouched.
:::

View file

@ -220,24 +220,24 @@ explanations of each sensor's purpose, attributes, and automation examples.
| <span id="ref-current_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_today_tomorrow` | Current Price Rank (Today+Tomorrow) | Aktueller Preisrang (heute+morgen) | Aktuell prisrang (i dag+i morgen) | Huidige prijsrang (vandaag+morgen) | Aktuellt prisrang (idag+imorgon) | ❌ |
| <span id="ref-current_interval_price_rank_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_tomorrow` | Current Price Rank (Tomorrow) | Aktueller Preisrang (morgen) | Aktuell prisrang (i morgen) | Huidige prijsrang (morgen) | Aktuellt prisrang (imorgon) | ❌ |
| <span id="ref-current_price_phase" class="entity-anchor"></span>`current_price_phase` | Current Price Phase | Aktuelle Preisphase | Gjeldende Prisfase | Huidige Prijsfase | Aktuell Prisfas | ✅ |
| <span id="ref-current_price_phase_duration" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_duration` | Current Phase Duration | Current Phase Duration | Current Phase Duration | Current Phase Duration | Current Phase Duration | ❌ |
| <span id="ref-current_price_phase_end_time" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_end_time` | Current Phase End Time | Current Phase End Time | Current Phase End Time | Current Phase End Time | Current Phase End Time | ✅ |
| <span id="ref-current_price_phase_progress" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_progress` | Current Phase Progress | Current Phase Progress | Current Phase Progress | Current Phase Progress | Current Phase Progress | ❌ |
| <span id="ref-current_price_phase_remaining_minutes" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_remaining_minutes` | Current Phase Remaining | Current Phase Remaining | Current Phase Remaining | Current Phase Remaining | Current Phase Remaining | ✅ |
| <span id="ref-current_price_phase_duration" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_duration` | Current Phase Duration | Aktuelle Phase Dauer | Gjeldende fase varighet | Huidige Fase Duur | Aktuell fas varaktighet | ❌ |
| <span id="ref-current_price_phase_end_time" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_end_time` | Current Phase End Time | Aktuelle Phase Endzeit | Gjeldende fase sluttid | Huidige Fase Eindtijd | Aktuell fas sluttid | ✅ |
| <span id="ref-current_price_phase_progress" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_progress` | Current Phase Progress | Aktuelle Phase Fortschritt | Gjeldende fase fremgang | Huidige Fase Voortgang | Aktuell fas framsteg | ❌ |
| <span id="ref-current_price_phase_remaining_minutes" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_remaining_minutes` | Current Phase Remaining | Aktuelle Phase verbleibend | Gjeldende fase gjenværende | Huidige Fase Resterend | Aktuell fas återstående | ✅ |
| <span id="ref-day_pattern_today" class="entity-anchor"></span>`day_pattern_today` | Today's Price Pattern | Preismuster Heute | Prismønster i dag | Prijspatroon Vandaag | Prismönster Idag | ✅ |
| <span id="ref-day_pattern_tomorrow" class="entity-anchor"></span>`day_pattern_tomorrow` | Tomorrow's Price Pattern | Preismuster Morgen | Prismønster i morgen | Prijspatroon Morgen | Prismönster Imorgon | ❌ |
| <span id="ref-day_pattern_yesterday" class="entity-anchor"></span>`day_pattern_yesterday` | Yesterday's Price Pattern | Preismuster Gestern | Prismønster i går | Prijspatroon Gisteren | Prismönster Igår | ❌ |
| <span id="ref-next_falling_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_falling_phase_in_minutes` | Time to Next Falling Phase | Time to Next Falling Phase | Time to Next Falling Phase | Time to Next Falling Phase | Time to Next Falling Phase | ❌ |
| <span id="ref-next_falling_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_falling_phase_start_time` | Next Falling Phase Start | Next Falling Phase Start | Next Falling Phase Start | Next Falling Phase Start | Next Falling Phase Start | ❌ |
| <span id="ref-next_flat_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_flat_phase_in_minutes` | Time to Next Flat Phase | Time to Next Flat Phase | Time to Next Flat Phase | Time to Next Flat Phase | Time to Next Flat Phase | ❌ |
| <span id="ref-next_flat_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_flat_phase_start_time` | Next Flat Phase Start | Next Flat Phase Start | Next Flat Phase Start | Next Flat Phase Start | Next Flat Phase Start | ❌ |
| <span id="ref-next_falling_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_falling_phase_in_minutes` | Time to Next Falling Phase | Zeit bis nächste fallende Phase | Tid til neste fallende fase | Tijd tot Volgende Dalende Fase | Tid till nästa fallande fas | ❌ |
| <span id="ref-next_falling_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_falling_phase_start_time` | Next Falling Phase Start | Nächste fallende Phase Start | Neste fallende fase start | Volgende Dalende Fase Start | Nästa fallande fas start | ❌ |
| <span id="ref-next_flat_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_flat_phase_in_minutes` | Time to Next Flat Phase | Zeit bis nächste flache Phase | Tid til neste flate fase | Tijd tot Volgende Vlakke Fase | Tid till nästa flata fas | ❌ |
| <span id="ref-next_flat_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_flat_phase_start_time` | Next Flat Phase Start | Nächste flache Phase Start | Neste flate fase start | Volgende Vlakke Fase Start | Nästa flata fas start | ❌ |
| <span id="ref-next_hour_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_hour_price_rank_today` | ⌀ Hourly Price Next Rank (Today) | ⌀ Stündlicher Preisrang Nächste (heute) | ⌀ Timesprisrang neste (i dag) | ⌀ Uurlijkse prijsrang volgende (vandaag) | ⌀ Timprisrang nästa (idag) | ❌ |
| <span id="ref-next_hour_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_hour_price_rank_today_tomorrow` | ⌀ Hourly Price Next Rank (Today+Tomorrow) | ⌀ Stündlicher Preisrang Nächste (heute+morgen) | ⌀ Timesprisrang neste (i dag+i morgen) | ⌀ Uurlijkse prijsrang volgende (vandaag+morgen) | ⌀ Timprisrang nästa (idag+imorgon) | ❌ |
| <span id="ref-next_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_interval_price_rank_today` | Next Price Rank (Today) | Nächster Preisrang (heute) | Neste prisrang (i dag) | Volgende prijsrang (vandaag) | Nästa prisrang (idag) | ❌ |
| <span id="ref-next_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_interval_price_rank_today_tomorrow` | Next Price Rank (Today+Tomorrow) | Nächster Preisrang (heute+morgen) | Neste prisrang (i dag+i morgen) | Volgende prijsrang (vandaag+morgen) | Nästa prisrang (idag+imorgon) | ❌ |
| <span id="ref-next_price_phase" class="entity-anchor"></span>`next_price_phase` | Next Price Phase | Nächste Preisphase | Neste Prisfase | Volgende Prijsfase | Nästa Prisfas | ✅ |
| <span id="ref-next_rising_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_rising_phase_in_minutes` | Time to Next Rising Phase | Time to Next Rising Phase | Time to Next Rising Phase | Time to Next Rising Phase | Time to Next Rising Phase | ❌ |
| <span id="ref-next_rising_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_rising_phase_start_time` | Next Rising Phase Start | Next Rising Phase Start | Next Rising Phase Start | Next Rising Phase Start | Next Rising Phase Start | ❌ |
| <span id="ref-next_rising_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_rising_phase_in_minutes` | Time to Next Rising Phase | Zeit bis nächste steigende Phase | Tid til neste stigende fase | Tijd tot Volgende Stijgende Fase | Tid till nästa stigande fas | ❌ |
| <span id="ref-next_rising_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_rising_phase_start_time` | Next Rising Phase Start | Nächste steigende Phase Start | Neste stigende fase start | Volgende Stijgende Fase Start | Nästa stigande fas start | ❌ |
| <span id="ref-previous_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`previous_interval_price_rank_today` | Previous Price Rank (Today) | Vorheriger Preisrang (heute) | Forrige prisrang (i dag) | Vorige prijsrang (vandaag) | Förra prisrang (idag) | ❌ |
| <span id="ref-previous_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`previous_interval_price_rank_today_tomorrow` | Previous Price Rank (Today+Tomorrow) | Vorheriger Preisrang (heute+morgen) | Forrige prisrang (i dag+i morgen) | Vorige prijsrang (vandaag+morgen) | Förra prisrang (idag+imorgon) | ❌ |
## Binary Sensors
@ -259,9 +259,9 @@ explanations of each sensor's purpose, attributes, and automation examples.
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-in_falling_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_falling_price_phase` | In Falling Price Phase | In Falling Price Phase | In Falling Price Phase | In Falling Price Phase | In Falling Price Phase | ✅ |
| <span id="ref-in_flat_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_flat_price_phase` | In Flat Price Phase | In Flat Price Phase | In Flat Price Phase | In Flat Price Phase | In Flat Price Phase | ✅ |
| <span id="ref-in_rising_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_rising_price_phase` | In Rising Price Phase | In Rising Price Phase | In Rising Price Phase | In Rising Price Phase | In Rising Price Phase | ✅ |
| <span id="ref-in_falling_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_falling_price_phase` | In Falling Price Phase | In fallender Preisphase | I fallende prisfase | In Dalende Prijsfase | I fallande prisfas | ✅ |
| <span id="ref-in_flat_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_flat_price_phase` | In Flat Price Phase | In flacher Preisphase | I flat prisfase | In Vlakke Prijsfase | I flat prisfas | ✅ |
| <span id="ref-in_rising_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_rising_price_phase` | In Rising Price Phase | In steigender Preisphase | I stigende prisfase | In Stijgende Prijsfase | I stigande prisfas | ✅ |
## Number Entities (Configuration Overrides)
> These entities allow runtime adjustment of period calculation parameters without changing the integration configuration. All are **disabled by default**.

View file

@ -0,0 +1,73 @@
# Actions Overview
Tibber Prices provides **actions** (formerly called "services") that you can use in automations, scripts, and dashboards. Home Assistant surfaces them in **Developer Tools → Actions** and in the automation/script editor.
Behind the scenes, YAML still uses the `service:` key — but the UI calls them "actions".
## Finding Your Config Entry ID
Most actions accept an optional `entry_id` parameter that identifies the **config entry** (= integration instance) of the Tibber home you want to query. **If you only have one home configured, you can omit `entry_id` entirely** — the integration auto-selects your only config entry. If you have multiple homes, you need to specify which one.
### In the Action UI — no lookup needed
When you use the action through the Home Assistant interface (Developer Tools → Actions, or the Action picker inside the automation / script editor), the `entry_id` field renders as a **dropdown list** showing all your configured Tibber Prices instances. Just select your home from the drop-down and Home Assistant fills in the correct ID automatically. You never have to deal with the raw ID string.
### In YAML — copy from the integration menu
When you write YAML directly (automations, scripts, Lovelace dashboard cards), you need the actual ID string. The quickest way to get it:
1. Go to **Settings → Devices & Services**
2. Find the **Tibber Prices** integration card
3. Click the **⋮** (three-dot) menu on the card
4. Choose **"Copy Config Entry ID"**
5. Paste the value wherever you see `YOUR_CONFIG_ENTRY_ID` in the YAML examples
The ID looks like a long alphanumeric string, for example `01JKPC7AB3EF4GH5IJ6KL7MN8P`.
:::tip Multiple homes?
If you have configured more than one Tibber home, each home has its own config entry ID. Repeat the steps above for each integration card to get the individual IDs.
:::
## All Actions at a Glance
### Scheduling Actions
:::warning Experimental
The scheduling actions and `plan_charging` are **experimental** and still undergoing testing. Their parameters and response formats may change in future releases. Use them with care and [report issues](https://github.com/jpawlowski/hass.tibber_prices/issues).
:::
Find the cheapest (or most expensive) time windows for your appliances. Ideal for automating when to run devices based on real price data.
| Action | Description | Best For |
|--------|-------------|----------|
| [`find_cheapest_block`](scheduling-actions.md#find-cheapest-block) | Cheapest contiguous window | Dishwasher, washing machine, dryer |
| [`find_cheapest_hours`](scheduling-actions.md#find-cheapest-hours) | Cheapest N hours (non-contiguous OK) | EV charging, battery storage, pool pump |
| [`find_cheapest_schedule`](scheduling-actions.md#find-cheapest-schedule) | Multiple appliances, no overlap | Dishwasher + washing machine overnight |
| [`find_most_expensive_block`](scheduling-actions.md#find-most-expensive-block) | Most expensive contiguous window | Peak avoidance, battery discharge |
| [`find_most_expensive_hours`](scheduling-actions.md#find-most-expensive-hours) | Most expensive N hours | Demand response, consumption shifting |
| [`plan_charging`](plan-charging-action.md) | Battery/EV schedule from SoC + power | Home battery, EV, deadline-aware charging |
**→ [Scheduling Actions — Full Guide](scheduling-actions.md)** with parameters, response formats, decision flowchart, and automation examples.
**→ [Plan Charging Action — Guide](plan-charging-action.md)** for battery/EV charging scheduled from SoC and power (not duration).
### Chart & Visualization Actions
Generate chart-ready data and ApexCharts configurations for your dashboards.
| Action | Description |
|--------|-------------|
| [`get_chartdata`](chart-actions.md#tibber_pricesget_chartdata) | Price data in chart-friendly formats (arrays, filtering, rolling windows) |
| [`get_apexcharts_yaml`](chart-actions.md#tibber_pricesget_apexcharts_yaml) | Auto-generated ApexCharts card configuration with color-coded price levels |
**→ [Chart & Visualization Actions — Full Guide](chart-actions.md)** with parameters, examples, rolling window modes, and migration guide.
### Data & Utility Actions
Fetch raw price data or refresh cached information.
| Action | Description |
|--------|-------------|
| [`get_price`](data-actions.md#tibber_pricesget_price) | Fetch raw price intervals for any time range (with intelligent caching) |
| [`refresh_user_data`](data-actions.md#tibber_pricesrefresh_user_data) | Force-refresh user data (homes, subscriptions) from Tibber API |
**→ [Data & Utility Actions — Full Guide](data-actions.md)** with parameters and response formats.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,368 @@
# Chart & Visualization Actions
Actions for generating chart data and ApexCharts configurations from your Tibber price data.
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
---
## tibber_prices.get_chartdata
**Purpose:** Returns electricity price data in chart-friendly formats for visualization and analysis.
**Key Features:**
- **Flexible Output Formats**: Array of objects or array of arrays
- **Time Range Selection**: Filter by day (yesterday, today, tomorrow)
- **Price Filtering**: Filter by price level or rating
- **Period Support**: Return best/peak price period summaries instead of intervals
- **Resolution Control**: Interval (15-minute) or hourly aggregation
- **Customizable Field Names**: Rename output fields to match your chart library
- **Currency Control**: Override integration default - use base (€/kWh, kr/kWh) or subunit (ct/kWh, øre/kWh)
**Basic Example:**
<details>
<summary>Show YAML: Chart Data Action</summary>
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: ["today", "tomorrow"]
output_format: array_of_objects
response_variable: chart_data
```
</details>
**Response Format:**
<details>
<summary>Show JSON: Chart Data Response</summary>
```json
{
"data": [
{
"start_time": "2025-11-17T00:00:00+01:00",
"price_per_kwh": 0.2534
},
{
"start_time": "2025-11-17T00:15:00+01:00",
"price_per_kwh": 0.2498
}
]
}
```
</details>
**Common Parameters:**
| Parameter | Description | Default |
| ---------------- | ------------------------------------------- | ----------------------- |
| `entry_id` | Config entry ID (optional — auto-selects if only one home) | Auto |
| `day` | Days to include: yesterday, today, tomorrow | `["today", "tomorrow"]` |
| `output_format` | `array_of_objects` or `array_of_arrays` | `array_of_objects` |
| `resolution` | `interval` (15-min) or `hourly` | `interval` |
| `price_source` | Primary price series: `total`, `energy`, or `tax` | `total` |
| `subunit_currency` | Override display mode: `true` for subunit (ct/øre), `false` for base (€/kr) | Integration setting |
| `round_decimals` | Decimal places (0-10) | 2 (subunit) or 4 (base) |
**Rolling Window Mode:**
Omit the `day` parameter to get a dynamic 48-hour rolling window that automatically adapts to data availability:
<details>
<summary>Show YAML: Rolling Window Mode</summary>
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
# Omit 'day' for rolling window
output_format: array_of_objects
response_variable: chart_data
```
</details>
**Behavior:**
- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow
- **When tomorrow data not available**: Returns yesterday + today
This is useful for charts that should always show a 48-hour window without manual day selection.
**Period Filter Example:**
Get best price periods as summaries instead of intervals:
<details>
<summary>Show YAML: Period Filter</summary>
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
period_filter: best_price # or peak_price
day: ["today", "tomorrow"]
include_level: true
include_rating_level: true
response_variable: periods
```
</details>
**Advanced Filtering:**
<details>
<summary>Show YAML: Advanced Filtering</summary>
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
level_filter: ["VERY_CHEAP", "CHEAP"] # Only cheap periods
rating_level_filter: ["LOW"] # Only low-rated prices
insert_nulls: segments # Add nulls at segment boundaries
```
</details>
**Complete Documentation:**
For detailed parameter descriptions, open **Developer Tools → Actions** and select `tibber_prices.get_chartdata`. The inline documentation is stored in `services.yaml` because actions are backed by services.
### Price Source (`price_source`)
By default, charts use the **total** price (energy + taxes + fees) as the main price series. With `price_source` you can switch the **primary** value to a single component instead:
| Value | Main price series |
|-------|-------------------|
| `total` (default) | Full price including energy, taxes, and fees |
| `energy` | Raw spot/energy price only (excluding taxes and fees) |
| `tax` | Tax and fee component only |
<details>
<summary>Show YAML: Chart based on the raw energy price</summary>
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: ["today", "tomorrow"]
price_source: energy # main price = spot price only
response_variable: chart_data
```
</details>
:::tip `price_source` vs. `include_energy` / `include_tax`
These solve different problems:
- **`price_source`** changes *which* component becomes the **main** price value (one series).
- **`include_energy` / `include_tax`** (see below) keep the total price as main value but **add extra fields** so you can show energy and tax **alongside** the total in the same chart.
:::
This parameter also applies to [`get_apexcharts_yaml`](#tibber_pricesget_apexcharts_yaml).
### Energy & Tax Fields
You can include the raw energy price (spot price) and/or tax component in chart data output. This is useful for visualizing how the total price is composed over time, or for feed-in calculations.
| Parameter | Description | Default |
|-----------|-------------|---------|
| `include_energy` | Include raw energy/spot price per data point | `false` |
| `include_tax` | Include tax/fees component per data point | `false` |
| `energy_field` | Custom field name for energy price | `energy_price` |
| `tax_field` | Custom field name for tax | `tax` |
**Example: Chart with price composition**
<details>
<summary>Show YAML: Energy and Tax Fields</summary>
```yaml
service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: ["today", "tomorrow"]
include_energy: true
include_tax: true
response_variable: chart_data
```
</details>
Returns data points like:
<details>
<summary>Show JSON: Returns data points like</summary>
```json
{
"start_time": "2025-11-17T14:00:00+01:00",
"price_per_kwh": 25.34,
"energy_price": 12.18,
"tax": 13.16
}
```
</details>
**Use case — Solar feed-in chart:** Overlay the energy price (what you earn by exporting) alongside the total price to visualize the best export windows. See [Energy & Tax Attributes](sensors-energy-tax.md) for more use cases.
---
## tibber_prices.get_apexcharts_yaml
> ⚠️ **IMPORTANT:** This action generates a **basic example configuration** as a starting point, NOT a complete solution for all ApexCharts features.
>
> This integration is primarily a **data provider**. The generated YAML demonstrates how to use the `get_chartdata` action to fetch price data. Due to the segmented nature of our data (different time periods per series) and the use of Home Assistant's service API instead of entity attributes, many advanced ApexCharts features (like `in_header`, certain transformations) are **not compatible** or require manual customization.
>
> **You are welcome to customize** the generated YAML for your specific needs, but comprehensive ApexCharts configuration support is beyond the scope of this integration. Community contributions with improved configurations are always appreciated!
>
> **For custom solutions:** Use the `get_chartdata` action directly to build your own charts with full control over the data format and visualization.
**Purpose:** Generates a basic ApexCharts card YAML configuration example for visualizing electricity prices with automatic color-coding by price level.
**Prerequisites:**
- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations)
- [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window modes - enables dynamic Y-axis scaling)
**Key Features:**
- **Automatic Color-Coded Series**: Separate series for each price level (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) or rating (LOW, NORMAL, HIGH)
- **Dynamic Y-Axis Scaling**: Rolling window modes automatically use `chart_metadata` sensor for optimal Y-axis bounds
- **Best Price Period Highlights**: Optional vertical bands showing detected best price periods
- **Translated Labels**: Automatically uses your Home Assistant language setting
- **Clean Gap Visualization**: Proper NULL insertion for missing data segments
**Quick Example:**
<details>
<summary>Show YAML: Quick Example</summary>
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom
level_type: rating_level # or "level" for 5-level classification
highlight_best_price: true # Show best price period overlays
response_variable: apexcharts_config
```
</details>
**Day Parameter Options:**
- **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies
- **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability
- **Includes dynamic Y-axis scaling** via `chart_metadata` sensor
- **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes)
- **Includes dynamic Y-axis scaling** via `chart_metadata` sensor
**Dynamic Y-Axis Scaling (Rolling Window Modes):**
Rolling window configurations automatically integrate with the `chart_metadata` sensor for optimal chart appearance:
- **Automatic bounds**: Y-axis min/max adjust to data range
- **No manual configuration**: Works out of the box if sensor is enabled
- **Fallback behavior**: If sensor is disabled, uses ApexCharts auto-scaling
- **Real-time updates**: Y-axis adapts when price data changes
**Example: Today's Prices (Static View)**
<details>
<summary>Show YAML: Today Static View</summary>
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: today
level_type: rating_level
response_variable: config
# Use in dashboard:
type: custom:apexcharts-card
# ... paste generated config
```
</details>
**Example: Rolling 48h Window (Dynamic View)**
<details>
<summary>Show YAML: Rolling 48h Dynamic View</summary>
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_CONFIG_ENTRY_ID
# Omit 'day' for rolling window (or use 'rolling_window')
level_type: level # 5-level classification
highlight_best_price: true
response_variable: config
# Use in dashboard:
type: custom:config-template-card
entities:
- binary_sensor.<home_name>_tomorrow_s_data_available
- sensor.<home_name>_chart_metadata # For dynamic Y-axis
card:
# ... paste generated config
```
</details>
**Level Type Options:**
- **`rating_level`** (default): 3 series (LOW, NORMAL, HIGH) - based on your personal thresholds
- **`level`**: 5 series (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE) - absolute price ranges
**Price Source Option:**
Like `get_chartdata`, this action accepts a `price_source` parameter (`total`, `energy`, or `tax`) to choose which price component the generated chart plots. Defaults to `total`. See [Price Source](#price-source-price_source) above for details.
**Best Price Period Highlights:**
When `highlight_best_price: true`:
- Vertical bands overlay the chart showing detected best price periods
- Tooltip shows "Best Price Period" label when hovering over highlighted areas
- Only appears when best price periods are configured and detected
**Important Notes:**
- **Config Template Card** is only required for rolling window modes (enables dynamic Y-axis)
- Fixed day views (`today`, `tomorrow`, `yesterday`) work with ApexCharts Card alone
- Generated YAML is a starting point - customize colors, styling, features as needed
- All labels are automatically translated to your Home Assistant language
Use the response in Lovelace dashboards by copying the generated YAML.
**Documentation:** Refer to **Developer Tools → Actions** for descriptions of the fields exposed by this action.
---
## Migration from Chart Data Export Sensor
If you're still using the `sensor.<home_name>_chart_data_export` sensor, consider migrating to the `tibber_prices.get_chartdata` action:
**Benefits:**
- No HA restart required for configuration changes
- More flexible filtering and formatting options
- Better performance (on-demand instead of polling)
- Future-proof (active development)
**Migration Steps:**
1. Note your current sensor configuration (Step 7 in Options Flow)
2. Create automation/script that calls `tibber_prices.get_chartdata` with the same parameters
3. Test the new approach
4. Disable the old sensor when satisfied

View file

@ -0,0 +1,372 @@
# Chart Examples
This guide showcases the different chart configurations available through the `tibber_prices.get_apexcharts_yaml` action.
> **Quick Start:** Call the action with your desired parameters, copy the generated YAML, and paste it into your Lovelace dashboard!
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
:::info Finding your Entry ID (`entry_id`)
Every example below contains `entry_id: YOUR_CONFIG_ENTRY_ID`. This value identifies which Tibber home (integration instance) the action targets.
**In the Action UI (Developer Tools → Actions or the automation editor):** The `entry_id` field is a **dropdown** — just select your Tibber home and HA fills in the correct ID automatically.
**In YAML:** Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** (three-dot) menu, and choose **"Copy Config Entry ID"**. Paste the copied value in place of `YOUR_CONFIG_ENTRY_ID`.
:::
## Overview
The integration can generate 4 different chart modes, each optimized for specific use cases:
| Mode | Description | Best For | Dependencies |
|------|-------------|----------|--------------|
| **Today** | Static 24h view of today's prices | Quick daily overview | ApexCharts Card |
| **Tomorrow** | Static 24h view of tomorrow's prices | Planning tomorrow | ApexCharts Card |
| **Rolling Window** | Dynamic 48h view (today+tomorrow or yesterday+today) | Always-current overview | ApexCharts + Config Template Card |
| **Rolling Window Auto-Zoom** | Dynamic view that zooms in as day progresses | Real-time focus on remaining day | ApexCharts + Config Template Card |
**Screenshots available for:**
- ✅ Today (static) - Representative of all fixed day views
- ✅ Rolling Window - Shows dynamic Y-axis scaling
- ✅ Rolling Window Auto-Zoom - Shows progressive zoom effect
## All Chart Modes
### 1. Today's Prices (Static)
**When to use:** Simple daily price overview, no dynamic updates needed.
**Dependencies:** ApexCharts Card only
**Generate:**
<details>
<summary>Show YAML: Today's Prices (Static)</summary>
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: today
level_type: rating_level
highlight_best_price: true
```
</details>
**Screenshot:**
![Today's Prices - Static 24h View](/img/charts/today.jpg)
**Key Features:**
- ✅ Color-coded price levels (LOW, NORMAL, HIGH)
- ✅ Best price period highlights (vertical bands)
- ✅ Static 24-hour view (00:00 - 23:59)
- ✅ Works with ApexCharts Card alone
**Note:** Tomorrow view (`day: tomorrow`) works identically to Today view, just showing tomorrow's data. All fixed day views (yesterday/today/tomorrow) use the same visualization approach.
---
### 2. Rolling 48h Window (Dynamic)
**When to use:** Always-current view that automatically switches between yesterday+today and today+tomorrow.
**Dependencies:** ApexCharts Card + Config Template Card
**Generate:**
<details>
<summary>Show YAML: Rolling 48h Window</summary>
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_CONFIG_ENTRY_ID
# Omit 'day' for rolling window
level_type: rating_level
highlight_best_price: true
```
</details>
**Screenshot:**
![Rolling 48h Window with Dynamic Y-Axis Scaling](/img/charts/rolling-window.jpg)
**Key Features:**
- ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor
- ✅ Automatic data selection: today+tomorrow (when available) or yesterday+today
- ✅ Always shows 48 hours of data
- ✅ Updates automatically when tomorrow's data arrives
- ✅ Color gradients for visual appeal
**How it works:**
- Before ~13:00: Shows yesterday + today
- After ~13:00: Shows today + tomorrow
- Y-axis automatically adjusts to data range for optimal visualization
---
### 3. Rolling Window Auto-Zoom (Dynamic)
**When to use:** Real-time focus on remaining day - progressively zooms in as day advances.
**Dependencies:** ApexCharts Card + Config Template Card
**Generate:**
<details>
<summary>Show YAML: Rolling Auto-Zoom</summary>
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: rolling_window_autozoom
level_type: rating_level
highlight_best_price: true
```
</details>
**Screenshot:**
![Rolling Window Auto-Zoom - Progressive Zoom Effect](/img/charts/rolling-window-autozoom.jpg)
**Key Features:**
- ✅ **Progressive zoom:** Graph span decreases every 15 minutes
- ✅ **Dynamic Y-axis scaling** via `chart_metadata` sensor
- ✅ Always shows: 2 hours lookback + remaining time until midnight
- ✅ Perfect for real-time price monitoring
- ✅ Example: At 18:00, shows 16:00 → 00:00 (8h window)
**How it works:**
- 00:00: Shows full 48h window (same as rolling window)
- 06:00: Shows 04:00 → midnight (20h window)
- 12:00: Shows 10:00 → midnight (14h window)
- 18:00: Shows 16:00 → midnight (8h window)
- 23:45: Shows 21:45 → midnight (2.25h window)
This creates a "zooming in" effect that focuses on the most relevant remaining time.
---
## Comparison: Level Type Options
### Rating Level (3 series)
Based on **your personal price thresholds** (configured in Options Flow):
- **LOW** (Green): Below your "cheap" threshold
- **NORMAL** (Blue): Between thresholds
- **HIGH** (Red): Above your "expensive" threshold
**Best for:** Personal decision-making based on your budget
### Level (5 series)
Based on **absolute price ranges** (calculated from daily min/max):
- **VERY_CHEAP** (Dark Green): Bottom 20%
- **CHEAP** (Light Green): 20-40%
- **NORMAL** (Blue): 40-60%
- **EXPENSIVE** (Orange): 60-80%
- **VERY_EXPENSIVE** (Red): Top 20%
**Best for:** Objective price distribution visualization
---
## Dynamic Y-Axis Scaling
Rolling window modes (2 & 3) automatically integrate with the `chart_metadata` sensor for optimal visualization:
**Without chart_metadata sensor (disabled):**
<details>
<summary>Show chart illustration: Without Chart Metadata</summary>
```
┌─────────────────────┐
│ │ ← Lots of empty space
│ ___ │
___/ \___
│_/ \_ │
├─────────────────────┤
0 100 ct
```
</details>
**With chart_metadata sensor (enabled):**
<details>
<summary>Show chart illustration: With Chart Metadata</summary>
```
┌─────────────────────┐
│ ___ │ ← Y-axis fitted to data
___/ \___
│_/ \_ │
├─────────────────────┤
18 28 ct ← Optimal range
```
</details>
**Requirements:**
- ✅ The `sensor.<home_name>_chart_metadata` must be **enabled** (it's enabled by default!)
- ✅ That's it! The generated YAML automatically uses the sensor for dynamic scaling
**Important:** Do NOT disable the `chart_metadata` sensor if you want optimal Y-axis scaling in rolling window modes!
**Note:** Fixed day views (`today`, `tomorrow`) use ApexCharts' built-in auto-scaling and don't require the metadata sensor.
---
## Best Price Period Highlights
When `highlight_best_price: true`, vertical bands overlay the chart showing detected best price periods:
**Example:**
<details>
<summary>Show chart illustration: Best Price Period Highlights</summary>
```
Price
30│ ┌─────────┐ Normal prices
│ │ │
25│ ▓▓▓▓▓▓│ │ ← Best price period (shaded)
│ ▓▓▓▓▓▓│ │
20│─────▓▓▓▓▓▓│─────────│
│ ▓▓▓▓▓▓
└─────────────────────── Time
06:00 12:00 18:00
```
</details>
**Features:**
- Automatic detection based on your configuration (see [Period Calculation Guide](period-calculation.md))
- Tooltip shows "Best Price Period" label
- Only appears when periods are configured and detected
- Can be disabled with `highlight_best_price: false`
---
## Prerequisites
### Required for All Modes
- **[ApexCharts Card](https://github.com/RomRider/apexcharts-card)**: Core visualization library
<details>
<summary>Show shell command: Prerequisite for All Modes</summary>
```bash
# Install via HACS
HACS → Frontend → Search "ApexCharts Card" → Download
```
</details>
### Required for Rolling Window Modes Only
- **[Config Template Card](https://github.com/iantrich/config-template-card)**: Enables dynamic configuration
<details>
<summary>Show shell command: Rolling Window Prerequisite</summary>
```bash
# Install via HACS
HACS → Frontend → Search "Config Template Card" → Download
```
</details>
**Note:** Fixed day views (`today`, `tomorrow`) work with ApexCharts Card alone!
---
## Tips & Tricks
### Customizing Colors
Edit the `colors` array in the generated YAML:
<details>
<summary>Show YAML: Custom Color Palette</summary>
```yaml
apex_config:
colors:
- "#00FF00" # Change LOW/VERY_CHEAP color
- "#0000FF" # Change NORMAL color
- "#FF0000" # Change HIGH/VERY_EXPENSIVE color
```
</details>
### Changing Chart Height
Add to the card configuration:
<details>
<summary>Show YAML: Custom Chart Height</summary>
```yaml
type: custom:apexcharts-card
graph_span: 48h
header:
show: true
title: My Custom Title
apex_config:
chart:
height: 400 # Adjust height in pixels
```
</details>
### Combining with Other Cards
Wrap in a vertical stack for dashboard integration:
<details>
<summary>Show YAML: Vertical Stack Integration</summary>
```yaml
type: vertical-stack
cards:
- type: entity
entity: sensor.<home_name>_current_electricity_price
- type: custom:apexcharts-card
# ... generated chart config
```
</details>
---
## Next Steps
- **[Chart Actions Guide](chart-actions.md)**: Complete documentation of `get_apexcharts_yaml` parameters
- **[Chart Metadata Sensor](sensors-overview.md#chart-metadata)**: Learn about dynamic Y-axis scaling
- **[Period Calculation Guide](period-calculation.md)**: Configure best price period detection
---
## Screenshots
### Gallery
1. **Today View (Static)** - Representative of all fixed day views (yesterday/today/tomorrow)
![Today View](/img/charts/today.jpg)
2. **Rolling Window (Dynamic)** - Shows dynamic Y-axis scaling and 48h window
![Rolling Window](/img/charts/rolling-window.jpg)
3. **Rolling Window Auto-Zoom (Dynamic)** - Shows progressive zoom effect
![Rolling Window Auto-Zoom](/img/charts/rolling-window-autozoom.jpg)
**Note:** Tomorrow view is visually identical to Today view (same chart type, just different data).

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>Show YAML: 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: Feed-In Compensation Sensors</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: Feed-In 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: Preparing for the End of Saldering</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</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

@ -0,0 +1,116 @@
# Core Concepts
Understanding the fundamental concepts behind the Tibber Prices integration.
## How Data Flows
```mermaid
flowchart LR
subgraph API["☁️ Tibber API"]
raw["Raw prices<br/>(quarter-hourly)"]
end
subgraph Integration["⚙️ Integration"]
direction TB
enrich["Enrichment<br/><small>24h averages, differences</small>"]
classify["Classification"]
enrich --> classify
end
subgraph Sensors["📊 Your Sensors"]
direction TB
prices["Price sensors<br/><small>current, min, max, avg</small>"]
ratings["Ratings & Levels<br/><small>LOW / NORMAL / HIGH</small>"]
periods["Periods<br/><small>best & peak windows</small>"]
trends["Trends & Volatility<br/><small>falling / stable / rising</small>"]
end
raw -->|every 15 min| enrich
classify --> prices
classify --> ratings
classify --> periods
classify --> trends
style API fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style Integration fill:#fff9e6,stroke:#ffb800,stroke-width:2px
style Sensors fill:#e6fff5,stroke:#00c853,stroke-width:2px
```
The integration fetches raw quarter-hourly prices from Tibber, enriches them with statistical context (averages, differences), and exposes the results as sensors you can use in automations and dashboards.
## Price Intervals
The integration works with **quarter-hourly intervals** (15 minutes):
- Each interval has a start time (e.g., 14:00, 14:15, 14:30, 14:45)
- Prices are fixed for the entire interval
- Synchronized with Tibber's smart meter readings
## Price Ratings
Prices are automatically classified into **rating levels**:
- **VERY_CHEAP** - Exceptionally low prices (great for energy-intensive tasks)
- **CHEAP** - Below average prices (good for flexible loads)
- **NORMAL** - Around average prices (regular consumption)
- **EXPENSIVE** - Above average prices (reduce consumption if possible)
- **VERY_EXPENSIVE** - Exceptionally high prices (avoid heavy loads)
Rating is based on **statistical analysis** comparing current price to:
- Daily average
- Trailing 24-hour average
- User-configured thresholds
## Price Periods
**Best Price Periods** and **Peak Price Periods** are automatically detected time windows:
- **Best Price Period** - Consecutive intervals with favorable prices (for scheduling energy-heavy tasks)
- **Peak Price Period** - Time windows with highest prices (to avoid or shift consumption)
Periods can:
- Span multiple hours
- Cross midnight boundaries
- Adapt based on your configuration (flex, min_distance, rating levels)
See [Period Calculation](period-calculation.md) for detailed configuration.
## Statistical Analysis
The integration enriches every interval with context:
- **Trailing 24h Average** - Average price over the last 24 hours
- **Leading 24h Average** - Average price over the next 24 hours
- **Price Difference** - How much current price deviates from average (in %)
- **Volatility** - Price stability indicator (LOW, MEDIUM, HIGH)
This helps you understand if current prices are exceptional or typical.
## V-Shaped and U-Shaped Price Days
Some days have a price curve with a clear dip in the middle:
- **V-shaped**: Prices drop sharply, hit a brief minimum, then rise sharply again (common during short midday solar surplus)
- **U-shaped**: Prices drop to a low level and stay there for an extended period before rising (common during nighttime or extended low-demand periods)
Both shapes are reported as **`valley`** by the [Day Pattern sensor](sensors-price-phases.md#day-pattern-sensors) — V and U are informal descriptions of the same structural pattern. The width of the cheap window is reflected in the `valley_start` and `valley_end` attributes: a V-shaped day has these close together, a U-shaped day has them far apart.
**Why this matters:** On these days, the Best Price Period may be short (12 hours, covering only the absolute minimum), but prices can remain favorable for 46 hours. By combining [trend sensors](sensors-trends.md) with [price levels](sensors-ratings-levels.md) in automations, you can ride the full cheap wave instead of only using the detected period.
See [Automation Examples → Heat Pump Smart Boost](automation-examples.md#heat-pump-smart-boost-with-trend-awareness) for practical patterns.
## Multi-Home Support
You can add multiple Tibber homes to track prices for:
- Different locations
- Different electricity contracts
- Comparison between regions
Each home gets its own set of sensors with unique entity IDs.
---
💡 **Next Steps:**
- [Glossary](glossary.md) - Detailed term definitions
- [Sensors Overview](sensors-overview.md) - How to use sensor data
- [Automation Examples](automation-examples.md) - Practical use cases

View file

@ -0,0 +1,88 @@
---
sidebar_label: 💚 Best Price Period
---
# 💚 Best Price Period
**Settings → Devices & Services → Tibber Prices → Configure → 💚 Best Price Period**
---
Best Price Period sensors detect windows of time when electricity is cheap enough to be worth scheduling loads (dishwasher, washing machine, EV charging, water heater). The binary sensor `is_best_price_period` is `on` during these windows.
See **[Period Calculation](period-calculation.md)** for an in-depth explanation of the detection algorithm and [Period Relaxation](period-relaxation.md) for how the relaxation strategy works.
## Settings
### Period Settings
| Setting | Default | Description |
|---------|---------|-------------|
| **Minimum period length** | 60 min | Shortest window to report as a period — filters out tiny sub-hour dips |
| **Maximum price level** | CHEAP | Only intervals at this Tibber level or cheaper qualify |
| **Gap tolerance** | 1 | Consecutive above-threshold intervals allowed inside a period — bridges small price bumps between two cheap windows |
### Flexibility Settings
| Setting | Default | Description |
|---------|---------|-------------|
| **Flex percentage** | 15% | How far above the daily minimum a price can be and still qualify. Higher = more intervals qualify |
| **Minimum distance from average** | 5% | Qualifying intervals must be at least this far below the daily average — ensures periods are meaningfully cheap, not just "not expensive" |
### Relaxation & Target
| Setting | Default | Description |
|---------|---------|-------------|
| **Enable minimum period target** | On | Automatically loosens criteria (relaxation) until the target count is reached |
| **Target periods per day** | 2 | How many distinct periods the algorithm aims to find per day |
| **Relaxation attempts** | 11 | How many times to loosen the criteria before giving up. 11 steps × 3% increment = up to ~48% flex |
:::tip Start with defaults
The defaults are tuned for typical European electricity markets. If you're unsure, leave them as-is and observe the binary sensor over a few days.
:::
## Runtime Override Entities
You can override these settings at runtime through automations — useful for seasonal adjustments or dynamic schedules — without opening the configuration menu.
These entities are **disabled by default**. Enable them in **Settings → Devices & Services → Tibber Prices → Entities**.
| Entity | Type | Range | Overrides |
|--------|------|-------|-----------|
| `number.<home_name>_best_price_flexibility` | Number | 050% | Flex percentage |
| `number.<home_name>_best_price_minimum_distance` | Number | -500% | Minimum distance from average |
| `number.<home_name>_best_price_minimum_period_length` | Number | 15180 min | Minimum period length |
| `number.<home_name>_best_price_minimum_periods` | Number | 110 | Target periods per day |
| `number.<home_name>_best_price_relaxation_attempts` | Number | 112 | Relaxation attempts |
| `number.<home_name>_best_price_gap_tolerance` | Number | 08 | Gap tolerance |
| `switch.<home_name>_best_price_enable_relaxation` | Switch | On/Off | Enable relaxation |
When an override entity is **enabled**, its value takes precedence over the menu setting. When **disabled** (default), the menu setting is used.
Changing a value triggers immediate period recalculation. Entity values are restored automatically after HA restarts.
### Example: Stricter detection in winter
<details>
<summary>Show YAML: Seasonal override automation</summary>
```yaml
automation:
- alias: "Winter: Stricter Best Price Detection"
trigger:
- platform: time
at: "00:00:00"
condition:
- condition: template
value_template: "{{ now().month in [11, 12, 1, 2] }}"
action:
- service: number.set_value
target:
entity_id: number.<home_name>_best_price_flexibility
data:
value: 10 # Stricter than default 15%
```
</details>
See **[Runtime Override Entities](config-runtime-overrides.md)** for more details, including how overrides work, how to view entity descriptions, and recorder optimization.

View file

@ -0,0 +1,23 @@
---
sidebar_label: 📊 Chart Data Export
---
# 📊 Chart Data Export Sensor (Legacy)
**Settings → Devices & Services → Tibber Prices → Configure → 📊 Chart Data Export**
---
:::caution Legacy feature
The Chart Data Export **sensor** is a legacy mechanism from early versions of this integration. For new setups, use the **[get_chartdata action](chart-actions.md)** instead — it is more flexible, does not require a dedicated sensor, and returns data on demand.
:::
## What this page does
This configuration page controls whether the legacy chart data export sensor is active. If you already use this sensor in existing dashboards or automations and don't want to migrate yet, leave it enabled.
## Migration to actions
The [Chart Actions](chart-actions.md) page covers the recommended approach for fetching chart data via HA actions (formerly services), including ready-to-use examples for ApexCharts and other chart cards.
If you have existing automations or cards using the legacy sensor, the [Chart Data Export legacy reference](chart-actions.md) includes migration guidance.

View file

@ -0,0 +1,62 @@
---
sidebar_label: 💱 Currency Display
---
# 💱 Currency Display
**Settings → Devices & Services → Tibber Prices → Configure → 💱 Currency Display**
---
## Display Mode
Choose whether price sensor states show values in **base currency** or **subunit**:
| Mode | Example | Smart default |
|------|---------|---------------|
| **Base currency** | 0.2534 €/kWh, 2.53 kr/kWh | NOK, SEK, DKK |
| **Subunit** (default for EUR) | 25.34 ct/kWh, 25.3 øre/kWh | EUR |
The smart default is automatically applied when you first set up the integration based on your Tibber account currency.
:::caution Decide before building automations
Switching the display mode later changes **all** price sensor state values (e.g., 25.34 → 0.2534). This will break:
- Numeric thresholds in automations and conditions
- Template sensors and conditional cards with hardcoded values
**If you do switch later:**
1. A **repair notification** from this integration appears immediately in your sidebar — it reminds you to update automations and dashboards.
2. HA's Recorder detects the unit mismatch and shows a **"The unit has changed"** dialog (may take a few minutes or until the next statistics run). Choose **"Delete all old statistic data"** to start fresh. Do _not_ choose "Update the unit without converting" — that re-labels old numbers with the new unit, making historical values factually wrong.
3. Update every **automation trigger and condition** with a numeric price value.
4. Update **dashboard cards** with hardcoded thresholds or unit labels.
:::
## Price Precision and Rounding
All prices are received from the Tibber API in base currency and processed without loss of precision. The sensor **state value** is rounded and stored as follows:
| Display Mode | Stored precision | Example |
|---|---|---|
| **Subunit** (ct, øre) | 2 decimal places | 25.34 ct/kWh |
| **Base currency** (€, kr) | 4 decimal places | 0.2534 €/kWh |
This applies to both sensor states and attributes (e.g., `energy_price`, `price_mean`, `price_min`).
### Default display precision
Home Assistant shows fewer decimals than the stored value by default — enough for a quick glance. The integration sets these defaults per sensor type:
| Sensor type | Subunit default | Base currency default |
|---|---|---|
| **Current / Next / Previous interval price** | 2 decimals (25.34 ct) | 4 decimals (0.2534 €) |
| **All other price sensors** (averages, min/max, …) | 1 decimal (25.3 ct) | 2 decimals (0.25 €) |
| **Energy Dashboard sensor** | — | 4 decimals (always) |
You can override the displayed precision per entity in the HA UI:
1. Go to **Settings → Devices & Services → Entities**
2. Select a price sensor → click the gear icon
3. Change **Display precision** to your preference
**Practical ceiling:** Subunit values have exactly 2 decimal places stored — setting more than 2 shows trailing zeros. Base currency values have 4 decimal places stored — 34 decimals are meaningful.

View file

@ -0,0 +1,73 @@
---
sidebar_label: ⚙️ General Settings
---
# ⚙️ General Settings
**Settings → Devices & Services → Tibber Prices → Configure → ⚙️ General Settings**
---
## Extended Entity Descriptions
Controls whether sensor attributes include detailed explanations and usage tips.
| State | Attributes included |
|-------|---------------------|
| **Disabled** (default) | `description` only — brief one-liner |
| **Enabled** | `description` + `long_description` + `usage_tips` |
Enable this while getting familiar with the integration. Once you know what each sensor does, disabling it reduces attribute clutter in your Developer Tools / history views.
## Average Sensor Display
Controls which statistical measure the sensor **state value** shows for all average price sensors. The other value is always available as an attribute regardless of this setting.
| Mode | Shows | Best for |
|------|-------|----------|
| **Median** (default) | Middle value when prices are sorted | Dashboards, typical price level |
| **Arithmetic Mean** | Mathematical average of all prices | Cost calculations, budgeting |
### Why the difference matters
Consider a day with these prices: `10, 12, 13, 15, 80 ct/kWh`
- **Median = 13 ct/kWh** — "typical" price (ignores the expensive spike)
- **Mean = 26 ct/kWh** — average cost if consuming evenly (spike included)
The median gives a better feel for what the day was like. The mean is more accurate for calculating what you actually paid on average.
### Both values always available
You can always access both values as attributes from any average sensor, regardless of this display setting:
```yaml
{{ state_attr('sensor.<home_name>_price_today', 'price_median') }}
{{ state_attr('sensor.<home_name>_price_today', 'price_mean') }}
```
This means you can change the display setting at any time without breaking automations that use attributes.
### Affected sensors
This setting applies to:
- Daily average sensors (today, tomorrow)
- 24-hour rolling averages (trailing, leading)
- Hourly smoothed prices (current hour, next hour)
- Future forecast sensors (next 1h, 2h, 3h, … 12h)
See **[Average Sensors](sensors-average.md)** for detailed examples.
### Choosing your mode
**Choose Median if:**
- 👥 You show prices to users ("What's today like?")
- 📊 You want dashboard values representing typical conditions
- 🎯 You compare price levels across days
**Choose Mean if:**
- 💰 You calculate costs and budgets
- 🧮 You need mathematical accuracy for financial planning
- 📊 You track actual average costs over time
**Pro tip:** Most users prefer **Median** for displays, but use the `price_mean` attribute in cost calculation automations.

View file

@ -0,0 +1,56 @@
---
sidebar_label: 🔴 Peak Price Period
---
# 🔴 Peak Price Period
**Settings → Devices & Services → Tibber Prices → Configure → 🔴 Peak Price Period**
---
Peak Price Period sensors detect windows of time when electricity is expensive enough that you should avoid or postpone consumption. The binary sensor `is_peak_price_period` is `on` during these windows.
The detection algorithm mirrors [Best Price Period](config-best-price.md), but in reverse — looking for expensive intervals rather than cheap ones.
See **[Period Calculation](period-calculation.md)** for an in-depth explanation of the detection algorithm and [Period Relaxation](period-relaxation.md) for how relaxation works.
## Settings
### Period Settings
| Setting | Default | Description |
|---------|---------|-------------|
| **Minimum period length** | 60 min | Shortest window to report as a period |
| **Minimum price level** | EXPENSIVE | Only intervals at this Tibber level or more expensive qualify |
| **Gap tolerance** | 1 | Consecutive below-threshold intervals allowed inside a period — bridges small price dips between two expensive windows |
### Flexibility Settings
| Setting | Default | Description |
|---------|---------|-------------|
| **Flex percentage** | -20% | How far below the daily maximum a price can be and still qualify (negative value = below maximum) |
| **Minimum distance from average** | 5% | Qualifying intervals must be at least this far above the daily average |
### Relaxation & Target
| Setting | Default | Description |
|---------|---------|-------------|
| **Enable minimum period target** | On | Automatically loosens criteria until the target count is reached |
| **Target periods per day** | 2 | How many distinct peak periods the algorithm aims to find per day |
| **Relaxation attempts** | 11 | How many times to loosen the criteria before giving up |
## Runtime Override Entities
Same concept as [Best Price overrides](config-best-price.md#runtime-override-entities) — disabled by default, enable individually in Entities.
| Entity | Type | Range | Overrides |
|--------|------|-------|-----------|
| `number.<home_name>_peak_price_flexibility` | Number | -500% | Flex percentage |
| `number.<home_name>_peak_price_minimum_distance` | Number | 050% | Minimum distance from average |
| `number.<home_name>_peak_price_minimum_period_length` | Number | 15180 min | Minimum period length |
| `number.<home_name>_peak_price_minimum_periods` | Number | 110 | Target periods per day |
| `number.<home_name>_peak_price_relaxation_attempts` | Number | 112 | Relaxation attempts |
| `number.<home_name>_peak_price_gap_tolerance` | Number | 08 | Gap tolerance |
| `switch.<home_name>_peak_price_enable_relaxation` | Switch | On/Off | Enable relaxation |
See **[Runtime Override Entities](config-runtime-overrides.md)** for full details on how overrides work, viewing entity descriptions, and recorder optimization.

View file

@ -0,0 +1,37 @@
---
sidebar_label: 🏷️ Price Level
---
# 🏷️ Price Level Gap Tolerance
**Settings → Devices & Services → Tibber Prices → Configure → 🏷️ Price Level**
---
Tibber's API assigns each interval a price level: VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, or VERY_EXPENSIVE. In practice, a single interval can jump to a different level briefly before jumping back — creating isolated "noise" intervals that make sensors flicker.
Gap tolerance smooths this out.
## Setting
| Setting | Default | Description |
|---------|---------|-------------|
| **Gap tolerance** | 1 | Number of consecutive "mismatched" intervals to fill in automatically |
### Example
With gap tolerance = 1, a lone NORMAL interval surrounded by CHEAP on both sides is automatically corrected to CHEAP:
```
Before: CHEAP CHEAP NORMAL CHEAP CHEAP
After: CHEAP CHEAP CHEAP CHEAP CHEAP
↑ filled in
```
With gap tolerance = 0, no smoothing is applied and every interval uses the raw API level.
## Notes
- This applies to Tibber's own level classification (separate from the [Price Rating](config-price-rating.md) which is calculated by this integration)
- Increasing gap tolerance beyond 2 is rarely useful — larger gaps usually represent genuine price differences
- The gap tolerance here only affects level sensors; the separate gap tolerance in [Best Price](config-best-price.md) and [Peak Price](config-peak-price.md) settings controls period merging behavior

View file

@ -0,0 +1,44 @@
---
sidebar_label: 📊 Price Rating
---
# 📊 Price Rating Thresholds
**Settings → Devices & Services → Tibber Prices → Configure → 📊 Price Rating**
---
Price ratings classify each 15-minute interval as **LOW**, **NORMAL**, or **HIGH** relative to the 24-hour trailing average. Sensors and automations can use these ratings to decide when to run appliances.
See **[Ratings & Levels](sensors-ratings-levels.md)** for a full explanation of how ratings work and which sensors expose them.
## Settings
| Setting | Default | Description |
|---------|---------|-------------|
| **Low threshold** | -10% | Prices this far below the trailing average → rated **LOW** |
| **High threshold** | +10% | Prices this far above the trailing average → rated **HIGH** |
| **Hysteresis** | 2% | Buffer zone around thresholds — prevents rapid flickering when a price hovers right at the boundary |
| **Gap tolerance** | 1 | Smooths isolated rating blocks: a lone NORMAL interval surrounded by LOW on both sides gets corrected to LOW |
## How thresholds are applied
```
Trailing 24h average: 20 ct/kWh
Low threshold: -10% → prices ≤ 18 ct → LOW
High threshold: +10% → prices ≥ 22 ct → HIGH
Everything else → NORMAL
```
Hysteresis adds an inner dead-band: once a rating is set to LOW, it stays LOW until the price rises above `18 ct + 2% = 18.36 ct`. This prevents sensors from flickering between LOW and NORMAL when prices are right at the boundary.
## Adjusting for your market
**Markets with low daily price variation** (e.g., day typically stays within ±5%):
- Lower the thresholds: try -5% / +5%
- This keeps meaningful LOW/HIGH periods even on calm days
**Markets with high daily variation** (e.g., ±30% swings):
- Raise the thresholds: try -15% / +15%
- This reserves LOW/HIGH for genuinely exceptional periods only
- Consider using [Volatility](config-volatility.md) sensors alongside ratings on such days

View file

@ -0,0 +1,40 @@
---
sidebar_label: 📈 Price Trend
---
# 📈 Price Trend Thresholds
**Settings → Devices & Services → Tibber Prices → Configure → 📈 Price Trend**
---
Price trend sensors compare the upcoming price average to the current price and report whether prices are rising, falling, or stable. These thresholds define how much of a change is required before the trend sensor changes state.
See **[Trend Sensors](sensors-trends.md)** for a full explanation of all trend sensors, how volatility-adaption works, and automation examples.
## Settings
| Setting | Default | Description |
|---------|---------|-------------|
| **Rising** | +3% | Future average this much above current → `rising` |
| **Strongly rising** | +9% | Future average far above current → `strongly_rising` |
| **Falling** | -3% | Future average this much below current → `falling` |
| **Strongly falling** | -9% | Future average far below current → `strongly_falling` |
Prices within the rising/falling range are reported as `stable`.
## Volatility-adaptive thresholds
On high-volatility days, the thresholds automatically widen to prevent the trend sensor from flickering constantly due to natural price variation. The effective threshold is scaled based on the day's [volatility level](config-volatility.md):
- **Low volatility**: Thresholds used as-is
- **Moderate volatility**: Thresholds slightly widened
- **High / Very High volatility**: Thresholds significantly widened
This means the same `rising` threshold (3%) may correspond to a 5% effective threshold on a volatile day. The scaling is automatic — you only need to configure the baseline values here.
## Adjusting for your market
- If trend sensors flicker too often on typical days → increase all thresholds slightly (e.g., 4% / 12%)
- If trend sensors rarely change even on obviously moving price days → decrease thresholds (e.g., 2% / 6%)
- For markets with structural day/night patterns, consider using the `strongly_*` states in automations to ensure only major movements trigger actions

View file

@ -0,0 +1,110 @@
---
sidebar_label: 🔁 Runtime Override Entities
---
# Runtime Override Entities
The integration provides optional **number** and **switch** entities that let you change Best Price and Peak Price detection settings at runtime — through automations or the HA UI — without going into the configuration menu.
These entities are **disabled by default**. Enable them individually in:
**Settings → Devices & Services → Tibber Prices → Entities**
---
## How overrides work
1. **Entity disabled** (default): The configuration menu setting is used
2. **Entity enabled**: The entity value overrides the menu setting
3. **Value changes**: Trigger immediate period recalculation
4. **HA restart**: Entity values are restored automatically
This lets you write automations that adjust detection criteria seasonally, based on weather forecasts, or based on other conditions — without manual configuration changes.
## Available entities
### Best Price Period
| Entity | Type | Range | Overrides |
|--------|------|-------|-----------|
| `number.<home_name>_best_price_flexibility` | Number | 050% | [Flex percentage](config-best-price.md) |
| `number.<home_name>_best_price_minimum_distance` | Number | -500% | [Minimum distance from average](config-best-price.md) |
| `number.<home_name>_best_price_minimum_period_length` | Number | 15180 min | [Minimum period length](config-best-price.md) |
| `number.<home_name>_best_price_minimum_periods` | Number | 110 | [Target periods per day](config-best-price.md) |
| `number.<home_name>_best_price_relaxation_attempts` | Number | 112 | [Relaxation attempts](config-best-price.md) |
| `number.<home_name>_best_price_gap_tolerance` | Number | 08 | [Gap tolerance](config-best-price.md) |
| `switch.<home_name>_best_price_enable_relaxation` | Switch | On/Off | [Enable relaxation](config-best-price.md) |
### Peak Price Period
| Entity | Type | Range | Overrides |
|--------|------|-------|-----------|
| `number.<home_name>_peak_price_flexibility` | Number | -500% | [Flex percentage](config-peak-price.md) |
| `number.<home_name>_peak_price_minimum_distance` | Number | 050% | [Minimum distance from average](config-peak-price.md) |
| `number.<home_name>_peak_price_minimum_period_length` | Number | 15180 min | [Minimum period length](config-peak-price.md) |
| `number.<home_name>_peak_price_minimum_periods` | Number | 110 | [Target periods per day](config-peak-price.md) |
| `number.<home_name>_peak_price_relaxation_attempts` | Number | 112 | [Relaxation attempts](config-peak-price.md) |
| `number.<home_name>_peak_price_gap_tolerance` | Number | 08 | [Gap tolerance](config-peak-price.md) |
| `switch.<home_name>_peak_price_enable_relaxation` | Switch | On/Off | [Enable relaxation](config-peak-price.md) |
## Viewing entity descriptions
Each override entity has a `description` attribute explaining what the setting does — the same text shown in the configuration menu.
**Note for Number entities:** Home Assistant shows a history graph by default in the entity detail view, which hides the attributes panel. To see the description:
1. Go to **Developer Tools → States**
2. Search for the entity (e.g., `number.<home_name>_best_price_flexibility`)
3. Expand the attributes section
Switch entities show their attributes normally in the entity details view.
## Example: Seasonal adjustment
<details>
<summary>Show YAML: Stricter detection in winter months</summary>
```yaml
automation:
- alias: "Winter: Stricter Best Price Detection"
trigger:
- platform: time
at: "00:00:00"
condition:
- condition: template
value_template: "{{ now().month in [11, 12, 1, 2] }}"
action:
- service: number.set_value
target:
entity_id: number.<home_name>_best_price_flexibility
data:
value: 10 # Stricter than default 15%
```
</details>
## Recorder optimization (optional)
These entities are already designed to minimize database impact:
- **EntityCategory.CONFIG** — excluded from Long-Term Statistics
- All attributes excluded from history recording
- Only state value (the number/switch state) is recorded
If you want to **completely exclude** these entities from the recorder (no history graph, no database entries at all):
<details>
<summary>Show YAML: Exclude from recorder</summary>
```yaml
recorder:
exclude:
entity_globs:
- number.*_best_price_*
- number.*_peak_price_*
- switch.*_best_price_*
- switch.*_peak_price_*
```
</details>
This is useful if you rarely change these settings and want the smallest possible database footprint.

View file

@ -0,0 +1,36 @@
---
sidebar_label: 💨 Price Volatility
---
# 💨 Price Volatility Thresholds
**Settings → Devices & Services → Tibber Prices → Configure → 💨 Price Volatility**
---
Volatility sensors measure how much prices vary throughout the day using the **Coefficient of Variation (CV)** — the ratio of the standard deviation to the mean. A higher CV means more extreme price swings and greater optimization potential.
See **[Volatility Sensors](sensors-volatility.md)** for a full explanation of all volatility sensors and how to use them in automations.
## Thresholds
These thresholds define the boundaries between volatility levels:
| Level | Default CV | Meaning |
|-------|-----------|---------|
| **Moderate** | ≥ 15% | Noticeable variation — some optimization potential |
| **High** | ≥ 30% | Significant swings — good for timing optimization |
| **Very High** | ≥ 50% | Extreme volatility — maximum optimization benefit |
Days below the Moderate threshold are classified as **Low** volatility.
## Adjusting for your market
The defaults work well for most European electricity markets. You may want to adjust if:
- **Your market rarely exceeds 20% CV**: Lower the Moderate threshold to 10% so you still get meaningful classifications
- **Your market routinely hits 50%+ CV**: Raise the Very High threshold to 70%+ to distinguish truly exceptional days
:::tip Volatility affects Trend thresholds too
The [Price Trend](config-price-trend.md) thresholds automatically widen on high-volatility days to prevent constant state changes. Changes here indirectly affect trend sensitivity.
:::

View file

@ -0,0 +1,68 @@
# Configuration
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
## Initial Setup
After [installing](installation.md) the integration:
1. Go to **Settings → Devices & Services**
2. Click **+ Add Integration**
3. Search for **Tibber Price Information & Ratings**
4. **Enter your API token** from [developer.tibber.com](https://developer.tibber.com/settings/access-token)
5. **Select your Tibber home** from the dropdown (if you have multiple)
6. Click **Submit** — the integration starts fetching price data
The integration will immediately create sensors for your home. Data typically arrives within 12 minutes.
### Adding Additional Homes
If you have multiple Tibber homes (e.g., different locations):
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** → **Add another home**
3. Select the additional home from the dropdown
4. Each home gets its own set of sensors with unique entity IDs
## Options Menu
After initial setup, open the configuration menu at:
**Settings → Devices & Services → Tibber Prices → Configure**
A menu appears with all configuration sections. Pick any section, adjust settings, then return to the menu — there is no required order. All sections have sensible defaults and can be revisited independently at any time.
```mermaid
graph LR
Menu["⚙️ Configure"]
Menu --> GS["⚙️ General Settings"]
Menu --> DS["💱 Currency Display"]
Menu --> PR["📊 Price Rating"]
Menu --> PL["🏷️ Price Level"]
Menu --> VO["💨 Price Volatility"]
Menu --> BP["💚 Best Price Period"]
Menu --> PP["🔴 Peak Price Period"]
Menu --> PT["📈 Price Trend"]
Menu --> CD["📊 Chart Data Export"]
Menu --> RD["🔄 Reset to Defaults"]
style Menu fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style BP fill:#e6fff5,stroke:#00c853,stroke-width:2px
style PP fill:#fff0f0,stroke:#ff5252,stroke-width:2px
```
| Section | What you configure |
|---------|-------------------|
| [⚙️ General Settings](config-general.md) | Extended descriptions, average sensor display mode (Median / Mean) |
| [💱 Currency Display](config-currency.md) | Base currency vs. subunit display, price precision |
| [📊 Price Rating](config-price-rating.md) | LOW / HIGH thresholds, hysteresis, gap tolerance |
| [🏷️ Price Level](config-price-level.md) | Gap tolerance for Tibber's API-provided level classifications |
| [💨 Price Volatility](config-volatility.md) | CV thresholds for Moderate / High / Very High volatility |
| [💚 Best Price Period](config-best-price.md) | Cheap window detection: flex, distance, relaxation, runtime overrides |
| [🔴 Peak Price Period](config-peak-price.md) | Expensive window detection: same settings, opposite direction |
| [📈 Price Trend](config-price-trend.md) | Rising / Falling thresholds for trend sensors |
| [📊 Chart Data Export](config-chart-export.md) | Legacy chart export sensor (new setups: use [Chart Actions](chart-actions.md)) |
Advanced: [🔁 Runtime Override Entities](config-runtime-overrides.md) — number and switch entities for automating configuration changes at runtime.

View file

@ -0,0 +1,230 @@
# Dashboard Examples
Beautiful dashboard layouts using Tibber Prices sensors.
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
## Basic Price Display Card
Simple card showing current price with dynamic color:
<details>
<summary>Show YAML: Current Price Card</summary>
```yaml
type: entities
title: Current Electricity Price
entities:
- entity: sensor.<home_name>_current_electricity_price
name: Current Price
icon: mdi:flash
- entity: sensor.<home_name>_current_price_rating
name: Price Rating
- entity: sensor.<home_name>_next_electricity_price
name: Next Price
```
</details>
## Period Status Cards
Show when best/peak price periods are active:
<details>
<summary>Show YAML: Best and Peak Period Card</summary>
```yaml
type: horizontal-stack
cards:
- type: entity
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Active
icon: mdi:currency-eur-off
- type: entity
entity: binary_sensor.<home_name>_peak_price_period
name: Peak Price Active
icon: mdi:alert
```
</details>
## Custom Button Card Examples
### Price Level Card
<details>
<summary>Show YAML: Price Level Card</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Price Level
show_state: true
styles:
card:
- background: |
[[[
if (entity.state === 'LOWEST') return 'linear-gradient(135deg, #00ffa3 0%, #00d4ff 100%)';
if (entity.state === 'LOW') return 'linear-gradient(135deg, #4dddff 0%, #00ffa3 100%)';
if (entity.state === 'NORMAL') return 'linear-gradient(135deg, #ffd700 0%, #ffb800 100%)';
if (entity.state === 'HIGH') return 'linear-gradient(135deg, #ff8c00 0%, #ff6b00 100%)';
if (entity.state === 'HIGHEST') return 'linear-gradient(135deg, #ff4500 0%, #dc143c 100%)';
return 'var(--card-background-color)';
]]]
```
</details>
## Lovelace Layouts
### Compact Mobile View
Optimized for mobile devices:
<details>
<summary>Show YAML: Optimized for mobile devices</summary>
```yaml
type: vertical-stack
cards:
- type: custom:mini-graph-card
entities:
- entity: sensor.<home_name>_current_electricity_price
name: Today's Prices
hours_to_show: 24
points_per_hour: 4
- type: glance
entities:
- entity: sensor.<home_name>_best_price_start
name: Best Period Starts
- entity: binary_sensor.<home_name>_best_price_period
name: Active Now
```
</details>
### Desktop Dashboard
Full-width layout for desktop:
<details>
<summary>Show YAML: Full-width layout for desktop</summary>
```yaml
type: grid
columns: 3
square: false
cards:
- type: custom:apexcharts-card
# See chart-examples.md for ApexCharts config
- type: vertical-stack
cards:
- type: entities
title: Current Status
entities:
- sensor.<home_name>_current_electricity_price
- sensor.<home_name>_current_price_rating
- type: vertical-stack
cards:
- type: entities
title: Statistics
entities:
- sensor.<home_name>_price_today
- sensor.<home_name>_today_s_lowest_price
- sensor.<home_name>_today_s_highest_price
```
</details>
## Icon Color Integration
Using the `icon_color` attribute for dynamic colors:
<details>
<summary>Show YAML: Icon Color Integration</summary>
```yaml
type: custom:mushroom-chips-card
chips:
- type: entity
entity: sensor.<home_name>_current_electricity_price
icon_color: "{{ state_attr('sensor.<home_name>_current_electricity_price', 'icon_color') }}"
- type: entity
entity: binary_sensor.<home_name>_best_price_period
icon_color: green
- type: entity
entity: binary_sensor.<home_name>_peak_price_period
icon_color: red
```
</details>
See [Icon Colors](icon-colors.md) for detailed color mapping.
## Picture Elements Dashboard
Advanced interactive dashboard:
<details>
<summary>Show YAML: Advanced interactive dashboard</summary>
```yaml
type: picture-elements
image: /local/electricity_dashboard_bg.png
elements:
- type: state-label
entity: sensor.<home_name>_current_electricity_price
style:
top: 20%
left: 50%
font-size: 32px
font-weight: bold
- type: state-badge
entity: binary_sensor.<home_name>_best_price_period
style:
top: 40%
left: 30%
# Add more elements...
```
</details>
## Auto-Entities Dynamic Lists
Automatically list all price sensors:
<details>
<summary>Show YAML: Auto-Entities Sensor List</summary>
```yaml
type: custom:auto-entities
card:
type: entities
title: All Price Sensors
filter:
include:
- entity_id: "sensor.<home_name>_*_price"
exclude:
- state: unavailable
sort:
method: state
numeric: true
```
</details>
---
💡 **Related:**
- [Chart Examples](chart-examples.md) - ApexCharts configurations
- [Dynamic Icons](dynamic-icons.md) - Icon behavior
- [Icon Colors](icon-colors.md) - Color attributes

View file

@ -0,0 +1,85 @@
# Data & Utility Actions
Actions for fetching raw price data and managing integration state.
---
## tibber_prices.get_price
**Purpose:** Fetches raw price interval data for any time range. Uses intelligent caching — only intervals not already cached are fetched from the Tibber API.
**Parameters:**
| Parameter | Description | Required |
|-----------|-------------|----------|
| `entry_id` | Config entry ID | Yes |
| `start_time` | Start of the time range | Yes |
| `end_time` | End of the time range | Yes |
**Example:**
<details>
<summary>Show YAML: Get Price</summary>
```yaml
service: tibber_prices.get_price
data:
entry_id: YOUR_CONFIG_ENTRY_ID
start_time: "2025-11-01T00:00:00"
end_time: "2025-11-02T00:00:00"
response_variable: price_data
```
</details>
**Response Format:**
<details>
<summary>Show JSON: Get Price Response</summary>
```json
{
"home_id": "abc-123",
"start_time": "2025-11-01T00:00:00+01:00",
"end_time": "2025-11-02T00:00:00+01:00",
"interval_count": 96,
"price_info": [
{
"startsAt": "2025-11-01T00:00:00+01:00",
"total": 0.2534,
"energy": 0.1218,
"tax": 0.1316
}
]
}
```
</details>
**Use cases:**
- Fetching historical price data for analysis
- Comparing prices across arbitrary date ranges
- Building custom charts with historical data
**Note:** Times are automatically converted to your Tibber home's timezone. The interval pool caches previously fetched intervals, so repeated calls for the same range are fast.
---
## tibber_prices.refresh_user_data
**Purpose:** Forces an immediate refresh of user data (homes, subscriptions) from the Tibber API.
**Example:**
<details>
<summary>Show YAML: Refresh User Data</summary>
```yaml
service: tibber_prices.refresh_user_data
data:
entry_id: YOUR_CONFIG_ENTRY_ID
```
</details>
**Note:** User data is cached for 24 hours. Trigger this action only when you need immediate updates (e.g., after changing Tibber subscriptions).

View file

@ -0,0 +1,217 @@
# Dynamic Icons
Many sensors in the Tibber Prices integration automatically change their icon based on their current state. This provides instant visual feedback about price levels, trends, and periods without needing to read the actual values.
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
## What are Dynamic Icons?
Instead of having a fixed icon, some sensors update their icon to reflect their current state:
- **Price level sensors** show different cash/money icons depending on whether prices are cheap or expensive
- **Price rating sensors** show thumbs up/down based on how the current price compares to average
- **Volatility sensors** show different chart types based on price stability
- **Binary sensors** show different icons when ON vs OFF (e.g., piggy bank when in best price period)
The icons change automatically - no configuration needed!
## How to Check if a Sensor Has Dynamic Icons
To see which icon a sensor currently uses:
1. Go to **Developer Tools****States** in Home Assistant
2. Search for your sensor (e.g., `sensor.<home_name>_current_price_level`)
3. Look at the icon displayed in the entity row
4. Change conditions (wait for price changes) and check if the icon updates
**Common sensor types with dynamic icons:**
- Price level sensors (e.g., `current_price_level``current_interval_price_level`)
- Price rating sensors (e.g., `current_price_rating``current_interval_price_rating`)
- Volatility sensors (e.g., `today_s_price_volatility``today_volatility`)
- Binary sensors (e.g., `best_price_period`, `peak_price_period`)
## Using Dynamic Icons in Your Dashboard
### Standard Entity Cards
Dynamic icons work automatically in standard Home Assistant cards:
<details>
<summary>Show YAML: Standard Entity Cards</summary>
```yaml
type: entities
entities:
- entity: sensor.<home_name>_current_price_level
- entity: sensor.<home_name>_current_price_rating
- entity: sensor.<home_name>_today_s_price_volatility
- entity: binary_sensor.<home_name>_best_price_period
```
</details>
The icons will update automatically as the sensor states change.
### Glance Card
<details>
<summary>Show YAML: Glance Card</summary>
```yaml
type: glance
entities:
- entity: sensor.<home_name>_current_price_level
name: Price Level
- entity: sensor.<home_name>_current_price_rating
name: Rating
- entity: binary_sensor.<home_name>_best_price_period
name: Best Price
```
</details>
### Custom Button Card
<details>
<summary>Show YAML: Custom Button Card</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
# Icon updates automatically - no need to specify it!
```
</details>
### Mushroom Entity Card
<details>
<summary>Show YAML: Mushroom Entity Card</summary>
```yaml
type: custom:mushroom-entity-card
entity: sensor.<home_name>_today_s_price_volatility
name: Price Volatility
# Icon changes automatically based on volatility level
```
</details>
## Overriding Dynamic Icons
If you want to use a fixed icon instead of the dynamic one:
### In Entity Cards
<details>
<summary>Show YAML: Fixed Icon in Entity Card</summary>
```yaml
type: entities
entities:
- entity: sensor.<home_name>_current_price_level
icon: mdi:lightning-bolt # Fixed icon, won't change
```
</details>
### In Custom Button Card
<details>
<summary>Show YAML: Fixed Icon in Button Card</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_rating
name: Price Rating
icon: mdi:chart-line # Fixed icon overrides dynamic behavior
show_state: true
```
</details>
## Combining with Dynamic Colors
Dynamic icons work great together with dynamic colors! See the **[Dynamic Icon Colors Guide](icon-colors.md)** for examples.
**Example: Dynamic icon AND color**
<details>
<summary>Show YAML: Dynamic Icon and Color</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price
show_state: true
# Icon changes automatically (cheap/expensive cash icons)
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
```
</details>
This gives you both:
- ✅ Different icon based on state (e.g., cash-plus when cheap, cash-remove when expensive)
- ✅ Different color based on state (e.g., green when cheap, red when expensive)
## Icon Behavior Details
### Binary Sensors
Binary sensors may have different icons for different states:
- **ON state**: Typically shows an active/alert icon
- **OFF state**: May show different icons depending on whether future periods exist
- Has upcoming periods: Timer/waiting icon
- No upcoming periods: Sleep/inactive icon
**Example:** `binary_sensor.<home_name>_best_price_period`
- When ON: Shows a piggy bank (good time to save money)
- When OFF with future periods: Shows a timer (waiting for next period)
- When OFF without future periods: Shows a sleep icon (no periods expected soon)
### State-Based Icons
Sensors with text states (like `cheap`, `normal`, `expensive`) typically show icons that match the meaning:
- Lower/better values → More positive icons
- Higher/worse values → More cautionary icons
- Normal/average values → Neutral icons
The exact icons are chosen to be intuitive and meaningful in the Home Assistant ecosystem.
## Troubleshooting
**Icon not changing:**
- Wait for the sensor state to actually change (prices update every 15 minutes)
- Check in Developer Tools → States that the sensor state is changing
- If you've set a custom icon in your card, it will override the dynamic icon
**Want to see the icon code:**
- Look at the entity in Developer Tools → States
- The `icon` attribute shows the current Material Design icon code (e.g., `mdi:cash-plus`)
**Want different icons:**
- You can override icons in your card configuration (see examples above)
- Or create a template sensor with your own icon logic
## See Also
- [Dynamic Icon Colors](icon-colors.md) - Color your icons based on state
- [Sensors Overview](sensors-overview.md) - Complete list of available sensors
- [Automation Examples](automation-examples.md) - Use dynamic icons in automations

View file

@ -0,0 +1,171 @@
# FAQ - Frequently Asked Questions
Common questions about the Tibber Prices integration.
## General Questions
### Why don't I see tomorrow's prices yet?
Tomorrow's prices are published by Tibber around **13:00 CET** (12:00 UTC in winter, 11:00 UTC in summer).
- **Before publication**: Sensors show `unavailable` or use today's data
- **After publication**: Integration automatically fetches new data within 15 minutes
- **No manual refresh needed** - polling happens automatically
### How often does the integration update data?
- **API Polling**: Every 15 minutes
- **Sensor Updates**: On quarter-hour boundaries (00, 15, 30, 45 minutes)
- **Cache**: Price data cached until midnight (reduces API load)
### Can I use multiple Tibber homes?
Yes! Use the **"Add another home"** option:
1. Settings → Devices & Services → Tibber Prices
2. Click "Configure" → "Add another home"
3. Select additional home from dropdown
4. Each home gets separate sensors with unique entity IDs
### Does this work without a Tibber subscription?
No, you need:
- Active Tibber electricity contract
- API token from [developer.tibber.com](https://developer.tibber.com/)
The integration is free, but requires Tibber as your electricity provider.
## Configuration Questions
### What are good values for price thresholds?
**Default values work for most users:**
- High Price Threshold: 30% above average
- Low Price Threshold: 15% below average
**Adjust if:**
- You're in a market with high volatility → increase thresholds
- You want more sensitive ratings → decrease thresholds
- Seasonal changes → review every few months
### How do I optimize Best Price Period detection?
**Key parameters:**
- **Flex**: 15-20% is optimal (default 15%)
- **Min Distance**: 5-10% recommended (default 5%)
- **Rating Levels**: Start with "CHEAP + VERY_CHEAP" (default)
- **Relaxation**: Keep enabled (helps find periods on expensive days)
See [Period Calculation](period-calculation.md) for detailed tuning guide.
### Why do I sometimes only get 1 period instead of 2?
This happens on **high-price days** when:
- Few intervals meet your criteria
- Relaxation is disabled
- Flex is too low
- Min Distance is too strict
**Solutions:**
1. Enable relaxation (recommended)
2. Increase flex to 20-25%
3. Reduce min_distance to 3-5%
4. Add more rating levels (include "NORMAL")
## Troubleshooting
### Sensors show "unavailable"
**Common causes:**
1. **API Token invalid** → Check token at developer.tibber.com
2. **No internet connection** → Check HA network
3. **Tibber API down** → Check [status.tibber.com](https://status.tibber.com)
4. **Integration not loaded** → Restart Home Assistant
### Best Price Period is ON all day
This means **all intervals meet your criteria** (very cheap day!):
- Not an error - enjoy the low prices!
- Consider tightening filters (lower flex, higher min_distance)
- Or add automation to only run during first detected period
### Prices are in wrong currency or wrong units
**Currency** is determined by your Tibber subscription (cannot be changed).
**Display mode** (base vs. subunit) is configurable:
- Configure in: `Settings > Devices & Services > Tibber Prices > Configure`
- Options:
- **Base currency**: €/kWh, kr/kWh (stored at 4 decimal places, default display: 2 decimals, e.g., 0.25)
- **Subunit**: ct/kWh, øre/kWh (stored at 2 decimal places, default display: 1 decimal, e.g., 25.3)
- Smart defaults: EUR → subunit, NOK/SEK/DKK → base currency
- You can increase the displayed decimals per entity in the HA UI (see [Currency Display](config-currency.md))
If you see unexpected units, check your configuration in the integration options.
### Tomorrow data not appearing at all
**Check:**
1. Your Tibber home has hourly price contract (not fixed price)
2. API token has correct permissions
3. Integration logs for API errors (`/config/home-assistant.log`)
4. Tibber actually published data (check Tibber app)
## Automation Questions
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
### How do I run dishwasher during cheap period?
<details>
<summary>Show YAML: Dishwasher During Cheap Period</summary>
```yaml
automation:
- alias: "Dishwasher during Best Price"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
- condition: time
after: "20:00:00" # Only start after 8 PM
action:
- service: switch.turn_on
target:
entity_id: switch.dishwasher
```
</details>
See [Automation Examples](automation-examples.md) for more recipes.
### Can I avoid peak prices automatically?
Yes! Use Peak Price Period binary sensor:
<details>
<summary>Show YAML: Avoid Peak Prices Automatically</summary>
```yaml
automation:
- alias: "Disable charging during peak prices"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_peak_price_period
to: "on"
action:
- service: switch.turn_off
target:
entity_id: switch.ev_charger
```
</details>
---
💡 **Still need help?**
- [Troubleshooting Guide](troubleshooting.md)
- [GitHub Issues](https://github.com/jpawlowski/hass.tibber_prices/issues)

View file

@ -0,0 +1,119 @@
---
comments: false
---
# Glossary
Quick reference for terms used throughout the documentation.
## A
**API Token**
: Your personal access key from Tibber. Get it at [developer.tibber.com](https://developer.tibber.com/settings/access-token).
**Attributes**
: Additional data attached to each sensor (timestamps, statistics, metadata). Access via `state_attr()` in templates.
## B
**Best Price Period**
: Automatically detected time window with favorable electricity prices. Ideal for scheduling dishwashers, heat pumps, EV charging.
**Binary Sensor**
: Sensor with ON/OFF state (e.g., "Best Price Period Active"). Used in automations as triggers.
## C
**Config Entry ID** (also: `entry_id`)
: A unique identifier assigned by Home Assistant to each configured integration instance. When this integration is used with multiple Tibber homes, each home gets its own Config Entry ID. All actions (`get_chartdata`, `get_apexcharts_yaml`, etc.) require this value as the `entry_id` parameter so that Home Assistant knows which home to query.
- **In the Action UI**: The field appears as a dropdown — select your home and HA fills in the ID automatically.
- **In YAML**: Go to **Settings → Devices & Services**, find the **Tibber Prices** card, open the **⋮** menu, and choose **"Copy Config Entry ID"**.
**Currency Display Mode**
: Configurable setting for how prices are shown. Choose base currency (€, kr) or subunit (ct, øre). Smart defaults apply: EUR → subunit, NOK/SEK/DKK → base.
**Coordinator**
: Home Assistant component managing data fetching and updates. Polls Tibber API every 15 minutes.
## D
**Dynamic Icons**
: Icons that change based on sensor state (e.g., battery icons showing price level). See [Dynamic Icons](dynamic-icons.md).
## F
**Flex (Flexibility)**
: Configuration parameter controlling how strict period detection is. Higher flex = more periods found, but potentially at higher prices.
## I
**Interval**
: 15-minute time slot with fixed electricity price (00:00-00:15, 00:15-00:30, etc.).
## L
**Level**
: Price classification within a day (LOWEST, LOW, NORMAL, HIGH, HIGHEST). Based on daily min/max prices.
## M
**Min Distance**
: Threshold requiring periods to be at least X% below daily average. Prevents detecting "cheap" periods during expensive days.
## P
**Peak Price Period**
: Time window with highest electricity prices. Use to avoid heavy consumption.
**Price Info**
: Complete dataset with all intervals (yesterday, today, tomorrow) including enriched statistics.
## Q
**Quarter-Hourly**
: 15-minute precision (4 intervals per hour, 96 per day).
## R
**Rating**
: Statistical price classification (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE). Based on 24h averages and thresholds.
**Relaxation**
: Automatic loosening of period detection filters when target period count isn't met. Ensures you always get usable periods.
## S
**State**
: Current value of a sensor (e.g., price in ct/kWh, "ON"/"OFF" for binary sensors).
**State Class**
: Home Assistant classification for long-term statistics (MEASUREMENT, TOTAL, or none).
## T
**Trailing Average**
: Average price over the past 24 hours from current interval.
**Leading Average**
: Average price over the next 24 hours from current interval.
**Trend**
: Directional price movement indicator. Simple trends compare current price to future averages (1h12h). Current trend represents the ongoing price direction using a 3-hour outlook. Uses a 5-level scale: strongly_falling, falling, stable, rising, strongly_rising.
**Trend Hysteresis**
: Stability mechanism for trend change prediction. Requires 2 consecutive intervals confirming a different trend before reporting a change. Prevents false alarms from single-interval price spikes.
## V
**V-Shaped Day** (also: U-Shaped Day)
: Informal term for a day where prices dip to a minimum in the middle and rise again on both sides. Both V-shaped (sharp, brief dip) and U-shaped (broad, extended plateau) days are classified as **`valley`** by the [Day Pattern sensor](sensors-price-phases.md#day-pattern-sensors). The Best Price Period covers only the absolute minimum, but favorable conditions may last much longer. See [V-Shaped Days](concepts.md#v-shaped-and-u-shaped-price-days).
**Volatility**
: Measure of price stability (LOW, MEDIUM, HIGH). High volatility = large price swings = good for timing optimization.
---
💡 **See Also:**
- [Core Concepts](concepts.md) - In-depth explanations
- [Sensors Overview](sensors-overview.md) - How sensors use these concepts
- [Period Calculation](period-calculation.md) - Deep dive into period detection

View file

@ -0,0 +1,516 @@
---
comments: false
---
# Dynamic Icon Colors
Many sensors in the Tibber Prices integration provide an `icon_color` attribute that allows you to dynamically color elements in your dashboard based on the sensor's state. This is particularly useful for visual dashboards where you want instant recognition of price levels or states.
**What makes icon_color special:** Instead of writing complex if/else logic to interpret the sensor state, you can simply use the `icon_color` value directly - it already contains the appropriate CSS color variable for the current state.
> **Related:** Many sensors also automatically change their **icon** based on state. See the **[Dynamic Icons Guide](dynamic-icons.md)** for details.
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
## What is icon_color?
The `icon_color` attribute contains a **CSS variable name** (not a direct color value) that changes based on the sensor's state. For example:
- **Price level sensors**: `var(--success-color)` for cheap, `var(--error-color)` for expensive
- **Binary sensors**: `var(--success-color)` when in best price period, `var(--error-color)` during peak price
- **Volatility**: `var(--success-color)` for low volatility, `var(--error-color)` for very high
### Why CSS Variables?
Using CSS variables like `var(--success-color)` instead of hardcoded colors (like `#00ff00`) has important advantages:
- ✅ **Automatic theme adaptation** - Colors change with light/dark mode
- ✅ **Consistent with your theme** - Uses your theme's color scheme
- ✅ **Future-proof** - Works with custom themes and future HA updates
You can use the `icon_color` attribute directly in your card templates, or interpret the sensor state yourself if you prefer custom colors (see examples below).
## Which Sensors Support icon_color?
Many sensors provide the `icon_color` attribute for dynamic styling. To see if a sensor has this attribute:
1. Go to **Developer Tools****States** in Home Assistant
2. Search for your sensor (e.g., `sensor.<home_name>_current_price_level`)
3. Look for `icon_color` in the attributes section
**Common sensor types with icon_color:**
- Price level sensors (e.g., `current_price_level``current_interval_price_level`)
- Price rating sensors (e.g., `current_price_rating``current_interval_price_rating`)
- Volatility sensors (e.g., `today_s_price_volatility``today_volatility`)
- Price outlook sensors (e.g., `price_outlook_3h`)
- Binary sensors (e.g., `best_price_period`, `peak_price_period`)
- Timing sensors (e.g., `best_price_time_until_start``best_price_next_in_minutes`, `best_price_progress`)
The colors adapt to the sensor's state - cheaper prices typically show green, expensive prices red, and neutral states gray.
## When to Use icon_color vs. State Value
**Use `icon_color` when:**
- ✅ You can apply the CSS variable directly (icons, text colors, borders)
- ✅ Your card supports CSS variable substitution
- ✅ You want simple, clean code without if/else logic
**Use the state value directly when:**
- ⚠️ You need to convert the color (e.g., CSS variable → RGBA with transparency)
- ⚠️ You need different colors than what `icon_color` provides
- ⚠️ You're building complex conditional logic anyway
**Example of when NOT to use icon_color:**
<details>
<summary>Show YAML: Conversion vs Direct State Logic</summary>
```yaml
# ❌ DON'T: Converting icon_color requires if/else anyway
card:
- background: |
[[[
const color = entity.attributes.icon_color;
if (color === 'var(--success-color)') return 'rgba(76, 175, 80, 0.1)';
if (color === 'var(--error-color)') return 'rgba(244, 67, 54, 0.1)';
// ... more if statements
]]]
# ✅ DO: Interpret state directly if you need custom logic
card:
- background: |
[[[
const level = entity.state;
if (level === 'very_cheap' || level === 'cheap') return 'rgba(76, 175, 80, 0.1)';
if (level === 'very_expensive' || level === 'expensive') return 'rgba(244, 67, 54, 0.1)';
return 'transparent';
]]]
```
</details>
The advantage of `icon_color` is simplicity - if you need complex logic, you lose that advantage.
## How to Use icon_color in Your Dashboard
### Method 1: Custom Button Card (Recommended)
The [custom:button-card](https://github.com/custom-cards/button-card) from HACS supports dynamic icon colors.
**Example: Icon color only**
<details>
<summary>Show YAML: Icon color only</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
icon: mdi:cash
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
```
</details>
**Example: Icon AND state value with same color**
<details>
<summary>Show YAML: Button Card Icon and State Text</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
icon: mdi:cash
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
state:
- color: |
[[[
return entity.attributes.icon_color || 'var(--primary-text-color)';
]]]
- font-weight: bold
```
</details>
### Method 2: Entities Card with card_mod
Use Home Assistant's built-in entities card with card_mod for icon and state colors:
<details>
<summary>Show YAML: Entities Card with card-mod</summary>
```yaml
type: entities
entities:
- entity: sensor.<home_name>_current_price_level
card_mod:
style:
hui-generic-entity-row:
$: |
state-badge {
color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }} !important;
}
.info {
color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }} !important;
}
```
</details>
### Method 3: Mushroom Cards
The [Mushroom cards](https://github.com/piitaya/lovelace-mushroom) support card_mod for icon and text colors:
**Icon color only:**
<details>
<summary>Show YAML: Mushroom Icon Color Only</summary>
```yaml
type: custom:mushroom-entity-card
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Period
icon: mdi:piggy-bank
card_mod:
style: |
ha-card {
--card-mod-icon-color: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'icon_color') }};
}
```
</details>
**Icon and state value:**
<details>
<summary>Show YAML: Icon and state value</summary>
```yaml
type: custom:mushroom-entity-card
entity: sensor.<home_name>_current_price_level
name: Price Level
card_mod:
style: |
ha-card {
--card-mod-icon-color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }};
--primary-text-color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }};
}
```
</details>
### Method 4: Glance Card with card_mod
Combine multiple sensors with dynamic colors:
<details>
<summary>Show YAML: Multi-Sensor Dynamic Colors</summary>
```yaml
type: glance
entities:
- entity: sensor.<home_name>_current_price_level
- entity: sensor.<home_name>_today_s_price_volatility
- entity: binary_sensor.<home_name>_best_price_period
card_mod:
style: |
ha-card div.entity:nth-child(1) state-badge {
color: {{ state_attr('sensor.<home_name>_current_price_level', 'icon_color') }} !important;
}
ha-card div.entity:nth-child(2) state-badge {
color: {{ state_attr('sensor.<home_name>_today_s_price_volatility', 'icon_color') }} !important;
}
ha-card div.entity:nth-child(3) state-badge {
color: {{ state_attr('binary_sensor.<home_name>_best_price_period', 'icon_color') }} !important;
}
```
</details>
## Complete Dashboard Example
Here's a complete example combining multiple sensors with dynamic colors:
<details>
<summary>Show YAML: Complete Dashboard Example</summary>
```yaml
type: vertical-stack
cards:
# Current price status
- type: horizontal-stack
cards:
- type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Price Level
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
- type: custom:button-card
entity: sensor.<home_name>_current_price_rating
name: Price Rating
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
# Binary sensors for periods
- type: horizontal-stack
cards:
- type: custom:button-card
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Period
show_state: true
icon: mdi:piggy-bank
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
- type: custom:button-card
entity: binary_sensor.<home_name>_peak_price_period
name: Peak Price Period
show_state: true
icon: mdi:alert-circle
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
# Volatility and trends
- type: horizontal-stack
cards:
- type: custom:button-card
entity: sensor.<home_name>_today_s_price_volatility
name: Volatility
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
- type: custom:button-card
entity: sensor.<home_name>_price_outlook_3h
name: Next 3h Outlook
show_state: true
styles:
icon:
- color: |
[[[
return entity.attributes.icon_color || 'var(--state-icon-color)';
]]]
```
</details>
## CSS Color Variables
The integration uses Home Assistant's standard CSS variables for theme compatibility:
- `var(--success-color)` - Green (good/cheap/low)
- `var(--info-color)` - Blue (informational)
- `var(--warning-color)` - Orange (caution/expensive)
- `var(--error-color)` - Red (alert/very expensive/high)
- `var(--state-icon-color)` - Gray (neutral/normal)
- `var(--disabled-color)` - Light gray (no data/inactive)
These automatically adapt to your theme's light/dark mode and custom color schemes.
### Using Custom Colors
If you want to override the theme colors with your own, you have two options:
#### Option 1: Use icon_color but Override in Your Theme
Define custom colors in your theme configuration (`themes.yaml`):
<details>
<summary>Show YAML: Theme-Level Color Overrides</summary>
```yaml
my_custom_theme:
# Override standard variables
success-color: "#00C853" # Custom green
error-color: "#D32F2F" # Custom red
warning-color: "#F57C00" # Custom orange
info-color: "#0288D1" # Custom blue
```
</details>
The `icon_color` attribute will automatically use your custom theme colors.
#### Option 2: Interpret State Value Directly
Instead of using `icon_color`, read the sensor state and apply your own colors:
**Example: Custom colors for price level**
<details>
<summary>Show YAML: Custom colors for price level</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_level
name: Current Price Level
show_state: true
icon: mdi:cash
styles:
icon:
- color: |
[[[
const level = entity.state;
if (level === 'very_cheap') return '#00E676'; // Bright green
if (level === 'cheap') return '#66BB6A'; // Light green
if (level === 'normal') return '#9E9E9E'; // Gray
if (level === 'expensive') return '#FF9800'; // Orange
if (level === 'very_expensive') return '#F44336'; // Red
return 'var(--state-icon-color)'; // Fallback
]]]
```
</details>
**Example: Custom colors for binary sensor**
<details>
<summary>Show YAML: Custom colors for binary sensor</summary>
```yaml
type: custom:button-card
entity: binary_sensor.<home_name>_best_price_period
name: Best Price Period
show_state: true
icon: mdi:piggy-bank
styles:
icon:
- color: |
[[[
// Use state directly, not icon_color
return entity.state === 'on' ? '#4CAF50' : '#9E9E9E';
]]]
card:
- background: |
[[[
return entity.state === 'on' ? 'rgba(76, 175, 80, 0.1)' : 'transparent';
]]]
```
</details>
**Example: Custom colors for volatility**
<details>
<summary>Show YAML: Custom colors for volatility</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_today_s_price_volatility
name: Volatility Today
show_state: true
styles:
icon:
- color: |
[[[
const volatility = entity.state;
if (volatility === 'low') return '#4CAF50'; // Green
if (volatility === 'moderate') return '#2196F3'; // Blue
if (volatility === 'high') return '#FF9800'; // Orange
if (volatility === 'very_high') return '#F44336'; // Red
return 'var(--state-icon-color)';
]]]
```
</details>
**Example: Custom colors for price rating**
<details>
<summary>Show YAML: Custom colors for price rating</summary>
```yaml
type: custom:button-card
entity: sensor.<home_name>_current_price_rating
name: Price Rating
show_state: true
styles:
icon:
- color: |
[[[
const rating = entity.state;
if (rating === 'low') return '#00C853'; // Dark green
if (rating === 'normal') return '#78909C'; // Blue-gray
if (rating === 'high') return '#D32F2F'; // Dark red
return 'var(--state-icon-color)';
]]]
```
</details>
### Which Approach Should You Use?
| Use Case | Recommended Approach |
| ------------------------------------- | ---------------------------------- |
| Want theme-consistent colors | ✅ Use `icon_color` directly |
| Want light/dark mode support | ✅ Use `icon_color` directly |
| Want custom theme colors | ✅ Override CSS variables in theme |
| Want specific hardcoded colors | ⚠️ Interpret state value directly |
| Multiple themes with different colors | ✅ Use `icon_color` directly |
**Recommendation:** Use `icon_color` whenever possible for better theme integration. Only interpret the state directly if you need very specific color values that shouldn't change with themes.
## Troubleshooting
**Icons not changing color:**
- Make sure you're using a card that supports custom styling (like custom:button-card or card_mod)
- Check that the entity actually has the `icon_color` attribute (inspect in Developer Tools → States)
- Verify your Home Assistant theme supports the CSS variables
**Colors look wrong:**
- The colors are theme-dependent. Try switching themes to see if they appear correctly
- Some custom themes may override the standard CSS variables with unexpected colors
**Want different colors?**
- You can override the colors in your theme configuration
- Or use conditional logic in your card templates based on the state value instead of `icon_color`
## See Also
- [Sensors Overview](sensors-overview.md) - Complete list of available sensors
- [Automation Examples](automation-examples.md) - Use color-coded sensors in automations
- [Configuration Guide](configuration.md) - Adjust thresholds for price levels and ratings

View file

@ -0,0 +1,79 @@
# Installation
## HACS Installation (Recommended)
[HACS](https://hacs.xyz/) (Home Assistant Community Store) is the easiest way to install and keep the integration up to date.
### Prerequisites
- Home Assistant 2025.10.0 or newer
- [HACS](https://hacs.xyz/docs/use/) installed and configured
- A [Tibber API token](https://developer.tibber.com/settings/access-token)
### Steps
1. Open HACS in your Home Assistant sidebar
2. Go to **Integrations**
3. Click the **⋮** menu (top right) → **Custom repositories**
4. Add the repository URL:
<details>
<summary>Show example: Repository URL</summary>
```
https://github.com/jpawlowski/hass.tibber_prices
```
</details>
Category: **Integration**
5. Click **Add**
6. Find **Tibber Price Information & Ratings** in the integration list
7. Click **Download**
8. **Restart Home Assistant**
9. Continue with [Configuration](configuration.md)
### Updating
HACS will show a notification when updates are available:
1. Open HACS → **Integrations**
2. Find **Tibber Price Information & Ratings**
3. Click **Update**
4. **Restart Home Assistant**
## Manual Installation
If you prefer not to use HACS:
1. Download the [latest release](https://github.com/jpawlowski/hass.tibber_prices/releases/latest) from GitHub
2. Extract the `custom_components/tibber_prices/` folder
3. Copy it to your Home Assistant `config/custom_components/` directory:
<details>
<summary>Show example: Folder Structure</summary>
```
config/
└── custom_components/
└── tibber_prices/
├── __init__.py
├── manifest.json
├── sensor/
├── binary_sensor/
└── ...
```
</details>
4. **Restart Home Assistant**
5. Continue with [Configuration](configuration.md)
## After Installation
Once installed and restarted, add the integration:
1. Go to **Settings → Devices & Services**
2. Click **+ Add Integration**
3. Search for **Tibber Price Information & Ratings**
4. Enter your [Tibber API token](https://developer.tibber.com/settings/access-token)
5. Select your Tibber home
6. The integration will start fetching price data
See the [Configuration Guide](configuration.md) for detailed setup options.

View file

@ -0,0 +1,59 @@
---
comments: false
---
# User Documentation
Welcome to the **Tibber Prices custom integration for Home Assistant**! This community-developed integration enhances your Home Assistant installation with detailed electricity price data from Tibber, featuring quarter-hourly precision, statistical analysis, and intelligent ratings.
:::info Not affiliated with Tibber
This is an independent, community-maintained custom integration. It is **not** an official Tibber product and is **not** affiliated with or endorsed by Tibber AS.
:::
## 📚 Documentation
- **[Installation](installation.md)** - How to install via HACS and configure the integration
- **[Configuration](configuration.md)** - Setting up your Tibber API token and price thresholds
- **[Period Calculation](period-calculation.md)** - How Best/Peak Price periods are calculated and configured
- **[Sensors](sensors-overview.md)** - Available sensors, their states, and attributes
- **[Dynamic Icons](dynamic-icons.md)** - State-based automatic icon changes
- **[Dynamic Icon Colors](icon-colors.md)** - Using icon_color attribute for color-coded dashboards
- **[Actions](actions.md)** - Scheduling, chart, and data actions for automations and dashboards
- **[Chart Examples](chart-examples.md)** - ✨ ApexCharts visualizations with screenshots
- **[Automation Examples](automation-examples.md)** - Ready-to-use automation recipes
- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions
## 🚀 Quick Start
1. **Install via HACS** (add as custom repository)
2. **Add Integration** in Home Assistant → Settings → Devices & Services
3. **Enter Tibber API Token** (get yours at [developer.tibber.com](https://developer.tibber.com/))
4. **Configure Price Thresholds** (optional, defaults work for most users)
5. **Start Using Sensors** in automations, dashboards, and scripts!
## ✨ Key Features
- **Quarter-hourly precision** - 15-minute intervals for accurate price tracking
- **Statistical analysis** - Trailing/leading 24h averages for context
- **Price ratings** - LOW/NORMAL/HIGH classification based on your thresholds
- **Best/Peak hour detection** - Automatic detection of cheapest/peak periods with configurable filters ([learn how](period-calculation.md))
- **Beautiful ApexCharts** - Auto-generated chart configurations with dynamic Y-axis scaling ([see examples](chart-examples.md))
- **Chart metadata sensor** - Dynamic chart configuration for optimal visualization
- **Flexible currency display** - Choose base currency (€, kr) or subunit (ct, øre) with smart defaults per currency
## 🔗 Useful Links
- [GitHub Repository](https://github.com/jpawlowski/hass.tibber_prices)
- [Issue Tracker](https://github.com/jpawlowski/hass.tibber_prices/issues)
- [Release Notes](https://github.com/jpawlowski/hass.tibber_prices/releases)
- [Home Assistant Community](https://community.home-assistant.io/)
## 🤝 Need Help?
- Check the [Troubleshooting Guide](troubleshooting.md)
- Search [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues)
- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) if needed
---
**Note:** These guides are for end users. If you want to contribute to development, see the [Developer Documentation](https://jpawlowski.github.io/hass.tibber_prices/developer/).

View file

@ -0,0 +1,987 @@
# Period Calculation
Learn how Best Price and Peak Price periods work, and how to configure them for your needs.
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
## Table of Contents
- [Quick Start](#quick-start)
- [How It Works](#how-it-works) — Algorithm overview, all 7 phases explained
- [Configuration Guide](#configuration-guide)
- [Understanding Relaxation](#understanding-relaxation) → [Full Guide](period-relaxation.md)
- [Common Scenarios](#common-scenarios)
- [Troubleshooting](#troubleshooting)
- [Advanced Topics](#advanced-topics)
---
## Quick Start
### What Are Price Periods?
The integration finds time windows when electricity is especially **cheap** (Best Price) or **expensive** (Peak Price):
- <EntityRef id="best_price_period">Best Price Periods</EntityRef> 🟢 - When to run your dishwasher, charge your EV, or heat water
- <EntityRef id="peak_price_period">Peak Price Periods</EntityRef> 🔴 - When to reduce consumption or defer non-essential loads
### Default Behavior
Out of the box, the integration:
1. **Best Price**: Finds cheapest 1-hour+ windows that are at least 5% below the daily average
2. **Peak Price**: Finds most expensive 30-minute+ windows that are at least 5% above the daily average
3. **Relaxation**: Automatically loosens filters if not enough periods are found
**Most users don't need to change anything!** The defaults work well for typical use cases.
<details>
<summary> Why do Best Price and Peak Price have different defaults?</summary>
The integration sets different **initial defaults** because the features serve different purposes:
**Best Price (60 min, 15% flex):**
- Longer duration ensures appliances can complete their cycles
- Stricter flex (15%) focuses on genuinely cheap times
- Use case: Running dishwasher, EV charging, water heating
**Peak Price (30 min, 20% flex):**
- Shorter duration acceptable for early warnings
- More flexible (20%) catches price spikes earlier
- Use case: Alerting to expensive periods, even brief ones
**You can adjust all these values** in the configuration if the defaults don't fit your use case. The asymmetric defaults simply provide good starting points for typical scenarios.
</details>
### Example Timeline
```mermaid
gantt
title Typical Day — Best Price & Peak Price Periods
dateFormat HH:mm
axisFormat %H:%M
tickInterval 2hour
section Best Price
Cheap prices (00:0004:00) :active, 00:00, 04:00
section Normal
Normal prices (04:0008:00) : 04:00, 08:00
section Peak Price
Expensive prices (08:0012:00) :crit, 08:00, 12:00
section Normal
Normal prices (12:0016:00) : 12:00, 16:00
section Peak Price
Expensive prices (16:0020:00) :crit, 16:00, 20:00
section Best Price
Cheap prices (20:0000:00) :active, 20:00, 24:00
```
---
## How It Works
### The Basic Idea
Each day, the integration analyzes all 96 quarter-hourly price intervals and identifies **continuous time ranges** that meet specific criteria.
:::info What "Best Price" means
A Best Price Period is the **cheapest contiguous block** of time — a stretch of consecutive intervals where prices stay close to the daily minimum. It is **not** a fixed-length sliding window that shifts to center around the minimum.
On a V-shaped day (prices drop sharply to a minimum, then rise again), the period therefore starts **at** the low point, not before it — because the intervals leading up to the minimum did not yet meet the price criteria. Only the intervals near the bottom of the V qualify as a coherent cheap block.
**For flexible loads** (e.g., heat pump, battery charging): you can easily ride the full cheap wave by combining the period sensor with the price level and trend sensors. See [Heat Pump Smart Boost in Automation Examples](./automation-examples.md#heat-pump-smart-boost-with-trend-awareness).
:::
### Algorithm Overview
The period calculation is a multi-phase pipeline. Each phase builds on the previous one, progressively refining the result:
```mermaid
flowchart TD
A["📊 Raw price data<br/><small>yesterday + today + tomorrow<br/>(~288 intervals)</small>"]
A --> B["🔇 Phase 1: Spike Smoothing<br/><small>Neutralize isolated price outliers<br/>to prevent period fragmentation</small>"]
B --> C["📐 Phase 2: Day Pattern Detection<br/><small>Classify each day's shape<br/>(valley, peak, duck curve, flat…)</small>"]
C --> D["🔍 Phase 3: Period Detection<br/><small>Find continuous intervals matching<br/>flex + distance + level criteria</small>"]
D --> E["📏 Phase 4: Duration &amp; Quality<br/><small>Remove too-short periods,<br/>calculate statistics</small>"]
E --> F["🌙 Phase 5: Cross-Day Handling<br/><small>Bridge midnight-split periods,<br/>filter day-boundary artifacts</small>"]
F --> G{"Enough periods<br/>per day?"}
G -->|Yes| H["✅ Done"]
G -->|No| I["🔄 Phase 6: Relaxation<br/><small>Gradually loosen filters<br/>(+3% flex per step)</small>"]
I -->|retry| D
I -->|exhausted| J["⚠️ Phase 7: Fallback<br/><small>Reduce minimum duration<br/>(last resort)</small>"]
J --> H
style A fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style H fill:#e6fff5,stroke:#00c853,stroke-width:2px
style G fill:#fff8e1,stroke:#ffc107,stroke-width:2px
```
**Why this order?**
| Phase | What it does | Why it's needed |
|-------|-------------|-----------------|
| **1. Spike Smoothing** | Replaces isolated price spikes with trend predictions | A single 15-minute spike would split a 4-hour cheap period into two short fragments |
| **2. Day Patterns** | Classifies each day's price shape (valley, peak, duck curve, flat…) | Enables geometric flex bonuses — periods in a detected valley/peak zone get extra margin |
| **3. Period Detection** | Scans all intervals through flex, distance, and level filters | Core logic: finds contiguous blocks where prices are close to the daily min (or max) |
| **4. Duration & Quality** | Removes periods shorter than the configured minimum, calculates statistics | A 15-minute "period" isn't useful for running an appliance |
| **5. Cross-Day Handling** | Bridges midnight-split periods, filters day-boundary artifacts | Without this, a cheap period split by midnight into two fragments can't be recognized as one continuous period |
| **6. Relaxation** | Loosens filters step by step (+3% flex) until enough periods are found | On some days, the configured flex isn't enough to find 2 periods — relaxation adapts automatically |
| **7. Fallback** | Progressively reduces minimum duration (60→45→30 min) | Last resort for days where even full relaxation finds zero periods |
The following sections explain each phase in detail.
### Phase 1: Spike Smoothing (Preprocessing)
Before any period detection begins, isolated price spikes are detected and smoothed. This prevents a single expensive 15-minute interval from splitting what should be one long cheap period into two short fragments.
<details>
<summary>Show example: Automatic Price Spike Smoothing</summary>
```
Original prices: 18, 19, 35, 20, 19 ct ← 35 ct is an isolated outlier
Smoothed: 18, 19, 19, 20, 19 ct ← Spike replaced with trend prediction
Result: Continuous period 00:00-01:15 instead of split periods
```
</details>
**How it works:** The algorithm looks at 3 intervals before and after each price point, calculates an expected trend, and flags prices that deviate significantly. On flat days (low price variation), smoothing is more conservative; on volatile days, it's more aggressive. Daily minimum and maximum prices are never smoothed — they serve as reference points for period detection.
**Important:**
- Original prices are always preserved in all statistics (min/max/avg show real values)
- Smoothing only affects which intervals are combined into periods
- The attribute `period_interval_smoothed_count` shows how many intervals were smoothed
### Phase 2: Day Pattern Detection
The integration classifies each day's price shape to optimize period detection for different market conditions:
| Pattern | Shape | Description |
|---------|-------|-------------|
| `valley` | | Single cheap window (e.g., solar midday dip) |
| `peak` | ∩ | Single expensive window (e.g., evening demand) |
| `double_dip` | W | Two cheap windows (e.g., cheap morning + cheap midday) |
| `duck_curve` | M | Two expensive peaks (e.g., morning + evening demand, named after the energy industry's [duck curve](https://en.wikipedia.org/wiki/Duck_curve)) |
| `flat` | ─ | Little variation throughout the day |
| `rising` | / | Prices climb steadily |
| `falling` | \ | Prices drop steadily |
**Why this matters:** On a detected valley day, the period detection gets a geometric flex bonus for intervals within the valley zone — making it easier to capture the full cheap window even if some intervals are slightly above the normal flex threshold. On flat days, the target number of periods is automatically reduced to 1 (see [Flat Day Detection](#fewer-periods-than-configured)).
Day patterns are also exposed as dedicated sensors — see [Price Phases & Day Pattern](./sensors-price-phases.md) for details.
### Phase 3: Period Detection (Core Logic)
This is the heart of the algorithm. Each smoothed interval is tested against three filters. Only consecutive intervals that pass **all three** form a period:
```mermaid
flowchart TD
A["Each 15-min interval"] --> B{"① Flexibility<br/><small>Close to daily MIN/MAX?</small>"}
B -->|Yes| C{"② Distance<br/><small>Meaningfully different<br/>from daily average?</small>"}
B -->|No| X1["❌ excluded"]
C -->|Yes| D{"③ Level filter<br/><small>(optional, user-configured)</small>"}
C -->|No| X2["❌ excluded"]
D -->|Pass| E["✅ Add to current period"]
D -->|Fail| X3["❌ filtered"]
style A fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style E fill:#e6fff5,stroke:#00c853,stroke-width:2px
style X1 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X2 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
style X3 fill:#fff0f0,stroke:#ff5252,stroke-width:1px,color:#999
```
#### ① Flexibility (Search Range)
**Best Price:** How much MORE than the daily minimum can a price be?
<details>
<summary>Show example: Search Range Flexibility</summary>
```
Daily MIN: 20 ct/kWh
Flexibility: 15% (default)
→ Search for times ≤ 23 ct/kWh (20 + 15%)
```
</details>
**Peak Price:** How much LESS than the daily maximum can a price be?
<details>
<summary>Show example: Peak Price Flexibility</summary>
```
Daily MAX: 40 ct/kWh
Flexibility: -15% (default)
→ Search for times ≥ 34 ct/kWh (40 - 15%)
```
</details>
**Why flexibility?** Prices rarely stay at exactly MIN/MAX. Flexibility lets you capture realistic time windows. At high flex values (>20%), the distance filter is automatically scaled down to prevent conflicting constraints.
#### ② Distance from Average (Quality Gate)
Periods must be meaningfully different from the daily average:
<details>
<summary>Show example: Distance from Average</summary>
```
Daily AVG: 30 ct/kWh
Minimum distance: 5% (default)
Best Price: Must be ≤ 28.5 ct/kWh (30 - 5%)
Peak Price: Must be ≥ 31.5 ct/kWh (30 + 5%)
```
</details>
**Why?** This prevents marking mediocre times as "best" just because they're slightly below average.
#### ③ Level Filter (Optional)
You can optionally require intervals to have a specific Tibber price level (e.g., `CHEAP` or `EXPENSIVE`). With gap tolerance, a few "mediocre" intervals within an otherwise good period are allowed — preventing unnecessary splits.
### Phase 4: Duration & Quality
Consecutive intervals that passed all three filters are grouped into candidate periods. Short candidates are discarded:
<details>
<summary>Show example: Minimum Period Duration</summary>
```
Default: 60 minutes minimum
45-minute period → Discarded (too short to be useful)
90-minute period → Kept ✓
```
</details>
For each surviving period, the integration calculates statistics: mean, median, min, max, price spread, coefficient of variation, and volatility classification. Periods with very heterogeneous prices (CV > 25%) are flagged as low quality.
### Phase 5: Cross-Day Handling
Since the integration processes yesterday + today + tomorrow together, periods can naturally span midnight. This phase ensures correct behavior at day boundaries:
**Cross-midnight bridging:**
When two independently qualifying periods exist on **both sides** of midnight — separated only by a small gap (max 1 hour) caused by the per-day reference price change at the day boundary — they are merged into a single period. This requires evidence on both sides: a period ending at 21:30 will **not** be bridged, because it ended naturally (prices changed), not because of midnight. Only genuine midnight-split periods are merged.
Safety limits:
- Maximum gap of 4 intervals (1 hour) between the two periods
- The merged period must pass the CV quality gate (≤ 25% coefficient of variation)
**Day-boundary artifact filtering:**
Each day has its own min/max/avg — so the same absolute price can qualify as "cheap" or "peak" on one day but not the next. The integration catches these misleading artifacts with several automatic checks:
- **Both Best _and_ Peak periods** near midnight (00:00-05:59) must qualify against **both** adjacent days' reference prices. Without this, a 7 ct interval could become "best" only because today's minimum happens to be lower than yesterday's, or a 30 ct interval could become "peak" only because tomorrow's maximum happens to be lower than today's.
- **Peak periods** must exceed the daily average by at least 10% (overnight periods use the higher average of both days).
- Early-morning "peaks" that are significantly weaker than yesterday's late-evening peak are recognized as artifacts and filtered out.
These checks run automatically — no configuration needed.
### Phase 6: Relaxation (Adaptive)
If the baseline detection didn't find enough periods per day, the integration gradually loosens filters:
```mermaid
flowchart LR
A["Baseline:<br/>flex 15%"] -->|"not enough"| B["Step 1:<br/>flex 18%"]
B -->|"not enough"| C["Step 2:<br/>flex 21%"]
C -->|"…"| D["Step N:<br/>flex up to 50%"]
D -->|"still not enough"| E["Level filter<br/>removed"]
style A fill:#e6f7ff,stroke:#00b9e7
style E fill:#fff8e1,stroke:#ffc107
```
- Each step increases flex by 3% and retries period detection
- Each day is evaluated independently — a day that already has enough periods is skipped
- On **flat days** (price variation < 10%), the target is automatically reduced to 1 period
- Hard limit: flex never exceeds 50%
- The `relaxation_active` and `relaxation_level` attributes show if and how relaxation was applied
**See [Relaxation Guide](period-relaxation.md)** for a deep dive.
### Phase 7: Fallback (Last Resort)
If all relaxation steps are exhausted and some days still have **zero** periods:
- Minimum duration is progressively reduced: 60 → 45 → 30 minutes (so a shorter price window is enough to qualify as a period).
- The flex level reached during relaxation is kept (typically 45-48%) — it is **not** maxed out further.
- The minimum-distance-from-average filter is halved (not disabled) so genuinely flat days still surface no period rather than a misleading one.
- The price level filter is dropped (any level allowed).
- Periods found this way are marked with `duration_fallback_active: true`.
This ensures that every day has at least one period under realistic market conditions, while still suppressing phantom periods on extremely flat days where no meaningful "best" or "peak" window exists.
### Visual Example
**Timeline for a typical day:**
Daily MIN: 18 ct | Daily MAX: 35 ct | Daily AVG: 26 ct
| Hour | Price | Best Price threshold<br/>≤ 20.7 ct (15% flex) | Peak Price threshold<br/>≥ 29.75 ct (15% flex) |
|------|------:|:---:|:---:|
| 00:00 | **18 ct** | ✅ Best Price | |
| 01:00 | **19 ct** | ✅ Best Price | |
| 02:00 | **20 ct** | ✅ Best Price | |
| 03:00 | 28 ct | | |
| 04:00 | 29 ct | | |
| 05:00 | 29 ct | | |
| 06:00 | **35 ct** | | 🔴 Peak Price |
| 07:00 | **34 ct** | | 🔴 Peak Price |
| 08:00 | **33 ct** | | 🔴 Peak Price |
| 09:00 | **32 ct** | | 🔴 Peak Price |
| 10:00 | **30 ct** | | 🔴 Peak Price |
| 11:00 | 28 ct | | |
| 12:00 | 25 ct | | |
| 13:00 | 24 ct | | |
| 14:00 | 26 ct | | |
| 15:00 | 28 ct | | |
| 16:00 | 30 ct | | 🔴 Peak Price |
| 17:00 | 32 ct | | 🔴 Peak Price |
| 18:00 | 31 ct | | 🔴 Peak Price |
| 19:00 | **22 ct** | | |
| 20:00 | **20 ct** | ✅ Best Price | |
| 21:00 | **19 ct** | ✅ Best Price | |
| 22:00 | **19 ct** | ✅ Best Price | |
| 23:00 | **18 ct** | ✅ Best Price | |
**Result:** Two Best Price periods (00:0003:00 and 19:0000:00) and two Peak Price periods (06:0011:00 and 16:0019:00).
---
## Configuration Guide
### Basic Settings
#### Flexibility
**What:** How far from MIN/MAX to search for periods
**Default:** 15% (Best Price), -15% (Peak Price)
**Range:** 0-100%
<details>
<summary>Show YAML: Flexibility</summary>
```yaml
best_price_flex: 15 # Can be up to 15% more expensive than daily MIN
peak_price_flex: -15 # Can be up to 15% less expensive than daily MAX
```
</details>
**When to adjust:**
- **Increase (20-25%)** → Find more/longer periods
- **Decrease (5-10%)** → Find only the very best/worst times
**💡 Tip:** Very high flexibility (>30%) is rarely useful. **Recommendation:** Start with 15-20% and enable relaxation it adapts automatically to each day's price pattern.
#### Minimum Period Length
**What:** How long a period must be to show it
**Default:** 60 minutes (Best Price), 30 minutes (Peak Price)
**Range:** 15-240 minutes
<details>
<summary>Show YAML: Minimum Period Length</summary>
```yaml
best_price_min_period_length: 60
peak_price_min_period_length: 30
```
</details>
**When to adjust:**
- **Increase (90-120 min)** → Only show longer periods (e.g., for heat pump cycles)
- **Decrease (30-45 min)** → Show shorter windows (e.g., for quick tasks)
#### Distance from Average
**What:** How much better than average a period must be
**Default:** 5%
**Range:** 0-20%
<details>
<summary>Show YAML: Distance from Average</summary>
```yaml
best_price_min_distance_from_avg: 5
peak_price_min_distance_from_avg: 5
```
</details>
**When to adjust:**
- **Increase (5-10%)** → Only show clearly better times
- **Decrease (0-1%)** → Show any time below/above average
** Note:** Both flexibility and distance filters must be satisfied. When using high flexibility values (>30%), the distance filter may become the limiting factor. For best results, use moderate flexibility (15-20%) with relaxation enabled.
### Optional Filters
#### Level Filter (Absolute Quality)
**What:** Only show periods with CHEAP/EXPENSIVE intervals (not just below/above average)
**Default:** `any` (disabled)
**Options:** `any` | `cheap` | `very_cheap` (Best Price) | `expensive` | `very_expensive` (Peak Price)
<details>
<summary>Show YAML: Level Filter</summary>
```yaml
best_price_max_level: any # Show any period below average
best_price_max_level: cheap # Only show if at least one interval is CHEAP
```
</details>
**Use case:** "Only notify me when prices are objectively cheap/expensive"
** Volatility Thresholds:** The level filter also supports volatility-based levels (`volatility_low`, `volatility_medium`, `volatility_high`). These use **fixed internal thresholds** (LOW < 10%, MEDIUM < 20%, HIGH 20%) that are separate from the sensor volatility thresholds you configure in the UI. This separation ensures that changing sensor display preferences doesn't affect period calculation behavior.
#### Gap Tolerance (for Level Filter)
**What:** Allow some "mediocre" intervals within an otherwise good period
**Default:** 0 (strict)
**Range:** 0-10
<details>
<summary>Show YAML: Gap Tolerance</summary>
```yaml
best_price_max_level: cheap
best_price_max_level_gap_count: 2 # Allow up to 2 NORMAL intervals per period
```
</details>
**Use case:** "Don't split periods just because one interval isn't perfectly CHEAP"
### Tweaking Strategy: What to Adjust First?
When you're not happy with the default behavior, adjust settings in this order:
#### 1. **Start with Relaxation (Easiest)**
If you're not finding enough periods:
<details>
<summary>Show YAML: Relaxation Defaults</summary>
```yaml
enable_min_periods_best: true # Already default!
min_periods_best: 2 # Already default!
relaxation_attempts_best: 11 # Already default!
```
</details>
**Why start here?** Relaxation automatically finds the right balance for each day. Much easier than manual tuning.
#### 2. **Adjust Period Length (Simple)**
If periods are too short/long for your use case:
<details>
<summary>Show YAML: Longer or Shorter Periods</summary>
```yaml
best_price_min_period_length: 90 # Increase from 60 for longer periods
# OR
best_price_min_period_length: 45 # Decrease from 60 for shorter periods
```
</details>
**Safe to change:** This only affects duration, not price selection logic.
#### 3. **Fine-tune Flexibility (Moderate)**
If you consistently want more/fewer periods:
<details>
<summary>Show YAML: Flexibility Tuning</summary>
```yaml
best_price_flex: 20 # Increase from 15% for more periods
# OR
best_price_flex: 10 # Decrease from 15% for stricter selection
```
</details>
**⚠️ Watch out:** Values >25% may conflict with distance filter. Use relaxation instead.
#### 4. **Adjust Distance from Average (Advanced)**
Only if periods seem "mediocre" (not really cheap/expensive):
<details>
<summary>Show YAML: Distance from Average</summary>
```yaml
best_price_min_distance_from_avg: 10 # Increase from 5% for stricter quality
```
</details>
**⚠️ Careful:** High values (>10%) can make it impossible to find periods on flat price days.
#### 5. **Enable Level Filter (Expert)**
Only if you want absolute quality requirements:
<details>
<summary>Show YAML: Strict Level Filter</summary>
```yaml
best_price_max_level: cheap # Only show objectively CHEAP periods
```
</details>
**⚠️ Very strict:** Many days may have zero qualifying periods. **Always enable relaxation when using this!**
### Common Mistakes to Avoid
**Don't increase flexibility to >30% manually** → Use relaxation instead
**Don't combine high distance (>10%) with strict level filter** → Too restrictive
**Don't disable relaxation with strict filters** → You'll get zero periods on some days
**Don't change all settings at once** → Adjust one at a time and observe results
**Do use defaults + relaxation** → Works for 90% of cases
**Do adjust one setting at a time** → Easier to understand impact
**Do check sensor attributes** → Shows why periods were/weren't found
---
## Understanding Relaxation
As described in [Phase 6](#phase-6-relaxation-adaptive), relaxation automatically loosens filters until a minimum number of periods is found — enabled by default.
**Key benefits:**
- Each day gets exactly the flexibility it needs (per-day independence)
- Uses a matrix approach: N flex levels × 2 filter combinations
- Stops as soon as enough periods are found
**→ [Full Relaxation Guide](period-relaxation.md)** — How it works, the adaptive matrix, choosing attempts, and diagnostics.
---
## Common Scenarios
:::tip V-shaped price days
On days with a sharp price dip (e.g. solar midday surplus), the Best Price Period covers only the absolute minimum. The surrounding cheap hours are intentional — the integration shows you the cheapest contiguous block, not a fixed-length window centered on the minimum. To run a device during the full cheap window, see [Heat Pump Smart Boost](./automation-examples.md#heat-pump-smart-boost-with-trend-awareness) in the Automation Examples.
:::
### Scenario 1: Simple Best Price (Default)
**Goal:** Find the cheapest time each day to run dishwasher
**Configuration:**
<details>
<summary>Show YAML: Simple Best Price</summary>
```yaml
# Use defaults - no configuration needed!
best_price_flex: 15 # (default)
best_price_min_period_length: 60 # (default)
best_price_min_distance_from_avg: 5 # (default)
```
</details>
**What you get:**
- 1-3 periods per day with prices ≤ MIN + 15%
- Each period at least 1 hour long
- All periods at least 5% cheaper than daily average
**Automation example:**
<details>
<summary>Show YAML: Dishwasher in Best Price Period</summary>
```yaml
automation:
- trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
action:
- service: switch.turn_on
target:
entity_id: switch.dishwasher
```
</details>
---
## Troubleshooting
### Fewer Periods Than Configured
**Symptom:** You configured `min_periods_best: 2` or `min_periods_peak: 2` but the sensor shows fewer periods on some days, and the attributes contain `flat_days_detected: 1` or `relaxation_incomplete: true`.
**If `flat_days_detected` is present:**
This is **expected behavior** on days with very uniform electricity prices. When prices vary by less than ~10% across the day (e.g. on sunny spring days with high solar generation), there is no meaningful second "cheap window" or second "expensive peak" all hours are equally priced. The integration automatically reduces the target to 1 period for that day. This applies to both Best Price and Peak Price periods.
<details>
<summary>Show YAML: Flat Day Detection</summary>
```yaml
# Best Price example:
min_periods_configured: 2
period_count_today: 1
flat_days_detected: 1 # Uniform prices today → 1 period is the right answer
# Peak Price example:
min_periods_configured: 2
period_count_today: 1
flat_days_detected: 1 # No distinct peaks today → 1 period is the right answer
```
</details>
You don't need to change anything. This is the integration protecting you from artificial periods.
**If `relaxation_incomplete` is present (without `flat_days_detected`):**
Relaxation tried all configured attempts but couldn't reach your target. Options:
1. **Increase relaxation attempts** (tries more flexibility levels before giving up)
<details>
<summary>Show YAML example (increase relaxation attempts)</summary>
```yaml
relaxation_attempts_best: 12 # Default: 11
```
</details>
2. **Reduce minimum period count**
<details>
<summary>Show YAML example (reduce min periods)</summary>
```yaml
min_periods_best: 1 # Only require 1 period per day
```
</details>
3. **Check filter settings** very strict `best_price_min_distance_from_avg` values block relaxation
---
### No Periods Found
**Symptom:** `binary_sensor.<home_name>_best_price_period` never turns "on"
**Common Solutions:**
1. **Check if relaxation is enabled**
<details>
<summary>Show YAML example (relaxation enabled)</summary>
```yaml
enable_min_periods_best: true # Should be true (default)
min_periods_best: 2 # Try to find at least 2 periods
```
</details>
2. **If still no periods, check filters**
- Look at sensor attributes: `relaxation_active` and `relaxation_level`
- If relaxation exhausted all attempts: Filters too strict or flat price day
3. **Try increasing flexibility slightly**
<details>
<summary>Show YAML example (increase flexibility)</summary>
```yaml
best_price_flex: 20 # Increase from default 15%
```
</details>
4. **Or reduce period length requirement**
<details>
<summary>Show YAML example (reduce period length)</summary>
```yaml
best_price_min_period_length: 45 # Reduce from default 60 minutes
```
</details>
### Periods Split Into Small Pieces
**Symptom:** Many short periods instead of one long period
**Common Solutions:**
1. **If using level filter, add gap tolerance**
<details>
<summary>Show YAML example (level gap tolerance)</summary>
```yaml
best_price_max_level: cheap
best_price_max_level_gap_count: 2 # Allow 2 NORMAL intervals
```
</details>
2. **Slightly increase flexibility**
<details>
<summary>Show YAML example (wider flexibility)</summary>
```yaml
best_price_flex: 20 # From 15% → captures wider price range
```
</details>
3. **Check for price spikes**
- Automatic smoothing should handle this
- Check attribute: `period_interval_smoothed_count`
- If 0: Not isolated spikes, but real price levels
### Understanding Sensor Attributes
**Key attributes to check:**
<details>
<summary>Show YAML: Key attributes to check</summary>
```yaml
# Entity: binary_sensor.<home_name>_best_price_period
# When "on" (period active):
start: "2025-11-11T02:00:00+01:00" # Period start time
end: "2025-11-11T05:00:00+01:00" # Period end time
duration_minutes: 180 # Duration in minutes
price_mean: 18.5 # Arithmetic mean price in the period
price_median: 18.3 # Median price in the period
rating_level: "LOW" # All intervals have LOW rating
# Relaxation info (shows if filter loosening was needed):
relaxation_active: true # This day needed relaxation
relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed
# Calculation summary (always shown diagnostic overview of this calculation run):
min_periods_configured: 2 # What you configured as target
period_count_today: 2 # How many periods are scheduled today
period_count_tomorrow: 2 # How many periods are scheduled tomorrow (when data available)
# Optional (only shown when relevant):
period_interval_smoothed_count: 2 # Number of price spikes smoothed
period_interval_level_gap_count: 1 # Number of "mediocre" intervals tolerated
flat_days_detected: 1 # Days where prices were so flat that 1 period is enough
relaxation_incomplete: true # Some days couldn't reach the configured target
```
</details>
#### What the diagnostic attributes mean
**`min_periods_configured` / `period_count_today`**
These two values together quickly show whether the calculation achieved its goal:
<details>
<summary>Show YAML: Configured Target vs Found Periods</summary>
```yaml
min_periods_configured: 2 # You asked for 2 periods per day
period_count_today: 2 # ✅ Today: target reached
period_count_tomorrow: 2 # ✅ Tomorrow: target reached
```
```yaml
min_periods_configured: 2
period_count_today: 1 # ⚠️ Today: only 1 period found
period_count_tomorrow: 2 # ✅ Tomorrow: target reached
```
</details>
**`flat_days_detected`**
This is the most important diagnostic for days with very uniform prices (e.g. sunny spring/summer days with high solar generation):
<details>
<summary>Show YAML: Flat Days Detected Attribute</summary>
```yaml
min_periods_configured: 2
period_count_today: 1
flat_days_detected: 1 # ← This explains why you got 1 instead of 2
```
</details>
When prices barely change across the day typically a variation of less than 10% the integration automatically reduces the target from your configured value to 1. There is no meaningful second "best price window" when all prices are essentially equal.
**This is expected and correct behavior**, not a problem. It prevents the sensor from generating artificial periods that don't represent genuinely cheaper windows.
**`relaxation_incomplete`**
This flag appears when even after all relaxation attempts, at least one day could not reach the configured minimum number of periods:
<details>
<summary>Show YAML: Relaxation Incomplete Attribute</summary>
```yaml
min_periods_configured: 2
period_count_today: 1
relaxation_incomplete: true # ← Relaxation tried everything, still short
```
</details>
This is most common on very flat days (see above) or with very strict filter settings. If you see this repeatedly on normal days, consider:
- Reducing `min_periods_best` to 1
- Increasing `relaxation_attempts_best`
- Checking if your `best_price_min_distance_from_avg` is too high
### Midnight Price Classification Changes
**Symptom:** A Best Price period at 23:45 changes classification at 00:00 (or vice versa), even though the absolute price barely changed.
**Why This Happens:**
Each day has its own price statistics (min, max, avg) calculated independently from its 96 intervals. Periods are classified based on their **position within the day's price range**, not absolute prices. This means the same absolute price can be "cheap" on one day and "average" on the next.
**Example:**
<details>
<summary>Show YAML: Midnight Price Classification Changes</summary>
```yaml
# Day 1 (low volatility, narrow range)
Price range: 18-22 ct/kWh (4 ct span)
Daily average: 20 ct/kWh
23:45: 18.5 ct/kWh → 7.5% below average → BEST PRICE ✅
# Day 2 (low volatility, narrow range)
Price range: 17-21 ct/kWh (4 ct span)
Daily average: 19 ct/kWh
00:00: 18.6 ct/kWh → 2.1% below average → NORMAL ❌
# Absolute price barely changed (18.5 → 18.6 ct)
# But relative position changed because Day 2 has a different price range
```
</details>
**How the Integration Handles This:**
The [cross-day handling](#phase-5-cross-day-handling) automatically prevents misleading period boundaries at midnight:
- **Best _and_ Peak periods** near midnight are validated against **both** adjacent days' statistics
- **Peak periods** must exceed the daily average by at least 10%, with overnight periods checked against the higher average of both days
- **Cross-midnight bridging** merges periods split by midnight only when qualifying periods exist on **both** sides (gap ≤ 1 hour, CV quality gate applies)
These checks run automatically and require no configuration. They ensure that midnight period boundaries reflect genuine price differences, not just day-boundary artifacts.
**Additional Strategies for Automations:**
For extra robustness, you can also make your automations aware of the price environment:
<details>
<summary>Show YAML: Volatility-Aware Automation</summary>
```yaml
# Option 1: Only act on high-volatility days (meaningful price differences)
automation:
- alias: "Dishwasher - Best Price (High Volatility Only)"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_today_s_price_volatility
above: 15 # Only act if volatility > 15%
action:
- service: switch.turn_on
entity_id: switch.dishwasher
# Option 2: Use absolute price threshold instead of classification
automation:
- alias: "Heat Water - Cheap Enough"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_current_electricity_price
below: 20 # Absolute threshold: < 20 ct/kWh
action:
- service: switch.turn_on
entity_id: switch.water_heater
```
</details>
**Summary:**
- ✅ **Expected behavior:** Each day has independent price statistics — midnight is a natural boundary
- ✅ **Automatic handling:** Cross-day bridging and quality checks prevent misleading period artifacts
- ✅ **Extra safety:** Use volatility sensors or absolute price thresholds in automations for additional robustness
---
## Advanced Topics
For advanced configuration patterns and technical deep-dive, see:
- [Automation Examples](./automation-examples.md) - Real-world automation patterns
- [Chart Actions](./chart-actions.md) - Using the `tibber_prices.get_chartdata` action for custom visualizations
### Quick Reference
**Configuration Parameters:**
| Parameter | Default | Range | Purpose |
| ---------------------------------- | ------- | ---------------- | ------------------------------ |
| `best_price_flex` | 15% | 0-100% | Search range from daily MIN |
| `best_price_min_period_length` | 60 min | 15-240 | Minimum duration |
| `best_price_min_distance_from_avg` | 5% | 0-20% | Quality threshold |
| `best_price_max_level` | any | any/cheap/vcheap | Absolute quality |
| `best_price_max_level_gap_count` | 0 | 0-10 | Gap tolerance |
| `enable_min_periods_best` | true | true/false | Enable relaxation |
| `min_periods_best` | 2 | 1-10 | Target periods per day |
| `relaxation_attempts_best` | 11 | 1-12 | Flex levels (attempts) per day |
**Peak Price:** Same parameters with `peak_price_*` prefix (defaults: flex=-15%, same otherwise)
### Price Levels Reference
The Tibber API provides price levels for each 15-minute interval:
**Levels (based on trailing 24h average):**
- `VERY_CHEAP` - Significantly below average
- `CHEAP` - Below average
- `NORMAL` - Around average
- `EXPENSIVE` - Above average
- `VERY_EXPENSIVE` - Significantly above average

View file

@ -0,0 +1,146 @@
# Understanding Relaxation
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
Relaxation is the automatic filter-loosening mechanism that ensures your [Best/Peak Price periods](period-calculation.md) always find results — even on days with unusual price patterns.
---
## What Is Relaxation?
Sometimes, strict filters find too few periods (or none). **Relaxation automatically loosens filters** until a minimum number of periods is found.
## How to Enable
<details>
<summary>Show YAML: Enable Relaxation</summary>
```yaml
enable_min_periods_best: true
min_periods_best: 2 # Try to find at least 2 periods per day
relaxation_attempts_best: 11 # Flex levels to test (default: 11 steps = 22 filter combinations)
```
</details>
**Good news:** Relaxation is **enabled by default** with sensible settings. Most users don't need to change anything here!
Set the matching `relaxation_attempts_peak` value when tuning Peak Price periods. Both sliders accept 1-12 attempts, and the default of 11 flex levels translates to 22 filter-combination tries (11 flex levels × 2 filter combos) for each of Best and Peak calculations. Lower it for quick feedback, or raise it when either sensor struggles to hit the minimum-period target on volatile days.
## Why Relaxation Is Better Than Manual Tweaking
**Problem with manual settings:**
- You set flex to 25% → Works great on Monday (volatile prices)
- Same 25% flex on Tuesday (flat prices) → Finds "best price" periods that aren't really cheap
- You're stuck with one setting for all days
**Solution with relaxation:**
- Monday (volatile): Uses flex 15% (original) → Finds 2 perfect periods ✓
- Tuesday (flat): Escalates to flex 21% → Finds 2 decent periods ✓
- Wednesday (mixed): Uses flex 18% → Finds 2 good periods ✓
**Each day gets exactly the flexibility it needs!**
## How It Works (Adaptive Matrix)
Relaxation uses a **matrix approach** - trying _N_ flexibility levels (your configured **relaxation attempts**) with 2 filter combinations per level. With the default of 11 attempts, that means 11 flex levels × 2 filter combinations = **22 total filter-combination tries per day**; fewer attempts mean fewer flex increases, while more attempts extend the search further before giving up.
**Important:** The flexibility increment is **fixed at 3% per step** (hard-coded for reliability). This means:
- Base flex 15% → 18% → 21% → 24% → ... → 48% (with 11 attempts)
- Base flex 20% → 23% → 26% → 29% → ... → 50% (with 11 attempts)
### Phase Matrix
For each day, the system tries:
```mermaid
flowchart TD
Start["Start: base flex<br/><small>(e.g. 15%)</small>"] --> A1
subgraph Attempt1["Attempt 1 — flex 15%"]
A1["Your filters"] -->|not enough| A2["Level = any"]
end
A2 -->|not enough| B1
subgraph Attempt2["Attempt 2 — flex 18%"]
B1["Your filters"] -->|not enough| B2["Level = any"]
end
B2 -->|not enough| C1
subgraph Attempt3["Attempt 3 — flex 21%"]
C1["Your filters"] --> C2["Level = any"]
end
C1 -->|"✅ enough"| Done
A1 -->|"✅ enough"| Done
A2 -->|"✅ enough"| Done
B1 -->|"✅ enough"| Done
B2 -->|"✅ enough"| Done
C2 -->|"✅ / not enough → next …"| Done
Done["✅ Done<br/><small>stops at first success</small>"]
style Start fill:#e6f7ff,stroke:#00b9e7,stroke-width:2px
style Done fill:#e6fff5,stroke:#00c853,stroke-width:2px
style Attempt1 fill:#f0f9ff,stroke:#00b9e7
style Attempt2 fill:#fff9e6,stroke:#ffb800
style Attempt3 fill:#fff0f0,stroke:#ff8a80
```
Each attempt adds +3% flexibility and tries two filter combinations. The system **stops as soon as enough periods are found** — it doesn't keep trying the full matrix.
## Choosing the Number of Attempts
- **Default (11 attempts)** balances speed and completeness for most grids (22 combinations per day for both Best and Peak)
- **Lower (4-8 attempts)** if you only want mild relaxation and keep processing time minimal (reaches ~27-39% flex)
- **Higher (12 attempts)** for extremely volatile days when you must reach near the 50% maximum (24 combinations)
- Remember: each additional attempt adds two more filter combinations because every new flex level still runs both filter overrides (original + level=any)
## Per-Day Independence
**Critical:** Each day relaxes **independently**:
<details>
<summary>Show formula: Each day relaxes independently</summary>
```
Day 1: Finds 2 periods with flex 15% (original) → No relaxation needed
Day 2: Needs flex 21% + level=any → Uses relaxed settings
Day 3: Finds 2 periods with flex 15% (original) → No relaxation needed
```
</details>
**Why?** Price patterns vary daily. Some days have clear cheap/expensive windows (strict filters work), others don't (relaxation needed).
## Diagnosing Relaxation Behavior
Check the period sensor attributes to understand what happened:
<details>
<summary>Show YAML: Diagnosing Relaxation Behavior</summary>
```yaml
# Entity: binary_sensor.<home_name>_best_price_period
relaxation_active: true # This day needed relaxation
relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed
min_periods_configured: 2 # Your target
period_count_today: 3 # What was actually found today
```
</details>
| Attribute | Meaning |
|-----------|---------|
| `relaxation_active: false` | Original filters were sufficient |
| `relaxation_active: true` | Filters were loosened to find enough periods |
| `relaxation_level` | Shows exactly which flex% and filter combo succeeded |
| `relaxation_incomplete: true` | All attempts exhausted, still short of target |
| `flat_days_detected: 1` | Uniform prices → target reduced to 1 (expected) |
**See also:** [Period Calculation — Troubleshooting](period-calculation.md#troubleshooting) for more diagnostic guidance.

View file

@ -0,0 +1,172 @@
# Plan Charging Action
The `plan_charging` action turns **battery parameters** into a complete **cost-minimized charging schedule**. Instead of manually computing energy, duration, and power, you describe the battery (capacity, current SoC, target SoC, max power) and the action returns a per-interval plan with SoC progression, cost totals, and segment grouping.
:::warning Experimental
The `plan_charging` action is **experimental** and still undergoing testing. Its parameters, response format, and behavior may change in future releases. Use it in automations with care, and please [report any issues](https://github.com/jpawlowski/hass.tibber_prices/issues).
:::
:::tip When to use this
If you already know the duration in minutes and just need the cheapest time window, use [`find_cheapest_hours`](scheduling-actions.md#find-cheapest-hours) or [`find_cheapest_block`](scheduling-actions.md#find-cheapest-block). Use `plan_charging` when you know your battery/EV parameters and want the integration to compute the duration, account for charging losses, and produce a SoC progression.
:::
## At a Glance
| Situation | Example |
|-----------|---------|
| Home battery: "Charge from 20% to 80%, efficiency 0.92" | `current_soc_percent: 20`, `target_soc_percent: 80`, `battery_capacity_kwh: 10` |
| EV with 3-phase charger: "Use 1/2/3 phases as needed" | `charge_power_steps_w: [1380, 4140, 11000]` |
| Battery with modulation: "30 W 1200 W continuous" | `min_charge_power_w: 30`, `max_charge_power_w: 1200` |
| Deadline-aware: "At least 50% before next peak" | `must_reach_soc_percent: 50`, `must_reach_by_event: next_peak_period` |
| Arbitrage: "Only charge if later discharge is profitable" | `expected_discharge_price: 0.28`, `reserve_for_discharge: true` |
## Required Inputs
| Field | Description |
|-------|-------------|
| `max_charge_power_w` | Maximum charging power in watts (upper bound for every interval). |
| `current_soc_percent` **or** `current_soc_kwh` | Current battery state of charge. |
| `target_soc_percent` **or** `target_soc_kwh` | Desired battery state of charge. |
| `battery_capacity_kwh` | Required when you use percent values. |
All other inputs (deadline, power steps, grid limit, economics, search range) are optional.
## Choosing Between Fixed / Continuous / Stepped Power
| Mode | Trigger | Behavior |
|------|---------|----------|
| **Fixed** | Only `max_charge_power_w` set | Every selected interval charges at full power. Last interval may over-shoot the target slightly (rounding up). |
| **Continuous** | Add `min_charge_power_w` | Planner can reduce the final partial interval down to the minimum power — no over-shoot. |
| **Stepped** | Add `charge_power_steps_w: [a, b, c]` | Planner picks the smallest allowed step that covers the remaining energy. Mutually exclusive with `min_charge_power_w`. |
## Deadlines
Combine a **minimum SoC** with a **deadline**:
- `must_reach_soc_percent` / `must_reach_soc_kwh` — the minimum you need by the deadline.
- Then pick one of:
- `must_reach_by` — absolute datetime.
- `must_reach_by_event` — one of `midnight`, `next_peak_period`, `next_best_period_end`.
The planner runs a two-pass schedule: first guarantee the minimum SoC before the deadline using the cheapest pre-deadline intervals, then fill the remaining target with the cheapest intervals from the full search range.
## Economics (Arbitrage)
For "charge cheap now, discharge expensive later" use cases:
- `discharging_efficiency` — fraction still usable when discharged (default `1.0`).
- `expected_discharge_price` — expected price per kWh at discharge time (in your configured display unit).
- `reserve_for_discharge` — when `true`, discards intervals that are unprofitable given the round-trip efficiency.
- `max_cost_per_kwh` — a hard ceiling; any interval above this price is discarded before scheduling.
The response's `economics.break_even_price` tells you the maximum charging price at which the round-trip still breaks even.
## Examples
### Home battery — charge from 20% to 80% overnight
<details>
<summary>Show YAML</summary>
```yaml
service: tibber_prices.plan_charging
data:
battery_capacity_kwh: 10
current_soc_percent: 20
target_soc_percent: 80
charging_efficiency: 0.92
max_charge_power_w: 2500
search_scope: remaining_today
response_variable: plan
```
</details>
### EV — 3-phase, at least 50% before next peak period
<details>
<summary>Show YAML</summary>
```yaml
service: tibber_prices.plan_charging
data:
battery_capacity_kwh: 60
current_soc_percent: 30
target_soc_percent: 80
must_reach_soc_percent: 50
must_reach_by_event: next_peak_period
max_charge_power_w: 11000
charge_power_steps_w: [1380, 4140, 11000]
grid_import_limit_w: 16000
response_variable: plan
```
</details>
### Battery arbitrage — only if profitable
<details>
<summary>Show YAML</summary>
```yaml
service: tibber_prices.plan_charging
data:
battery_capacity_kwh: 10
current_soc_percent: 10
target_soc_percent: 100
charging_efficiency: 0.92
discharging_efficiency: 0.92
expected_discharge_price: 0.28 # ct/kWh value expected when discharging
reserve_for_discharge: true
max_charge_power_w: 3000
search_scope: next_48h
response_variable: plan
```
</details>
## Response Structure
The response contains the following top-level keys:
| Key | Description |
|-----|-------------|
| `intervals_found` | `true` when a schedule was produced. |
| `battery` | Normalized SoC / capacity / efficiency / `achieved_soc_kwh` (what you actually reach with the returned schedule). |
| `charging` | Mode, total duration, total energy, total cost, and the `schedule` block. |
| `charging.schedule` | `segments[]`, `intervals[]`, `segment_count`, `seconds_until_start`, `seconds_until_end`, and price statistics. |
| `deadline` | Present when a deadline was set — includes `must_reach_by`, `must_reach_soc_kwh`, `achieved_soc_kwh`, `deadline_met`. |
| `economics` | Present when any economic parameter was set — includes `break_even_price`, `expected_net_savings`, `round_trip_efficiency`. |
| `price_comparison` | Difference between the selected schedule and the most expensive equivalent window. |
| `relaxation_applied` / `relaxation_steps` | Whether the schedule was relaxed to fit available data. |
| `reason` | Stable reason code when no schedule was found (see below). |
### Per-Interval Fields
Each entry in `charging.schedule.intervals[]` includes:
- `starts_at`, `ends_at`, `price`, `level`, `rating_level`
- `power_w` — power assigned to this interval (watts)
- `grid_energy_kwh` — energy drawn from the grid
- `stored_energy_kwh` — energy actually stored after losses
- `soc_after_kwh`, `soc_after_percent` — cumulative SoC after this interval
## Reason Codes
When no schedule is found, `reason` contains one of:
| Code | Meaning |
|------|---------|
| `already_at_target` | Current SoC is already at or above target — no charging needed. |
| `no_data_in_range` | The search range has no price data. |
| `no_intervals_matching_level_filter` | `min_price_level` / `max_price_level` filtered everything out. |
| `no_intervals_after_economic_filter` | `max_cost_per_kwh` or `reserve_for_discharge` filtered everything out. |
| `energy_unreachable` | The energy needed cannot be charged within the available intervals + power limits. |
| `energy_unreachable_by_deadline` | The minimum SoC cannot be reached before the deadline with the available intervals. |
| `selection_above_distance_threshold` | `min_distance_from_avg` is not satisfied by the cheapest selection. |
## Related
- [`find_cheapest_hours`](scheduling-actions.md#find-cheapest-hours) — when you already know the duration in minutes.
- [`find_cheapest_block`](scheduling-actions.md#find-cheapest-block) — for appliances that must run uninterrupted.
- [Scheduling Actions](scheduling-actions.md) — shared parameters (search range, price filters, relaxation).

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,302 @@
---
comments: false
---
# Entity Reference (All Languages)
<EntitySearch />
## How to Find Your Entity in Home Assistant
**Entity ID pattern:** `sensor.<home_name>_<suffix>`
- `<home_name>` is generated from your Tibber home display name (lowercase, spaces replaced with underscores)
- `<suffix>` is shown in the **Entity ID suffix** column below
**Three ways to find an entity:**
1. **Search above** — Type the entity name in your language to filter the tables below
2. **Device page** — Go to **Settings → Devices & Services → Tibber Prices**
click your home device → all entities are listed
3. **Developer Tools** — Go to **Developer Tools → States**
type `tibber` in the filter
:::tip
You can also use your browser's built-in search (**Ctrl+F** / **Cmd+F**) to search the full page text.
:::
**Enabled by default:** The ✅ column shows whether a sensor is enabled by default.
Sensors marked ❌ must be enabled manually via
**Settings → Devices & Services → Entities** → find the entity → toggle **Enabled**.
**Detailed documentation:** See the **[Sensors Overview](sensors-overview.md)** for detailed
explanations of each sensor's purpose, attributes, and automation examples.
---
## Sensors
### Core Price Sensors
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-current_interval_price" class="entity-anchor"></span>`current_interval_price` | Current Electricity Price | Aktueller Strompreis | Nåværende strømpris | Huidige Elektriciteitsprijs | Aktuellt elpris | ✅ |
| <span id="ref-current_interval_price_base" class="entity-anchor"></span>`current_interval_price_base` | Current Electricity Price (Energy Dashboard) | Aktueller Strompreis (Energie-Dashboard) | Nåværende strømpris (Energi-dashboard) | Huidige Elektriciteitsprijs (Energie Dashboard) | Aktuellt elpris (Energidashboard) | ✅ |
| <span id="ref-next_interval_price" class="entity-anchor"></span>`next_interval_price` | Next Electricity Price | Nächster Strompreis | Neste strømpris | Volgende Elektriciteitsprijs | Nästa elpris | ✅ |
| <span id="ref-previous_interval_price" class="entity-anchor"></span>`previous_interval_price` | Previous Electricity Price | Vorheriger Strompreis | Forrige strømpris | Vorige Elektriciteitsprijs | Föregående elpris | ❌ |
### Hourly Average Sensors
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-current_hour_average_price" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`current_hour_average_price` | ⌀ Hourly Price Current | ⌀ Stunden-Preis aktuell | ⌀ Timepris nåværende | ⌀ Uurprijs Huidig | ⌀ Timpris aktuell | ✅ |
| <span id="ref-next_hour_average_price" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`next_hour_average_price` | ⌀ Hourly Price Next | ⌀ Stunden-Preis nächste Stunde | ⌀ Timepris neste | ⌀ Uurprijs Volgend | ⌀ Timpris nästa | ✅ |
### Daily Statistics
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-lowest_price_today" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`lowest_price_today` | Today's Lowest Price | Mindestpreis heute | Dagens laveste pris | Laagste Prijs Vandaag | Dagens lägsta pris | ✅ |
| <span id="ref-highest_price_today" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`highest_price_today` | Today's Highest Price | Höchstpreis heute | Dagens høyeste pris | Hoogste Prijs Vandaag | Dagens högsta pris | ✅ |
| <span id="ref-average_price_today" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`average_price_today` | ⌀ Price Today | ⌀ Preis heute | ⌀ Pris i dag | ⌀ Prijs Vandaag | ⌀ Pris idag | ✅ |
| <span id="ref-lowest_price_tomorrow" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`lowest_price_tomorrow` | Tomorrow's Lowest Price | Mindestpreis morgen | Morgendagens laveste pris | Laagste Prijs Morgen | Morgondagens lägsta pris | ✅ |
| <span id="ref-highest_price_tomorrow" class="entity-anchor" data-refs="sensors-overview#daily-min-max"></span>`highest_price_tomorrow` | Tomorrow's Highest Price | Höchstpreis morgen | Morgendagens høyeste pris | Hoogste Prijs Morgen | Morgondagens högsta pris | ✅ |
| <span id="ref-average_price_tomorrow" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`average_price_tomorrow` | ⌀ Price Tomorrow | ⌀ Preis morgen | ⌀ Pris i morgen | ⌀ Prijs Morgen | ⌀ Pris imorgon | ✅ |
### 24h Window Sensors
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-trailing_price_average" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`trailing_price_average` | ⌀ Price Trailing 24h | ⌀ Preis nachlaufend 24h | ⌀ Pris glidende 24t | ⌀ Prijs Afgelopen 24u | ⌀ Pris glidande 24h | ❌ |
| <span id="ref-leading_price_average" class="entity-anchor" data-refs="sensors-average#available-average-sensors"></span>`leading_price_average` | ⌀ Price Leading 24h | ⌀ Preis vorlaufend 24h | ⌀ Pris fremtidig 24t | ⌀ Prijs Komende 24u | ⌀ Pris framåt 24h | ❌ |
| <span id="ref-trailing_price_min" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`trailing_price_min` | Trailing 24h Minimum Price | 24h-Mindestpreis nachlaufend | Glidende 24t minimumspris | Afgelopen 24u Minimumprijs | Glidande 24h minimipris | ❌ |
| <span id="ref-trailing_price_max" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`trailing_price_max` | Trailing 24h Maximum Price | 24h-Höchstpreis nachlaufend | Glidende 24t maksimumspris | Afgelopen 24u Maximumprijs | Glidande 24h maximipris | ❌ |
| <span id="ref-leading_price_min" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`leading_price_min` | Leading 24h Minimum Price | 24h-Mindestpreis vorlaufend | Fremtidig 24t minimumspris | Komende 24u Minimumprijs | Framåt 24h minimipris | ❌ |
| <span id="ref-leading_price_max" class="entity-anchor" data-refs="sensors-overview#24-hour-rolling-min-max"></span>`leading_price_max` | Leading 24h Maximum Price | 24h-Höchstpreis vorlaufend | Fremtidig 24t maksimumspris | Komende 24u Maximumprijs | Framåt 24h maximipris | ❌ |
### Future Price Averages
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-next_avg_1h" class="entity-anchor"></span>`next_avg_1h` | ⌀ Price Next 1h | ⌀ Preis nächste 1h | ⌀ Pris neste 1t | ⌀ Prijs Komende 1u | ⌀ Pris nästa 1h | ✅ |
| <span id="ref-next_avg_2h" class="entity-anchor"></span>`next_avg_2h` | ⌀ Price Next 2h | ⌀ Preis nächste 2h | ⌀ Pris neste 2t | ⌀ Prijs Komende 2u | ⌀ Pris nästa 2h | ✅ |
| <span id="ref-next_avg_3h" class="entity-anchor"></span>`next_avg_3h` | ⌀ Price Next 3h | ⌀ Preis nächste 3h | ⌀ Pris neste 3t | ⌀ Prijs Komende 3u | ⌀ Pris nästa 3h | ✅ |
| <span id="ref-next_avg_4h" class="entity-anchor"></span>`next_avg_4h` | ⌀ Price Next 4h | ⌀ Preis nächste 4h | ⌀ Pris neste 4t | ⌀ Prijs Komende 4u | ⌀ Pris nästa 4h | ✅ |
| <span id="ref-next_avg_5h" class="entity-anchor"></span>`next_avg_5h` | ⌀ Price Next 5h | ⌀ Preis nächste 5h | ⌀ Pris neste 5t | ⌀ Prijs Komende 5u | ⌀ Pris nästa 5h | ✅ |
| <span id="ref-next_avg_6h" class="entity-anchor"></span>`next_avg_6h` | ⌀ Price Next 6h | ⌀ Preis nächste 6h | ⌀ Pris neste 6t | ⌀ Prijs Komende 6u | ⌀ Pris nästa 6h | ❌ |
| <span id="ref-next_avg_8h" class="entity-anchor"></span>`next_avg_8h` | ⌀ Price Next 8h | ⌀ Preis nächste 8h | ⌀ Pris neste 8t | ⌀ Prijs Komende 8u | ⌀ Pris nästa 8h | ❌ |
| <span id="ref-next_avg_12h" class="entity-anchor"></span>`next_avg_12h` | ⌀ Price Next 12h | ⌀ Preis nächste 12h | ⌀ Pris neste 12t | ⌀ Prijs Komende 12u | ⌀ Pris nästa 12h | ❌ |
### Price Level Sensors
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-current_interval_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`current_interval_price_level` | Current Price Level | Aktuelles Preisniveau | Nåværende prisnivå | Huidig Prijsniveau | Aktuell prisnivå | ✅ |
| <span id="ref-next_interval_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`next_interval_price_level` | Next Price Level | Nächstes Preisniveau | Neste prisnivå | Volgend Prijsniveau | Nästa prisnivå | ✅ |
| <span id="ref-previous_interval_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`previous_interval_price_level` | Previous Price Level | Vorheriges Preisniveau | Forrige prisnivå | Vorig Prijsniveau | Föregående prisnivå | ❌ |
| <span id="ref-current_hour_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`current_hour_price_level` | Current Hour Price Level | Aktuelles Stunden-Preisniveau | Nåværende timepris nivå | Huidig Uur Prijsniveau | Aktuell timprisnivå | ✅ |
| <span id="ref-next_hour_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`next_hour_price_level` | Next Hour Price Level | Nächstes Stunden-Preisniveau | Neste timepris nivå | Volgend Uur Prijsniveau | Nästa timprisnivå | ✅ |
| <span id="ref-yesterday_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`yesterday_price_level` | Yesterday's Price Level | Preisniveau gestern | Prisnivå i går | Gisteren Prijsniveau | Gårdagens prisnivå | ❌ |
| <span id="ref-today_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`today_price_level` | Today's Price Level | Preisniveau heute | Prisnivå i dag | Vandaag Prijsniveau | Dagens prisnivå | ✅ |
| <span id="ref-tomorrow_price_level" class="entity-anchor" data-refs="sensors-ratings-levels#available-level-sensors"></span>`tomorrow_price_level` | Tomorrow's Price Level | Preisniveau morgen | Prisnivå i morgen | Morgen Prijsniveau | Morgondagens prisnivå | ✅ |
### Price Rating Sensors
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-current_interval_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`current_interval_price_rating` | Current Price Rating | Aktuelle Preisbewertung | Nåværende prisvurdering | Huidige Prijsbeoordeling | Aktuellt prisbetyg | ❌ |
| <span id="ref-next_interval_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`next_interval_price_rating` | Next Price Rating | Nächste Preisbewertung | Neste prisvurdering | Volgende Prijsbeoordeling | Nästa prisbetyg | ❌ |
| <span id="ref-previous_interval_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`previous_interval_price_rating` | Previous Price Rating | Vorherige Preisbewertung | Forrige prisvurdering | Vorige Prijsbeoordeling | Föregående prisbetyg | ❌ |
| <span id="ref-current_hour_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`current_hour_price_rating` | Current Hour Price Rating | Aktuelle Stunden-Preisbewertung | Nåværende timeprisvurdering | Huidig Uur Prijsbeoordeling | Aktuellt timprisbetyg | ❌ |
| <span id="ref-next_hour_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`next_hour_price_rating` | Next Hour Price Rating | Nächste Stunden-Preisbewertung | Neste timeprisvurdering | Volgend Uur Prijsbeoordeling | Nästa timprisbetyg | ❌ |
| <span id="ref-yesterday_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`yesterday_price_rating` | Yesterday's Price Rating | Preisbewertung gestern | Prisvurdering i går | Gisteren Prijsbeoordeling | Gårdagens prisbetyg | ❌ |
| <span id="ref-today_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`today_price_rating` | Today's Price Rating | Preisbewertung heute | Prisvurdering i dag | Vandaag Prijsbeoordeling | Dagens prisbetyg | ❌ |
| <span id="ref-tomorrow_price_rating" class="entity-anchor" data-refs="sensors-ratings-levels#available-rating-sensors"></span>`tomorrow_price_rating` | Tomorrow's Price Rating | Preisbewertung morgen | Prisvurdering i morgen | Morgen Prijsbeoordeling | Morgondagens prisbetyg | ❌ |
| <span id="ref-daily_rating" class="entity-anchor"></span>`daily_rating` | Daily Price Rating | Tägliche Preisbewertung | Daglig prisvurdering | Dagelijkse Prijsbeoordeling | Dagligt prisbetyg | ✅ |
| <span id="ref-monthly_rating" class="entity-anchor"></span>`monthly_rating` | Monthly Price Rating | Monatliche Preisbewertung | Månedlig prisvurdering | Maandelijkse Prijsbeoordeling | Månatligt prisbetyg | ✅ |
### Price Outlook & Trend
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-current_price_trend" class="entity-anchor"></span>`current_price_trend` | Current Price Trend | Aktueller Preistrend | Nåværende pristrend | Huidige Prijstrend | Aktuell pristrend | ✅ |
| <span id="ref-next_price_trend_change" class="entity-anchor"></span>`next_price_trend_change` | Next Price Trend Change | Nächste Trendänderung | Neste trendendring | Volgende Prijstrend Wijziging | Nästa pristrendändring | ✅ |
| <span id="ref-next_price_trend_change_in" class="entity-anchor"></span>`next_price_trend_change_in` | Next Price Trend Change In | Nächste Trendänderung in | Neste trendendring om | Volgende Prijstrend Wijziging over | Nästa pristrendändring om | ✅ |
| <span id="ref-price_outlook_1h" class="entity-anchor"></span>`price_outlook_1h` | Price Outlook (1h) | Preisausblick (1h) | Prisutblikk (1t) | Prijsvooruitzicht (1u) | Prisöversikt (1h) | ✅ |
| <span id="ref-price_outlook_2h" class="entity-anchor"></span>`price_outlook_2h` | Price Outlook (2h) | Preisausblick (2h) | Prisutblikk (2t) | Prijsvooruitzicht (2u) | Prisöversikt (2h) | ✅ |
| <span id="ref-price_outlook_3h" class="entity-anchor"></span>`price_outlook_3h` | Price Outlook (3h) | Preisausblick (3h) | Prisutblikk (3t) | Prijsvooruitzicht (3u) | Prisöversikt (3h) | ✅ |
| <span id="ref-price_outlook_4h" class="entity-anchor"></span>`price_outlook_4h` | Price Outlook (4h) | Preisausblick (4h) | Prisutblikk (4t) | Prijsvooruitzicht (4u) | Prisöversikt (4h) | ✅ |
| <span id="ref-price_outlook_5h" class="entity-anchor"></span>`price_outlook_5h` | Price Outlook (5h) | Preisausblick (5h) | Prisutblikk (5t) | Prijsvooruitzicht (5u) | Prisöversikt (5h) | ✅ |
| <span id="ref-price_outlook_6h" class="entity-anchor"></span>`price_outlook_6h` | Price Outlook (6h) | Preisausblick (6h) | Prisutblikk (6t) | Prijsvooruitzicht (6u) | Prisöversikt (6h) | ❌ |
| <span id="ref-price_outlook_8h" class="entity-anchor"></span>`price_outlook_8h` | Price Outlook (8h) | Preisausblick (8h) | Prisutblikk (8t) | Prijsvooruitzicht (8u) | Prisöversikt (8h) | ❌ |
| <span id="ref-price_outlook_12h" class="entity-anchor"></span>`price_outlook_12h` | Price Outlook (12h) | Preisausblick (12h) | Prisutblikk (12t) | Prijsvooruitzicht (12u) | Prisöversikt (12h) | ❌ |
| <span id="ref-price_trajectory_2h" class="entity-anchor"></span>`price_trajectory_2h` | Price Trajectory (2h) | Preisverlauf (2h) | Prisforløp (2t) | Prijstrajectorie (2u) | Prisutveckling (2h) | ✅ |
| <span id="ref-price_trajectory_3h" class="entity-anchor"></span>`price_trajectory_3h` | Price Trajectory (3h) | Preisverlauf (3h) | Prisforløp (3t) | Prijstrajectorie (3u) | Prisutveckling (3h) | ✅ |
| <span id="ref-price_trajectory_4h" class="entity-anchor"></span>`price_trajectory_4h` | Price Trajectory (4h) | Preisverlauf (4h) | Prisforløp (4t) | Prijstrajectorie (4u) | Prisutveckling (4h) | ✅ |
| <span id="ref-price_trajectory_5h" class="entity-anchor"></span>`price_trajectory_5h` | Price Trajectory (5h) | Preisverlauf (5h) | Prisforløp (5t) | Prijstrajectorie (5u) | Prisutveckling (5h) | ✅ |
| <span id="ref-price_trajectory_6h" class="entity-anchor"></span>`price_trajectory_6h` | Price Trajectory (6h) | Preisverlauf (6h) | Prisforløp (6t) | Prijstrajectorie (6u) | Prisutveckling (6h) | ❌ |
| <span id="ref-price_trajectory_8h" class="entity-anchor"></span>`price_trajectory_8h` | Price Trajectory (8h) | Preisverlauf (8h) | Prisforløp (8t) | Prijstrajectorie (8u) | Prisutveckling (8h) | ❌ |
| <span id="ref-price_trajectory_12h" class="entity-anchor"></span>`price_trajectory_12h` | Price Trajectory (12h) | Preisverlauf (12h) | Prisforløp (12t) | Prijstrajectorie (12u) | Prisutveckling (12h) | ❌ |
### Volatility Sensors
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-today_volatility" class="entity-anchor" data-refs="sensors-volatility#available-volatility-sensors"></span>`today_volatility` | Today's Price Volatility | Volatilität heute | Volatilitet i dag | Vandaag Prijsvolatiliteit | Dagens prisvolatilitet | ✅ |
| <span id="ref-tomorrow_volatility" class="entity-anchor" data-refs="sensors-volatility#available-volatility-sensors"></span>`tomorrow_volatility` | Tomorrow's Price Volatility | Volatilität morgen | Volatilitet i morgen | Morgen Prijsvolatiliteit | Morgondagens prisvolatilitet | ❌ |
| <span id="ref-next_24h_volatility" class="entity-anchor"></span>`next_24h_volatility` | Next 24h Price Volatility | Volatilität der nächsten 24h | Volatilitet neste 24t | Komende 24u Prijsvolatiliteit | Nästa 24h prisvolatilitet | ❌ |
| <span id="ref-today_tomorrow_volatility" class="entity-anchor" data-refs="sensors-volatility#available-volatility-sensors"></span>`today_tomorrow_volatility` | Today+Tomorrow Price Volatility | Volatilität heute+morgen | Volatilitet i dag+i morgen | Vandaag+Morgen Prijsvolatiliteit | Idag+Imorgon prisvolatilitet | ❌ |
### Best Price Timing
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-best_price_end_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_end_time` | Best Price End | Bestpreis endet | Beste pris slutter | Beste Prijs Einde | Bästa pris slutar | ✅ |
| <span id="ref-best_price_period_duration" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_period_duration` | Best Price Duration | Bestpreis Dauer | Beste pris varighet | Beste Prijs Duur | Bästa pris varaktighet | ❌ |
| <span id="ref-best_price_remaining_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_remaining_minutes` | Best Price Remaining Time | Bestpreis verbleibend | Beste pris gjenværende tid | Beste Prijs Resterende Tijd | Bästa pris återstående tid | ✅ |
| <span id="ref-best_price_progress" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_progress` | Best Price Progress | Bestpreis Fortschritt | Beste pris fremgang | Beste Prijs Voortgang | Bästa pris framsteg | ✅ |
| <span id="ref-best_price_next_start_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_next_start_time` | Best Price Start | Bestpreis startet | Beste pris starter | Beste Prijs Start | Bästa pris startar | ✅ |
| <span id="ref-best_price_next_in_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`best_price_next_in_minutes` | Best Price Starts In | Bestpreis startet in | Beste pris starter om | Beste Prijs Start Over | Bästa pris startar om | ✅ |
### Peak Price Timing
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-peak_price_end_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_end_time` | Peak Price End | Spitzenpreis endet | Topppris slutter | Piekprijs Einde | Topppris slutar | ✅ |
| <span id="ref-peak_price_period_duration" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_period_duration` | Peak Price Duration | Spitzenpreis Dauer | Topppris varighet | Piekprijs Duur | Topppris varaktighet | ❌ |
| <span id="ref-peak_price_remaining_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_remaining_minutes` | Peak Price Remaining Time | Spitzenpreis verbleibend | Topppris gjenværende tid | Piekprijs Resterende Tijd | Topppris återstående tid | ✅ |
| <span id="ref-peak_price_progress" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_progress` | Peak Price Progress | Spitzenpreis Fortschritt | Topppris fremgang | Piekprijs Voortgang | Topppris framsteg | ✅ |
| <span id="ref-peak_price_next_start_time" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_next_start_time` | Peak Price Start | Spitzenpreis startet | Topppris starter | Piekprijs Start | Topppris startar | ✅ |
| <span id="ref-peak_price_next_in_minutes" class="entity-anchor" data-refs="sensors-timing#available-timing-sensors"></span>`peak_price_next_in_minutes` | Peak Price Starts In | Spitzenpreis startet in | Topppris starter om | Piekprijs Start Over | Topppris startar om | ✅ |
### Home & Metering Metadata
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-home_type" class="entity-anchor"></span>`home_type` | Home Type | Wohnungstyp | Boligtype | Huistype | Hemtyp | ❌ |
| <span id="ref-home_size" class="entity-anchor"></span>`home_size` | Home Size | Wohnfläche | Boligareal | Huisgrootte | Hemstorlek | ❌ |
| <span id="ref-main_fuse_size" class="entity-anchor"></span>`main_fuse_size` | Main Fuse Size | Hauptsicherung | Hovedsikring | Hoofdzekering Grootte | Huvudsäkringsstorlek | ❌ |
| <span id="ref-number_of_residents" class="entity-anchor"></span>`number_of_residents` | Number of Residents | Anzahl Bewohner | Antall beboere | Aantal Bewoners | Antal boende | ❌ |
| <span id="ref-primary_heating_source" class="entity-anchor"></span>`primary_heating_source` | Primary Heating Source | Primäre Heizquelle | Primær varmekilde | Primaire Verwarmingsbron | Primär värmekälla | ❌ |
| <span id="ref-grid_company" class="entity-anchor"></span>`grid_company` | Grid Company | Netzbetreiber | Nettselskap | Netbedrijf | Nätbolag | ✅ |
| <span id="ref-grid_area_code" class="entity-anchor"></span>`grid_area_code` | Grid Area Code | Netzgebietscode | Nettområdekode | Netgebiedcode | Nätområdeskod | ❌ |
| <span id="ref-price_area_code" class="entity-anchor"></span>`price_area_code` | Price Area Code | Preiszonencode | Prisområdekode | Prijsgebiedcode | Prisområdeskod | ❌ |
| <span id="ref-consumption_ean" class="entity-anchor"></span>`consumption_ean` | Consumption EAN | Verbrauchs-EAN | Forbruks-EAN | Verbruik EAN | Förbruknings-EAN | ❌ |
| <span id="ref-production_ean" class="entity-anchor"></span>`production_ean` | Production EAN | Erzeugungs-EAN | Produksjons-EAN | Productie EAN | Produktions-EAN | ❌ |
| <span id="ref-energy_tax_type" class="entity-anchor"></span>`energy_tax_type` | Energy Tax Type | Energiesteuertyp | Energiavgiftstype | Energiebelasting Type | Energiskattetyp | ❌ |
| <span id="ref-vat_type" class="entity-anchor"></span>`vat_type` | VAT Type | Mehrwertsteuertyp | MVA-type | BTW Type | Momstyp | ❌ |
| <span id="ref-estimated_annual_consumption" class="entity-anchor"></span>`estimated_annual_consumption` | Estimated Annual Consumption | Geschätzter Jahresverbrauch | Estimert årlig forbruk | Geschat Jaarverbruik | Beräknad årlig förbrukning | ✅ |
| <span id="ref-subscription_status" class="entity-anchor"></span>`subscription_status` | Subscription Status | Abonnementstatus | Abonnementsstatus | Abonnement Status | Abonnemangsstatus | ❌ |
### Data & Diagnostics
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-data_lifecycle_status" class="entity-anchor"></span>`data_lifecycle_status` | Data Lifecycle Status | Datenlebenszyklus-Status | Datalivssyklus-status | Data Levenscyclus Status | Datalivscykelstatus | ✅ |
| <span id="ref-chart_data_export" class="entity-anchor"></span>`chart_data_export` | Chart Data Export | Diagramm-Datenexport | Diagramdataeksport | Grafiekdata Export | Diagramdataexport | ❌ |
| <span id="ref-chart_metadata" class="entity-anchor"></span>`chart_metadata` | Chart Metadata | Diagramm-Metadaten | Diagrammetadata | Grafiek Metadata | Diagrammetadata | ✅ |
### Other
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-current_hour_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_hour_price_rank_today` | ⌀ Hourly Price Current Rank (Today) | ⌀ Stündlicher Preisrang Aktuell (heute) | ⌀ Timesprisrang nå (i dag) | ⌀ Uurlijkse prijsrang huidig (vandaag) | ⌀ Timprisrang aktuell (idag) | ❌ |
| <span id="ref-current_hour_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_hour_price_rank_today_tomorrow` | ⌀ Hourly Price Current Rank (Today+Tomorrow) | ⌀ Stündlicher Preisrang Aktuell (heute+morgen) | ⌀ Timesprisrang nå (i dag+i morgen) | ⌀ Uurlijkse prijsrang huidig (vandaag+morgen) | ⌀ Timprisrang aktuell (idag+imorgon) | ❌ |
| <span id="ref-current_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_today` | Current Price Rank (Today) | Aktueller Preisrang (heute) | Aktuell prisrang (i dag) | Huidige prijsrang (vandaag) | Aktuellt prisrang (idag) | ✅ |
| <span id="ref-current_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_today_tomorrow` | Current Price Rank (Today+Tomorrow) | Aktueller Preisrang (heute+morgen) | Aktuell prisrang (i dag+i morgen) | Huidige prijsrang (vandaag+morgen) | Aktuellt prisrang (idag+imorgon) | ❌ |
| <span id="ref-current_interval_price_rank_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`current_interval_price_rank_tomorrow` | Current Price Rank (Tomorrow) | Aktueller Preisrang (morgen) | Aktuell prisrang (i morgen) | Huidige prijsrang (morgen) | Aktuellt prisrang (imorgon) | ❌ |
| <span id="ref-current_price_phase" class="entity-anchor"></span>`current_price_phase` | Current Price Phase | Aktuelle Preisphase | Gjeldende Prisfase | Huidige Prijsfase | Aktuell Prisfas | ✅ |
| <span id="ref-current_price_phase_duration" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_duration` | Current Phase Duration | Aktuelle Phase Dauer | Gjeldende fase varighet | Huidige Fase Duur | Aktuell fas varaktighet | ❌ |
| <span id="ref-current_price_phase_end_time" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_end_time` | Current Phase End Time | Aktuelle Phase Endzeit | Gjeldende fase sluttid | Huidige Fase Eindtijd | Aktuell fas sluttid | ✅ |
| <span id="ref-current_price_phase_progress" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_progress` | Current Phase Progress | Aktuelle Phase Fortschritt | Gjeldende fase fremgang | Huidige Fase Voortgang | Aktuell fas framsteg | ❌ |
| <span id="ref-current_price_phase_remaining_minutes" class="entity-anchor" data-refs="sensors-price-phases#current-phase-timing-4-sensors"></span>`current_price_phase_remaining_minutes` | Current Phase Remaining | Aktuelle Phase verbleibend | Gjeldende fase gjenværende | Huidige Fase Resterend | Aktuell fas återstående | ✅ |
| <span id="ref-day_pattern_today" class="entity-anchor"></span>`day_pattern_today` | Today's Price Pattern | Preismuster Heute | Prismønster i dag | Prijspatroon Vandaag | Prismönster Idag | ✅ |
| <span id="ref-day_pattern_tomorrow" class="entity-anchor"></span>`day_pattern_tomorrow` | Tomorrow's Price Pattern | Preismuster Morgen | Prismønster i morgen | Prijspatroon Morgen | Prismönster Imorgon | ❌ |
| <span id="ref-day_pattern_yesterday" class="entity-anchor"></span>`day_pattern_yesterday` | Yesterday's Price Pattern | Preismuster Gestern | Prismønster i går | Prijspatroon Gisteren | Prismönster Igår | ❌ |
| <span id="ref-next_falling_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_falling_phase_in_minutes` | Time to Next Falling Phase | Zeit bis nächste fallende Phase | Tid til neste fallende fase | Tijd tot Volgende Dalende Fase | Tid till nästa fallande fas | ❌ |
| <span id="ref-next_falling_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_falling_phase_start_time` | Next Falling Phase Start | Nächste fallende Phase Start | Neste fallende fase start | Volgende Dalende Fase Start | Nästa fallande fas start | ❌ |
| <span id="ref-next_flat_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_flat_phase_in_minutes` | Time to Next Flat Phase | Zeit bis nächste flache Phase | Tid til neste flate fase | Tijd tot Volgende Vlakke Fase | Tid till nästa flata fas | ❌ |
| <span id="ref-next_flat_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_flat_phase_start_time` | Next Flat Phase Start | Nächste flache Phase Start | Neste flate fase start | Volgende Vlakke Fase Start | Nästa flata fas start | ❌ |
| <span id="ref-next_hour_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_hour_price_rank_today` | ⌀ Hourly Price Next Rank (Today) | ⌀ Stündlicher Preisrang Nächste (heute) | ⌀ Timesprisrang neste (i dag) | ⌀ Uurlijkse prijsrang volgende (vandaag) | ⌀ Timprisrang nästa (idag) | ❌ |
| <span id="ref-next_hour_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_hour_price_rank_today_tomorrow` | ⌀ Hourly Price Next Rank (Today+Tomorrow) | ⌀ Stündlicher Preisrang Nächste (heute+morgen) | ⌀ Timesprisrang neste (i dag+i morgen) | ⌀ Uurlijkse prijsrang volgende (vandaag+morgen) | ⌀ Timprisrang nästa (idag+imorgon) | ❌ |
| <span id="ref-next_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_interval_price_rank_today` | Next Price Rank (Today) | Nächster Preisrang (heute) | Neste prisrang (i dag) | Volgende prijsrang (vandaag) | Nästa prisrang (idag) | ❌ |
| <span id="ref-next_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`next_interval_price_rank_today_tomorrow` | Next Price Rank (Today+Tomorrow) | Nächster Preisrang (heute+morgen) | Neste prisrang (i dag+i morgen) | Volgende prijsrang (vandaag+morgen) | Nästa prisrang (idag+imorgon) | ❌ |
| <span id="ref-next_price_phase" class="entity-anchor"></span>`next_price_phase` | Next Price Phase | Nächste Preisphase | Neste Prisfase | Volgende Prijsfase | Nästa Prisfas | ✅ |
| <span id="ref-next_rising_phase_in_minutes" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_rising_phase_in_minutes` | Time to Next Rising Phase | Zeit bis nächste steigende Phase | Tid til neste stigende fase | Tijd tot Volgende Stijgende Fase | Tid till nästa stigande fas | ❌ |
| <span id="ref-next_rising_phase_start_time" class="entity-anchor" data-refs="sensors-price-phases#next-phase-by-type-6-sensors"></span>`next_rising_phase_start_time` | Next Rising Phase Start | Nächste steigende Phase Start | Neste stigende fase start | Volgende Stijgende Fase Start | Nästa stigande fas start | ❌ |
| <span id="ref-previous_interval_price_rank_today" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`previous_interval_price_rank_today` | Previous Price Rank (Today) | Vorheriger Preisrang (heute) | Forrige prisrang (i dag) | Vorige prijsrang (vandaag) | Förra prisrang (idag) | ❌ |
| <span id="ref-previous_interval_price_rank_today_tomorrow" class="entity-anchor" data-refs="sensors-volatility#available-sensors"></span>`previous_interval_price_rank_today_tomorrow` | Previous Price Rank (Today+Tomorrow) | Vorheriger Preisrang (heute+morgen) | Forrige prisrang (i dag+i morgen) | Vorige prijsrang (vandaag+morgen) | Förra prisrang (idag+imorgon) | ❌ |
## Binary Sensors
### Binary Sensors
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-best_price_period" class="entity-anchor" data-refs="period-calculation#what-are-price-periods,sensors-overview#best-price-period-peak-price-period"></span>`best_price_period` | Best Price Period | Bestpreis-Zeitraum | Lavpris-periode | Beste Prijs Periode | Bästa Prisperiod | ✅ |
| <span id="ref-peak_price_period" class="entity-anchor" data-refs="period-calculation#what-are-price-periods,sensors-overview#best-price-period-peak-price-period"></span>`peak_price_period` | Peak Price Period | Spitzenpreis-Zeitraum | Toppris-periode | Piekprijs Periode | Topprisperiod | ✅ |
| <span id="ref-connection" class="entity-anchor"></span>`connection` | Tibber API Connection | Tibber-API-Verbindung | Tibber API-tilkobling | Tibber API Verbinding | Tibber API-anslutning | ✅ |
| <span id="ref-tomorrow_data_available" class="entity-anchor"></span>`tomorrow_data_available` | Tomorrow's Data Available | Morgige Daten verfügbar | Morgendagens data tilgjengelig | Morgen Gegevens Beschikbaar | Morgondagens data tillgänglig | ✅ |
| <span id="ref-has_ventilation_system" class="entity-anchor"></span>`has_ventilation_system` | Has Ventilation System | Hat Lüftungsanlage | Har ventilasjonsanlegg | Heeft Ventilatiesysteem | Har ventilationssystem | ❌ |
| <span id="ref-realtime_consumption_enabled" class="entity-anchor"></span>`realtime_consumption_enabled` | Realtime Consumption Enabled | Echtzeitverbrauch aktiviert | Sanntidsforbruk aktivert | Realtime Verbruik Ingeschakeld | Realtidsförbrukning aktiverad | ❌ |
### Other
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-in_falling_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_falling_price_phase` | In Falling Price Phase | In fallender Preisphase | I fallende prisfase | In Dalende Prijsfase | I fallande prisfas | ✅ |
| <span id="ref-in_flat_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_flat_price_phase` | In Flat Price Phase | In flacher Preisphase | I flat prisfase | In Vlakke Prijsfase | I flat prisfas | ✅ |
| <span id="ref-in_rising_price_phase" class="entity-anchor" data-refs="sensors-price-phases#binary-phase-sensors"></span>`in_rising_price_phase` | In Rising Price Phase | In steigender Preisphase | I stigende prisfase | In Stijgende Prijsfase | I stigande prisfas | ✅ |
## Number Entities (Configuration Overrides)
> These entities allow runtime adjustment of period calculation parameters without changing the integration configuration. All are **disabled by default**.
### Best Price Configuration
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-best_price_flex_override" class="entity-anchor"></span>`best_price_flex_override` | Best Price: Flexibility | Bestpreis: Flexibilität | Beste pris: Fleksibilitet | Beste prijs: Flexibiliteit | Bästa pris: Flexibilitet | ❌ |
| <span id="ref-best_price_min_distance_override" class="entity-anchor"></span>`best_price_min_distance_override` | Best Price: Minimum Distance | Bestpreis: Mindestabstand | Beste pris: Minimumsavstand | Beste prijs: Minimale afstand | Bästa pris: Minimiavstånd | ❌ |
| <span id="ref-best_price_min_period_length_override" class="entity-anchor"></span>`best_price_min_period_length_override` | Best Price: Minimum Period Length | Bestpreis: Mindestperiodenlänge | Beste pris: Minimum periodelengde | Beste prijs: Minimale periodelengte | Bästa pris: Minsta periodlängd | ❌ |
| <span id="ref-best_price_min_periods_override" class="entity-anchor"></span>`best_price_min_periods_override` | Best Price: Minimum Periods | Bestpreis: Mindestperioden | Beste pris: Minimum perioder | Beste prijs: Minimum periodes | Bästa pris: Minsta antal perioder | ❌ |
| <span id="ref-best_price_relaxation_attempts_override" class="entity-anchor"></span>`best_price_relaxation_attempts_override` | Best Price: Relaxation Attempts | Bestpreis: Lockerungsversuche | Beste pris: Lemping forsøk | Beste prijs: Versoepeling pogingen | Bästa pris: Lättnadsförsök | ❌ |
| <span id="ref-best_price_gap_count_override" class="entity-anchor"></span>`best_price_gap_count_override` | Best Price: Gap Tolerance | Bestpreis: Lückentoleranz | Beste pris: Gaptoleranse | Beste prijs: Gap tolerantie | Bästa pris: Glaptolerans | ❌ |
### Peak Price Configuration
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-peak_price_flex_override" class="entity-anchor"></span>`peak_price_flex_override` | Peak Price: Flexibility | Spitzenpreis: Flexibilität | Topppris: Fleksibilitet | Piekprijs: Flexibiliteit | Topppris: Flexibilitet | ❌ |
| <span id="ref-peak_price_min_distance_override" class="entity-anchor"></span>`peak_price_min_distance_override` | Peak Price: Minimum Distance | Spitzenpreis: Mindestabstand | Topppris: Minimumsavstand | Piekprijs: Minimale afstand | Topppris: Minimiavstånd | ❌ |
| <span id="ref-peak_price_min_period_length_override" class="entity-anchor"></span>`peak_price_min_period_length_override` | Peak Price: Minimum Period Length | Spitzenpreis: Mindestperiodenlänge | Topppris: Minimum periodelengde | Piekprijs: Minimale periodelengte | Topppris: Minsta periodlängd | ❌ |
| <span id="ref-peak_price_min_periods_override" class="entity-anchor"></span>`peak_price_min_periods_override` | Peak Price: Minimum Periods | Spitzenpreis: Mindestperioden | Topppris: Minimum perioder | Piekprijs: Minimum periodes | Topppris: Minsta antal perioder | ❌ |
| <span id="ref-peak_price_relaxation_attempts_override" class="entity-anchor"></span>`peak_price_relaxation_attempts_override` | Peak Price: Relaxation Attempts | Spitzenpreis: Lockerungsversuche | Topppris: Lemping forsøk | Piekprijs: Versoepeling pogingen | Topppris: Lättnadsförsök | ❌ |
| <span id="ref-peak_price_gap_count_override" class="entity-anchor"></span>`peak_price_gap_count_override` | Peak Price: Gap Tolerance | Spitzenpreis: Lückentoleranz | Topppris: Gaptoleranse | Piekprijs: Gap tolerantie | Topppris: Glaptolerans | ❌ |
## Switch Entities (Configuration Overrides)
> These switches control whether the relaxation algorithm is active for period detection. All are **disabled by default**.
### Switches
| Entity ID suffix | 🇬🇧 English | 🇩🇪 Deutsch | 🇳🇴 Norsk | 🇳🇱 Nederlands | 🇸🇪 Svenska | Default |
|---|---|---|---|---|---|---|
| <span id="ref-best_price_enable_relaxation_override" class="entity-anchor"></span>`best_price_enable_relaxation_override` | Best Price: Achieve Minimum Count | Bestpreis: Mindestanzahl erreichen | Beste pris: Oppnå minimumsantall | Beste prijs: Minimum aantal bereiken | Bästa pris: Uppnå minimiantal | ❌ |
| <span id="ref-peak_price_enable_relaxation_override" class="entity-anchor"></span>`peak_price_enable_relaxation_override` | Peak Price: Achieve Minimum Count | Spitzenpreis: Mindestanzahl erreichen | Topppris: Oppnå minimumsantall | Piekprijs: Minimum aantal bereiken | Topppris: Uppnå minimiantal | ❌ |

View file

@ -0,0 +1,206 @@
# Average & Statistics Sensors
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
The integration provides several sensors that calculate average electricity prices over different time windows. These sensors show a **typical** price value that represents the overall price level, helping you make informed decisions about when to use electricity.
## Available Average Sensors
| Sensor | Description | Time Window |
|--------|-------------|-------------|
| <EntityRef id="average_price_today">Average Price Today</EntityRef> | Typical price for current calendar day | 00:00 - 23:59 today |
| <EntityRef id="average_price_tomorrow">Average Price Tomorrow</EntityRef> | Typical price for next calendar day | 00:00 - 23:59 tomorrow |
| <EntityRef id="trailing_price_average">Trailing Price Average</EntityRef> | Typical price for last 24 hours | Rolling 24h backward |
| <EntityRef id="leading_price_average">Leading Price Average</EntityRef> | Typical price for next 24 hours | Rolling 24h forward |
| <EntityRef id="current_hour_average_price">Current Hour Average</EntityRef> | Smoothed price around current time | 5 intervals (~75 min) |
| <EntityRef id="next_hour_average_price">Next Hour Average</EntityRef> | Smoothed price around next hour | 5 intervals (~75 min) |
| **Next N Hours Average** (`next_avg_1h``next_avg_12h`) | Future price forecast | 1h, 2h, 3h, 4h, 5h, 6h, 8h, 12h |
## Configurable Display: Median vs Mean
All average sensors support **two different calculation methods** for the state value:
- **Median** (default): The "middle value" when all prices are sorted. Resistant to extreme price spikes, shows the **typical** price level you experienced.
- **Arithmetic Mean**: The mathematical average including all prices. Better for **cost calculations** but affected by extreme spikes.
**Why two values matter:**
<details>
<summary>Show YAML: Why two values matter</summary>
```yaml
# Example price data for one day:
# Prices: 10, 12, 13, 15, 80 ct/kWh (one extreme spike)
#
# Median = 13 ct/kWh ← "Typical" price level (middle value)
# Mean = 26 ct/kWh ← Mathematical average (affected by spike)
```
</details>
The median shows you what price level was **typical** during that period, while the mean shows the actual **average cost** if you consumed evenly throughout the period.
## Configuring the Display
You can choose which value is displayed in the sensor state:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** on your home
3. Navigate to **Step 6: Average Sensor Display Settings**
4. Choose between:
- **Median** (default) - Shows typical price level, resistant to spikes
- **Arithmetic Mean** - Shows actual mathematical average
**Important:** Both values are **always available** as sensor attributes, regardless of your choice! This ensures your automations continue to work if you change the display setting.
## Using Both Values in Automations
Both `price_mean` and `price_median` are always available as attributes:
<details>
<summary>Show YAML: Mean and Median in Automations</summary>
```yaml
# Example: Get both values regardless of display setting
sensor:
- platform: template
sensors:
daily_price_analysis:
friendly_name: "Daily Price Analysis"
value_template: >
{% set median = state_attr('sensor.<home_name>_price_today', 'price_median') %}
{% set mean = state_attr('sensor.<home_name>_price_today', 'price_mean') %}
{% set current = states('sensor.<home_name>_current_electricity_price') | float %}
{% if current < median %}
Below typical ({{ ((1 - current/median) * 100) | round(1) }}% cheaper)
{% elif current < mean %}
Typical price range
{% else %}
Above average ({{ ((current/mean - 1) * 100) | round(1) }}% more expensive)
{% endif %}
```
</details>
## Practical Examples
**Example 1: Smart dishwasher control**
Run dishwasher only when price is significantly below the daily typical level:
<details>
<summary>Show YAML: Practical Examples</summary>
```yaml
automation:
- alias: "Start Dishwasher When Cheap"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_best_price_period
to: "on"
condition:
# Only if current price is at least 20% below typical (median)
- condition: template
value_template: >
{% set current = states('sensor.<home_name>_current_electricity_price') | float %}
{% set median = state_attr('sensor.<home_name>_price_today', 'price_median') | float %}
{{ current < (median * 0.8) }}
action:
- service: switch.turn_on
entity_id: switch.dishwasher
```
</details>
**Example 2: Cost-aware heating control**
Use mean for actual cost calculations:
<details>
<summary>Show YAML: Mean for Cost Calculations</summary>
```yaml
automation:
- alias: "Heating Budget Control"
trigger:
- platform: time
at: "06:00:00"
action:
# Calculate expected daily heating cost
- variables:
mean_price: "{{ state_attr('sensor.<home_name>_price_today', 'price_mean') | float }}"
heating_kwh_per_day: 15 # Estimated consumption
daily_cost: "{{ (mean_price * heating_kwh_per_day / 100) | round(2) }}"
- service: notify.mobile_app
data:
title: "Heating Cost Estimate"
message: "Expected cost today: €{{ daily_cost }} (avg price: {{ mean_price }} ct/kWh)"
```
</details>
**Example 3: Smart charging based on rolling average**
Use trailing average to understand recent price trends:
<details>
<summary>Show YAML: Practical Examples</summary>
```yaml
automation:
- alias: "EV Charging - Price Trend Based"
trigger:
- platform: state
entity_id: sensor.ev_battery_level
condition:
# Start charging if current price < 90% of recent 24h average
- condition: template
value_template: >
{% set current = states('sensor.<home_name>_current_electricity_price') | float %}
{% set trailing_avg = state_attr('sensor.<home_name>_price_trailing_24h', 'price_median') | float %}
{{ current < (trailing_avg * 0.9) }}
# And battery < 80%
- condition: numeric_state
entity_id: sensor.ev_battery_level
below: 80
action:
- service: switch.turn_on
entity_id: switch.ev_charger
```
</details>
## Key Attributes
All average sensors provide these attributes:
| Attribute | Description | Example |
|-----------|-------------|---------|
| `price_mean` | Arithmetic mean (always available) | 25.3 ct/kWh |
| `price_median` | Median value (always available) | 22.1 ct/kWh |
| `interval_count` | Number of intervals included | 96 |
| `timestamp` | Reference time for calculation | 2025-12-18T00:00:00+01:00 |
**Note:** The `price_mean` and `price_median` attributes are **always present** regardless of which value you configured for display. This ensures automation compatibility when changing the display setting.
## When to Use Which Value
**Use Median for:**
- ✅ Comparing "typical" price levels across days
- ✅ Determining if current price is unusually high/low
- ✅ User-facing displays ("What was today like?")
- ✅ Volatility analysis (comparing typical vs extremes)
**Use Mean for:**
- ✅ Cost calculations and budgeting
- ✅ Energy cost estimations
- ✅ Comparing actual average costs between periods
- ✅ Financial planning and forecasting
**Both values tell different stories:**
- High median + much higher mean = Expensive spikes occurred
- Low median + higher mean = Generally cheap with occasional spikes
- Similar median and mean = Stable prices (low volatility)

View file

@ -0,0 +1,122 @@
# Energy & Tax Attributes
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
Most price sensors include **energy price** and **tax** attributes that break down the total price into its components:
<details>
<summary>Show formula</summary>
```
total = energy_price + tax
```
</details>
These attributes appear on **all price sensors that display a raw price** (not on percentage, level, or trend sensors). The `energy_price` is the raw spot/market price, while `tax` includes all fees, surcharges, and taxes added by your electricity provider.
:::note Transition After Update
After updating the integration, the `energy_price` and `tax` attributes will appear gradually as new price data is fetched from the Tibber API. Existing cached intervals (up to ~2 days old) won't have these fields yet — the attributes will simply be absent until fresh data replaces them. No action needed.
:::
## Where These Attributes Appear
| Sensor Type | `energy_price` | `tax` | Notes |
|-------------|:-:|:-:|-------|
| Current/Next/Previous Interval Price | ✅ | ✅ | Single interval values |
| Rolling Hour Average | ✅ | ✅ | Averaged across 5 intervals |
| Daily Min/Max/Average | ✅ | ✅ | Aggregated for the day |
| Trailing/Leading 24h | ✅ | ✅ | Aggregated across window |
| Future Average (N-hour) | ✅ | ✅ | Averaged across future window |
| Levels, Ratings, Trends | ❌ | ❌ | Not price sensors |
| Volatility | ❌ | ❌ | Statistical, not price |
## Use Cases
### Solar Feed-In & Net Metering (Saldering)
In countries like the Netherlands, solar feed-in compensation is based on the **raw energy/spot price**, not the total consumer price. The `energy_price` attribute gives you exactly this value — no more reverse-engineering from the total price with fragile template calculations.
<details>
<summary>Show YAML: Solar Feed-In and Net Metering</summary>
```yaml
# Example: Decide whether to export solar power or consume it
# Compare energy price (what you'd earn by exporting) vs. total price (what you'd pay)
automation:
- alias: "Solar: Export or Consume"
trigger:
- platform: numeric_state
entity_id: sensor.solar_production_power
above: 2000 # Producing more than 2 kW
condition:
- condition: template
value_template: >
{% set energy = state_attr('sensor.<home_name>_current_electricity_price', 'energy_price') %}
{% set total = states('sensor.<home_name>_current_electricity_price') | float %}
{# Export when energy price is high relative to total — you earn more #}
{{ energy is not none and energy > (total * 0.4) }}
action:
- service: switch.turn_off
entity_id: switch.battery_charging # Don't charge battery, export instead
```
</details>
### Price Composition Analysis
Understand how your electricity price is structured — useful for comparing across days or spotting trends in market prices vs. fees:
<details>
<summary>Show YAML: Price Composition Analysis</summary>
```yaml
# Template sensor showing tax share
template:
- sensor:
- name: "Electricity Tax Share"
unit_of_measurement: "%"
state: >
{% set tax = state_attr('sensor.<home_name>_current_electricity_price', 'tax') %}
{% set total = states('sensor.<home_name>_current_electricity_price') | float %}
{% if tax is not none and total > 0 %}
{{ ((tax / total) * 100) | round(1) }}
{% else %}
unavailable
{% endif %}
```
</details>
### Dashboard: Daily Cost Breakdown
Show users how today's average price splits into energy vs. tax:
<details>
<summary>Show YAML: Daily Cost Breakdown</summary>
```yaml
# Mushroom chips card showing the split
type: custom:mushroom-chips-card
chips:
- type: template
icon: mdi:flash
content: >
⚡ {{ state_attr('sensor.<home_name>_price_today', 'energy_price_mean') | round(1) }} ct
- type: template
icon: mdi:receipt-text
content: >
🏛️ {{ state_attr('sensor.<home_name>_price_today', 'tax_mean') | round(1) }} ct
```
</details>
## 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
The `energy_price` and `tax` fields are also available in the `get_chartdata` action. See [Chart Actions — Energy & Tax Fields](./chart-actions.md#energy--tax-fields) for details.

View file

@ -0,0 +1,195 @@
# Sensors Overview
> **Tip:** Many sensors have dynamic icons and colors! See the **[Dynamic Icons Guide](dynamic-icons.md)** and **[Dynamic Icon Colors Guide](icon-colors.md)** to enhance your dashboards.
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
The integration provides **100+ sensors** organized by purpose. This page gives a quick overview and links to detailed guides for each sensor family.
| Sensor Family | Purpose | Guide |
|---|---|---|
| **Binary Sensors** | Period on/off indicators | [below](#binary-sensors) |
| **Core Price** | Current, next, previous interval prices | [below](#core-price-sensors) |
| **Average & Statistics** | Daily averages, rolling averages, median/mean | [Average Sensors](sensors-average.md) |
| **Ratings & Levels** | Price classification (3-level ratings, 5-level API levels) | [Ratings & Levels](sensors-ratings-levels.md) |
| **Min/Max** | Daily and rolling 24h extremes | [below](#minmax-sensors) |
| **Volatility** | Price fluctuation analysis | [Volatility Sensors](sensors-volatility.md) |
| **Trends** | Price outlook, trajectory, direction | [Trend Sensors](sensors-trends.md) |
| **Timing** | Period countdown, progress, duration | [Timing Sensors](sensors-timing.md) |
| **Energy & Tax** | Spot price and tax breakdown | [Energy & Tax](sensors-energy-tax.md) |
| **Diagnostic** | Chart metadata, data export | [below](#diagnostic-sensors) |
---
## Binary Sensors
### Best Price Period & Peak Price Period
These binary sensors indicate when you're in a detected best or peak price period. See the **[Period Calculation Guide](period-calculation.md)** for a detailed explanation of how these periods are calculated and configured.
**Quick overview:**
- <EntityRef id="best_price_period">Best Price Period</EntityRef>: Turns ON during periods with significantly lower prices than the daily average
- <EntityRef id="peak_price_period">Peak Price Period</EntityRef>: Turns ON during periods with significantly higher prices than the daily average
Both sensors include rich attributes with period details, intervals, relaxation status, and more.
## Core Price Sensors
The integration provides price sensors for the **current**, **next**, and **previous** 15-minute interval. Each exposes the total price as sensor state, with `energy_price` and `tax` available as attributes (see [Energy & Tax Breakdown](sensors-energy-tax.md)).
**Next N Hours Average** sensors (`next_avg_1h``next_avg_12h`) provide future price forecasts for 1h, 2h, 3h, 4h, 5h, 6h, 8h, and 12h windows.
For detailed average sensor behavior (median vs mean, configuration, automation examples), see **[Average & Statistics Sensors](sensors-average.md)**.
## Min/Max Sensors
These sensors show the lowest and highest prices for calendar days and rolling windows:
### Daily Min/Max
| Sensor | Description |
|--------|-------------|
| <EntityRef id="lowest_price_today">Today's Lowest Price</EntityRef> | Minimum price today (00:0023:59) |
| <EntityRef id="highest_price_today">Today's Highest Price</EntityRef> | Maximum price today (00:0023:59) |
| <EntityRef id="lowest_price_tomorrow">Tomorrow's Lowest Price</EntityRef> | Minimum price tomorrow |
| <EntityRef id="highest_price_tomorrow">Tomorrow's Highest Price</EntityRef> | Maximum price tomorrow |
### 24-Hour Rolling Min/Max
| Sensor | Description |
|--------|-------------|
| <EntityRef id="trailing_price_min">Trailing Price Min</EntityRef> | Lowest price in the last 24 hours |
| <EntityRef id="trailing_price_max">Trailing Price Max</EntityRef> | Highest price in the last 24 hours |
| <EntityRef id="leading_price_min">Leading Price Min</EntityRef> | Lowest price in the next 24 hours |
| <EntityRef id="leading_price_max">Leading Price Max</EntityRef> | Highest price in the next 24 hours |
### Key Attributes
All min/max sensors include:
| Attribute | Description |
|-----------|-------------|
| `timestamp` | When the extreme price occurs/occurred |
| `price_diff_from_daily_min` | Difference from daily minimum |
| `price_diff_from_daily_min_%` | Percentage difference |
## Diagnostic Sensors
### Chart Metadata
**Entity ID:** `sensor.<home_name>_chart_metadata`
> **✨ New Feature**: This sensor provides dynamic chart configuration metadata for optimal visualization. Perfect for use with the `get_apexcharts_yaml` action!
This diagnostic sensor provides essential chart configuration values as sensor attributes, enabling dynamic Y-axis scaling and optimal chart appearance in rolling window modes.
**Key Features:**
- **Dynamic Y-Axis Bounds**: Automatically calculates optimal `yaxis_min` and `yaxis_max` for your price data
- **Automatic Updates**: Refreshes when price data changes (coordinator updates)
- **Lightweight**: Metadata-only mode (no data processing) for fast response
- **State Indicator**: Shows `pending` (initialization), `ready` (data available), or `error` (service call failed)
**Attributes:**
- **`timestamp`**: When the metadata was last fetched
- **`yaxis_min`**: Suggested minimum value for Y-axis (optimal scaling)
- **`yaxis_max`**: Suggested maximum value for Y-axis (optimal scaling)
- **`currency`**: Currency code (e.g., "EUR", "NOK")
- **`resolution`**: Interval duration in minutes (usually 15)
- **`error`**: Error message if service call failed
**Usage:**
The `tibber_prices.get_apexcharts_yaml` action **automatically uses this sensor** for dynamic Y-axis scaling in `rolling_window` and `rolling_window_autozoom` modes! No manual configuration needed - just enable the action's result with `config-template-card` and the sensor provides optimal Y-axis bounds automatically.
See the **[Chart Examples Guide](chart-examples.md)** for practical examples!
---
### Chart Data Export
**Entity ID:** `sensor.<home_name>_chart_data_export`
**Default State:** Disabled (must be manually enabled)
> **⚠️ Legacy Feature**: This sensor is maintained for backward compatibility. For new integrations, use the **`tibber_prices.get_chartdata`** action instead, which offers more flexibility and better performance.
This diagnostic sensor provides cached chart-friendly price data that can be consumed by chart cards (ApexCharts, custom cards, etc.).
**Key Features:**
- **Configurable via Options Flow**: Service parameters can be configured through the integration's options menu (Step 7 of 7)
- **Automatic Updates**: Data refreshes on coordinator updates (every 15 minutes)
- **Attribute-Based Output**: Chart data is stored in sensor attributes for easy access
- **State Indicator**: Shows `pending` (before first call), `ready` (data available), or `error` (service call failed)
**Important Notes:**
- ⚠️ Disabled by default - must be manually enabled in entity settings
- ⚠️ Consider using the action instead for better control and flexibility
- ⚠️ Configuration updates require HA restart
**Attributes:**
The sensor exposes chart data with metadata in attributes:
- **`timestamp`**: When the data was last fetched
- **`error`**: Error message if service call failed
- **`data`** (or custom name): Array of price data points in configured format
**Configuration:**
To configure the sensor's output format:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** on your Tibber home
3. Navigate through the options wizard to **Step 7: Chart Data Export Settings**
4. Configure output format, filters, field names, and other options
5. Save and restart Home Assistant
**Available Settings:**
See the `tibber_prices.get_chartdata` action documentation for a complete list of available parameters. All action parameters can be configured through the options flow.
**Example Usage:**
<details>
<summary>Show YAML: Example Usage</summary>
```yaml
# ApexCharts card consuming the sensor
type: custom:apexcharts-card
series:
- entity: sensor.<home_name>_chart_data_export
data_generator: |
return entity.attributes.data;
```
</details>
**Migration Path:**
If you're currently using this sensor, consider migrating to the action:
<details>
<summary>Show YAML: Chart Data Export</summary>
```yaml
# Old approach (sensor)
- service: apexcharts_card.update
data:
entity: sensor.<home_name>_chart_data_export
# New approach (action)
- service: tibber_prices.get_chartdata
data:
entry_id: YOUR_CONFIG_ENTRY_ID
day: ["today", "tomorrow"]
output_format: array_of_objects
response_variable: chart_data
```
</details>

View file

@ -0,0 +1,365 @@
# Price Phase Sensors
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
Price Phase sensors tell you **where you are in the intra-day price curve** — whether prices are currently rising, falling, or flat, how long that phase lasts, and when specific phase types will occur next. They complement the [Trend Sensors](sensors-trends.md) (which compare current price against future averages) by giving you the structural shape of the day.
---
## Price Phases vs. Price Trends — What's the Difference?
Both sensor families answer "What are prices doing?" — but from different angles:
| | Price Phase Sensors | Trend Sensors |
|--|---------------------|--------------|
| **What they answer** | "Are we in a rising or falling stretch right now?" | "Is now cheaper or more expensive than the next N hours?" |
| **Based on** | Structural shape of the intra-day price curve | Comparison of current price vs. future window average |
| **Best for** | Understanding position in the day's price arc | Deciding whether to act now or wait |
| **Example** | "We're in a falling phase that ends at 15:30" | "Current price is 12% below the next 3h average" |
Think of price phases as the **skeleton of the day** and trend sensors as **real-time navigation**. Phases show you the map; trends show you which direction to drive.
---
## The Three Price Phases
Prices within a day are split into consecutive monotone segments — stretches where the direction is consistently one of:
```mermaid
stateDiagram-v2
direction LR
R: 📈 rising<br/><small>prices moving up</small>
F: 📉 falling<br/><small>prices moving down</small>
FL: ➡️ flat<br/><small>prices stable</small>
R --> F: local peak reached
F --> R: local valley reached
F --> FL: price levels out
FL --> R: new upward trend starts
R --> FL: momentum fades
FL --> F: new downward trend starts
```
**Exactly one phase is always active.** The three [binary sensors](#binary-phase-sensors) mirror this — exactly one of `in_rising_price_phase`, `in_falling_price_phase`, and `in_flat_price_phase` is ON at any time.
:::info Why only 3 phases?
Each phase segment is **monotone by definition** — it is either purely rising, purely falling, or flat. A "double valley" (W-shape) is not a single phase; it is a sequence of: _falling → flat → rising → flat → falling_. The [Day Pattern sensor](#day-pattern-sensors) gives you the composite day shape; the phase sensors tell you your current position within it.
:::
---
## Day Pattern Sensors
These sensors classify the **overall shape** of the day's price curve:
| Sensor | Entity ID | Default |
|--------|-----------|---------|
| **Today's Price Pattern** (`day_pattern_today`) | `sensor.<home_name>_day_pattern_today` | ✅ enabled |
| **Tomorrow's Price Pattern** (`day_pattern_tomorrow`) | `sensor.<home_name>_day_pattern_tomorrow` | ❌ disabled |
| **Yesterday's Price Pattern** (`day_pattern_yesterday`) | `sensor.<home_name>_day_pattern_yesterday` | ❌ disabled |
**States:**
| State | Shape | Description |
|-------|-------|-------------|
| `valley` | | Cheap in the middle of the day — covers both **V-shaped** (short, sharp dip) and **U-shaped** (extended cheap plateau) curves. Common during solar midday surplus or low-demand nights. |
| `peak` | ∩ | Expensive in the middle — cheap mornings and evenings. Covers both sharp Λ-peaks and broad plateau shapes. |
| `double_dip` | W | Two cheap windows — classic with cheap morning + cheap midday |
| `duck_curve` | M | Two expensive peaks — common on workdays with morning and evening demand (named after the energy industry's [duck curve](https://en.wikipedia.org/wiki/Duck_curve)) |
| `flat` | ─ | Little variation throughout the day |
| `rising` | / | Prices climb steadily through the day |
| `falling` | \ | Prices drop steadily through the day |
| `mixed` | ∿ | Irregular shape that doesn't fit a clean category |
**Key attributes:**
| Attribute | Description | Example |
|-----------|-------------|---------|
| `confidence` | Detection reliability (01) | `0.87` |
| `coefficient_of_variation` | Relative price spread (volatility measure) | `0.24` |
| `valley_start` / `valley_end` | Start/end of the primary cheap window | `11:00` / `15:00` |
| `peak_start` / `peak_end` | Start/end of the primary expensive window | `07:00` / `09:00` |
| `segment_count` | Number of intra-day phase segments | `4` |
**Example — Schedule based on tomorrow's pattern:**
<details>
<summary>Show YAML: Pre-schedule heat pump based on tomorrow's valley</summary>
```yaml
automation:
- alias: "Pre-schedule: Heat pump tomorrow's valley"
trigger:
- platform: time
at: "20:00:00"
condition:
- condition: template
value_template: >
{{ states('sensor.<home_name>_day_pattern_tomorrow') in ['valley', 'double_dip'] }}
- condition: template
value_template: >
{{ is_state('binary_sensor.<home_name>_tomorrow_data_available', 'on') }}
action:
- service: notify.mobile_app
data:
message: >
Tomorrow is a {{ states('sensor.<home_name>_day_pattern_tomorrow') }} day.
Cheap window: {{ state_attr('sensor.<home_name>_day_pattern_tomorrow', 'valley_start') }}
to {{ state_attr('sensor.<home_name>_day_pattern_tomorrow', 'valley_end') }}.
```
</details>
---
## Current and Next Price Phase
### Current Price Phase
**Entity ID:** `sensor.<home_name>_current_price_phase`
Shows which price phase is active right now: `rising`, `falling`, or `flat`. Along with the [binary phase sensors](#binary-phase-sensors) and [timing sensors](#phase-timing-sensors), this gives you a complete picture of your position in the day's price arc.
**Key attributes:**
| Attribute | Description | Example |
|-----------|-------------|---------|
| `start` | When this phase started | `2025-11-08T12:00:00+01:00` |
| `end` | When this phase ends | `2025-11-08T15:30:00+01:00` |
| `price_min` | Lowest price in this phase | `11.2` |
| `price_max` | Highest price in this phase | `18.7` |
| `price_mean` | Average price in this phase | `14.5` |
| `segment_index` | Position of this phase (0-based) | `1` |
| `segment_count` | Total number of phases today | `4` |
| `all_segments` | Full list of today's phases with times and prices | `[...]` |
**Tip:** `segment_index` and `segment_count` tell you your position in the day. If `segment_index=0` and the phase is `rising`, prices have been rising since midnight. If `segment_index=segment_count-1`, this is the final phase of the day.
### Next Price Phase
**Entity ID:** `sensor.<home_name>_next_price_phase`
Shows the phase that will follow after the current one ends. When the current phase is the last of the day, this sensor becomes unavailable. Same states and attributes as the Current Price Phase sensor.
**Use case:** Combine current and next to anticipate transitions:
| Current → Next | Interpretation |
|----------------|----------------|
| `rising``flat` | Peak is about to level off — consider acting now before it falls further |
| `falling``rising` | We're at or near the daily minimum — best window to start flexible loads |
| `falling``flat` | Approaching the stable bottom — prices won't drop much more |
| `flat``rising` | Stable prices are ending — prices will start climbing |
| `flat``falling` | Further drops are coming — wait if you can |
---
## Binary Phase Sensors
Three binary sensors let you trigger automations directly on the current phase type:
| Sensor | Entity ID | Default |
|--------|-----------|---------|
| <EntityRef id="in_rising_price_phase">In Rising Price Phase</EntityRef> | `binary_sensor.<home_name>_in_rising_price_phase` | ✅ enabled |
| <EntityRef id="in_falling_price_phase">In Falling Price Phase</EntityRef> | `binary_sensor.<home_name>_in_falling_price_phase` | ✅ enabled |
| <EntityRef id="in_flat_price_phase">In Flat Price Phase</EntityRef> | `binary_sensor.<home_name>_in_flat_price_phase` | ✅ enabled |
**Exactly one** of these is ON at any time — they are mutually exclusive mirrors of the `current_price_phase` sensor state.
**Example — Only run the dishwasher when prices are falling:**
<details>
<summary>Show YAML: Start dishwasher during falling price phase</summary>
```yaml
automation:
- alias: "Dishwasher: Start during falling phase"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_in_falling_price_phase
to: "on"
condition:
- condition: state
entity_id: binary_sensor.dishwasher_finished
state: "off"
# Make sure there's enough time left before prices turn
- condition: numeric_state
entity_id: sensor.<home_name>_current_price_phase_remaining_minutes
# dishwasher ECO cycle takes ~2 hours
above: 0.33 # displayed in hours → ~20 minutes minimum
action:
- service: switch.turn_on
target:
entity_id: switch.dishwasher
```
</details>
---
## Phase Timing Sensors
These sensors mirror the Best/Peak Price [timing sensors](sensors-timing.md) — but for the current price phase instead of best/peak price periods.
### Current Phase Timing (4 sensors)
| Sensor | Entity ID | Default | Updates |
|--------|-----------|---------|---------|
| <EntityRef id="current_price_phase_end_time">Phase End Time</EntityRef> | `sensor.<home_name>_current_price_phase_end_time` | ✅ enabled | every 15 min |
| <EntityRef id="current_price_phase_remaining_minutes">Phase Remaining Minutes</EntityRef> | `sensor.<home_name>_current_price_phase_remaining_minutes` | ✅ enabled | every minute |
| <EntityRef id="current_price_phase_duration">Phase Duration</EntityRef> | `sensor.<home_name>_current_price_phase_duration` | ❌ disabled | every 15 min |
| <EntityRef id="current_price_phase_progress">Phase Progress</EntityRef> | `sensor.<home_name>_current_price_phase_progress` | ❌ disabled | every minute |
**Duration vs. Remaining:** Duration is the *total* length of the current phase (doesn't change minute-by-minute). Remaining is the *countdown* (decreases every minute). Together they tell you both "how long is this phase?" and "how much is left?".
**Progress** (0100%) = `(duration remaining) / duration × 100`.
:::tip Know how far through a phase you are without maths
Enable `current_price_phase_progress` and use it in a dashboard bar card to visualise how far through the current phase you are. Near 100% means the phase is ending soon.
:::
### Next Phase by Type (6 sensors)
These sensors scan the remaining phases of today **and** tomorrow to find the next occurrence of each specific phase type:
| Sensor | Entity ID | Default | Updates |
|--------|-----------|---------|---------|
| <EntityRef id="next_rising_phase_start_time">Next Rising Phase Start Time</EntityRef> | `sensor.<home_name>_next_rising_phase_start_time` | ❌ disabled | every 15 min |
| <EntityRef id="next_falling_phase_start_time">Next Falling Phase Start Time</EntityRef> | `sensor.<home_name>_next_falling_phase_start_time` | ❌ disabled | every 15 min |
| <EntityRef id="next_flat_phase_start_time">Next Flat Phase Start Time</EntityRef> | `sensor.<home_name>_next_flat_phase_start_time` | ❌ disabled | every 15 min |
| <EntityRef id="next_rising_phase_in_minutes">Next Rising Phase In Minutes</EntityRef> | `sensor.<home_name>_next_rising_phase_in_minutes` | ❌ disabled | every minute |
| <EntityRef id="next_falling_phase_in_minutes">Next Falling Phase In Minutes</EntityRef> | `sensor.<home_name>_next_falling_phase_in_minutes` | ❌ disabled | every minute |
| <EntityRef id="next_flat_phase_in_minutes">Next Flat Phase In Minutes</EntityRef> | `sensor.<home_name>_next_flat_phase_in_minutes` | ❌ disabled | every minute |
:::info Start time vs. in minutes — which to use?
- Use the **start time sensors** (timestamp) when you need to show or compare a specific clock time: "The next price drop starts at 14:15."
- Use the **in minutes sensors** (duration/countdown) for automations with numeric comparisons: "Trigger 30 minutes before the next price rise."
Both are updated from the same data — choose whichever fits your automation logic.
:::
**These sensors are disabled by default** because they are only needed for advanced automation scenarios. Enable the ones you need in **Settings → Devices & Services → Tibber Prices → [your home] → Entities**.
---
## How to Use Phase Sensors Together
### Pattern: "Run only during falling phases with enough time left"
The most common use case: start a flexible appliance (dishwasher, washing machine) when prices are falling AND there's at least N minutes left before the phase ends.
<details>
<summary>Show YAML: Start when falling, at least 90 min remaining</summary>
```yaml
automation:
- alias: "Washing machine: Optimal start"
trigger:
- platform: state
entity_id: binary_sensor.<home_name>_in_falling_price_phase
to: "on"
- platform: time_pattern
minutes: "/15"
condition:
- condition: state
entity_id: binary_sensor.<home_name>_in_falling_price_phase
state: "on"
- condition: numeric_state
entity_id: sensor.<home_name>_current_price_phase_remaining_minutes
# washing machine cycle ≈ 1.5 hours; sensor shows hours
above: 1.5
- condition: state
entity_id: input_boolean.washing_machine_needs_run
state: "on"
action:
- service: switch.turn_on
target:
entity_id: switch.washing_machine
- service: input_boolean.turn_off
target:
entity_id: input_boolean.washing_machine_needs_run
```
</details>
### Pattern: "Alert before prices start rising"
Enable `next_rising_phase_in_minutes` and trigger a notification when the next price rise is imminent:
<details>
<summary>Show YAML: Alert 30 min before prices start rising</summary>
```yaml
automation:
- alias: "Alert: Price rise imminent"
trigger:
- platform: numeric_state
entity_id: sensor.<home_name>_next_rising_phase_in_minutes
below: 0.5 # 30 minutes (displayed in hours)
condition:
- condition: state
entity_id: binary_sensor.<home_name>_in_falling_price_phase
state: "on"
action:
- service: notify.mobile_app
data:
title: "Prices rising in 30 minutes"
message: >
The current falling phase ends at
{{ state_attr('sensor.<home_name>_current_price_phase', 'end') | as_timestamp | timestamp_custom('%H:%M') }}.
Start flexible loads now.
```
</details>
### Pattern: "Complete this combination for best results"
For maximum precision, combine phase sensors with Best Price Period or trend sensors:
| You want to... | Combine with |
|----------------|-------------|
| Find the cheapest window today | `in_falling_price_phase` + [Best Price Period](sensors-timing.md) |
| Confirm price direction | `current_price_phase` + [Price Outlook sensors](sensors-trends.md) |
| Plan tomorrow | `day_pattern_tomorrow` + `next_falling_phase_start_time` |
| Know if near the bottom | Current = `falling`, Next = `rising` |
| Know if near the top | Current = `rising`, Next = `falling` |
| Act before costs climb | `in_falling_price_phase` + `next_rising_phase_in_minutes` |
---
## Sensor Summary Table
### Sensors (enabled by default)
| Sensor | Entity ID | What It Shows |
|--------|-----------|---------------|
| Today's Price Pattern | `sensor.<home_name>_day_pattern_today` | Overall shape of today's price curve |
| Current Price Phase | `sensor.<home_name>_current_price_phase` | Active phase: rising / falling / flat |
| Next Price Phase | `sensor.<home_name>_next_price_phase` | Phase coming after the current one |
| Phase End Time | `sensor.<home_name>_current_price_phase_end_time` | Timestamp when current phase ends |
| Phase Remaining Minutes | `sensor.<home_name>_current_price_phase_remaining_minutes` | Countdown to end of current phase |
### Binary Sensors (all enabled by default)
| Sensor | Entity ID | ON when... |
|--------|-----------|-----------|
| In Rising Price Phase | `binary_sensor.<home_name>_in_rising_price_phase` | Prices are going up right now |
| In Falling Price Phase | `binary_sensor.<home_name>_in_falling_price_phase` | Prices are going down right now |
| In Flat Price Phase | `binary_sensor.<home_name>_in_flat_price_phase` | Prices are stable right now |
### Sensors (disabled by default)
| Sensor | Entity ID | What It Shows |
|--------|-----------|---------------|
| Yesterday's Price Pattern | `sensor.<home_name>_day_pattern_yesterday` | Yesterday's price shape (reference) |
| Tomorrow's Price Pattern | `sensor.<home_name>_day_pattern_tomorrow` | Tomorrow's price shape (once available) |
| Phase Duration | `sensor.<home_name>_current_price_phase_duration` | Total length of current phase |
| Phase Progress | `sensor.<home_name>_current_price_phase_progress` | 0100% through current phase |
| Next Rising Phase Start Time | `sensor.<home_name>_next_rising_phase_start_time` | When the next rising phase begins |
| Next Falling Phase Start Time | `sensor.<home_name>_next_falling_phase_start_time` | When the next falling phase begins |
| Next Flat Phase Start Time | `sensor.<home_name>_next_flat_phase_start_time` | When the next flat phase begins |
| Next Rising Phase In Minutes | `sensor.<home_name>_next_rising_phase_in_minutes` | Minutes until next rising phase |
| Next Falling Phase In Minutes | `sensor.<home_name>_next_falling_phase_in_minutes` | Minutes until next falling phase |
| Next Flat Phase In Minutes | `sensor.<home_name>_next_flat_phase_in_minutes` | Minutes until next flat phase |

View file

@ -0,0 +1,152 @@
# Ratings & Levels
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
The integration provides **two** classification systems for electricity prices. Both are useful, but serve different purposes.
## Ratings vs Levels at a Glance
| | Ratings | Levels |
|--|---------|--------|
| **Source** | Calculated by integration | Provided by Tibber API |
| **Scale** | 3 levels (LOW, NORMAL, HIGH) | 5 levels (VERY_CHEAP → VERY_EXPENSIVE) |
| **Basis** | Trailing 24h average | Daily min/max range |
| **Best for** | Automations (simple thresholds) | Dashboard displays (fine granularity) |
| **Configurable** | Yes (thresholds) | Gap tolerance only |
| **Automation attribute** | `rating_level` (always lowercase English) | `level` (always uppercase English) |
**Which to use?**
- **Automations**: Use **ratings** (3 simple states, configurable thresholds, hysteresis)
- **Dashboards**: Use **levels** (5 color-coded states, more visual granularity)
- **Advanced automations**: Combine both (e.g., "LOW rating AND VERY_CHEAP level")
---
## Rating Sensors
Rating sensors classify prices relative to the **trailing 24-hour average**, answering: "Is the current price cheap, normal, or expensive compared to recent history?"
### How Ratings Work
The integration calculates a **percentage difference** between the current price and the trailing 24-hour average:
<details>
<summary>Show formula: How Ratings Work</summary>
```
difference = ((current_price - trailing_avg) / abs(trailing_avg)) × 100%
```
</details>
This percentage is then classified:
| Rating | Condition (default) | Meaning |
|--------|---------------------|---------|
| **LOW** | difference ≤ -10% | Significantly below recent average |
| **NORMAL** | -10% < difference < +10% | Within normal range |
| **HIGH** | difference ≥ +10% | Significantly above recent average |
**Hysteresis** (default 2%) prevents flickering: once a rating enters LOW, it must cross -8% (not -10%) to return to NORMAL. This avoids rapid switching at threshold boundaries.
```mermaid
stateDiagram-v2
direction LR
LOW: 🟢 LOW<br/><small>price ≤ 10%</small>
NORMAL: 🟡 NORMAL<br/><small>10% … +10%</small>
HIGH: 🔴 HIGH<br/><small>price ≥ +10%</small>
LOW --> NORMAL: crosses 8%<br/><small>(hysteresis)</small>
NORMAL --> LOW: drops below 10%
NORMAL --> HIGH: rises above +10%
HIGH --> NORMAL: crosses +8%<br/><small>(hysteresis)</small>
```
> **The 2% gap** between entering (10%) and leaving (8%) a state prevents the sensor from flickering back and forth when prices hover near a threshold.
### Available Rating Sensors
| Sensor | Scope | Description |
|--------|-------|-------------|
| <EntityRef id="current_interval_price_rating">Current Price Rating</EntityRef> | Current interval | Rating of the current 15-minute price |
| <EntityRef id="next_interval_price_rating">Next Price Rating</EntityRef> | Next interval | Rating for the upcoming 15-minute price |
| <EntityRef id="previous_interval_price_rating">Previous Price Rating</EntityRef> | Previous interval | Rating for the past 15-minute price |
| <EntityRef id="current_hour_price_rating">Current Hour Price Rating</EntityRef> | Rolling 5-interval | Smoothed rating around the current hour |
| <EntityRef id="next_hour_price_rating">Next Hour Price Rating</EntityRef> | Rolling 5-interval | Smoothed rating around the next hour |
| <EntityRef id="yesterday_price_rating">Yesterday's Price Rating</EntityRef> | Calendar day | Aggregated rating for yesterday |
| <EntityRef id="today_price_rating">Today's Price Rating</EntityRef> | Calendar day | Aggregated rating for today |
| <EntityRef id="tomorrow_price_rating">Tomorrow's Price Rating</EntityRef> | Calendar day | Aggregated rating for tomorrow |
### Key Attributes
| Attribute | Description | Example |
|-----------|-------------|---------|
| `rating_level` | Language-independent rating (always lowercase) | `low` |
| `difference` | Percentage difference from trailing average | `-12.5` |
| `trailing_avg_24h` | The reference average used for classification | `22.3` |
### Usage in Automations
**Best Practice:** Always use the `rating_level` attribute (lowercase English) instead of the sensor state (which is translated to your HA language):
<details>
<summary>Show YAML: Usage in Automations</summary>
```yaml
# ✅ Correct — language-independent
condition:
- condition: template
value_template: >
{{ state_attr('sensor.<home_name>_current_price_rating', 'rating_level') == 'low' }}
# ❌ Avoid — breaks when HA language changes
condition:
- condition: state
entity_id: sensor.<home_name>_current_price_rating
state: "Low" # "Niedrig" in German, "Lav" in Norwegian...
```
</details>
### Configuration
Rating thresholds can be adjusted in the options flow:
1. Go to **Settings → Devices & Services → Tibber Prices → Configure**
2. Navigate to **Price Rating Thresholds**
3. Adjust LOW/HIGH thresholds, hysteresis, and gap tolerance
See [Price Rating Thresholds](config-price-rating.md) for details.
---
## Level Sensors
Level sensors show the **Tibber API's own price classification** with a 5-level scale:
| Level | Meaning | Numeric Value |
|-------|---------|---------------|
| **VERY_CHEAP** | Exceptionally low | -2 |
| **CHEAP** | Below average | -1 |
| **NORMAL** | Typical range | 0 |
| **EXPENSIVE** | Above average | +1 |
| **VERY_EXPENSIVE** | Exceptionally high | +2 |
### Available Level Sensors
| Sensor | Scope |
|--------|-------|
| <EntityRef id="current_interval_price_level">Current Price Level</EntityRef> | Current interval |
| <EntityRef id="next_interval_price_level">Next Price Level</EntityRef> | Next interval |
| <EntityRef id="previous_interval_price_level">Previous Price Level</EntityRef> | Previous interval |
| <EntityRef id="current_hour_price_level">Current Hour Price Level</EntityRef> | Rolling 5-interval window |
| <EntityRef id="next_hour_price_level">Next Hour Price Level</EntityRef> | Rolling 5-interval window |
| <EntityRef id="yesterday_price_level">Yesterday's Price Level</EntityRef> | Calendar day (aggregated) |
| <EntityRef id="today_price_level">Today's Price Level</EntityRef> | Calendar day (aggregated) |
| <EntityRef id="tomorrow_price_level">Tomorrow's Price Level</EntityRef> | Calendar day (aggregated) |
**Gap tolerance** smoothing is applied to prevent isolated level flickers (e.g., a single NORMAL between two CHEAPs → corrected to CHEAP). Configure in [Price Level Gap Tolerance](config-price-level.md).

View file

@ -0,0 +1,102 @@
# Timing Sensors
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
Timing sensors provide **real-time information about Best Price and Peak Price periods**: when they start, end, how long they last, and your progress through them.
```mermaid
stateDiagram-v2
direction LR
IDLE: ⏸️ IDLE<br/><small>No active period</small>
ACTIVE: ▶️ ACTIVE<br/><small>In period</small>
GRACE: ⏳ GRACE<br/><small>60s buffer</small>
IDLE --> ACTIVE: period starts
ACTIVE --> GRACE: period ends
GRACE --> IDLE: 60s elapsed
GRACE --> ACTIVE: new period starts<br/><small>(within grace)</small>
```
**IDLE** = waiting for next period (shows countdown via `next_in_minutes`). **ACTIVE** = inside a period (shows `progress` 0100% and `remaining_minutes`). **GRACE** = short buffer after a period ends, allowing back-to-back periods to merge seamlessly.
## Available Timing Sensors
For each period type (Best Price and Peak Price):
| Sensor | When Period Active | When No Active Period |
|--------|-------------------|----------------------|
| <EntityRef id="best_price_end_time" also="peak_price_end_time">End Time</EntityRef> | Current period's end time | Next period's end time |
| <EntityRef id="best_price_period_duration" also="peak_price_period_duration">Period Duration</EntityRef> | Current period length (minutes) | Next period length |
| <EntityRef id="best_price_remaining_minutes" also="peak_price_remaining_minutes">Remaining Minutes</EntityRef> | Minutes until current period ends | 0 |
| <EntityRef id="best_price_progress" also="peak_price_progress">Progress</EntityRef> | 0100% through current period | 0 |
| <EntityRef id="best_price_next_start_time" also="peak_price_next_start_time">Next Start Time</EntityRef> | When next-next period starts | When next period starts |
| <EntityRef id="best_price_next_in_minutes" also="peak_price_next_in_minutes">Next In Minutes</EntityRef> | Minutes to next-next period | Minutes to next period |
## Usage Examples
### Show Countdown to Next Cheap Window
<details>
<summary>Show YAML: Countdown to Next Cheap Window</summary>
```yaml
type: custom:mushroom-entity-card
entity: sensor.<home_name>_best_price_next_in_minutes
name: Next Cheap Window
icon: mdi:clock-fast
```
</details>
### Display Period Progress Bar
<details>
<summary>Show YAML: Display Period Progress Bar</summary>
```yaml
type: custom:bar-card
entity: sensor.<home_name>_best_price_progress
name: Best Price Progress
min: 0
max: 100
severity:
- from: 0
to: 50
color: green
- from: 50
to: 80
color: orange
- from: 80
to: 100
color: red
```
</details>
### Notify When Period Is Almost Over
<details>
<summary>Show YAML: Period Ending Notification</summary>
```yaml
automation:
- alias: "Warn: Best Price Ending Soon"
trigger:
- platform: numeric_state
entity_id: sensor.<home_name>_best_price_remaining_minutes
below: 15
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_best_price_remaining_minutes
above: 0
action:
- service: notify.mobile_app
data:
title: "Best Price Ending Soon"
message: "Only {{ states('sensor.<home_name>_best_price_remaining_minutes') }} minutes left!"
```
</details>

View file

@ -0,0 +1,345 @@
# Trend Sensors
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
Trend sensors help you understand **whether to act now or wait**. The integration provides two complementary families:
- **Price Outlook Sensors (1h12h):** Compare current price vs. future window average — "Is now cheaper than the next Nh on average?"
- **Price Trajectory Sensors (2h12h):** Compare first half vs. second half of the window — "Are prices rising or falling *within* the window?"
---
## Price Outlook Sensors (1h12h)
These sensors compare the **current price** with the **average price** of the next N hours:
| Sensor | Compares Against |
|--------|-----------------|
| **Price Outlook (1h)** (`price_outlook_1h`) | Average of next 1 hour |
| **Price Outlook (2h)** (`price_outlook_2h`) | Average of next 2 hours |
| **Price Outlook (3h)** (`price_outlook_3h`) | Average of next 3 hours |
| **Price Outlook (4h)** (`price_outlook_4h`) | Average of next 4 hours |
| **Price Outlook (5h)** (`price_outlook_5h`) | Average of next 5 hours |
| **Price Outlook (6h)** (`price_outlook_6h`) | Average of next 6 hours |
| **Price Outlook (8h)** (`price_outlook_8h`) | Average of next 8 hours |
| **Price Outlook (12h)** (`price_outlook_12h`) | Average of next 12 hours |
:::info Same Starting Point — All Outlook Sensors Use Your Current Price
All outlook sensors share the **same base: your current 15-minute price**. They differ only in how far ahead they average. The windows **overlap** — the 3h average includes ALL intervals from the 1h and 2h windows, plus one more hour.
**This means:**
- `price_outlook_3h` shows "current price vs. average of the **entire** next 3 hours" — **not** "what happens between hour 2 and hour 3"
- If 1h shows `falling` but 6h shows `rising`: near-term prices are below your current price, but looking at the full 6h window (which includes expensive evening hours), the overall average is above your current price
- Larger windows smooth out short-term fluctuations — a 30-minute price spike affects the 1h average more than the 6h average
**⚠️ At a price minimum, outlook sensors can be misleading!** If you're at the minimum and prices are about to rise, `price_outlook_3h` may still show `strongly_falling` because the cheap minimum pulls the 3h average below your current high price. Use `price_trajectory_3h` to see the direction *within* the window.
:::
**States:** Each sensor has one of five states:
```mermaid
stateDiagram-v2
direction LR
SF: ⬇️⬇️ strongly_falling<br/><small>2 · future ≤ 9%</small>
F: ⬇️ falling<br/><small>1 · future ≤ 3%</small>
S: ➡️ stable<br/><small>0 · within ±3%</small>
R: ⬆️ rising<br/><small>+1 · future ≥ +3%</small>
SR: ⬆️⬆️ strongly_rising<br/><small>+2 · future ≥ +9%</small>
SF --> F: price recovers
F --> S: approaches average
S --> R: future rises
R --> SR: accelerates
SR --> R: slows down
R --> S: stabilizes
S --> F: future drops
F --> SF: accelerates
```
| State | Meaning | `trend_value` |
|-------|---------|---------------|
| `strongly_falling` | Prices will drop significantly | -2 |
| `falling` | Prices will drop | -1 |
| `stable` | Prices staying roughly the same | 0 |
| `rising` | Prices will increase | +1 |
| `strongly_rising` | Prices will increase significantly | +2 |
**Key attributes:**
| Attribute | Description | Example |
|-----------|-------------|---------|
| `trend_value` | Numeric value for automations (-2 to +2) | `-1` |
| `trend_Nh_%` | Percentage difference from current price | `-12.3` |
| `next_Nh_avg` | Average price in the future window | `18.5` |
| `second_half_Nh_avg` | Average price in later half of window | `16.2` |
| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` |
| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` |
| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` |
| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` |
| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` |
**Tip:** The `trend_value` attribute (`-2` to `+2`) is ideal for automations — use numeric comparisons instead of matching translated state strings.
---
## Price Trajectory Sensors (2h12h)
These sensors compare the **first half** of the future window against the **second half** — revealing the price *direction within* the window.
| Sensor | Compares |
|--------|----------|
| **Price Trajectory (2h)** (`price_trajectory_2h`) | Avg of hour 1 vs avg of hour 2 |
| **Price Trajectory (3h)** (`price_trajectory_3h`) | Avg of first 1.5h vs avg of second 1.5h |
| **Price Trajectory (4h)** (`price_trajectory_4h`) | Avg of first 2h vs avg of second 2h |
| **Price Trajectory (5h)** (`price_trajectory_5h`) | Avg of first 2.5h vs avg of second 2.5h |
| **Price Trajectory (6h)** (`price_trajectory_6h`) | Avg of first 3h vs avg of second 3h |
| **Price Trajectory (8h)** (`price_trajectory_8h`) | Avg of first 4h vs avg of second 4h |
| **Price Trajectory (12h)** (`price_trajectory_12h`) | Avg of first 6h vs avg of second 6h |
**States:** Same 5-level scale as outlook sensors (`strongly_falling` → `strongly_rising`).
:::info Why trajectory sensors complement outlook sensors
**At a price minimum** — the exact moment you should act — `price_outlook_3h` may show `strongly_falling` because the cheap minimum pulls the entire 3h average below your current high price. But `price_trajectory_3h` shows `rising` because the second half (after the minimum) is more expensive than the first half.
| Combination | Interpretation |
|-------------|----------------|
| Outlook `falling` + Trajectory `rising` | **You're AT the minimum** — act now |
| Outlook `falling` + Trajectory `falling` | Prices still dropping — wait |
| Outlook `rising` + Trajectory `rising` | Strong signal to act now |
| Outlook `rising` + Trajectory `falling` | Short spike, then cheaper — wait |
:::
**Key attributes:**
| Attribute | Description | Example |
|-----------|-------------|---------|
| `trend_value` | Numeric value for automations (-2 to +2) | `1` |
| `first_half_avg` | Average price in first half of window | `12.4` |
| `second_half_avg` | Average price in second half of window | `18.1` |
| `half_diff_%` | Percentage difference (second vs first half) | `46.0` |
---
## Current Price Trend
**Entity ID:** `sensor.<home_name>_current_price_trend`
This sensor shows the **currently active trend direction** based on a 3-hour future outlook with volatility-adaptive thresholds.
Unlike the simple trend sensors that always compare current price vs future average, the current price trend represents the **ongoing trend** — it remains stable between updates and only changes when the underlying price direction actually shifts.
**States:** Same 5-level scale as simple trends.
**Key attributes:**
| Attribute | Description | Example |
|-----------|-------------|---------|
| `previous_direction` | Price direction before the current trend started | `falling` |
| `price_direction_duration_minutes` | How long prices have been moving in this direction | `45` |
| `price_direction_since` | Timestamp when prices started moving in this direction | `2025-11-08T14:00:00+01:00` |
---
## Next Price Trend Change
**Entity ID:** `sensor.<home_name>_next_price_trend_change`
This sensor predicts **when the current trend will change** by scanning future intervals. It requires 3 consecutive intervals (configurable: 26) confirming the new trend before reporting a change (hysteresis), which prevents false alarms from short-lived price spikes.
**Important:** Only **direction changes** count as trend changes. The five states are grouped into three directions:
| Direction | States |
|-----------|--------|
| **falling** | `strongly_falling`, `falling` |
| **stable** | `stable` |
| **rising** | `rising`, `strongly_rising` |
A change from `rising` to `strongly_rising` (same direction) is **not** reported as a trend change — only actual reversals like `rising``stable` or `falling``rising`.
**State:** Timestamp of the next trend change (or unavailable if no change predicted).
**Key attributes:**
| Attribute | Description | Example |
|-----------|-------------|---------|
| `direction` | What the trend will change TO | `rising` |
| `from_direction` | Current trend (will change FROM) | `falling` |
| `minutes_until_change` | Minutes until trend changes | `90` |
| `price_at_change` | Price at the change point | `13.8` |
| `price_avg_after_change` | Average price after change | `18.1` |
| `threshold_rising_%` | Active rising threshold after volatility adjustment | `3.0` |
| `threshold_rising_strongly_%` | Active strongly-rising threshold after volatility adjustment | `4.8` |
| `threshold_falling_%` | Active falling threshold after volatility adjustment | `-3.0` |
| `threshold_falling_strongly_%` | Active strongly-falling threshold after volatility adjustment | `-4.8` |
| `volatility_factor` | Applied multiplier (0.6 = low, 1.0 = moderate, 1.4 = high volatility) | `0.8` |
:::info How the detection works — 3-hour lookahead mean
At each future 15-minute interval, the sensor does a single comparison:
> **interval price** vs. **average price of the following 3 hours**
If that 3-hour-ahead average has moved in the opposite direction from the current trend, the interval counts as a "candidate" for a trend change. Once 3 consecutive candidates are found (hysteresis), the sensor reports the first one as the change timestamp.
**Why a 3-hour average and not the next price?**
A single future price is noisy — one cheap or expensive interval doesn't mean the trend has reversed. Averaging the next 3 hours smooths out spikes and gives a stable signal about where prices are broadly heading.
:::
:::caution On V-shaped price days — expect an early signal
On days with a sharp V-shaped curve (price drops steeply to a minimum, then rises steeply), this sensor typically fires **3060 minutes before the exact price minimum**.
**Why?** When the price is still falling toward the bottom, the 3-hour lookahead window already reaches across the minimum and starts including the rising prices on the other side. As soon as those rising prices pull the 3-hour average above the current falling price, the sensor detects "trend changing to rising" — even though the cheapest interval is still 24 steps ahead.
**Timeline example — V-shaped day:**
```
Time Price 3h-ahead avg Signal
13:00 24 ct 18 ct falling (avg still below current)
13:15 20 ct 17 ct falling
13:30 16 ct 17 ct ← avg crosses above current → RISING signal ✓
13:45 13 ct 18 ct (actual minimum — sensor already fired earlier)
14:00 16 ct 20 ct
14:15 21 ct 22 ct
```
**What to use instead if you need the exact minimum:**
Compare with the [Best Price Period](sensors-timing.md) start time — it finds the actual cheapest window, not the first moment the direction looks like it's changing.
| Goal | Use |
|------|-----|
| "When will prices broadly start rising?" | **Next Price Trend Change** (may fire early) |
| "When is the cheapest interval / window to run my appliance?" | **Best Price Period** (`best_price_period`, `best_price_next_start_time`) |
| "Am I at the minimum right now?" | **Outlook `falling` + Trajectory `rising`** combination |
:::
---
## Next Price Trend Change In (Countdown)
**Entity ID:** `sensor.<home_name>_next_price_trend_change_in`
A **countdown timer** companion to the Next Price Trend Change sensor above. Instead of a timestamp, it shows **how many minutes** remain until the trend changes direction.
**State:** Duration in minutes until the next trend change (displayed in hours via HA unit conversion). Unavailable if no change is predicted.
**Use cases:**
- Dashboard countdown: "Trend changes in 1.5 h"
- Automation trigger: "If trend change is less than 15 minutes away, prepare for price direction change"
**Example automation:**
<details>
<summary>Show YAML: Example automation</summary>
```yaml
trigger:
- platform: numeric_state
entity_id: sensor.<home_name>_next_price_trend_change_in
below: 0.25 # 15 minutes (displayed in hours)
action:
- service: notify.mobile_app
data:
message: "Price trend is about to change direction!"
```
</details>
**Tip:** Use this sensor for "HOW LONG" and the Next Price Trend Change sensor (timestamp) for "WHEN".
---
## How to Use Trend Sensors for Decisions
:::danger Common Misconception — Don't "Wait for Stable"!
A natural intuition is to treat trend states like a stock ticker:
- ❌ "It's **falling** → I'll wait until it reaches **stable** (the bottom)"
- ❌ "It's **rising** → too late, I missed the best price"
- ❌ "It's **stable** → now is the perfect time to act!"
**This is wrong.** Trend sensors don't show a trajectory — they show a **comparison** between your current price and future prices. The correct interpretation is the opposite:
| State | What the Sensor Calculates | ✅ Correct Action |
|-------|---------------------------|-------------------|
| `falling` | Current price **higher** than future average | **WAIT** — cheaper prices are coming |
| `strongly_falling` | Current price **much higher** than future average | **DEFINITELY WAIT** — significant savings ahead |
| `stable` | Current price **≈ equal** to future average | **Timing doesn't matter** — start whenever convenient |
| `rising` | Current price **lower** than future average | **ACT NOW** — it only gets more expensive |
| `strongly_rising` | Current price **much lower** than future average | **ACT IMMEDIATELY** — best price right now |
**"Rising" is NOT "too late" — it means NOW is the best time because prices will be higher later.**
:::
### Basic Automation Pattern
For most appliances (dishwasher, washing machine, dryer), a single outlook sensor is enough:
<details>
<summary>Show YAML: Basic Automation Pattern</summary>
```yaml
# Example: Start dishwasher when prices are favorable
trigger:
- platform: state
entity_id: sensor.my_home_price_outlook_3h
condition:
- condition: numeric_state
entity_id: sensor.my_home_price_outlook_3h
attribute: trend_value
# rising (1) or strongly_rising (2) = act now
above: 0
action:
- service: switch.turn_on
target:
entity_id: switch.dishwasher
```
</details>
### Combining Multiple Windows
When short-term and long-term trends disagree, you get richer insight:
| 1h Outlook | 6h Outlook | Interpretation | Recommendation |
|----------|----------|---------------|----------------|
| `rising` | `rising` | Prices going up across the board | **Start now** |
| `falling` | `falling` | Prices dropping across the board | **Wait** |
| `falling` | `rising` | Brief dip, then expensive evening | **Wait briefly**, then start during the dip |
| `rising` | `falling` | Short spike, but cheaper hours ahead | **Wait** if you can — better prices coming |
| `stable` | any | Short-term doesn't matter | Use the **longer window** for your decision |
### Dashboard Quick-Glance
On your dashboard, trend sensors give an instant overview:
- 🟢 All **falling/strongly_falling** → "Relax, prices are dropping — wait"
- 🔴 All **rising/strongly_rising** → "Start everything you can — it only gets more expensive"
- 🟡 **Mixed** → Compare short-term vs. long-term sensors, or check the Best Price Period sensor
---
## Outlook & Trajectory vs Average Sensors
Both sensor families provide future price information, but serve different purposes:
| | Outlook/Trajectory Sensors | Average Sensors |
|--|---------------------------|-----------------|
| **Purpose** | Dashboard display, quick visual overview | Automations, precise numeric comparisons |
| **Output** | Classification (falling/stable/rising) | Exact price values (ct/kWh) |
| **Best for** | "Should I worry about prices?" | "Is the future average below 15 ct?" |
| **Use in** | Dashboard icons, status displays | Template conditions, numeric thresholds |
**Design principle:** Use **trend sensors** (enum) for visual feedback at a glance, use **average sensors** (numeric) for precise decision-making in automations.
## Configuration
Trend thresholds can be adjusted in the options flow:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click **Configure** on your home
3. Navigate to **📈 Price Trend Thresholds**
4. Adjust the rising/falling and strongly rising/falling percentages
The thresholds are **volatility-adaptive**: on days with high price volatility, thresholds are widened automatically to prevent constant state changes. This means the trend sensors give more stable readings during volatile market conditions.

View file

@ -0,0 +1,308 @@
# Volatility Sensors
:::tip Entity ID tip
`<home_name>` is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. **Can't find a sensor?** Use the **[Entity Reference (All Languages)](sensor-reference.md)** to search by name in your language.
:::
Volatility sensors help you understand how much electricity prices fluctuate over a given period. Instead of just looking at the absolute price, they measure the **relative price variation**, which is a great indicator of whether it's a good day for price-based energy optimization.
The calculation is based on the **Coefficient of Variation (CV)**, a standardized statistical measure defined as:
`CV = (Standard Deviation / Arithmetic Mean) * 100%`
This results in a percentage that shows how much prices deviate from the average. A low CV means stable prices, while a high CV indicates significant price swings and thus, a high potential for saving money by shifting consumption.
The sensor's state can be `low`, `moderate`, `high`, or `very_high`, based on configurable thresholds.
## Available Volatility Sensors
| Sensor | Description | Time Window |
| --------------------------------------------------------------------------------------- | ----------------------------------------- | ---------------------- |
| <EntityRef id="today_volatility">Today's Price Volatility</EntityRef> | Volatility for the current calendar day | 00:00 - 23:59 today |
| <EntityRef id="tomorrow_volatility">Tomorrow's Price Volatility</EntityRef> | Volatility for the next calendar day | 00:00 - 23:59 tomorrow |
| **Next 24h Price Volatility** (`next_24h_volatility`) | Volatility for the next 24 hours from now | Rolling 24h forward |
| <EntityRef id="today_tomorrow_volatility">Today + Tomorrow Price Volatility</EntityRef> | Volatility across both today and tomorrow | Up to 48 hours |
## Configuration
You can adjust the CV thresholds that determine the volatility level:
1. Go to **Settings → Devices & Services → Tibber Prices**.
2. Click **Configure**.
3. Go to the **Price Volatility Thresholds** step.
Default thresholds are:
- **Moderate:** 15%
- **High:** 30%
- **Very High:** 50%
## Key Attributes
All volatility sensors provide these attributes:
| Attribute | Description | Example |
| ------------------------------- | ---------------------------------------------------------------- | ------------ |
| `price_volatility` | Volatility level (language-independent, always English) | `"moderate"` |
| `price_coefficient_variation_%` | The calculated Coefficient of Variation | `23.5` |
| `price_spread` | The difference between the highest and lowest price | `12.3` |
| `price_min` | The lowest price in the period | `10.2` |
| `price_max` | The highest price in the period | `22.5` |
| `price_mean` | The arithmetic mean of all prices in the period | `15.1` |
| `price_median` | Median price (50th percentile, robust to outliers) | `14.8` |
| `price_q25` | 25th percentile — lower quartile price | `11.0` |
| `price_q75` | 75th percentile — upper quartile price | `19.5` |
| `price_typical_spread` | Typical price band width — IQR (Q75 Q25, the middle 50% of prices) | `8.5` |
| `price_typical_spread_%` | Typical price band as a percentage of the median (IQR%) | `57.4` |
| `price_spike_count` | Intervals outside the Tukey fence (Q251.5×IQR … Q75+1.5×IQR) — spikes/dips | `3` |
| `interval_count` | Number of price intervals included in the calculation | `96` |
## Usage in Automations & Best Practices
You can use the volatility sensor to decide if a price-based optimization is worth it. For example, if your solar battery has conversion losses, you might only want to charge and discharge it on days with high volatility.
### Best Practice: Use the `price_volatility` Attribute
For automations, it is strongly recommended to use the `price_volatility` attribute instead of the sensor's main state.
- **Why?** The main `state` of the sensor is translated into your Home Assistant language (e.g., "Hoch" in German). If you change your system language, automations based on this state will break. The `price_volatility` attribute is **always in lowercase English** (`"low"`, `"moderate"`, `"high"`, `"very_high"`) and therefore provides a stable, language-independent value.
**Good Example (Robust Automation):**
This automation triggers only if the volatility is classified as `high` or `very_high`, respecting your central settings and working independently of the system language.
<details>
<summary>Show YAML: Good Example (Robust Automation)</summary>
```yaml
automation:
- alias: "Enable battery optimization only on volatile days"
trigger:
- platform: template
value_template: >
{{ state_attr('sensor.<home_name>_today_s_price_volatility', 'price_volatility') in ['high', 'very_high'] }}
action:
- service: input_boolean.turn_on
entity_id: input_boolean.battery_optimization_enabled
```
</details>
---
### Avoid Hard-Coding Numeric Thresholds
You might be tempted to use the numeric `price_coefficient_variation_%` attribute directly in your automations. This is not recommended.
- **Why?** The integration provides central configuration options for the volatility thresholds. By using the classified `price_volatility` attribute, your automations automatically adapt if you decide to change what you consider "high" volatility (e.g., changing the threshold from 30% to 35%). Hard-coding values means you would have to find and update them in every single automation.
**Bad Example (Brittle Automation):**
This automation uses a hard-coded value. If you later change the "High" threshold in the integration's options to 35%, this automation will not respect that change and might trigger at the wrong time.
<details>
<summary>Show YAML: Bad Example (Brittle Automation)</summary>
```yaml
automation:
- alias: "Brittle - Enable battery optimization"
trigger:
#
# BAD: Avoid hard-coding numeric values
#
- platform: numeric_state
entity_id: sensor.<home_name>_today_s_price_volatility
attribute: price_coefficient_variation_%
above: 30
action:
- service: input_boolean.turn_on
entity_id: input_boolean.battery_optimization_enabled
```
</details>
By following the "Good Example", your automations become simpler, more readable, and much easier to maintain.
## Typical Price Band Statistics (IQR)
In addition to the CV-based volatility level, every volatility sensor provides **typical price band statistics** as attributes. These are derived from the **IQR (Interquartile Range)** — the spread of the middle 50% of prices — making them more **robust to isolated price spikes** than the CV.
| Metric | CV (state) | IQR attributes |
| --------------------- | ----------------------------------------- | ---------------------------------- |
| Sensitive to spikes? | ✅ Yes — spikes inflate CV | ❌ No — IQR ignores the outer 25% |
| Use for optimization? | "Is today worth optimizing?" | "How wide is the core price band?" |
| Best for | Triggering battery/EV charging strategies | Understanding price structure |
The `price_typical_spread_%` attribute (IQR as a percentage of the median) tells you how wide the **core** price band is relative to the median. Even on a high-CV day with isolated spikes, a low `price_typical_spread_%` means most of the day has stable prices — only a few intervals are outliers.
The `price_spike_count` attribute (Tukey fence method: Q25 1.5×IQR to Q75 + 1.5×IQR) tells you how many intervals fall outside the normal range. A high `price_spike_count` day with a high CV is a classic "spiky" day: mostly stable prices with a few expensive or cheap peaks.
---
## Price Rank Sensors (Percentile Rank)
The price rank sensors answer the simple question: **"Is this price cheap or expensive compared to the rest of the day?"**
Unlike the volatility sensors (which measure the _shape_ of the entire price distribution), price rank sensors place a _specific price_ within that distribution — technically its **percentile rank**. A value of **0% means cheapest interval of the reference set**, while a value near **99% means most expensive**.
Each sensor ranks a different **subject price** against a **reference window**:
- **Subject** — Which price is being ranked: current interval, next interval, previous interval, or the rolling hourly average
- **Reference window** — Which pool of slots to compare against: today only, tomorrow only, or today+tomorrow combined
### How It Works (Percentile Rank Formula)
```
Price rank (percentile rank) = (number of intervals strictly cheaper than subject) ÷ total intervals × 100
```
The cheapest interval always returns 0% — you can use `state == 0` to detect the absolute cheapest moment.
:::note 100% is never reached
By design, **the most expensive interval of the day will never show 100%**. The formula counts how many intervals are *strictly cheaper* than the subject price. For the daily maximum, every other interval is cheaper — but the interval itself is not counted. With 96 quarter-hour intervals per day, the maximum value is 95 ÷ 96 × 100 = **99.0%**.
This means:
- `state == 0` → cheapest interval of the reference set ✅
- `state == 100`**never true**
- `state >= 99` → most expensive interval of the day ✅ (use this instead)
:::
### Available Sensors
**Current interval** (price of the active quarter-hour):
| Sensor | Reference Set | Enabled by Default |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
| <EntityRef id="current_interval_price_rank_today">Current Price Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ✅ Yes |
| <EntityRef id="current_interval_price_rank_tomorrow">Current Price Rank (Tomorrow)</EntityRef> | Tomorrow's 96 intervals (once avail.) | ❌ No |
| <EntityRef id="current_interval_price_rank_today_tomorrow">Current Price Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
**Next interval** (price of the upcoming quarter-hour):
| Sensor | Reference Set | Enabled by Default |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
| <EntityRef id="next_interval_price_rank_today">Next Price Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
| <EntityRef id="next_interval_price_rank_today_tomorrow">Next Price Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
**Previous interval** (price of the just-ended quarter-hour):
| Sensor | Reference Set | Enabled by Default |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
| <EntityRef id="previous_interval_price_rank_today">Previous Price Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
| <EntityRef id="previous_interval_price_rank_today_tomorrow">Previous Price Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
**Rolling hourly average** (5-interval window, ~1 hour):
| Sensor | Reference Set | Enabled by Default |
| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------ |
| <EntityRef id="current_hour_price_rank_today">⌀ Hourly Price Current Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
| <EntityRef id="current_hour_price_rank_today_tomorrow">⌀ Hourly Price Current Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
| <EntityRef id="next_hour_price_rank_today">⌀ Hourly Price Next Rank (Today)</EntityRef> | Today's 96 quarter-hour intervals | ❌ No |
| <EntityRef id="next_hour_price_rank_today_tomorrow">⌀ Hourly Price Next Rank (Today+Tomorrow)</EntityRef> | Combined pool (up to 192 intervals) | ❌ No |
### Key Attributes
All price rank sensors share most of these attributes. The price attribute key reflects the subject:
| Attribute | Description | Subject |
| ------------------------ | -------------------------------------------------------- | ------------------ |
| `current_price` | The price being ranked (current interval) | Current interval |
| `next_price` | The price being ranked (next interval) | Next interval |
| `previous_price` | The price being ranked (previous interval) | Previous interval |
| `current_hour_avg_price` | The rolling average being ranked (current hour) | Current hour avg |
| `next_hour_avg_price` | The rolling average being ranked (next hour) | Next hour avg |
| `prices_below_count` | How many reference intervals are strictly cheaper | All sensors |
| `interval_count` | Total intervals in the reference set | All sensors |
| `reference_min` | The cheapest price in the reference set | All sensors |
| `reference_max` | The most expensive price in the reference set | All sensors |
| `reference_mean` | Average price of the reference set | All sensors |
### When to Use Which Sensor
- **Current (Today)** — Same-day scheduling. "Is the active quarter-hour within the cheapest 25% of today?"
- **Next (Today)** — Prepare for the next interval. "Should I pre-heat now so the device runs in the coming cheap slot?"
- **Current (Today+Tomorrow)** — Broadest view for flexible tasks. "Is this among the cheapest moments of a 48-hour window?"
- **Current (Tomorrow)** — Decide whether to wait until tomorrow. "Is today's price worse than what tomorrow offers?"
- **⌀ Hourly Current (Today)** — For tasks that take about an hour. "Is this hour cheap enough to start a 60-minute cycle?"
- **⌀ Hourly Next (Today)** — One-hour look-ahead. "Will the upcoming hour be cheap enough to start now?"
### Usage in Automations
<details>
<summary>Show YAML: Start dishwasher in bottom quartile</summary>
```yaml
automation:
- alias: "Start dishwasher at cheapest time of day"
trigger:
- platform: numeric_state
entity_id: sensor.<home_name>_current_price_rank_today
below: 25
condition:
- condition: state
entity_id: binary_sensor.<home_name>_best_price_period
state: "on"
action:
- service: switch.turn_on
entity_id: switch.dishwasher
```
</details>
<details>
<summary>Show YAML: Postpone task if tomorrow is cheaper</summary>
```yaml
automation:
- alias: "Skip charging tonight if tomorrow is cheaper"
trigger:
- platform: time
at: "21:00:00"
condition:
# Only postpone if tomorrow's cheapest quartile is better than the current price
- condition: template
value_template: >
{{ states('sensor.<home_name>_current_price_rank_tomorrow') | float(100) < 25 }}
action:
- service: input_boolean.turn_off
entity_id: input_boolean.ev_charge_tonight
```
</details>
<details>
<summary>Show YAML: Avoid running devices at the most expensive time of day</summary>
```yaml
automation:
- alias: "Pause non-essential devices at peak price"
trigger:
- platform: numeric_state
entity_id: sensor.<home_name>_current_price_rank_today
above: 99 # 100 is never reached — use >= 99 to catch the daily maximum
action:
- service: switch.turn_off
entity_id: switch.dishwasher
```
</details>
<details>
<summary>Show YAML: Pre-heat when the next interval is cheap</summary>
```yaml
automation:
- alias: "Pre-heat if next interval is top quartile cheapest"
trigger:
- platform: time_pattern
minutes: "/15"
condition:
- condition: numeric_state
entity_id: sensor.<home_name>_next_price_rank_today
below: 25
action:
- service: climate.set_hvac_mode
entity_id: climate.living_room
data:
hvac_mode: heat
```
</details>

View file

@ -0,0 +1,159 @@
---
comments: false
---
# Troubleshooting
## Common Issues
### Sensors Show "Unavailable"
**After initial setup or HA restart:**
This is normal. The integration needs up to one update cycle (15 minutes) to fetch data from the Tibber API. If sensors remain unavailable after 30 minutes:
1. Check your internet connection
2. Verify your Tibber API token is still valid at [developer.tibber.com](https://developer.tibber.com)
3. Check the logs for error messages (see [Debug Logging](#debug-logging) below)
**After working fine previously:**
- **API communication error**: Tibber's API may be temporarily down. The integration retries automatically — wait 1530 minutes.
- **Authentication expired**: If you see a "Reauth required" notification in HA, your API token needs to be re-entered. Go to **Settings → Devices & Services → Tibber Prices** and follow the reauth flow.
- **Rate limiting**: If you have multiple integrations using the same Tibber token, you may hit API rate limits. Check logs for "429" or "rate limit" messages.
### Tomorrow's Prices Not Available
Tomorrow's electricity prices are typically published by Tibber between **13:00 and 15:00 CET** (Central European Time). Before that time, all "tomorrow" sensors will show unavailable or their last known state.
The integration automatically polls more frequently in the afternoon to detect when tomorrow's data becomes available. No manual action is needed.
### Wrong Currency or Price Units
If prices show in the wrong currency or wrong unit (EUR vs ct):
1. Go to **Settings → Devices & Services → Tibber Prices → Configure**
2. Check the **Currency Display** step
3. Choose between base units (EUR, NOK, SEK) and sub-units (ct, øre)
Note: The currency is determined by your Tibber account's home country and cannot be changed — only the display unit (base vs. sub-unit) is configurable.
### No Best/Peak Price Periods Found
If the Best Price Period or Peak Price Period binary sensors never turn on:
1. **Check your flex settings**: A flex value that's too low may filter out all intervals. Try increasing it (e.g., from 10% to 20%).
2. **Enable relaxation**: In the options flow, enable relaxation for the affected period type. This automatically increases flex until periods are found.
3. **Check daily price variation**: On days with very flat prices (low volatility), periods may not meet the threshold criteria. This is expected behavior — the integration correctly identifies that no intervals stand out.
See the [Period Calculation Guide](period-calculation.md) for detailed configuration advice.
### Entities Duplicated After Reconfiguration
If you see duplicate entities after changing settings:
1. Go to **Settings → Devices & Services → Entities**
2. Filter by "Tibber Prices"
3. Remove any disabled or orphaned entities
4. Restart Home Assistant
### Integration Not Showing After Installation
If the integration doesn't appear in **Settings → Devices & Services → Add Integration**:
1. Confirm you restarted Home Assistant after installing via HACS
2. Clear your browser cache (Ctrl+Shift+R)
3. Check the HA logs for import errors related to `tibber_prices`
## Debug Logging
When reporting issues, debug logs help identify the problem quickly.
### Enable Debug Logging
Add this to your `configuration.yaml`:
<details>
<summary>Show YAML: configuration.yaml Logging</summary>
```yaml
logger:
default: warning
logs:
custom_components.tibber_prices: debug
```
</details>
Restart Home Assistant for the change to take effect.
### Targeted Logging
For specific subsystems, you can enable logging selectively:
<details>
<summary>Show YAML: Targeted Subsystem Logging</summary>
```yaml
logger:
default: warning
logs:
# API communication (requests, responses, errors)
custom_components.tibber_prices.api: debug
# Coordinator (data updates, caching, scheduling)
custom_components.tibber_prices.coordinator: debug
# Period calculation (best/peak price detection)
custom_components.tibber_prices.coordinator.period_handlers: debug
# Sensor value calculation
custom_components.tibber_prices.sensor: debug
```
</details>
### Temporary Debug Logging (No Restart)
You can also enable debug logging temporarily from the HA UI:
1. Go to **Developer Tools → Services**
2. Call service: `logger.set_level`
3. Data:
<details>
<summary>Show YAML example (temporary logger.set_level payload)</summary>
```yaml
custom_components.tibber_prices: debug
```
</details>
This resets when HA restarts.
### Downloading Diagnostics
For bug reports, include the integration's diagnostic dump:
1. Go to **Settings → Devices & Services → Tibber Prices**
2. Click the three-dot menu (⋮) on the integration card
3. Select **Download diagnostics**
The downloaded file includes configuration, cache status, period information, and recent errors — with sensitive data redacted.
### What to Include in Bug Reports
When opening a [GitHub issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new):
1. **Integration version** (from Settings → Devices & Services → Tibber Prices)
2. **Home Assistant version** (from Settings → About)
3. **Description** of the problem and expected behavior
4. **Debug logs** (relevant excerpts from the HA log)
5. **Diagnostics file** (downloaded as described above)
6. **Steps to reproduce** (if applicable)
## Getting Help
- Check [existing issues](https://github.com/jpawlowski/hass.tibber_prices/issues)
- Open a [new issue](https://github.com/jpawlowski/hass.tibber_prices/issues/new) with detailed information
- Include logs, configuration, and steps to reproduce

View file

@ -0,0 +1,174 @@
{
"tutorialSidebar": [
"intro",
{
"type": "category",
"label": "🚀 Getting Started",
"link": {
"type": "doc",
"id": "installation"
},
"items": [
"installation",
{
"type": "category",
"label": "⚙️ Configuration",
"link": {
"type": "doc",
"id": "configuration"
},
"items": [
"config-general",
"config-currency",
"config-price-rating",
"config-price-level",
"config-volatility",
"config-best-price",
"config-peak-price",
"config-price-trend",
"config-chart-export",
"config-runtime-overrides"
],
"collapsible": true,
"collapsed": true
}
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "📖 Core Concepts",
"link": {
"type": "doc",
"id": "concepts"
},
"items": [
"concepts",
"glossary"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "📊 Sensors",
"link": {
"type": "doc",
"id": "sensors-overview"
},
"items": [
"sensors-overview",
"sensors-average",
"sensors-ratings-levels",
"sensors-volatility",
"sensors-trends",
"sensors-price-phases",
"sensors-timing",
"sensors-energy-tax"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "⏰ Price Periods",
"link": {
"type": "doc",
"id": "period-calculation"
},
"items": [
"period-calculation",
"period-relaxation"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "🎨 Dashboards & Charts",
"link": {
"type": "doc",
"id": "dashboard-examples"
},
"items": [
"dynamic-icons",
"icon-colors",
"dashboard-examples",
"chart-examples"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "🤖 Automations & Usage",
"link": {
"type": "doc",
"id": "automation-examples"
},
"items": [
"automation-examples"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "⚡ Actions",
"link": {
"type": "doc",
"id": "actions"
},
"items": [
"actions",
"scheduling-actions",
"plan-charging-action",
"chart-actions",
"data-actions"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "📖 Reference",
"link": {
"type": "doc",
"id": "sensor-reference"
},
"items": [
"sensor-reference"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "👥 Community",
"link": {
"type": "doc",
"id": "community-examples"
},
"items": [
"community-examples"
],
"collapsible": true,
"collapsed": false
},
{
"type": "category",
"label": "🔧 Help & Support",
"link": {
"type": "doc",
"id": "faq"
},
"items": [
"faq",
"troubleshooting"
],
"collapsible": true,
"collapsed": false
}
]
}

View file

@ -1,4 +1,5 @@
[
"v0.31.0",
"v0.30.0",
"v0.29.0",
"v0.28.0",

View file

@ -290,4 +290,4 @@ property-decorators = ["propcache.api.cached_property"]
keep-runtime-typing = true
[project.optional-dependencies]
test = ["pytest-homeassistant-custom-component>=0.13.326"]
test = ["pytest-homeassistant-custom-component>=0.13.334"]

View file

@ -4,6 +4,5 @@ pre-commit>=4.6.0,<4.7.0
ruff>=0.15.15,<0.16.0
zlib_ng>=1.0.0,<1.1.0
isal>=1.8.0,<1.9.0
pytest>=9.0.3
pytest-asyncio>=1.4.0
pytest-homeassistant-custom-component>=0.13.326
# Must match HA_VERSION in scripts/setup/bootstrap exactly (1:1 mapping)
pytest-homeassistant-custom-component==0.13.322

View file

@ -46,6 +46,11 @@ fi
# shellcheck source=/dev/null
source "$HOME/.venv/bin/activate"
# Allow pre-release versions of homeassistant in this dev environment.
# phac pins an exact HA version (sometimes a beta/rc), and HA_VERSION may
# intentionally be set to a pre-release for testing purposes.
export UV_PRERELEASE=allow
log_header "Installing project dependencies"
uv pip install --requirement requirements.txt

106
scripts/setup/update-ha-version Executable file
View file

@ -0,0 +1,106 @@
#!/bin/bash
# scripts/setup/update-ha-version: Update Home Assistant version across dev environment
#
# Finds the pytest-homeassistant-custom-component version that matches the given
# HA version (1:1 mapping on PyPI) and updates both scripts/setup/bootstrap and
# requirements.txt atomically. This is the single command to run when upgrading
# the development environment to a new Home Assistant version.
#
# Usage:
# ./scripts/setup/update-ha-version VERSION
#
# Examples:
# ./scripts/setup/update-ha-version 2026.5.4
# ./scripts/setup/update-ha-version 2026.6.0b0
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR/../.."
# shellcheck source=scripts/.lib/output.sh
source "$SCRIPT_DIR/../.lib/output.sh"
BOOTSTRAP_FILE="scripts/setup/bootstrap"
REQUIREMENTS_FILE="requirements.txt"
# --- Argument validation ---
if [[ $# -ne 1 ]]; then
log_error "Usage: $0 VERSION (e.g. 2026.5.4 or 2026.6.0b0)"
exit 1
fi
NEW_HA_VERSION="$1"
# --- Read current versions from files ---
CURRENT_HA_VERSION=$(grep -oP 'HA_VERSION:-"\K[^"]+' "$BOOTSTRAP_FILE")
CURRENT_PHAC_VERSION=$(grep -oP 'pytest-homeassistant-custom-component==\K\S+' "$REQUIREMENTS_FILE")
log_header "Updating Home Assistant version"
log_info "Current: homeassistant==${CURRENT_HA_VERSION} / phac==${CURRENT_PHAC_VERSION}"
log_info "Target: homeassistant==${NEW_HA_VERSION}"
echo
if [[ "$NEW_HA_VERSION" == "$CURRENT_HA_VERSION" ]]; then
log_warning "Already on homeassistant==${NEW_HA_VERSION}, nothing to do."
exit 0
fi
# --- Find matching phac version via PyPI ---
log_step "Querying PyPI for matching pytest-homeassistant-custom-component version..."
NEW_PHAC_VERSION=$(python3 - "$NEW_HA_VERSION" <<'PYEOF'
import json, sys, urllib.request
target_ha = sys.argv[1]
# Fetch full list of all releases
url = "https://pypi.org/pypi/pytest-homeassistant-custom-component/json"
try:
data = json.loads(urllib.request.urlopen(url, timeout=15).read())
except Exception as e:
print(f"ERROR: Could not reach PyPI: {e}", file=sys.stderr)
sys.exit(1)
releases = sorted(data["releases"].keys(), reverse=True)
for version in releases:
try:
pkg_url = f"https://pypi.org/pypi/pytest-homeassistant-custom-component/{version}/json"
pkg_data = json.loads(urllib.request.urlopen(pkg_url, timeout=10).read())
requires = pkg_data["info"]["requires_dist"] or []
ha_req = next((r for r in requires if r.startswith("homeassistant==")), None)
if ha_req and target_ha in ha_req:
print(version)
sys.exit(0)
except Exception:
pass
print(f"ERROR: No phac version found for homeassistant=={target_ha}", file=sys.stderr)
sys.exit(1)
PYEOF
) || {
log_error "Could not find a pytest-homeassistant-custom-component version for homeassistant==${NEW_HA_VERSION}"
log_info "Check https://pypi.org/pypi/pytest-homeassistant-custom-component/json for available versions"
exit 1
}
log_result 0 "pytest-homeassistant-custom-component==${NEW_PHAC_VERSION}"
echo
# --- Apply changes ---
log_step "Updating ${BOOTSTRAP_FILE}..."
sed -i "s/HA_VERSION:-\"${CURRENT_HA_VERSION}\"/HA_VERSION:-\"${NEW_HA_VERSION}\"/" "$BOOTSTRAP_FILE"
log_step "Updating ${REQUIREMENTS_FILE}..."
sed -i "s/pytest-homeassistant-custom-component==${CURRENT_PHAC_VERSION}/pytest-homeassistant-custom-component==${NEW_PHAC_VERSION}/" "$REQUIREMENTS_FILE"
# --- Summary ---
echo
log_success "Done! Updated to:"
log_info " homeassistant==${NEW_HA_VERSION} (in ${BOOTSTRAP_FILE})"
log_info " pytest-homeassistant-custom-component==${NEW_PHAC_VERSION} (in ${REQUIREMENTS_FILE})"
echo
log_info "Next steps:"
log_info " 1. Rebuild the DevContainer to apply the new environment"
log_info " 2. Run ./scripts/test to verify the test suite still passes"