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.
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.
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.
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.
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.
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.
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.
Restructured 5 options flow steps (current_interval_price_rating, best_price,
peak_price, price_trend, volatility) to use Home Assistant's sections feature
for better UI organization and logical grouping.
Changes:
- current_interval_price_rating: Single section "price_rating_thresholds"
- best_price: Three sections (period_settings, flexibility_settings,
relaxation_and_target_periods)
- peak_price: Three sections (period_settings, flexibility_settings,
relaxation_and_target_periods)
- price_trend: Single section "price_trend_thresholds"
- volatility: Single section "volatility_thresholds"
Each section includes name, description, data fields, and data_description
fields following HA translation schema requirements.
Updated all 5 language files (de, en, nb, nl, sv) with new section structure
while preserving existing field descriptions and translations.
Impact: Options flow now displays configuration fields in collapsible,
logically grouped sections with clear section headers, improving UX for
complex multi-parameter configuration steps. No functional changes to
configuration logic or validation.
Add dynamic rolling window mode to get_chartdata and get_apexcharts_yaml
services that automatically adapts to data availability.
When 'day' parameter is omitted, services return 48-hour window:
- With tomorrow data (after ~13:00): today + tomorrow
- Without tomorrow data: yesterday + today
Changes:
- Implement rolling window logic in get_chartdata using has_tomorrow_data()
- Generate config-template-card wrapper in get_apexcharts_yaml for dynamic
ApexCharts span.offset based on tomorrow_data_available binary sensor
- Update service descriptions in services.yaml
- Add rolling window descriptions to all translations (de, en, nb, nl, sv)
- Document rolling window mode in docs/user/services.md
- Add ApexCharts examples with prerequisites in docs/user/automation-examples.md
BREAKING CHANGE: get_apexcharts_yaml rolling window mode requires
config-template-card in addition to apexcharts-card for dynamic offset
calculation.
Impact: Users can create auto-adapting 48h price charts without manual day
selection. Fixed day views (day: today/yesterday/tomorrow) still work with
apexcharts-card only.
Added complete localization support for time offset descriptions:
- Convert hardcoded English strings "(X days ago)" to translatable keys
- Add time_units translations (day/days, hour/hours, minute/minutes, ago, now)
- Support singular/plural forms in all 5 languages (de, en, nb, nl, sv)
- German: Proper Dativ case "Tagen" with preposition "vor"
- Compact format for mixed offsets: "7 Tagen - 02:30"
Config flow improvements:
- Replace hardcoded "Enter new API token" with translated "Add new Tibber account API token"
- Use get_translation() for account_choice dropdown labels
- Fix SelectOptionDict usage (no mixing with translation_key parameter)
- Convert days slider from float to int (prevents "2.0 Tage" display)
- DurationSelector: default {"hours": 0, "minutes": 0} to fix validation errors
Translation keys added:
- selector.account_choice.options.new_token
- time_units (day, days, hour, hours, minute, minutes, ago, now)
- config.step.time_offset_description guidance text
Impact: Config flow works fully translated in all 5 languages with proper grammar.
Best Price min_distance now uses negative values (-50 to 0) to match
semantic meaning "below average". Peak Price continues using positive
values (0 to 50) for "above average".
Uniform formula: avg * (1 + distance/100) works for both period types.
Sign indicates direction: negative = toward MIN (cheap), positive = toward MAX (expensive).
Changes:
- const.py: DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG = -5 (was 5)
- schemas.py: Best Price range -50 to 0, Peak Price range 0 to 50
- validators.py: Separate validate_best_price_distance_percentage()
- level_filtering.py: Simplified to uniform formula (removed conditionals)
- translations: Separate error messages for Best/Peak distance validation
- tests: 37 comprehensive validator tests (100% coverage)
Impact: Configuration UI now visually represents direction relative to average.
Users see intuitive negative values for "below average" pricing.
- Added new validation functions for various parameters including flexibility percentage, distance percentage, minimum periods, gap count, relaxation attempts, price rating thresholds, volatility threshold, and price trend thresholds.
- Updated constants in `const.py` to define maximum and minimum limits for the new validation criteria.
- Improved error messages in translations for invalid parameters to provide clearer guidance to users.
- Adjusted existing validation functions to ensure they align with the new constants and validation logic.
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.
Moved Chart Data Export sensor configuration from config flow textarea
to configuration.yaml for better maintainability and consistency with
Home Assistant standards.
Changes:
- __init__.py: Added async_setup() with CONFIG_SCHEMA for tibber_prices.chart_export
- const.py: Added DATA_CHART_CONFIG constant for hass.data storage
- options_flow.py: Simplified chart_data_export step to info-only page
- schemas.py: get_chart_data_export_schema() returns empty schema (no input fields)
- sensor/chart_data.py: Reads config from hass.data instead of config_entry.options
- All 5 translation files: Updated chart_data_export description with:
- Clear heading: "📊 Chart Data Export Sensor"
- Intro line explaining sensor purpose
- Legacy warning (⚠️) recommending service use
- Two valid use cases (✅): attribute-only tools, auto-updating data
- One discouraged use case (❌): automations should use service directly
- 3-step activation instructions
- YAML configuration example with all parameters
- Correct default behavior: today+tomorrow, 15-minute intervals, prices only
Impact: Users configure chart export in configuration.yaml instead of UI.
Sensor remains disabled by default (diagnostic sensor). Config flow shows
prominent info page guiding users toward service usage while keeping
sensor available for legacy dashboard tools that only read attributes.
Standardized config flow translations (nb, nl, sv) to match German/English
format with minimal field labels and comprehensive data_descriptions.
Changes across Norwegian, Dutch, and Swedish translations:
- Updated step_progress format: **{step_progress}** → _{step_progress}_
- Made all step descriptions bold with **text** formatting
- Simplified field labels (removed verbose explanations)
- Added data_description for price_rating (low/high thresholds)
- Added data_description for price_trend (rising/falling thresholds)
- Added data_description for volatility (moderate/high/very high thresholds)
- Ensured all steps have: emojis, italic step_progress, separator (---)
- Added missing emoji to Swedish price_rating step (📊)
Impact: All 5 languages now have consistent UX with minimal, scannable
field labels and detailed optional descriptions accessible via ⓘ icon.
Users get cleaner config flow with better clarity.
BREAKING CHANGE: Period overlap resolution now merges adjacent/overlapping periods
instead of marking them as extensions. This simplifies automation logic and provides
clearer period boundaries for users.
Previous Behavior:
- Adjacent periods created by relaxation were marked with is_extension=true
- Multiple short periods instead of one continuous period
- Complex logic needed to determine actual period length in automations
New Behavior:
- Adjacent/overlapping periods are merged into single continuous periods
- Newer period's relaxation attributes override older period's
- Simpler automation: one period = one continuous time window
Changes:
- Period Overlap Resolution (new file: period_overlap.py):
* Added merge_adjacent_periods() to combine periods and preserve attributes
* Rewrote resolve_period_overlaps() with simplified merge logic
* Removed split_period_by_overlaps() (no longer needed)
* Removed is_extension marking logic
* Removed unused parameters: min_period_length, baseline_periods
- Relaxation Strategy (relaxation.py):
* Removed all is_extension filtering from period counting
* Simplified standalone counting to just len(periods)
* Changed from period_merging import to period_overlap import
* Added MAX_FLEX_HARD_LIMIT constant (0.50)
* Improved debug logging for merged periods
- Code Quality:
* Fixed all remaining linter errors (N806, PLR2004, PLR0912)
* Extracted magic values to module-level constants:
- FLEX_SCALING_THRESHOLD = 0.20
- SCALE_FACTOR_WARNING_THRESHOLD = 0.8
- MAX_FLEX_HARD_LIMIT = 0.50
* Added appropriate noqa comments for unavoidable patterns
- Configuration (from previous work in this session):
* Removed CONF_RELAXATION_STEP_BEST, CONF_RELAXATION_STEP_PEAK
* Hard-coded 3% relaxation increment for reliability
* Optimized defaults: RELAXATION_ATTEMPTS 8→11, ENABLE_MIN_PERIODS False→True,
MIN_PERIODS undefined→2
* Removed relaxation_step UI fields from config flow
* Updated all 5 translation files
- Documentation:
* Updated period_handlers/__init__.py: period_merging → period_overlap
* No user-facing docs changes needed (already described continuous periods)
Rationale - Period Merging:
User experience was complicated by fragmented periods:
- Automations had to check multiple adjacent periods
- Binary sensors showed ON/OFF transitions within same cheap time
- No clear way to determine actual continuous period length
With merging:
- One continuous cheap time = one period
- Binary sensor clearly ON during entire period
- Attributes show merge history via merged_from dict
- Relaxation info preserved from newest/highest flex period
Rationale - Hard-Coded Relaxation Increment:
The configurable relaxation_step parameter proved problematic:
- High base flex + high step → rapid explosion (40% base + 10% step → 100% in 6 steps)
- Users don't understand the multiplicative nature
- 3% increment provides optimal balance: 11 attempts to reach 50% hard cap
Impact:
- Existing installations: Periods may appear longer (merged instead of split)
- Automations benefit from simpler logic (no is_extension checks needed)
- Custom relaxation_step values will use new 3% increment
- Users may need to adjust relaxation_attempts if they relied on high step sizes
Migrated chart_data_export from binary_sensor to sensor to enable
compatibility with dashboard integrations (ApexCharts, etc.) that
require sensor entities for data selection.
Changes:
- Moved chart_data_export from binary_sensor/ to sensor/ platform
- Changed from boolean state (ON/OFF) to ENUM states ("pending", "ready", "error")
- Maintained all functionality: service call, attribute structure, caching
- Updated translations in all 5 languages (de, en, nb, nl, sv)
- Updated user documentation (sensors.md, services.md)
- Removed all chart_data_export code from binary_sensor platform
Technical details:
- State: "pending" (before first call), "ready" (data available), "error" (service failed)
- Attributes: timestamp + error (metadata) → descriptions → service response data
- Cache (_chart_data_response) bridges async service call and sync property access
- Service call: Triggered on async_added_to_hass() and async_update()
Impact: Dashboard integrations can now select chart_data_export sensor
in their entity pickers. No breaking changes for existing users - entity ID
changes from binary_sensor.* to sensor.*, but functionality identical.
Added optional diagnostic binary sensor that exposes get_chartdata
service results as entity attributes for legacy dashboard tools.
Key features:
- Entity: binary_sensor.tibber_home_NAME_chart_data_export
- Configurable via Options Flow Step 7 (YAML parameters)
- Calls get_chartdata service with user configuration
- Exposes response as attributes for chart cards
- Disabled by default (opt-in)
- Auto-refreshes on coordinator updates
- Manual refresh via homeassistant.update_entity
Implementation details:
- Added chart_data_export entity description to definitions.py
- Implemented state/attribute logic in binary_sensor/core.py
- Added YAML configuration schema in schemas.py
- Added validation in options_flow.py (Step 7)
- Service call validation with detailed error messages
- Attribute ordering: metadata first, descriptions next, service data last
- Dynamic icon mapping (database-export/database-alert)
Translations:
- Added chart_data_export_config to all 5 languages
- Added Step 7 descriptions with legacy warning
- Added invalid_yaml_syntax/invalid_yaml_structure error messages
- Added custom_translations for sensor descriptions
Documentation:
- Added Chart Data Export section to sensors.md
- Added comprehensive service guide to services.md
- Migration path from sensor to service
- Configuration instructions via Options Flow
Impact: Provides backward compatibility for dashboard tools that can
only read entity attributes (e.g., older ApexCharts versions). New
integrations should use tibber_prices.get_chartdata service directly.
Complete overhaul of the ApexCharts integration service layer to support
modern chart card workflows with flexible data formatting and filtering.
Replaced services:
- Removed: get_price, get_apexcharts_data (legacy, entity-based)
- Added: get_chartdata (flexible data service)
- Improved: get_apexcharts_yaml (now uses get_chartdata internally)
New get_chartdata service features:
- Multiple output formats (array_of_objects, array_of_arrays)
- Customizable field names for chart compatibility
- Resolution options (15-min intervals, hourly averages)
- Advanced filtering (level_filter, rating_level_filter)
- NULL insertion modes (none, segments, all) for clean gaps
- Minor currency support (cents/øre) with custom rounding
- Optional fields (level, rating_level, average)
- Multi-day support (yesterday/today/tomorrow)
Enhanced get_apexcharts_yaml service:
- Direct entry_id parameter (no entity_id lookup needed)
- Uses get_chartdata with WebSocket API (data_generator)
- Improved ApexCharts configuration:
* Gradient fill (70% opacity → 20%)
* Grid styling with dashed lines
* Zoom & Pan tools (animations disabled for performance)
* Optimized legend (top-left, compact markers)
* Y-axis auto-scaling (min: 0 for visibility, supports negative prices)
* 2 decimal places (improved precision)
* Browser locale formatting (automatic comma/point)
* insert_nulls='segments' for clean gaps between levels
- Multi-language support (translated titles, series names)
- Day selection (yesterday/today/tomorrow with correct span config)
Service translations:
- Added comprehensive field descriptions (all 5 languages: de, en, nb, nl, sv)
- Selector translations for all options (day, resolution, output_format, etc.)
- ApexCharts title translations in custom_translations/
Technical improvements:
- Hourly aggregation uses exact 4-interval windows (:00/:15/:30/:45)
- Level/rating aggregation follows sensor logic (aggregate_level_data, aggregate_rating_data)
- Midnight extension for last interval of filtered data (seamless day transitions)
- Case-insensitive filter matching (normalized to uppercase)
- Ruff complexity fixed (extracted _get_level_translation helper)
Impact: Users can now generate production-ready ApexCharts YAML with a single
service call, or use get_chartdata flexibly with any chart card (ApexCharts,
Plotly, Mini Graph, etc.). Supports complex filtering scenarios (e.g., "show
only LOW rating periods") with clean visual gaps. Full multi-language support.
API Client:
- Changed async_get_price_info() to accept home_ids parameter
- Implemented _get_price_info_for_specific_homes() using GraphQL aliases
(home0: home(id: "abc") { ... }) for efficient multi-home queries
- Extended async_get_viewer_details() with comprehensive home metadata
(owner, address, meteringPointData, subscription, features)
- Removed deprecated async_get_data() method (combined query no longer needed)
- Updated _is_data_empty() to handle aliased response structure
Coordinator:
- Added _get_configured_home_ids() to collect all active config entries
- Modified _fetch_all_homes_data() to only query configured homes
- Added refresh_user_data() forcing user data refresh (bypasses cache)
- Improved get_user_profile() with detailed user info (name, login, accountType)
- Fixed get_user_homes() to extract from viewer object
Binary Sensors:
- Added has_ventilation_system sensor (home metadata)
- Added realtime_consumption_enabled sensor (features check)
- Refactored state getter mapping to dictionary pattern
Diagnostic Sensors (12 new):
- Home metadata: home_type, home_size, main_fuse_size, number_of_residents,
primary_heating_source
- Metering point: grid_company, grid_area_code, price_area_code,
consumption_ean, production_ean, energy_tax_type, vat_type,
estimated_annual_consumption
- Subscription: subscription_status
- Added available property override to hide diagnostic sensors with no data
Config Flow:
- Fixed subentry flow to exclude parent home_id from available homes
- Added debug logging for home title generation
Entity:
- Made attribution translatable (get_translation("attribution"))
- Removed hardcoded user name suffix from subentry device names
Impact: Enables multi-home setups with dedicated subentries. Each home gets
its own set of sensors and only configured homes are queried (reduces API
load). New diagnostic sensors provide comprehensive home metadata from Tibber
API. Users can track ventilation systems, heating types, metering point info,
and subscription status.
Added dedicated sensor for Home Assistant's Energy Dashboard integration and
new sensors to track total period duration for best/peak price periods.
New Sensors:
- current_interval_price_major: Shows price in major currency (EUR/kWh, NOK/kWh)
instead of minor units (ct/kWh, øre/kWh) for Energy Dashboard compatibility
- best_price_period_duration: Total length of current/next best price period
- peak_price_period_duration: Total length of current/next peak price period
Changes:
- sensor/definitions.py: Added 3 new sensor definitions with proper device_class,
state_class, and suggested_display_precision
- sensor/core.py: Extended native_unit_of_measurement property to return major
currency unit for Energy Dashboard sensor while keeping minor units for others
- sensor/core.py: Added _calc_period_duration() method to calculate period lengths
- sensor/core.py: Added handler mappings for new duration sensors
- const.py: Imported format_price_unit_major() for currency formatting
- translations/*.json: Added entity names for all 5 languages (de, en, nb, nl, sv)
- custom_translations/*.json: Added descriptions, long_descriptions, and usage_tips
for all new sensors in all 5 languages
Technical Details:
- Energy Dashboard sensor uses 4 decimal precision (0.2534 EUR/kWh) vs 2 decimals
for regular price sensors (25.34 ct/kWh)
- Duration sensors return minutes (UnitOfTime.MINUTES) with 0 decimal precision
- Duration sensors disabled by default (less commonly needed than end time)
- All MONETARY sensors now have explicit state_class=SensorStateClass.TOTAL
- All ENUM/TIMESTAMP sensors have explicit state_class=None for clarity
Impact: Users can now add electricity prices to Energy Dashboard for automatic
cost calculation. Duration sensors help users plan appliance usage by showing
how long cheap/expensive periods last. All price statistics now properly tracked
by Home Assistant's recorder.
Added 10 new timing sensors (5 for best_price, 5 for peak_price) to track
period timing and progress:
Timestamp sensors (quarter-hour updates):
- best_price_end_time / peak_price_end_time
Shows when current/next period ends (always useful reference time)
- best_price_next_start_time / peak_price_next_start_time
Shows when next period starts (even during active periods)
Countdown sensors (minute updates):
- best_price_remaining_minutes / peak_price_remaining_minutes
Minutes left in current period (0 when inactive)
- best_price_next_in_minutes / peak_price_next_in_minutes
Minutes until next period starts
- best_price_progress / peak_price_progress
Progress percentage through current period (0-100%)
Smart fallback behavior:
- Sensors always show useful values (no 'Unknown' during normal operation)
- Timestamp sensors show current OR next period end/start times
- Countdown sensors return 0 when no period is active
- Grace period: Progress stays at 100% for 60 seconds after period ends
Dynamic visual feedback:
- Progress icons differentiate 3 states at 0%:
* No data: mdi:help-circle-outline (gray)
* Waiting for next period: mdi:timer-pause-outline
* Period just started: mdi:circle-outline
- Progress 1-99%: mdi:circle-slice-1 to mdi:circle-slice-8 (pie chart)
- Timer icons based on urgency (alert/timer/timer-sand/timer-outline)
- Dynamic colors: green (best_price), orange/red (peak_price), gray (disabled)
- icon_color attribute for UI styling
Implementation details:
- Dual update mechanism: quarter-hour (timestamps) + minute (countdowns)
- Period state callbacks: Check if period is currently active
- IconContext dataclass: Reduced function parameters from 6 to 3
- Unit constants: UnitOfTime.MINUTES, PERCENTAGE from homeassistant.const
- Complete translations for 5 languages (de, en, nb, nl, sv)
Impact: Users can now build sophisticated automations based on period timing
('start dishwasher if remaining_minutes > 60'), display countdowns in
dashboards, and get clear visual feedback about period states. All sensors
provide meaningful values at all times, making automation logic simpler.
Added timestamp attributes to all sensors and enhanced the dynamic icon
system for comprehensive price sensor coverage with rolling hour support.
TIMESTAMP ATTRIBUTES:
Core Changes:
- sensor/attributes.py:
* Enhanced add_average_price_attributes() to track extreme intervals
for min/max sensors and add appropriate timestamps
* Added _update_extreme_interval() helper to reduce complexity
* Extended add_volatility_type_attributes() with timestamp logic for
all 4 volatility types (today/tomorrow/today_tomorrow/next_24h)
* Fixed current_interval_price timestamp assignment (use interval_data)
Timestamp Logic:
- Interval-based sensors: Use startsAt of specific 15-minute interval
- Min/Max sensors: Use startsAt of interval with extreme price
- Average sensors: Use startsAt of first interval in window
- Volatility sensors: Use midnight (00:00) for calendar day sensors,
current time for rolling 24h window
- Daily sensors: Already used fallback to midnight (verified)
ICON SYSTEM ENHANCEMENTS:
Major Extensions:
- entity_utils/icons.py:
* Created get_rolling_hour_price_level_for_icon() implementing
5-interval window aggregation matching sensor calculation logic
* Extended get_price_sensor_icon() coverage from 1 to 4 sensors:
- current_interval_price (existing)
- next_interval_price (NEW - dynamic instead of static)
- current_hour_average_price (NEW - uses rolling hour aggregation)
- next_hour_average_price (NEW - uses rolling hour aggregation)
* Added imports for aggregate_level_data and find_rolling_hour_center_index
Documentation:
- sensor/definitions.py:
* Updated 30+ sensor descriptions with detailed icon behavior comments
* Changed next_interval_price from static to dynamic icon
* Documented dynamic vs static icons for all sensor types
* Added clear icon mapping source documentation
SENSOR KEY RENAMING:
Renamed for clarity (current_hour_average → current_hour_average_price):
- sensor/core.py: Updated value getters and cached data lookup
- sensor/definitions.py: Updated entity descriptions
- sensor/attributes.py: Updated key references in attribute builders
- coordinator.py: Updated TIME_SENSITIVE_ENTITY_KEYS set
- const.py: Updated comment documentation
Translation Updates:
- custom_translations/*.json (5 files): Updated sensor keys
- translations/*.json (5 files): Updated sensor keys
Impact:
- All sensors now have timestamp attribute showing applicable time/interval
- Icon system provides richer visual feedback for more sensor types
- Consistent sensor naming improves code readability
- Users get temporal context for all sensor values
- Dynamic icons adapt to price conditions across more sensors
Renamed internal sensor keys to be more explicit about their temporal scope:
- current_price → current_interval_price
- price_level → current_interval_price_level
- price_rating → current_interval_price_rating
This naming makes it clearer that these sensors represent the current
15-minute interval, distinguishing them from hourly averages and other
time-based calculations.
Updated across all components:
- Sensor entity descriptions and handlers (sensor.py)
- Time-sensitive entity keys list (coordinator.py)
- Config flow step IDs (config_flow.py)
- Translation keys in all 5 languages (de, en, nb, nl, sv)
- Custom translations (entity descriptions, usage tips)
- Price level/rating lookups (const.py, sensor.py)
- Documentation examples (AGENTS.md, README.md)
Impact: Sensor entity IDs remain unchanged due to translation_key system.
Existing automations continue to work. Only internal code references and
translation structures updated for consistency.
Replaced absolute volatility thresholds (ct/øre) with relative coefficient
of variation (CV = std_dev / mean * 100%) for scale-independent volatility
measurement that works across all price levels.
Changes to volatility calculation:
- price_utils.py: Rewrote calculate_volatility_level() to accept price list
instead of spread value, using statistics.mean() and statistics.stdev()
- sensor.py: Updated volatility sensors to pass price lists (not spread)
- services.py: Modified _get_price_stats() to calculate CV from prices
- period_statistics.py: Extract prices for CV calculation in period summaries
- const.py: Updated default thresholds to 15%/30%/50% (was 5/15/30 ct)
with comprehensive documentation explaining CV-based approach
Dead code removal:
- period_utils/core.py: Removed filter_periods_by_volatility() function
(86 lines of code that was never actually called)
- period_utils/__init__.py: Removed dead function export
- period_utils/relaxation.py: Simplified callback signature from
Callable[[str|None, str|None], bool] to Callable[[str|None], bool]
- coordinator.py: Updated lambda callbacks to match new signature
- const.py: Replaced RELAXATION_VOLATILITY_ANY with RELAXATION_LEVEL_ANY
Bug fix:
- relaxation.py: Added int() conversion for max_relaxation_attempts
(line 435: attempts = max(1, int(max_relaxation_attempts)))
Fixes TypeError when config value arrives as float
Configuration UI:
- config_flow.py: Changed volatility threshold unit display from "ct" to "%"
Translations (all 5 languages):
- Updated volatility descriptions to explain coefficient of variation
- Changed threshold labels from "spread ≥ value" to "CV ≥ percentage"
- Languages: de, en, nb, nl, sv
Documentation:
- period-calculation.md: Removed volatility filter section (dead feature)
Impact: Breaking change for users with custom volatility thresholds.
Old absolute values (e.g., 5 ct) will be interpreted as percentages (5%).
However, new defaults (15%/30%/50%) are more conservative and work
universally across all currencies and price levels. No data migration
needed - existing configs continue to work with new interpretation.
- Skip asymmetry/zigzag rejection near the data tail and refactor spike
validation so legitimate end-of-day spikes stop breaking periods.
- Expose relaxation attempt sliders for both Best/Peak flows, wire the values
through the coordinator, and extend the relaxation engine to honor the new
max-attempt cap with richer logging & metadata.
- Raise the default attempt count to eight flex levels so the 25% increment
pattern can stretch much further before stopping, keeping translations and
docs (including the matrix explanation) in sync across all locales.
Impact: Tail spikes no longer get thrown out incorrectly, users can tune how
aggressively the period search relaxes, and the defaults now find more viable
periods on volatile days.
* feat(period-calc): adaptive defaults + remove volatility filter
Major improvements to period calculation with smarter defaults and
simplified configuration:
**Adaptive Defaults:**
- ENABLE_MIN_PERIODS: true (was false) - Always try to find periods
- MIN_PERIODS target: 2 periods/day (ensures coverage)
- BEST_PRICE_MAX_LEVEL: "cheap" (was "any") - Prefer genuinely cheap
- PEAK_PRICE_MIN_LEVEL: "expensive" (was "any") - Prefer genuinely expensive
- GAP_TOLERANCE: 1 (was 0) - Allow 1-level deviations in sequences
- MIN_DISTANCE_FROM_AVG: 5% (was 2%) - Ensure significance
- PEAK_PRICE_MIN_PERIOD_LENGTH: 30min (was 60min) - More responsive
- PEAK_PRICE_FLEX: -20% (was -15%) - Better peak detection
**Volatility Filter Removal:**
- Removed CONF_BEST_PRICE_MIN_VOLATILITY from const.py
- Removed CONF_PEAK_PRICE_MIN_VOLATILITY from const.py
- Removed volatility filter UI controls from config_flow.py
- Removed filter_periods_by_volatility() calls from coordinator.py
- Updated all 5 translations (de, en, nb, nl, sv)
**Period Calculation Logic:**
- Level filter now integrated into _build_periods() (applied during
interval qualification, not as post-filter)
- Gap tolerance implemented via _check_level_with_gap_tolerance()
- Short periods (<1.5h) use strict filtering (no gap tolerance)
- Relaxation now passes level_filter + gap_count directly to
PeriodConfig
- show_periods check skipped when relaxation enabled (relaxation
tries "any" as fallback)
**Documentation:**
- Complete rewrite of docs/user/period-calculation.md:
* Visual examples with timelines
* Step-by-step explanation of 4-step process
* Configuration scenarios (5 common use cases)
* Troubleshooting section with specific fixes
* Advanced topics (per-day independence, early stop, etc.)
- Updated README.md: "volatility" → "distance from average"
Impact: Periods now reliably appear on most days with meaningful
quality filters. Users get warned about expensive periods and notified
about cheap opportunities without manual tuning. Relaxation ensures
coverage while keeping filters as strict as possible.
Breaking change: Volatility filter removed (was never a critical
feature, often confused users). Existing configs continue to work
(removed keys are simply ignored).
* feat(periods): modularize period_utils and add statistical outlier filtering
Refactored monolithic period_utils.py (1800 lines) into focused modules
for better maintainability and added advanced outlier filtering with
smart impact tracking.
Modular structure:
- types.py: Type definitions and constants (89 lines)
- level_filtering.py: Level filtering with gap tolerance (121 lines)
- period_building.py: Period construction from intervals (238 lines)
- period_statistics.py: Statistics and summaries (318 lines)
- period_merging.py: Overlap resolution (382 lines)
- relaxation.py: Per-day relaxation strategy (547 lines)
- core.py: Main API orchestration (251 lines)
- outlier_filtering.py: Statistical spike detection (294 lines)
- __init__.py: Public API exports (62 lines)
New statistical outlier filtering:
- Linear regression for trend-based spike detection
- 2 standard deviation confidence intervals (95%)
- Symmetry checking to preserve legitimate price shifts
- Enhanced zigzag detection with relative volatility (catches clusters)
- Replaces simple average smoothing with trend-based predictions
Smart impact tracking:
- Tests if original price would have passed criteria
- Only counts smoothed intervals that actually changed period formation
- Tracks level gap tolerance usage separately
- Both attributes only appear when > 0 (clean UI)
New period attributes:
- period_interval_smoothed_count: Intervals kept via outlier smoothing
- period_interval_level_gap_count: Intervals kept via gap tolerance
Impact: Statistical outlier filtering prevents isolated price spikes from
breaking continuous periods while preserving data integrity. All statistics
use original prices. Smart tracking shows only meaningful interventions,
making it clear when tolerance mechanisms actually influenced results.
Backwards compatible: All public APIs re-exported from period_utils package.
* Update docs/user/period-calculation.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update custom_components/tibber_prices/const.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update custom_components/tibber_prices/coordinator.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update custom_components/tibber_prices/const.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* docs(periods): fix corrupted period-calculation.md and add outlier filtering documentation
Completely rewrote period-calculation.md after severe corruption (massive text
duplication throughout the file made it 2489 lines).
Changes:
- Fixed formatting: Removed all duplicate text and headers
- Reduced file size: 2594 lines down to 516 lines (clean, readable structure)
- Added section 5: "Statistical Outlier Filtering (NEW)" explaining:
- Linear regression-based spike detection (95% confidence intervals)
- Symmetry checking to preserve legitimate price shifts
- Enhanced zigzag detection with relative volatility
- Data integrity guarantees (original prices always used)
- New period attributes: period_interval_smoothed_count
- Added troubleshooting: "Price spikes breaking periods" section
- Added technical details: Algorithm constants and implementation notes
Impact: Users can now understand how outlier filtering prevents isolated
price spikes from breaking continuous periods. Documentation is readable
again with no duplicate content.
* fix(const): improve clarity in comments regarding period lengths for price alerts
* docs(periods): improve formatting and clarity in period-calculation.md
* Initial plan
* refactor: convert flexibility_pct to ratio once at function entry
Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>
* Update custom_components/tibber_prices/const.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update custom_components/tibber_prices/period_utils/period_building.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update custom_components/tibber_prices/period_utils/relaxation.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Julian Pawlowski <jpawlowski@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Implemented configurable gap tolerance (0-8 intervals) for best price and peak price
level filters to prevent periods from being split by occasional level deviations.
Key features:
- Gap tolerance only applies to periods ≥ MIN_INTERVALS_FOR_GAP_TOLERANCE (1.5h)
- Short periods (< 1.5h) use strict filtering (zero tolerance)
- Dynamic minimum distance between gaps: max(2, (interval_count // max_gap_count) // 2)
- 25% maximum cap on total gaps to prevent excessive outliers in long periods
- Intelligent period splitting at gap clusters (2+ consecutive non-qualifying intervals)
- Each sub-period independently validated with same gap tolerance rules
Technical implementation:
- Added CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT and CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT constants
- Added MIN_INTERVALS_FOR_GAP_TOLERANCE = 6 (1.5h minimum for gap tolerance)
- Implemented _split_at_gap_clusters() for period recovery
- Implemented _check_short_period_strict() for strict short-period filtering
- Implemented _check_level_filter_with_gaps() with fallback splitting logic
- Extracted _check_sequence_with_gap_tolerance() for reusable core validation
- Enhanced _check_level_filter() to use gap-tolerant validation
Configuration UI:
- Added NumberSelector (0-8, slider mode) for gap count in config flow
- Added translations for all 5 languages (de, en, nb, nl, sv)
- Default: 0 (strict filtering, backwards compatible)
Impact: Users can now configure how many occasional level deviations are acceptable
within qualifying price periods. This reduces period fragmentation while maintaining
meaningful price-based filtering. Long periods are protected by the 25% cap, and
gap clusters trigger intelligent splitting to recover usable sub-periods.
Implemented multi-phase filter relaxation system to ensure minimum number
of best-price and peak-price periods are found, even on days with unusual
price patterns.
New configuration options per period type (best/peak):
- enable_min_periods_{best|peak}: Toggle feature on/off
- min_periods_{best|peak}: Target number of periods (default: 2)
- relaxation_step_{best|peak}: Step size for threshold increase (default: 25%)
Relaxation phases (applied sequentially until target reached):
1. Flex threshold increase (up to 4 steps, e.g., 15% → 18.75% → 22.5% → ...)
2. Volatility filter bypass + continued flex increase
3. All filters off + continued flex increase
Changes to period calculation:
- New calculate_periods_with_relaxation() wrapper function
- filter_periods_by_volatility() now applies post-calculation filtering
- _resolve_period_overlaps() merges baseline + relaxed periods intelligently
- Relaxed periods marked with relaxation_level, relaxation_threshold_* attributes
- Overlap detection prevents double-counting same intervals
Binary sensor attribute ordering improvements:
- Added helper methods for consistent attribute priority
- Relaxation info grouped in priority 6 (after detail attributes)
- Only shown when period was actually relaxed (relaxation_active=true)
Translation updates:
- Added UI labels + descriptions for 6 new config options (all 5 languages)
- Explained relaxation concept with examples in data_description fields
- Clarified volatility filter now applies per-period, not per-day
Impact: Users can configure integration to guarantee minimum number of
periods per day. System automatically relaxes filters when needed while
preserving baseline periods found with strict filters. Particularly useful
for automation reliability on days with flat pricing or unusual patterns.
Fixes edge case where no periods were found despite prices varying enough
for meaningful optimization decisions.