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.
Changed all documentation links from version-specific tags (v0.20.0) to
main branch references. This makes documentation maintenance-free - links
stay current as code evolves.
Updated 38 files across:
- docs/developer/docs/ (7 files)
- docs/developer/versioned_docs/version-v0.21.0/ (8 files)
- docs/developer/versioned_docs/version-v0.22.0/ (8 files)
Impact: Documentation links no longer break when new versions are released.
Links always point to current code implementation.
Expanded user documentation with detailed guidance on average sensors:
1. sensors.md (+182 lines):
- New 'Average Price Sensors' section with mean vs median explanation
- 3 real-world automation examples (heat pump, dishwasher, EV charging)
- Display configuration guide with use-case recommendations
2. configuration.md (+75 lines):
- New 'Average Sensor Display Settings' section
- Comparison table of display modes (mean/median/both)
- Attribute availability details and recorder implications
3. Minor updates to installation.md and versioned docs
Impact: Users can now understand when to use mean vs median and how to
configure display format for their specific automation needs.
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.
Added new test suite and updated existing tests to verify always-both-attributes
behavior.
Changes:
- test_mean_median_display.py: NEW - Tests both attributes always present,
configurable state display, recorder exclusion, and config changes
- test_avg_none_fallback.py: Updated to test mean/median individually (65 lines)
- test_sensor_timer_assignment.py: Minor updates for compatibility (12 lines)
Coverage: All 399 tests passing, including new edge cases for attribute
presence and recorder integration.
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.