Add four optional parameters to the get_chartdata service:
- include_energy: Include raw energy/spot price (default: false)
- include_tax: Include tax component (default: false)
- energy_field: Custom field name (default: energy_price)
- tax_field: Custom field name (default: tax)
Custom field names allow direct compatibility with ApexCharts
and other charting tools without post-processing.
All code paths (all/segments/none insert_nulls modes) and the
last-interval handler include energy/tax when enabled.
Added translations for all 5 languages (en, de, nl, nb, sv).
Impact: Users can include price composition data in chart exports,
enabling visual breakdowns of energy cost vs. taxes in dashboards.
Add energy_price and tax attributes to interval and daily stat sensors:
- Interval sensors (current/next/previous): energy_price and tax from
the specific 15-minute interval
- Daily min/max sensors: energy_price and tax from the extreme interval
- Daily average sensors: energy_price_mean, energy_price_median,
tax_mean, tax_median — matching the existing mean/median pattern
used for the main price attribute
Calculator caches both mean and median for energy/tax using
calculate_median() from utils/average. All new attributes are
excluded from Recorder to prevent database bloat.
Impact: Users can see price composition (spot price vs. taxes) on
all major price sensors. Enables solar feed-in and net metering
automations based on raw energy prices.
Request `energy` and `tax` fields alongside `total` in both
quarter-hourly price queries. These represent the raw spot price and
the tax/fee component that together make up the total consumer price.
Updated hourly aggregation in formatters.py to carry energy/tax
values through to aggregated output.
Impact: Enables downstream consumers (sensors, services) to expose
price composition data. Useful for solar feed-in compensation and
net metering (saldering) calculations where the raw energy price
is needed separately from taxes.
Added brand/ directory to custom_components/tibber_prices/ with all
8 supported PNG variants, generated from existing SVGs in images/:
- icon.png / dark_icon.png (256×256)
- icon@2x.png / dark_icon@2x.png (512×512)
- logo.png / dark_logo.png (500×128)
- logo@2x.png / dark_logo@2x.png (1000×256)
Local brand images automatically take priority over CDN images and
are served via the HA brands proxy API (/api/brands/integration/).
Silently ignored on HA < 2026.3, no changes to manifest.json needed.
Updated AGENTS.md to document the brand/ directory under "ALLOWED in root".
Impact: Integration icon and logo now display correctly in HA ≥ 2026.3
without requiring a separate submission to the HA brands repository.
Renamed 8 price_trend_Xh entries to price_outlook_Xh and added 15 new
price_trajectory_Xh entries (2h–12h) in all 5 languages (de, en, nb, nl, sv).
translations/ (HA-native: name + 5 states per sensor):
- EN: "Price Outlook (Xh)" / "Price Trajectory (Xh)"
- DE: "Preisausblick (Xh)" / "Preisverlauf (Xh)"
- NB: "Prisutblikk (Xt)" / "Prisforløp (Xt)"
- NL: "Prijsvooruitzicht (Xu)" / "Prijstrajectorie (Xu)"
- SV: "Prisöversikt (Xh)" / "Prisutveckling (Xh)"
custom_translations/ (description + long_description + usage_tips):
- Outlook descriptions updated to explain window-average comparison
semantics (not price direction)
- Trajectory descriptions explain first-half vs second-half logic and
the "outlook: falling + trajectory: rising = you're AT the minimum" pattern
- Trajectory long_description and usage_tips in English for all languages;
description field in native language
Impact: Entity display names update to reflect the corrected semantic meaning.
Renamed 8 sensors to clarify what they actually measure, and added 7 new
sensors for a different (and often more useful) calculation.
--- WHY THE RENAME ---
The old name "price_trend_Xh" implied the sensor shows where prices are
heading. It doesn't — it compares CURRENT price vs the FUTURE WINDOW AVERAGE.
At a price minimum, it shows "strongly_falling" (because the cheap minimum
pulls the average below your current high price), which is the opposite of
intuitive. The name "price_outlook_Xh" correctly conveys: "is now cheaper
or more expensive than the next Nh on average?"
--- NEW: price_trajectory_Xh ---
These sensors compare FIRST HALF vs SECOND HALF of the window, revealing
actual price direction within the window:
price_trajectory_2h: avg(hour 1) vs avg(hour 2)
price_trajectory_3h: avg(first 1.5h) vs avg(second 1.5h)
price_trajectory_4h: avg(first 2h) vs avg(second 2h)
price_trajectory_5h: avg(first 2.5h) vs avg(second 2.5h)
price_trajectory_6h: avg(first 3h) vs avg(second 3h)
price_trajectory_8h: avg(first 4h) vs avg(second 4h)
price_trajectory_12h: avg(first 6h) vs avg(second 6h)
The key use case: at a price minimum, price_outlook_Xh shows "strongly_falling"
but price_trajectory_Xh shows "rising" — correctly revealing the upcoming
reversal. "outlook: falling + trajectory: rising" = you're AT the minimum.
--- IMPLEMENTATION ---
sensor/calculators/trend.py:
- get_price_outlook_value() (was: get_price_trend_value())
- New: get_price_trajectory_value(*, hours: int)
- New: _calculate_first_half_average(hours, next_interval_start)
- New: get_trajectory_attributes() → first_half_avg, second_half_avg, half_diff_%
- clear_trend_cache() also resets _trajectory_attributes
sensor/definitions.py:
- 8 SensorEntityDescription entries: key/translation_key price_trend_Xh → price_outlook_Xh
- New PRICE_TRAJECTORY_SENSORS tuple (2h–5h enabled by default, 6h/8h/12h disabled)
sensor/value_getters.py:
- 8 lambda entries renamed
- 7 new trajectory lambda entries added
sensor/attributes/trend.py:
- startswith("price_trend_") → startswith("price_outlook_")
- New elif branch routing price_trajectory_* to cached trajectory_attributes
sensor/core.py:
- startswith checks updated for both prefix families
- cached_data dict extended with "trajectory_attributes"
coordinator/constants.py:
- TIME_SENSITIVE_ENTITY_KEYS: 8 renamed + 7 new trajectory keys added
config_flow_handlers/entity_check.py:
- volatility + price_trend affected-entity lists: 8 renamed + 7 new
BREAKING CHANGE: Sensors price_trend_1h, price_trend_2h, price_trend_3h,
price_trend_4h, price_trend_5h, price_trend_6h, price_trend_8h,
price_trend_12h have been removed without a deprecation period.
Migration:
Replace price_trend_Xh → price_outlook_Xh everywhere (automations,
dashboards, templates). Behavior is identical — only the entity name
changed. If you want to detect actual price direction within the window
(e.g. "are prices rising or falling right now?"), use the new
price_trajectory_Xh sensors instead.
Impact: Users must update automations and dashboards. Entity IDs change from
sensor.<home>_price_trend_Xh to sensor.<home>_price_outlook_Xh. New
price_trajectory_Xh sensors provide complementary direction information.
Added `suggested_unit_of_measurement=UnitOfTime.HOURS` to all 7 DURATION
sensors to prevent HA from auto-selecting minutes as the display unit.
Without this, HA would pick "min" for small values (e.g., 0.75 h) and then
display large values as "1238 Min." instead of the intended "20 Std. 38 Min."
Affected sensors:
- trend_change_in_minutes
- best_price_period_duration / peak_price_period_duration
- best_price_remaining_minutes / peak_price_remaining_minutes
- best_price_next_in_minutes / peak_price_next_in_minutes
BREAKING CHANGE: Sensor state unit changes from minutes to hours for users
whose entity registry stored "min" as the display unit (the previous default).
Automations using the raw state value (e.g., `state < 60` for "less than 60
minutes") must be updated to use hours (e.g., `state < 1`).
The state attributes `remaining_minutes` and `next_in_minutes` continue to
provide integer minutes and are unaffected.
Impact: Duration sensors now display dynamically as "X h Y min" (e.g.,
"1 h 15 min") instead of a large minutes value like "1238 Min.". Users who
manually customized the unit in HA settings are not affected.
Added 3 new config fields to price trend options step:
- Trend Change Confirmation (2-6 intervals slider)
- Min Price Change for trend (display-unit-aware slider)
- Min Price Change for strong trend (display-unit-aware slider)
Price change sliders scale between base currency (EUR/NOK) storage and
display unit (ct/øre) presentation using get_display_unit_factor().
Added migration in __init__.py to convert old display-unit values to
base currency format.
Impact: Users can tune trend sensitivity: higher confirmation = fewer
false changes, higher min price change = no trends from tiny fluctuations.
New duration sensor showing time until next price trend change as hours
(e.g., 2.25 h). Registered in MINUTE_UPDATE_ENTITY_KEYS for per-minute
updates. Shares cached attributes with next_price_trend_change timestamp
sensor.
Added trend attributes to _unrecorded_attributes (threshold/volatility/diff
attributes excluded from recorder). Updated timer group size test expectation
from 6 to 7.
Impact: Users can display a live countdown to the next trend change on
dashboards and use it in automations (e.g., "if < 0.25 h, prepare").
Refactored trend calculator with direction-group-based trend change detection
(rising/strongly_rising treated as same group, falling/strongly_falling as same
group). Added minimum absolute price change thresholds (noise floor) to prevent
spurious trends at low price levels. Both percentage AND absolute conditions
must now be met.
Updated strongly threshold defaults from ±6% to ±9% (3x base for perceptual
scaling). Added missing strongly thresholds and new config keys to
get_default_options(). calculate_price_trend() now returns volatility_factor
as 4th tuple element for threshold transparency.
Added CONF_PRICE_TREND_CHANGE_CONFIRMATION (default: 3 intervals = 45min)
and CONF_PRICE_TREND_MIN_PRICE_CHANGE / _STRONGLY with validation limits.
Updated tests for new 4-tuple return value.
Impact: More stable trend detection — fewer false trend changes during low-price
periods. Direction-group logic prevents noise from "rising ↔ strongly_rising"
oscillations. Users can fine-tune noise floor for their market.
- Move UTC import from inline (inside _has_real_gaps_in_range) to
module-level in manager.py
- Hoist get_fetch_groups() out of while loop in _get_cached_intervals:
eliminates ~384 function calls per invocation
- Pre-compute criteria_by_day dict in build_periods before the for-loop:
eliminates ~381 redundant NamedTuple constructions per call; only
ref_price/avg_price vary by day (max 3 entries), flex/min_distance/
reverse_sort are constant throughout
Impact: Reduces unnecessary object creation during the hot paths called
every 15 minutes and during all relaxation phases.
PLW0108: Three lambdas were simple pass-throughs that added no value:
lambda data: aggregate_level_data(data) → aggregate_level_data
lambda: lifecycle_calculator.get_lifecycle_state() → lifecycle_calculator.get_lifecycle_state
Affected files:
sensor/calculators/rolling_hour.py (line 115)
sensor/helpers.py (line 139)
sensor/value_getters.py (line 220)
Impact: No behaviour change. Linter now passes with zero warnings.
Four non-MONETARY diagnostic sensors had state_class set, causing HA
Recorder to add them to long-term statistics tables unnecessarily:
- home_size (m²): SensorStateClass.MEASUREMENT
- main_fuse_size (A): SensorStateClass.MEASUREMENT
- number_of_residents: SensorStateClass.MEASUREMENT
- estimated_annual_consumption(kWh):SensorStateClass.TOTAL
All four are static user metadata retrieved from Tibber's user API
(cached for 24 h, rarely or never changes in practice). They carry no
time-series value: home_size and main_fuse_size don't change, and
estimated_annual_consumption is a rough Tibber estimate, not an actual
accumulating energy counter.
Setting state_class=None removes them from long-term statistics while
keeping normal state-change recording intact.
The three intentional non-None state_class values are unchanged:
- current_interval_price (MONETARY, TOTAL): Energy Dashboard
- current_interval_price_base (MONETARY, TOTAL): Energy Dashboard
- average_price_today (MONETARY, TOTAL): useful weekly/monthly trend
Impact: Reduced Recorder database growth; no user-visible sensor
behaviour change.
The timedelta import move (previous commit) was committed correctly, but a
subsequent automated formatting pass stripped the line that used it:
current_date = current_date + timedelta(days=1)
Without the increment the while loop never terminates, causing HA to hang
indefinitely during coordinator startup when period calculation is triggered.
Additionally, the timedelta module-level import was also reverted by the
same pass, so re-add it here.
Restore both the import and the loop increment so group_periods_by_day
correctly iterates one day at a time.
PLC0415 – suppress with noqa was already suppressing the warning, but the
import was still executed on every iteration of the while loop. Standard
library imports belong at module level both for correctness and performance.
Move 'from datetime import timedelta' to the top-level import block and
remove the now-unnecessary 'noqa: PLC0415' comment.
Impact: Negligible per-call overhead removed. More importantly, the code
no longer suppresses a linter warning that signals a real anti-pattern;
future static analysis runs will correctly flag any new inline imports.
RESOLUTION_CHANGE_ISO was a naive ISO string ("2025-10-01T00:00:00").
_split_at_resolution_boundary compared it against timezone-aware interval
strings via plain string ordering, which is unreliable when the strings
carry different UTC offsets (e.g. +02:00 vs +00:00). More critically,
the naive string was then split into ranges such as ("...", "2025-10-01T00:00:00")
which were parsed back to naive datetime objects in fetch_missing_ranges.
When routing.py then compared those naive objects against the tz-aware
boundary datetime, Python raised TypeError: can't compare offset-naive
and offset-aware datetimes.
Fix:
- Remove RESOLUTION_CHANGE_ISO; derive the boundary ISO string at
runtime from RESOLUTION_CHANGE_DATETIME.isoformat(), which produces
the UTC-normalised string "2025-10-01T00:00:00+00:00".
- Rewrite _split_at_resolution_boundary to parse each range's start/end
to datetime objects, normalise any defensively-naive values to UTC,
and compare against the RESOLUTION_CHANGE_DATETIME constant directly.
- Use the tz-aware boundary_iso string as the split point so downstream
fromisoformat() calls always return tz-aware datetime objects.
Impact: Ranges spanning 2025-10-01T00:00:00 UTC are now split correctly
regardless of the UTC offset carried by the original interval strings,
and no TypeError is raised when routing.py compares the boundary
endpoints to its own tz-aware boundary calculation.
On DST fall-back nights the clocks repeat an hour (e.g. 02:00 CET/CEST).
Tibber delivers quarter-hourly intervals for both the CEST (+02:00) and
CET (+01:00) copies of that hour. Both share the same 19-char naive local
key 'YYYY-MM-DDTHH:MM:SS', so _add_intervals treated the CET arrivals as
unwanted duplicates and sent them to _touch_intervals, which kept the CEST
data and silently discarded the CET price data. The fall-back hour's prices
were permanently lost from the pool.
Fix:
- Add module constant _DST_COLLISION_MAX_SAME_UTC_S (60 s) to distinguish
true duplicate arrivals (same UTC, ≤60 s apart) from DST collision pairs
(~3600 s apart).
- Add _handle_index_collision() helper that compares the UTC datetimes of
the existing and incoming interval. If they differ by more than the
threshold it stores the new interval in self._dst_extras keyed by the
normalised local timestamp and returns True.
- _add_intervals delegates every collision to _handle_index_collision and
only routes to touch when it returns False (true duplicate).
- _get_cached_intervals yields the saved extras after the main interval.
- After each GC run, stale entries are pruned from _dst_extras.
- to_dict / from_dict persist and restore _dst_extras across HA restarts.
Impact: The full fall-back hour (e.g. 02:00-02:45 CET) now appears in the
interval pool alongside the CEST copies, so sensors that query that hour
return correct prices instead of stale or missing data.
_get_sensor_interval_stats() computed expected_count via UTC time
arithmetic ((end - start).total_seconds() / 900 = 480 for 5 days), then
iterated through fixed-offset local timestamps adding timedelta(minutes=15).
On DST spring-forward days (e.g. last Sunday March in EU), clocks skip
from 02:00 to 03:00. The 4 local quarter-hour slots 02:00-02:45 never
exist, so the Tibber API never returns intervals for them. The iteration
still visits those 4 keys, finds them absent from the index, and reports
has_gaps=True (expected=480, actual=476). Since no API call can ever
fill those non-existent slots, the pool triggers an unnecessary gap-fill
fetch every 15 minutes for the entire spring-forward day.
Fix: keep the nominal expected_count for diagnostics, but determine
has_gaps via the new _has_real_gaps_in_range() helper that sorts
cached intervals by UTC time and checks consecutive UTC differences.
The 01:45+01:00 -> 03:00+02:00 transition is exactly 15 minutes in
UTC, so no gap is reported. Start/end boundary comparisons use naive
19-char local timestamps to stay consistent with the fixed-offset
arithmetic used by get_protected_range().
Impact: No spurious API fetches on DST spring-forward Sunday. Gap
detection for real missing data (API failures, first startup) remains
fully functional.
All three re-transform sites (_handle_options_update,
async_handle_config_override_update, _perform_midnight_data_rotation)
were building raw_data with only {'price_info': ...}, omitting
'currency' and 'home_id'.
data_transformation.py's transform_data() falls back to currency='EUR'
and home_id='' when those keys are missing. This caused all non-EUR
users (Norway/NOK, Sweden/SEK) to see wrong currency units in sensors
after every midnight turnover and after any options/override change,
until the next full API poll refilled coordinator.data with correct
values (up to 15 minutes of wrong units).
Fix: explicitly carry over coordinator.data's existing currency and
home_id into each raw_data dict. Also inline the redundant lambda
wrapper on calculate_periods_fn (PLW0108 ruff lint).
Impact: Norwegian and Swedish users no longer see EUR/ct units after
midnight or config changes. Sensor unit-of-measurement stays consistent
throughout the day regardless of re-transform triggers.
All entity descriptions had hardcoded English name= strings that were
never used at runtime: HA always prefers translations via translation_key
(entity.<platform>.<key>.name in translations/en.json), making the name=
fields dead code.
Removed 106 lines across all four platforms:
- sensor/definitions.py: 85 name= lines
- number/definitions.py: 12 name= lines
- binary_sensor/definitions.py: 6 name= lines
- switch/definitions.py: 3 name= lines
No functional change. The unique_id (entry_id + key) and translation_key
remain stable, ensuring entity IDs and friendly names are unaffected.
Impact: Cleaner definitions, no drift between name= strings and
translations. Aligns with HA standard: translations are the single
source of truth for entity names.
Previously all 26 MONETARY sensors had state_class=TOTAL, causing the
statistics and statistics_short_term tables to grow unbounded (never
auto-purged by HA).
Reduced to 3 sensors that genuinely benefit from long-term history:
- current_interval_price (main price sensor, trend over weeks/months)
- current_interval_price_base (required for Energy Dashboard)
- average_price_today (daily avg tracking over seasons)
Set state_class=None on 23 sensors where long-term history adds no
value: forecast/future sensors (next_avg_*h), daily snapshots
(lowest/highest_price_today), tomorrow sensors, rolling windows
(trailing/leading 24h), and next/previous interval sensors.
Note: state_class=None does not affect the States timeline (History
panel). Only the Statistics chart on entity detail pages is removed
for the affected sensors. Existing statistics data is retained.
Impact: ~88% reduction in statistics table writes. Prevents database
bloat reported by users with long-running installations.
Add calculation summary attributes to best_price_period and
peak_price_period binary sensors for diagnostic transparency.
New attributes (all excluded from recorder history):
- min_periods_configured: User's configured target per day (always shown)
- periods_found_total: Actual periods found across all days (always shown)
- flat_days_detected: Days where CV <= 10% reduced target to 1 (only when > 0)
- relaxation_incomplete: Some days couldn't reach the target (only when true)
These attributes explain observed behavior without requiring users to
read logs: seeing "flat_days_detected: 1" alongside
"min_periods_configured: 2, periods_found_total: 1" immediately
explains why the count is lower than configured on uniform-price days.
Implementation:
- _compute_day_effective_min() now returns (dict, int) tuple to propagate
the flat day count through to the metadata dict
- flat_days_detected added to metadata["relaxation"] in
calculate_periods_with_relaxation()
- build_final_attributes_simple() gains optional period_metadata parameter
- New add_calculation_summary_attributes() function handles the rendering
- Attributes are shown even when no period is currently active
Updated recorder-optimization.md: attribute counts + clarified that
timestamp is correctly excluded (entity native_value is recorded
separately by HA as the entity state itself).
Impact: Users can understand why they received fewer periods than
configured without enabling debug logging.
Three complementary fixes for pathological price days:
1. Adaptive min_periods for flat days (CV ≤ 10%):
On days with nearly uniform prices (e.g. solar surplus), enforcing
multiple distinct cheap periods is geometrically impossible.
_compute_day_effective_min() detects CV ≤ LOW_CV_FLAT_DAY_THRESHOLD
and reduces the effective target to 1 for that day (best price only;
peak price always runs full relaxation).
2. min_distance scaling on absolute low-price days:
When the daily average drops below 0.10 EUR (10 ct), percentage-based
min_distance becomes unreliable. The threshold is scaled linearly to
zero so the filter neither accepts the entire day nor blocks everything.
3. CV quality gate bypass for absolute low-price periods:
Periods with a mean below 0.10 EUR may show high relative CV even
though the absolute price differences are fractions of a cent.
Both _check_period_quality() and _check_merge_quality_gate() now
bypass the CV gate below this threshold.
Additionally: span-aware flex warnings now emit INFO/WARNING when
base_flex >= 25%/30% and at least one "normal" (non-V-shape) day
exists (FLEX_WARNING_VSHAPE_RATIO = 0.5). Previously the constants
were defined but never used.
Updated 3 test assertions in test_best_price_e2e.py: the flat-day
fixture (CV ~5.4%) correctly produces 1 period, not 2.
Impact: Best Price periods now appear reliably on V-shape solar days
and flat-price days. No more "0 periods" on days where the single
cheapest window is a valid and useful result.
_get_cached_intervals() used fixed-offset datetimes from fromisoformat()
for iteration. When start and end boundaries span a DST transition (e.g.,
+01:00 CET → +02:00 CEST), the loop's end check compared UTC values,
stopping 1 hour early on spring-forward days.
This caused the last 4 quarter-hourly intervals of "tomorrow" to be
missing, making the binary sensor "Tomorrow data available" show Off
even when full data was present.
Changed iteration to use naive local timestamps, matching the index key
format (timezone stripped via [:19]). The end boundary comparison now
works correctly regardless of DST transitions.
Impact: Binary sensor "Tomorrow data available" now correctly shows On
on DST spring-forward days. Affects all European users on the last
Sunday of March each year.
When runtime config override entities (number/switch) are enabled,
the Options Flow now displays warning indicators at the top of each
affected section. Users see which fields are being managed by config
entities and can still edit the base values if needed.
Changes:
- Add ConstantSelector warnings in Best Price/Peak Price sections
- Implement multi-language support for override warnings (de, en, nb, nl, sv)
- Add _get_override_translations() to load translated field labels
- Add _get_active_overrides() to detect enabled override entities
- Extend get_best_price_schema/get_peak_price_schema with translations param
- Add 14 number/switch config entities for runtime period tuning
- Document runtime configuration entities in user docs
Warning format adapts to overridden fields:
- Single: "⚠️ Flexibility controlled by config entity"
- Multiple: "⚠️ Flexibility and Minimum Distance controlled by config entity"
Impact: Users can now dynamically adjust period calculation parameters
via Home Assistant automations, scripts, or dashboards without entering
the Options Flow. Clear UI indicators show which settings are currently
overridden.
Add visual indicators to distinguish hourly aggregated data from
original 15-minute interval data in ApexCharts output.
Changes:
- Chart title: Append localized suffix like "(Ø hourly)" / "(Ø stündlich)"
- Y-axis label: Append "(Ø)" suffix, e.g., "øre/kWh (Ø)"
The suffix pattern avoids confusion with Scandinavian currency symbols
(øre/öre) which look similar to the average symbol (Ø) when used as prefix.
Added hourly_suffix translations for all 5 languages (en, de, sv, nb, nl).
Impact: Users can now clearly see when a chart displays averaged hourly
data rather than original 15-minute prices.
Implement clickable legend items to show/hide best/peak price period
overlays in generated ApexCharts YAML configuration.
Legend behavior by configuration:
- Only best price: No legend (overlay always visible)
- Only peak price: Legend shown, peak toggleable (starts hidden)
- Both enabled: Legend shown, both toggleable (best visible, peak hidden)
Changes:
- Best price overlay: in_legend only when peak also enabled
- Peak price overlay: always in_legend with hidden_by_default: true
- Enable experimental.hidden_by_default when peak price active
- Price level series (LOW/NORMAL/HIGH): hidden from legend when
overlays active, visible otherwise (preserves easy legend enable)
- Add triangle icons (▼/▲) before overlay names for visual distinction
- Custom legend markers (size: 0) only when overlays active
- Increased itemMargin for better visual separation
Impact: Users can toggle best/peak price period visibility directly
in the chart via legend click. Without overlays, legend behavior
unchanged - users can still enable it by setting show: true.
Add resolution parameter to get_chartdata and get_apexcharts_yaml services,
allowing users to choose between original 15-minute intervals or aggregated
hourly values for chart visualization.
Implementation uses rolling 5-interval window aggregation (-2, -1, 0, +1, +2
around :00 of each hour = 60 minutes total), matching the sensor rolling
hour methodology. Respects user's CONF_AVERAGE_SENSOR_DISPLAY setting for
mean vs median calculation.
Changes:
- formatters.py: Add aggregate_to_hourly() function preserving original
field names (startsAt, total, level, rating_level) for unified processing
- get_chartdata.py: Pre-aggregate data before processing when resolution is
'hourly', enabling same code path for filters/insert_nulls/connect_segments
- get_apexcharts_yaml.py: Add resolution parameter, pass to all 4 get_chartdata
service calls in generated JavaScript
- services.yaml: Add resolution field with interval/hourly selector
- icons.json: Add section icons for get_apexcharts_yaml fields
- translations: Add highlight_peak_price and resolution field translations
for all 5 languages (en, de, sv, nb, nl)
Impact: Users can now generate cleaner charts with 24 hourly data points
instead of 96 quarter-hourly intervals. The unified processing approach
ensures all chart features (filters, null insertion, segment connection)
work identically for both resolutions.
Added dynamic warnings when users configure settings for sensors that
are currently disabled. This improves UX by informing users that their
configuration changes won't have any visible effect until they enable
the relevant sensors.
Changes:
- Created entity_check.py helper module with sensor-to-step mappings
- Added check_relevant_entities_enabled() to detect disabled sensors
- Integrated warnings into 6 options flow steps (price_rating,
price_level, best_price, peak_price, price_trend, volatility)
- Made Chart Data Export info page content-aware: shows configuration
guide when sensor is enabled, shows enablement instructions when disabled
- Updated all 5 translation files (de, en, nb, nl, sv) with dynamic
placeholders {entity_warning} and {sensor_status_info}
Impact: Users now receive clear feedback when configuring settings for
disabled sensors, reducing confusion about why changes aren't visible.
Chart Data Export page now provides context-appropriate guidance.
Fixed inconsistency between "Current Electricity Price" and "Current Electricity Price
(Energy Dashboard)" sensors that were showing different prices and icons.
Changes:
- Add current_interval_price_base to TIME_SENSITIVE_ENTITY_KEYS so it updates at
quarter-hour boundaries instead of only on API polls. This ensures both sensors
update synchronously when a new 15-minute interval starts.
- Use interval_data["startsAt"] as timestamp for current interval price sensors
(both variants) instead of rounded calculation time. This prevents timestamp
divergence when sensors update at slightly different times.
- Include current_interval_price_base in icon color attribute mapping so both
sensors display the same dynamic cash icon based on current price level.
- Include current_interval_price_base in dynamic icon function so it gets the
correct icon based on current price level (VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE).
Impact: Both sensors now show identical prices, timestamps, and icons as intended.
They update synchronously at interval boundaries (00, 15, 30, 45 minutes) and
correctly represent the Energy Dashboard compatible variant without lag or
inconsistencies.
Convert best_price and peak_price timing sensors to display in hours (UI-friendly)
while retaining minute values in attributes (automation-friendly). This improves
readability in dashboards by using Home Assistant's automatic duration formatting
"1 h 35 min" instead of decimal "1.58 h".
BREAKING CHANGE: State unit changed from minutes to hours for 6 timing sensors.
Affected sensors:
* best_price_period_duration, best_price_remaining_minutes, best_price_next_in_minutes
* peak_price_period_duration, peak_price_remaining_minutes, peak_price_next_in_minutes
Migration guide for users:
- If your automations use {{ state_attr(..., 'remaining_time') }} or similar:
No action needed - attribute values remain in minutes
- If your automations use {{ states('sensor.best_price_remaining_minutes') }} directly:
Update to use the minute attribute instead: {{ state_attr('sensor.best_price_remaining_minutes', 'remaining_minutes') }}
- If your dashboards display the state value:
Values now show as "1 h 35 min" instead of "95" - this is the intended improvement
- If your templates do math with the state: multiply by 60 to convert hours back to minutes
Before: remaining * 60
After: remaining_minutes (use attribute directly)
Implementation details:
- Timing sensors now use device_class=DURATION, unit=HOURS, precision=2
- State values converted from minutes to hours via _minutes_to_hours()
- New minute-precision attributes added for automation compatibility:
* period_duration_minutes (for checking if period is long enough)
* remaining_minutes (for countdown-based automation logic)
* next_in_minutes (for time-to-event automation triggers)
- Translation improvements across all 5 languages (en, de, nb, nl, sv):
* Descriptions now clarify state in hours vs attributes in minutes
* Long descriptions explain dual-format architecture
* Usage tips updated to reference minute attributes for automations
* All translation files synchronized (fixed order, removed duplicates)
- Type safety: Added type assertions (cast) for timing calculator results to
satisfy Pyright type checking (handles both float and datetime return types)
Home Assistant now automatically formats these durations as "1 h 35 min" for improved
UX, matching the behavior of battery.remaining_time and other duration sensors.
Rationale for breaking change:
The previous minute-based state was unintuitive for users ("95 minutes" doesn't
immediately convey "1.5 hours") and didn't match Home Assistant's standard duration
formatting. The new hour-based state with minute attributes provides:
- Better UX: Automatic "1 h 35 min" formatting in UI
- Full automation compatibility: Minute attributes for all calculation needs
- Consistency: Matches HA's duration sensor pattern (battery, timer, etc.)
Impact: Timing sensors now display in human-readable hours with full backward
compatibility via minute attributes. Users relying on direct state access must
migrate to minute attributes (simple change, documented above).
- Remove pool stats/fetch-age from lifecycle sensor to avoid stale data under state-change filtering; add `next_api_poll` for transparency.
- Clean lifecycle calculator by dropping unused helpers/constants and delete the obsolete cache age test.
- Clarify lifecycle state is diagnostics-only in coordinator comments, keep state-change filtering in timer test, and retain quarter-hour precision notes in constants.
- Keep sensor core aligned with lifecycle state filtering.
Impact: Lifecycle sensor now exposes only state-relevant fields without recorder noise, next API poll is visible, and dead code/tests tied to removed attributes are gone.
Added `highlight_peak_price` (default: false) to `get_apexcharts_yaml` service
and implemented a subtle red overlay analogous to best price periods using
`period_filter: 'peak_price'`. Tooltips now dynamically exclude overlay
series to prevent overlay tooltips.
Impact: Users can visualize peak-price periods in ApexCharts cards
when desired, with default opt-out behavior.
Expose the `price_coefficient_variation_%` value across period statistics, binary sensor attributes, and the volatility calculator, and refresh the volatility descriptions/translations to mention the coefficient-of-variation metric.
The lifecycle sensor was always showing "fresh" state because
_last_price_update was set on every coordinator update, regardless of
whether data came from API or cache.
Changes:
- interval_pool/manager.py: get_intervals() and get_sensor_data() now
return tuple[data, bool] where bool indicates actual API call
- coordinator/price_data_manager.py: All fetch methods propagate
api_called flag through the call chain
- coordinator/core.py: Only update _last_price_update when api_called=True,
added debug logging to distinguish API calls from cached data
- services/get_price.py: Updated to handle new tuple return type
Impact: Lifecycle sensor now correctly shows "cached" during normal
15-minute updates (using pool cache) and only "fresh" within 5 minutes
of actual API calls. This fixes the issue where the sensor would never
leave the "fresh" state during frequent HA restarts or normal operation.
Fixed bug in best price flex filter that incorrectly excluded prices
when checking for periods. The filter was requiring price >= daily_min,
which is unnecessary and could theoretically exclude valid low prices.
Changed from:
in_flex = price >= criteria.ref_price and price <= flex_threshold
To:
in_flex = price <= flex_threshold
This ensures all low prices up to the threshold are included in best
price period consideration, matching the expected behavior described
in the period calculation documentation.
The fix addresses the user's observation that qualifying intervals
appearing after the daily minimum in chronological order should be
included if they meet the flex criteria.
Cache now stores only user metadata and timestamps. Price data is
managed exclusively by IntervalPool (single source of truth).
Changes:
- cache.py: Remove price_data and last_price_update fields
- core.py: Remove _cached_price_data, update references to use Pool
- core.py: Rename _data_fetcher to _price_data_manager
- AGENTS.md: Update class naming examples (DataFetcher → PriceDataManager)
This completes the Pool integration architecture where IntervalPool
handles all price data persistence and coordinator cache handles
only user account metadata.
Add debug service to clear tomorrow data from interval pool, enabling
testing of tomorrow data refresh cycle without waiting for next day.
Service available only in DevContainer (TIBBER_PRICES_DEV=1 env var).
Removes intervals from both Pool index and coordinator.data["priceInfo"]
so sensors properly show "unknown" state.
Changes:
- Add debug_clear_tomorrow.py service handler
- Register conditionally based on TIBBER_PRICES_DEV env var
- Add service schema and translations
- Set TIBBER_PRICES_DEV=1 in devcontainer.json
Usage: Developer Tools → Services → tibber_prices.debug_clear_tomorrow
Impact: Enables rapid testing of tomorrow data refresh cycle during
development without waiting or restarting HA.
Rename and refactor data_fetching.py → price_data_manager.py to reflect
actual responsibilities:
- User data: Fetches directly via API, validates, caches
- Price data: Delegates to IntervalPool (single source of truth)
Key changes:
- Add should_fetch_tomorrow_data() for intelligent API call decisions
- Add include_tomorrow parameter to prevent API spam before 13:00
- Remove cached_price_data property (Pool is source of truth)
- Update tests to use new class name
Impact: Clearer separation of concerns, reduced API calls through
intelligent tomorrow data fetching logic.
Replace cache-based metrics with Pool as single source of truth:
- get_cache_age_minutes() → get_sensor_fetch_age_minutes() (from Pool)
- Remove get_cache_validity_status(), get_data_completeness_status()
- Add get_pool_stats() for comprehensive pool statistics
- Add has_tomorrow_data() using Pool as source
Attributes now show:
- sensor_intervals_count/expected/has_gaps (protected range)
- cache_intervals_total/limit/fill_percent/extra (entire pool)
- last_sensor_fetch, cache_oldest/newest_interval timestamps
- tomorrow_available based on Pool state
Impact: More accurate lifecycle status, consistent with Pool as source
of truth, cleaner diagnostic information.
Return shallow copies from _get_cached_intervals() to prevent external
code (e.g., parse_all_timestamps()) from mutating Pool internal cache.
This fixes TypeError in check_coverage() caused by datetime objects in
cached interval dicts.
Additional improvements:
- Add TimeService support for time-travel testing in cache/manager
- Normalize startsAt to consistent format (handles datetime vs string)
- Rename detect_gaps() → check_coverage() for clarity
- Add get_sensor_data() for sensor data fetching with fetch/return separation
- Add get_pool_stats() for lifecycle sensor metrics
Impact: Fixes critical cache mutation bug, enables time-travel testing,
improves pool API for sensor integration.
Added async_shutdown() method for proper cleanup on unload - cancels
debounce and background tasks to prevent orphaned task leaks.
Added Phase 1.5 to GC: removes empty fetch groups after dead interval
cleanup, with index rebuild to maintain consistency.
Added update_batch() to TimestampIndex for efficient batch updates.
Touch operations now use batch updates instead of N remove+add calls.
Rewrote memory leak tests for modular architecture - all 9 tests now
pass using new component APIs (cache, index, gc).
Impact: Prevents task leaks on HA restart/reload, reduces memory
overhead from empty groups, improves touch operation performance.
Changed from fixed padding (0.5ct below min, 1ct above max) to
proportional padding based on data range (8% below, 15% above).
This ensures consistent visual "airiness" across all price ranges,
whether prices are at 30ct or 150ct. Both subunit (ct/øre) and
base currency (€/kr) now use the same proportional logic.
Previous fixed padding looked too tight on charts with large price
ranges (e.g., 0.6€-1.5€) compared to charts with small ranges
(e.g., 28-35ct).
Impact: Chart metadata sensor provides better-scaled yaxis_min/yaxis_max
values for all chart cards, making price visualizations more readable
with appropriate whitespace around data regardless of price range.
- coordinator/core.py: Fix return type for _get_threshold_percentages()
- coordinator/data_transformation.py: Add type ignore for cached data return
- sensor/core.py: Initialize _state_info with required unrecorded_attributes
Intelligent handling when tomorrow's price data arrives:
1. Cross-Day Extension
- Late-night periods (starting ≥20:00) can extend past midnight
- Extension continues while prices remain below daily_min × (1+flex)
- Maximum extension to 08:00 next day (covers typical night low)
2. Period Supersession
- Obsolete late-night today periods filtered when tomorrow is better
- Tomorrow must be ≥10% cheaper to supersede (SUPERSESSION_PRICE_IMPROVEMENT_PCT)
- Prevents stale relaxation periods from persisting
Impact: Late-night periods reflect tomorrow's data when available.
Prevent relaxation from creating heterogeneous periods:
1. CV-based Quality Gate (PERIOD_MAX_CV = 25%)
- Periods with internal CV >25% are rejected during relaxation
- CV field added to period statistics for transparency
2. Period Overlap Protection
- New periods cannot "swallow" existing smaller periods
- CV-based merge blocking prevents heterogeneous combinations
- Preserves good baseline periods from relaxation replacement
3. Constants in types.py
- PERIOD_MAX_CV, CROSS_DAY_*, SUPERSESSION_* thresholds
- TibberPricesPeriodStatistics extended with coefficient_of_variation field
Impact: Users get smaller, more homogeneous periods that better represent
actual cheap/expensive windows.
Outlier smoothing now adapts to daily price volatility (CV):
- Flat days (CV≤10%): conservative (confidence=2.5), fewer false positives
- Volatile days (CV≥30%): aggressive (confidence=1.5), catch more spikes
- Linear interpolation between thresholds
Uses calculate_coefficient_of_variation() for consistency with volatility sensors.
Impact: Better outlier detection that respects natural price variation patterns.
Flat days preserve more structure, volatile days get stronger smoothing.
Add calculate_coefficient_of_variation() as central utility function:
- CV = (std_dev / mean) * 100 as standardized volatility measure
- calculate_volatility_with_cv() returns both level and numeric CV
- Volatility sensors now expose CV in attributes for transparency
Used as foundation for quality gates, adaptive smoothing, and period statistics.
Impact: Volatility sensors show numeric CV percentage alongside categorical level,
enabling users to see exact price variation.
The outlier filter was incorrectly smoothing daily minimum/maximum prices,
causing best/peak price periods to miss their most important intervals.
Root cause: When the daily minimum (e.g., 0.5535 kr at 05:00) was surrounded
by higher prices, the trend-based prediction calculated an "expected" price
(0.6372 kr) that exceeded the flex threshold (0.6365 kr), causing the
interval to be excluded from the best price period.
Solution: Daily extremes are now protected from smoothing. Before applying
any outlier detection, we calculate daily min/max prices and skip smoothing
for any interval at or within 0.1% of these values.
Changes:
- Added _calculate_daily_extremes() to compute daily min/max
- Added _is_daily_extreme() to check if price should be protected
- Added EXTREMES_PROTECTION_TOLERANCE constant (0.1%)
- Updated filter_price_outliers() to skip extremes before analysis
- Enhanced logging to show protected interval count
Impact: Best price periods now correctly include daily minimum intervals,
and peak price periods correctly include daily maximum intervals. The
period for 2024-12-23 now extends from 03:15-05:30 (10 intervals) instead
of incorrectly stopping at 05:00 (7 intervals).
Implement gap tolerance smoothing for Tibber's price level classification
(VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE/VERY_EXPENSIVE), separate from the existing
rating_level gap tolerance (LOW/NORMAL/HIGH).
New feature:
- Add CONF_PRICE_LEVEL_GAP_TOLERANCE config option with separate UI step
- Implement _apply_level_gap_tolerance() using same bidirectional gravitational
pull algorithm as rating gap tolerance
- Add _build_level_blocks() and _merge_small_level_blocks() helper functions
Config flow changes:
- Add new "price_level" options step with dedicated schema
- Add menu entry "🏷️ Preisniveau" / "🏷️ Price Level"
- Include translations for all 5 languages (de, en, nb, nl, sv)
Bug fixes:
- Use copy.deepcopy() for price intervals before enrichment to prevent
in-place modification of cached raw API data, which caused gap tolerance
changes to not take effect when reverting settings
- Clear transformation cache in invalidate_config_cache() to ensure
re-enrichment with new settings
Logging improvements:
- Reduce options update handler from 4 INFO messages to 1 DEBUG message
- Move level_filtering and period_overlap debug logs to .details logger
for granular control via configuration.yaml
Technical details:
- level_gap_tolerance is tracked separately in transformation config hash
- Algorithm: Identifies small blocks (≤ tolerance) and merges them into
the larger neighboring block using gravitational pull calculation
- Default: 1 (smooth single isolated intervals), Range: 0-4
Impact: Users can now stabilize Tibber's price level classification
independently from the internal rating_level calculation. Prevents
automation flickering caused by brief price level changes in Tibber's API.
Added UI controls for price rating stabilization parameters that were
previously hardcoded. Users can now fine-tune rating stability to match
their automation needs.
Changes:
- Added CONF_PRICE_RATING_HYSTERESIS constant (0-5%, step 0.5%, default 2%)
- Added CONF_PRICE_RATING_GAP_TOLERANCE constant (0-4 intervals, default 1)
- Extended get_price_rating_schema() with two new sliders
- Updated data_transformation.py to pass both parameters to enrichment function
- Improved descriptions in all 5 languages (de, en, nb, nl, sv) to focus on
automation stability instead of chart appearance
- Both settings included in factory reset via get_default_options()
Hysteresis explanation: Prevents rapid state changes when prices hover near
thresholds (e.g., LOW requires price > threshold+hysteresis to leave).
Gap tolerance explanation: Merges small isolated rating blocks into dominant
neighboring blocks using "look through" algorithm (fixed in previous commit).
Impact: Users can now adjust rating stability for their specific use cases.
Lower hysteresis (0-1%) for responsive automations, higher (3-5%) for stable
long-running processes. Gap tolerance prevents brief rating spikes from
triggering unnecessary automation actions.
The gap tolerance algorithm now looks through small intermediate blocks
to find the first LARGE block (> gap_tolerance) in each direction.
This ensures small isolated rating intervals are merged into the
correct dominant block, not just the nearest neighbor.
Example: NORMAL(large) HIGH(1) NORMAL(1) HIGH(large)
Before: HIGH at 05:45 merged into NORMAL (wrong - nearest neighbor)
After: NORMAL at 06:00 merged into HIGH (correct - dominant block)
Also collects all merge decisions BEFORE applying them, preventing
order-dependent outcomes when multiple small blocks are adjacent.
Impact: Rating transitions now appear at visually logical positions
where prices actually change direction, not at arbitrary boundaries.
Problem: In segmented price charts with connect_segments=true, vertical lines
at price level transitions were always drawn by the ending segment. This meant
a price INCREASE showed a cheap-colored line going UP, and a price DECREASE
showed an expensive-colored line going DOWN - counterintuitive for users.
Solution: Implement directional bridge-point logic using price level hierarchy:
- Add _is_transition_to_more_expensive() helper using PRICE_LEVEL_MAPPING and
PRICE_RATING_MAPPING to determine transition direction
- Price INCREASE (cheap → expensive): The MORE EXPENSIVE segment draws the
vertical line UP via new start-bridge logic (end-bridge at segment start)
- Price DECREASE (expensive → cheap): The MORE EXPENSIVE segment draws the
vertical line DOWN via existing end-bridge logic (bridge at segment end)
Technical changes:
- Track prev_value and prev_price for segment start detection
- Add end-bridge points at segment starts for upward transitions
- Replace unconditional bridge points with directional hold/bridge logic
- Hold points extend segment horizontally when next segment handles transition
Impact: Vertical transition lines now consistently use the color of the more
expensive price level, making price movements more visually intuitive.
Problem: When using dual y-axes (price + hidden highlight for best-price overlay),
ApexCharts calculates tick intervals independently for each axis. This caused
misaligned horizontal grid lines - the grid follows the first y-axis ticks,
but if the hidden highlight axis had different tick calculations, visual
inconsistencies appeared (especially visible without best-price highlight).
Solution:
- Set tickAmount: 4 on BOTH y-axes to force identical tick intervals
- Add forceNiceScale: true to ensure rounded tick values despite fixed min/max
- Add showAlways: true to price axis in template modes to prevent axis
disappearing when toggling series via legend
Also add tooltip.shared: true to combine tooltips from all series at the
same x-value into a single tooltip, reducing visual clutter at data points.
Impact: Grid lines now align consistently regardless of which series are
visible. Y-axis remains stable when toggling series in legend.
Changed from iterating over each day separately to collecting all
intervals for selected days into one continuous list before processing.
Changes:
- Collect all intervals via get_intervals_for_day_offsets() with all
day_offsets at once
- Remove outer `for day in days:` loop around interval processing
- Build date->day_key mapping during average calculation for lookup
- Add _get_day_key_for_interval() helper for average_field assignment
- Simplify midnight handling: only extend at END of entire selection
- Remove complex "next day lookup" logic at midnight boundaries
The segment boundary handling (bridge points, NULL insertion) now works
automatically across midnight since intervals are processed as one list.
Impact: Fixes bridge point rendering at midnight when rating levels
change between days. Simplifies code structure by removing ~60 lines
of per-day midnight-specific logic.
Synchronized all translation files (de, en, nb, nl, sv) with:
1. Custom translations: Added 'configurable display format' messaging to
sensor descriptions
2. Standard translations: Added detailed bullet-point descriptions for
average_sensor_display config option
Changes affect both /custom_translations/ and /translations/ directories,
ensuring UI shows complete information about the new display configuration
option across all supported languages.
Implemented configurable display format (mean/median/both) while always
calculating and exposing both price_mean and price_median attributes.
Core changes:
- utils/average.py: Refactored calculate_mean_median() to always return both
values, added comprehensive None handling (117 lines changed)
- sensor/attributes/helpers.py: Always include both attributes regardless of
user display preference (41 lines)
- sensor/core.py: Dynamic _unrecorded_attributes based on display setting
(55 lines), extracted helper methods to reduce complexity
- Updated all calculators (rolling_hour, trend, volatility, window_24h) to
use new always-both approach
Impact: Users can switch display format in UI without losing historical data.
Automation authors always have access to both statistical measures.
Fixed issue #60 where Tibber API temporarily returning incomplete data
(None values during maintenance) caused AttributeError crashes.
Root cause: `.get(key, default)` returns None when key exists with None value,
causing chained `.get()` calls to crash (None.get() → AttributeError).
Changes:
- api/helpers.py: Use `or {}` pattern in flatten_price_info() to handle
None values (priceInfo, priceInfoRange, today, tomorrow)
- entity.py: Use `or {}` pattern in _get_fallback_device_info() for address dict
- coordinator/data_fetching.py: Add _validate_user_data() method (67 lines)
to reject incomplete API responses before caching
- coordinator/data_fetching.py: Modify _get_currency_for_home() to raise
exceptions instead of silent EUR fallback
- coordinator/data_fetching.py: Add home_id parameter to constructor
- coordinator/core.py: Pass home_id to TibberPricesDataFetcher
- tests/test_user_data_validation.py: Add 12 test cases for validation logic
Architecture improvement: Instead of defensive coding with fallbacks,
implement validation to reject incomplete data upfront. This prevents
caching temporary API errors and ensures currency is always known
(critical for price calculations).
Impact: Integration now handles API maintenance periods gracefully without
crashes. No silent EUR fallbacks - raises exceptions if currency unavailable,
ensuring data integrity. Users see clear errors instead of wrong calculations.
Fixes#60
Fixes configuration wizard not saving settings (#59):
Root cause was twofold:
1. Linear multi-step flow pattern didn't properly persist changes between steps
2. Best/peak price settings used nested sections format - values were saved
in sections (period_settings, flexibility_settings, etc.) but read from
flat structure, causing configured values to be ignored on subsequent runs
Solution:
- Replaced linear step-through flow with menu-based navigation system
- Each configuration area now has dedicated "Save & Back" buttons
- Removed nested sections from all steps except best/peak price (where they
provide better UX for grouping related settings)
- Fixed best/peak price steps to correctly extract values from sections:
period_settings, flexibility_settings, relaxation_and_target_periods
- Added reset-to-defaults functionality with confirmation dialog
UI/UX improvements:
- Menu structure: General Settings, Currency Display, Price Rating Thresholds,
Volatility, Best Price Period, Peak Price Period, Price Trend,
Chart Data Export, Reset to Defaults, Back
- Removed confusing step progress indicators ("{step_num} / {total_steps}")
- Changed all submit buttons from "Continue →" to "↩ Save & Back"
- Clear grouping of settings by functional area
Translation updates (nl.json + sv.json):
- Refined volatility threshold descriptions with CV formula explanations
- Clarified price trend thresholds (compares current vs. future N-hour average,
not "per hour increase")
- Standardized terminology (e.g., "entry" → "item", compound word consistency)
- Consistently formatted all sensor names and descriptions
- Added new data lifecycle status sensor names
Technical changes:
- Options flow refactored from linear to menu pattern with menu_options dict
- New reset_to_defaults step with confirmation and abort handlers
- Section extraction logic in best_price/peak_price steps now correctly reads
from nested structure (period_settings.*, flexibility_settings.*, etc.)
- Removed sections from general_settings, display_settings, volatility, etc.
(simpler flat structure via menu navigation)
Impact: Configuration wizard now reliably saves all settings. Users can
navigate between setting areas without restarting the flow. Reset function
enables quick recovery when experimenting with thresholds. Previously
configured best/peak price settings are now correctly applied.
Add user-configurable option to choose between median and arithmetic mean
as the displayed value for all 14 average price sensors, with the alternate
value exposed as attribute.
BREAKING CHANGE: Average sensor default changed from arithmetic mean to
median. Users who rely on arithmetic mean behavior may use the price_mean attribue now, or must manually reconfigure
via Settings → Devices & Services → Tibber Prices → Configure → General
Settings → "Average Sensor Display" → Select "Arithmetic Mean" to get this as sensor state.
Affected sensors (14 total):
- Daily averages: average_price_today, average_price_tomorrow
- 24h windows: trailing_price_average, leading_price_average
- Rolling hour: current_hour_average_price, next_hour_average_price
- Future forecasts: next_avg_3h, next_avg_6h, next_avg_9h, next_avg_12h
Implementation:
- All average calculators now return (mean, median) tuples
- User preference controls which value appears in sensor state
- Alternate value automatically added to attributes
- Period statistics (best_price/peak_price) extended with both values
Technical changes:
- New config option: CONF_AVERAGE_SENSOR_DISPLAY (default: "median")
- Calculator functions return tuples: (avg, median)
- Attribute builders: add_alternate_average_attribute() helper function
- Period statistics: price_avg → price_mean + price_median
- Translations: Updated all 5 languages (de, en, nb, nl, sv)
- Documentation: AGENTS.md, period-calculation.md, recorder-optimization.md
Migration path:
Users can switch back to arithmetic mean via:
Settings → Integrations → Tibber Prices → Configure
→ General Settings → "Average Sensor Display" → "Arithmetic Mean"
Impact: Median is more resistant to price spikes, providing more stable
automation triggers. Statistical analysis from coordinator still uses
arithmetic mean (e.g., trailing_avg_24h for rating calculations).
Co-developed-with: GitHub Copilot <copilot@github.com>
Add repair notification system with three auto-clearing repair types:
- Tomorrow data missing (after 18:00)
- API rate limit exceeded (3+ consecutive errors)
- Home not found in Tibber account
Includes:
- coordinator/repairs.py: Complete TibberPricesRepairManager implementation
- Enhanced API error handling with explicit 5xx handling
- Translations for 5 languages (EN, DE, NB, NL, SV)
- Developer documentation in docs/developer/docs/repairs-system.md
Impact: Users receive actionable notifications for important issues instead
of only seeing stale data in logs.
Implemented comprehensive entity lifecycle patterns following Home Assistant
best practices for proper state management and history tracking.
Changes:
- entity.py: Added available property to base class
- Returns False when coordinator has no data or last_update_success=False
- Prevents entities from showing stale data during errors
- Auth failures trigger reauth flow via ConfigEntryAuthFailed
- sensor/core.py: Added state restore and background task handling
- Changed inheritance: SensorEntity → RestoreSensor
- Restore native_value from SensorExtraStoredData in async_added_to_hass()
- Chart sensors restore response data from attributes
- Converted blocking service calls to background tasks using hass.async_create_task()
- Eliminates 194ms setup warning by making async_added_to_hass non-blocking
- binary_sensor/core.py: Added state restore and force_update
- Changed inheritance: BinarySensorEntity → RestoreEntity + BinarySensorEntity
- Restore is_on state in async_added_to_hass()
- Added available property override for connection sensor (always True)
- Added force_update property for connection sensor to track all state changes
- Other binary sensors use base available logic
- AGENTS.md: Documented entity lifecycle patterns in Common Pitfalls
- Added "Entity Lifecycle & State Management" section
- Documents available, state restore, and force_update patterns
- Explains why each pattern matters for proper HA integration
Impact: Entities no longer show stale data during errors, history has no gaps
after HA restart, connection state changes are properly tracked, and config
entry setup completes in <200ms (under HA threshold).
All patterns verified against HA developer documentation:
https://developers.home-assistant.io/docs/core/entity/
Implement _unrecorded_attributes in both sensor and binary_sensor
entities to prevent Home Assistant Recorder database bloat.
Excluded attributes (60-85% size reduction per state):
- Descriptions/help text (static, large strings)
- Large nested structures (periods, trend_attributes, chart data)
- Frequently changing diagnostics (icon_color, cache_age)
- Static/rarely changing config (currency, resolution)
- Temporary/time-bound data (next_api_poll, last_*)
- Redundant/derived data (price_spread, diff_%)
Kept for history analysis:
- timestamp (always first), all price values
- Period timing (start, end, duration_minutes)
- Price statistics (avg, min, max)
- Boolean status flags, relaxation_active
Impact: Reduces attribute size from ~3-8 KB to ~0.5-1.5 KB per state
change. Expected savings: ~1 GB per month for typical installation.
See: https://developers.home-assistant.io/docs/core/entity/#excluding-state-attributes-from-recorder-history
Unified enum representation across all translation files and improved
consistency of localization patterns.
Key changes:
- Replaced uppercase enum constants (VERY_CHEAP, LOW, RISING) with
localized lowercase values (sehr günstig, niedrig, steigend) across
all languages in both translations/ and custom_translations/
- Removed **bold** markdown from sensor attributes (custom_translations/)
as it doesn't render in extra_state_attributes UI
- Preserved **bold** in Config Flow descriptions (translations/) where
markdown is properly rendered
- Corrected German formality: "Sie" → "du" throughout all descriptions
- Completed missing Config Flow translations in Dutch, Swedish, and
Norwegian (~45 fields: period_settings, flexibility_settings,
relaxation_and_target_periods sections)
- Fixed chart_data_export and chart_metadata sensor classification
(moved from binary_sensor to sensor as they are ENUM type)
- Corrected sensor placement in custom_translations/ (all 5 languages)
Files changed: 10 (5 translations/ + 5 custom_translations/)
Lines: +203, -222
Impact: All 5 languages now use consistent, properly formatted
localized enum values. Config Flow UI displays correctly formatted
examples with bold highlighting. Sensor attributes show clean text
without raw markdown syntax. German uses informal "du" tone throughout.
- Created a new documentation file `chart-examples.md` detailing various chart configurations available through the `tibber_prices.get_apexcharts_yaml` action.
- Included descriptions, dependencies, and YAML generation examples for four chart modes: Today's Prices, Rolling 48h Window, and Rolling Window Auto-Zoom.
- Added a section on dynamic Y-axis scaling and best price period highlights.
- Established prerequisites for using the charts, including required cards and customization tips.
- Introduced a new `README.md` in the images/charts directory to document available chart screenshots and guidelines for capturing them.
Implementation flaw discovered: gradient_stop calculated as
`(avg - min) / (max - min)` for combined data produces one value
applied to ALL series. Each series (VERY_CHEAP, NORMAL, VERY_EXPENSIVE)
has different min/max ranges, so the same gradient stop position
represents a different absolute price in each series.
Example failure case:
- VERY_CHEAP: 10-20 ct → 50% at 15 ct (below overall avg!)
- VERY_EXPENSIVE: 40-50 ct → 50% at 45 ct (above overall avg!)
Conclusion: Gradient shows middle of each series range, not average
price position.
Solution: Fixed 50% gradient purely for visual appeal. Semantic
information provided by:
- Series colors (CHEAP/NORMAL/EXPENSIVE)
- Grid lines (implicitly show average)
- Dynamic Y-axis bounds (optimal scaling via chart_metadata sensor)
Changes:
- sensor/chart_metadata.py: Remove gradient_stop extraction
- services/get_apexcharts_yaml.py: Fixed gradient at [50, 100]
- custom_translations/*.json: Remove gradient_stop references
Impact: Honest visualization with no false semantic signals. Feature
was never released, clean removal without migration.
Implemented new chart_metadata diagnostic sensor that provides essential
chart configuration values (yaxis_min, yaxis_max, gradient_stop) as
attributes, enabling dynamic chart configuration without requiring
async service calls in templates.
Sensor implementation:
- New chart_metadata.py module with metadata-only service calls
- Automatically calls get_chartdata with metadata="only" parameter
- Refreshes on coordinator updates (new price data or user data)
- State values: "pending", "ready", "error"
- Enabled by default (critical for chart features)
ApexCharts YAML generator integration:
- Checks for chart_metadata sensor availability before generation
- Uses template variables to read sensor attributes dynamically
- Fallback to fixed values (gradient_stop=50%) if sensor unavailable
- Creates separate notifications for two independent issues:
1. Chart metadata sensor disabled (reduced functionality warning)
2. Required custom cards missing (YAML won't work warning)
- Both notifications explain YAML generation context and provide
complete fix instructions with regeneration requirement
Configuration:
- Supports configuration.yaml: tibber_prices.chart_metadata_config
- Optional parameters: day, minor_currency, resolution
- Defaults to minor_currency=True for ApexCharts compatibility
Translation additions:
- Entity name and state translations (all 5 languages)
- Notification messages for sensor unavailable and missing cards
- best_price_period_name for tooltip formatter
Binary sensor improvements:
- tomorrow_data_available now enabled by default (critical for automations)
- data_lifecycle_status now enabled by default (critical for debugging)
Impact: Users get dynamic chart configuration with optimized Y-axis scaling
and gradient positioning without manual calculations. ApexCharts YAML
generation now provides clear, actionable notifications when issues occur,
ensuring users understand why functionality is limited and how to fix it.
Implemented comprehensive metadata calculation for chart data export service
with automatic Y-axis scaling and gradient positioning based on actual price
statistics.
Changes:
- Added 'metadata' parameter to get_chartdata service (include/only/none)
- Implemented _calculate_metadata() with per-day price statistics
* min/max/avg/median prices
* avg_position and median_position (0-1 scale for gradient stops)
* yaxis_suggested bounds (floor(min)-1, ceil(max)+1)
* time_range with day boundaries
* currency info with symbol and unit
- Integrated metadata into rolling_window modes via config-template-card
* Pre-calculated yaxis bounds (no async issues in templates)
* Dynamic gradient stops based on avg_position
* Server-side calculation ensures consistency
Visual refinements:
- Best price overlay opacity reduced to 0.05 (ultra-subtle green hint)
- Stroke width increased to 1.5 for better visibility
- Gradient opacity adjusted to 0.45 with "light" shade
- Marker configuration: size 0, hover size 2, strokeWidth 1
- Header display: Only show LOW/HIGH rating_levels (min/max prices)
* Conditional logic excludes NORMAL and level types
* Entity state shows meaningful extrema values
- NOW marker label removed for rolling_window_autozoom mode
* Static position at 120min lookback makes label misleading
Code cleanup:
- Removed redundant all_series_config (server-side data formatting)
- Currency names capitalized (Cents, Øre, Öre, Pence)
Translation updates:
- Added metadata selector translations (de, en, nb, nl, sv)
- Added metadata field description in services
- Synchronized all language files
Impact: Users get dynamic Y-axis scaling based on actual price data,
eliminating manual configuration. Rolling window charts automatically
adjust axis bounds and gradient positioning. Header shows only
meaningful extreme values (daily min/max). All data transformation
happens server-side for optimal performance and consistency.
Added two new rolling window options for get_apexcharts_yaml service to provide
flexible dynamic chart visualization:
- rolling_window: Fixed 48h window that automatically shifts between
yesterday+today and today+tomorrow based on data availability
- rolling_window_autozoom: Same as rolling_window but with progressive zoom-in
(2h lookback + remaining time until midnight, updates every 15min)
Implementation changes:
- Updated service schema validation to accept new day options
- Added entity mapping patterns for both rolling modes
- Implemented minute-based graph_span calculation with quarter-hour alignment
- Added config-template-card integration for dynamic span updates
- Used current_interval_price sensor as 15-minute update trigger
- Unified data loading: both rolling modes omit day parameter for dynamic selection
- Applied ternary operator pattern for cleaner day_param logic
- Made grid lines more subtle (borderColor #f5f5f5, strokeDashArray 0)
Translation updates:
- Added selector options in all 5 languages (de, en, nb, nl, sv)
- Updated field descriptions to include default behavior and new options
- Documented that rolling window is default when day parameter omitted
Documentation updates:
- Updated user docs (actions.md, automation-examples.md) with new options
- Added detailed explanation of day parameter options
- Included examples for both rolling_window and rolling_window_autozoom modes
Impact: Users can now create auto-adapting ApexCharts that show 48h rolling
windows with optional progressive zoom throughout the day. Requires
config-template-card for dynamic behavior.