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.
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.
Created entity_utils/helpers.py with platform-agnostic utility functions:
- get_price_value(): Price unit conversion (major/minor currency)
- translate_level(): Price level translation
- translate_rating_level(): Rating level translation
- find_rolling_hour_center_index(): Rolling hour window calculations
These functions moved from sensor/helpers.py as they are used by both
sensor and binary_sensor platforms. Remaining sensor/helpers.py now
contains only sensor-specific helpers (aggregate_price_data, etc.).
Updated imports:
- sensor/core.py: Import from entity_utils instead of sensor.helpers
- entity_utils/icons.py: Fixed find_rolling_hour_center_index import
- binary_sensor platforms: Can now use shared helpers
Added clear docstrings explaining:
- entity_utils/helpers.py: Platform-agnostic utilities
- sensor/helpers.py: Sensor-specific aggregation functions
Impact: Better code reuse, clearer responsibility boundaries between
platform-specific and shared utilities.
This commit completes multiple refactoring efforts and documentation improvements:
Code Structure Changes:
- Move round_to_nearest_quarter_hour() from sensor/helpers.py to average_utils.py
- Resolve circular import between price_utils.py and sensor/helpers.py
- Split api.py into api/ package (client.py, queries.py, exceptions.py, helpers.py)
- Split coordinator.py into coordinator/ package (core.py, cache.py, listeners.py, etc.)
- Move period_utils/ to coordinator/period_handlers/ for better organization
- All lint checks passing (no PLC0415 local import warnings)
Documentation Additions:
- Add docs/development/architecture.md with Mermaid diagrams (end-to-end flow, cache coordination)
- Add docs/development/timer-architecture.md (comprehensive 3-timer system documentation)
- Add docs/development/caching-strategy.md (4-layer cache system with invalidation logic)
- Update docs/development/README.md with cross-references
- Update AGENTS.md with new module structure and patterns
Smart Boundary Tolerance:
- Implement ±2 second tolerance for quarter-hour rounding
- Prevents premature interval switching during HA restarts (14:59:30 stays at 14:45)
- Enables boundary snapping for timer jitter (14:59:58 → 15:00)
Atomic Midnight Coordination:
- Add _check_midnight_turnover_needed() for race-free midnight handling
- Coordinate Timer #1 (HA DataUpdateCoordinator) with Timer #2 (quarter-hour refresh)
- Whoever runs first performs turnover, other skips gracefully
Timer Optimization:
- Change timer scheduling from second=1 to second=0 (absolute-time scheduling)
- Document load distribution rationale (unsynchronized API polling prevents thundering herd)
- Comprehensive explanation of 3 independent timers and their coordination
Impact: Cleaner code structure with resolved circular dependencies, comprehensive
documentation of timer and caching systems, and improved reliability during
boundary conditions and midnight turnovers. All changes are developer-facing
improvements with no user-visible behavior changes.