Consolidate logic for determining current price phase and associated attributes by introducing shared helper functions. This enhances code maintainability and reduces duplication across components.
Impact: Improved clarity and efficiency in price phase handling for users.
Remove the DATA_STATISTICS_REVIEW_REQUIRED flag and all associated
persistence logic. The flag approach was over-engineered: we cannot
detect whether the Recorder statistics have been fixed, and requiring
the user to re-save display settings as acknowledgement is bad UX.
New design: show the repair notice once when the mode changes.
The user dismisses it when done reviewing. The HA Recorder will
independently show its own unit-change dialog — that is sufficient.
Changes:
- Remove DATA_STATISTICS_REVIEW_REQUIRED constant from const.py
- Remove _check_statistics_review_repair() from __init__.py
- Remove ir import from __init__.py (no longer needed there)
- Remove flag set/clear logic from options_flow.py
- Change is_persistent=False (no restart persistence needed)
- Update all 5 translations: restore simple "Dismiss this notice" ending
_resolve_time_with_day_offset() was calling dt_util.now() internally
instead of using the injected now parameter. This caused incorrect date
calculations in tests and any caller that passes a specific reference time.
Also add missing price_rank_* sensor keys to TIME_SENSITIVE_ENTITY_KEYS
in coordinator/constants.py so quarter-hour refresh is registered for all
11 price rank sensors (current/next/previous interval and hour variants).
Rename dt as dt_utils → dt as dt_util (ICN001) across 11 files to follow
the project-wide import alias convention. Apply ruff auto-fixes for import
ordering and collapsing single-item imports throughout the codebase.
Released-Bug: no
Add get_display_precision() to const.py returning DISPLAY_PRECISION_SUBUNIT (2)
or DISPLAY_PRECISION_BASE (4) based on config. Replace hardcoded round(..., 2)
with get_display_precision() in all calculators and attribute builders.
Add _update_suggested_precision() to sensor core; syncs entity registry
suggested_display_precision on every coordinator update.
Interval price sensors get full precision (2 or 4 dp); other MONETARY sensors
get half precision (1 or 2 dp) as sensible default.
Impact: Price sensor states and attributes now correctly use 4 decimal places
in base-currency mode (was always 2). Display precision in dashboards updates
automatically when currency mode changes.
Rename the three existing price rank sensors from price_rank_* to
current_interval_price_rank_* to clarify they rank the current
quarter-hour interval's price, not a daily aggregate — consistent with
current_interval_price_level / current_interval_price_rating naming.
Add 8 new rank sensors covering additional subjects and reference windows:
- next_interval_price_rank_{today,today_tomorrow}
- previous_interval_price_rank_{today,today_tomorrow}
- current_hour_price_rank_{today,today_tomorrow} (5-interval rolling avg)
- next_hour_price_rank_{today,today_tomorrow} (5-interval rolling avg)
All new sensors are disabled by default. The volatility calculator gains a
subject parameter (_get_subject_price / _get_subject_price_attr_key /
_get_rolling_hour_avg_price) to select which price to rank. Sensor key
routing in value_getters.py and attributes/__init__.py updated accordingly.
No migration entries needed — the original price_rank_* sensors were never
released to users.
All 5 translation files updated. sensor-reference.md regenerated (129 entities).
Impact: Users can now track price rank for the next interval (look-ahead),
the previous interval (logging), and rolling hourly averages — for both
same-day and two-day reference windows.
Add three new price rank sensors that show where today's/tomorrow's/combined
average price falls relative to all intervals in the evaluated window:
- price_rank_today: today's average price percentile rank (0–100%)
- price_rank_tomorrow: tomorrow's average price percentile rank
- price_rank_today_tomorrow: combined today+tomorrow percentile rank
Extend all volatility sensors with IQR-based band statistics:
- price_typical_spread: interquartile range (IQR) in currency subunit
- price_typical_spread_%: IQR as percentage of daily average
- price_spike_count: number of intervals outside Tukey fences (outliers)
Add calculate_iqr_stats() utility function in utils/price.py that computes
the 25th/75th percentiles, IQR, outer fences (Q1 - 1.5×IQR / Q3 + 1.5×IQR),
and outlier count for any list of price values. Entity keys and attribute
names use plain language (`price_rank`, `price_typical_spread`) as primary
labels; technical terms (percentile rank, IQR) are included parenthetically
in descriptions and documentation.
Impact: Users can now see where current day prices rank compared to their window and how tightly clustered or spike-prone a day's prices are.
Introduces a new day_pattern.py module that analyses the 15-min price curve
for each calendar day (yesterday/today/tomorrow) and classifies its shape.
New sensors:
day_pattern_yesterday / day_pattern_today / day_pattern_tomorrow
EntityCategory.DIAGNOSTIC, SensorDeviceClass.ENUM
Patterns: valley, peak, double_valley, double_peak, flat, rising, falling, mixed
The detector uses centred-rolling smoothing, prominence-filtered extrema,
Kneedle-based knee detection, and monotone segment building.
Coordinator populates transformed_data["dayPatterns"] after priceInfo enrichment.
Impact: Users can trigger automations based on the shape of the day's price
curve, e.g. pre-heat when tomorrow is a valley day.
Add UP037 to ruff ignore list to preserve quoted TYPE_CHECKING forward
references (PEP 649 lazy eval breaks get_type_hints() at runtime for
TYPE_CHECKING-guarded imports).
Move datetime imports into TYPE_CHECKING blocks in sensor/calculators
timing.py and trend.py (TC003, type-only usage confirmed).
Apply PEP 758 parenthesis-free except clauses across 7 files via
ruff format with target-version=py314.
Update hacs.json minimum HA version to 2026.4.0, the first HA release
requiring Python 3.14.
Impact: Linter config now correctly handles Python 3.14 semantics.
Users need HA >= 2026.4 (Python 3.14) to use this integration.
Renamed trend_change_in_minutes → next_price_trend_change_in to align
with its sibling sensor next_price_trend_change (timestamp variant).
Follows the established best/peak price naming pattern where related
sensors share a common prefix (e.g. best_price_next_start_time /
best_price_next_in_minutes).
Updated entity key, translation key, friendly names (all 5 languages),
custom translations, coordinator constants, attribute routing, and
cache-clear mapping.
BREAKING CHANGE: Entity ID changes from
sensor.<home>_trend_change_in_minutes to
sensor.<home>_next_price_trend_change_in. Automations and dashboards
referencing the old entity ID must be updated.
Impact: Users with automations or dashboard cards referencing the old
sensor name need to update references. The sensor retains identical
functionality and attributes.
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.
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.
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").
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.
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.
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.
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>
- Introduced `get_intervals_for_day_offsets` helper to streamline access to price intervals for yesterday, today, and tomorrow.
- Updated various components to replace direct access to `priceInfo` with the new helper, ensuring a flat structure for price intervals.
- Adjusted calculations and data processing methods to accommodate the new data structure.
- Enhanced documentation to reflect changes in caching strategy and data structure.
Removed custom lifecycle callback push-update mechanism after confirming
it was redundant with Home Assistant's built-in DataUpdateCoordinator
pattern.
Root cause analysis showed HA's async_update_listeners() is called
synchronously (no await) immediately after _async_update_data() returns,
making separate lifecycle callbacks unnecessary.
Changes:
- coordinator/core.py: Removed lifecycle callback methods and notifications
- sensor/core.py: Removed lifecycle callback registration and cleanup
- sensor/attributes/lifecycle.py: Removed next_tomorrow_check attribute
- sensor/calculators/lifecycle.py: Removed get_next_tomorrow_check_time()
Impact: Simplified coordinator pattern, no user-visible changes. Standard
HA coordinator mechanism provides same immediate update guarantee without
custom callback complexity.
Cache validity now checks _last_coordinator_update (within 30min)
instead of _api_calls_today counter. Fixes false "stale" status
when coordinator runs every 15min but cache validation was only
checking API call counter.
Bug #1: Cache validity shows "stale" at 05:57 AM
Bug #2: Cache age calculation incorrect after midnight turnover
Bug #3: get_cache_validity inconsistent with cache_age sensor
Changes:
- Coordinator: Use _last_coordinator_update for cache validation
- Lifecycle: Extract cache validation to dedicated helper function
- Tests: 7 new tests covering midnight scenarios and edge cases
Impact: Cache validity sensor now accurately reflects coordinator
activity, not just explicit API calls. Correctly handles midnight
turnover without false "stale" status.
Add comprehensive data_lifecycle_status sensor showing real-time cache
vs fresh API data status with 6 states and 13+ detailed attributes.
Key features:
- 6 lifecycle states: cached, fresh, refreshing, searching_tomorrow,
turnover_pending, error
- Push-update system for instant state changes (refreshing→fresh→error)
- Quarter-hour polling for turnover_pending detection at 23:45
- Accurate next_api_poll prediction using Timer #1 offset tracking
- Tomorrow prediction with actual timer schedule (not fixed 13:00)
- 13+ formatted attributes: cache_age, data_completeness, api_calls_today,
next_api_poll, etc.
Implementation:
- sensor/calculators/lifecycle.py: New calculator with state logic
- sensor/attributes/lifecycle.py: Attribute builders with formatting
- coordinator/core.py: Lifecycle tracking + callback system (+16 lines)
- sensor/core.py: Push callback registration (+3 lines)
- coordinator/constants.py: Added to TIME_SENSITIVE_ENTITY_KEYS
- Translations: All 5 languages (de, en, nb, nl, sv)
Timing optimization:
- Extended turnover warning: 5min → 15min (catches 23:45 quarter boundary)
- No minute-timer needed: quarter-hour updates + push = optimal
- Push-updates: <1sec latency for refreshing/fresh/error states
- Timer offset tracking: Accurate tomorrow predictions
Removed obsolete sensors:
- data_timestamp (replaced by lifecycle attributes)
- price_forecast (never implemented, removed from definitions)
Impact: Users can monitor data freshness, API call patterns, cache age,
and understand integration behavior. Perfect for troubleshooting and
visibility into when data updates occur.
Introduce TimeService as single source of truth for all datetime operations,
replacing direct dt_util calls throughout the codebase. This establishes
consistent time context across update cycles and enables future time-travel
testing capability.
Core changes:
- NEW: coordinator/time_service.py with timezone-aware datetime API
- Coordinator now creates TimeService per update cycle, passes to calculators
- Timer callbacks (#2, #3) inject TimeService into entity update flow
- All sensor calculators receive TimeService via coordinator reference
- Attribute builders accept time parameter for timestamp calculations
Key patterns replaced:
- dt_util.now() → time.now() (single reference time per cycle)
- dt_util.parse_datetime() + as_local() → time.get_interval_time()
- Manual interval arithmetic → time.get_interval_offset_time()
- Manual day boundaries → time.get_day_boundaries()
- round_to_nearest_quarter_hour() → time.round_to_nearest_quarter()
Import cleanup:
- Removed dt_util imports from ~30 files (calculators, attributes, utils)
- Restricted dt_util to 3 modules: time_service.py (operations), api/client.py
(rate limiting), entity_utils/icons.py (cosmetic updates)
- datetime/timedelta only for TYPE_CHECKING (type hints) or duration arithmetic
Interval resolution abstraction:
- Removed hardcoded MINUTES_PER_INTERVAL constant from 10+ files
- New methods: time.minutes_to_intervals(), time.get_interval_duration()
- Supports future 60-minute resolution (legacy data) via TimeService config
Timezone correctness:
- API timestamps (startsAt) already localized by data transformation
- TimeService operations preserve HA user timezone throughout
- DST transitions handled via get_expected_intervals_for_day() (future use)
Timestamp ordering preserved:
- Attribute builders generate default timestamp (rounded quarter)
- Sensors override when needed (next interval, daily midnight, etc.)
- Platform ensures timestamp stays FIRST in attribute dict
Timer integration:
- Timer #2 (quarter-hour): Creates TimeService, calls _handle_time_sensitive_update(time)
- Timer #3 (30-second): Creates TimeService, calls _handle_minute_update(time)
- Consistent time reference for all entities in same update batch
Time-travel readiness:
- TimeService.with_reference_time() enables time injection (not yet used)
- All calculations use time.now() → easy to simulate past/future states
- Foundation for debugging period calculations with historical data
Impact: Eliminates timestamp drift within update cycles (previously 60+ independent
dt_util.now() calls could differ by milliseconds). Establishes architecture for
time-based testing and debugging features.