Commit graph

165 commits

Author SHA1 Message Date
Julian Pawlowski
ac24f6a8cb refactor(services): split monolithic services.py into package
Split services.py (1,097 lines) into modular package (6 files, ~200-600 lines each):

Structure:
- services/__init__.py: Service registration (70 lines)
- services/helpers.py: Entry validation (55 lines)
- services/formatters.py: Data transformation (380 lines)
- services/chartdata.py: Chart data export handler (600 lines)
- services/apexcharts.py: ApexCharts YAML generator (240 lines)
- services/refresh_user_data.py: User data refresh (110 lines)

Benefits:
- Clear separation of concerns (helpers, formatters, handlers)
- Each service isolated and independently testable
- Consistent handler naming (handle_* pattern)
- Better code reuse through formatters module

All services working identically (get_chartdata, get_apexcharts_yaml,
refresh_user_data). Updated __init__.py to import from services package.

Impact: Improved maintainability, reduced max file size from 1,097
to 600 lines. Architecture quality improved from 7.5/10 to ~8.5/10.
2025-11-18 20:07:05 +00:00
Julian Pawlowski
d52eb6b788 refactor(utils): create utils package and consolidate constants
Reorganized utility modules into structured package:
- average_utils.py → utils/average.py
- price_utils.py → utils/price.py
- Created utils/__init__.py with clean exports

Moved MINUTES_PER_INTERVAL to const.py (centralized constant
management), with re-exports in utils modules for backward
compatibility during migration.

Added comprehensive package docstring explaining scope:
- Pure data transformation functions (stateless)
- No HA entity/coordinator dependencies
- Clear separation from entity_utils/ (entity-specific logic)

Impact: Cleaner module structure, easier navigation. Follows
file organization policy from AGENTS.md (keep root clean).
2025-11-18 20:06:46 +00:00
Julian Pawlowski
c316d5deef refactor: resolve circular imports and enhance documentation
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.
2025-11-18 17:32:36 +00:00
Julian Pawlowski
21b444c16d chore(release): bump version to 0.10.0 2025-11-17 04:11:45 +00:00
Julian Pawlowski
ef983d0a99 feat(sensor): migrate chart_data_export from binary_sensor to sensor platform
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.
2025-11-17 04:11:10 +00:00
Julian Pawlowski
e17f59c283 chore(release): bump version to 0.11.0 2025-11-17 03:14:19 +00:00
Julian Pawlowski
38ce1c4c50 feat(chart_export): add Chart Data Export diagnostic sensor
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.
2025-11-17 03:14:02 +00:00
Julian Pawlowski
0bf810f0d5 chore(release): bump version to 0.10.0 2025-11-16 23:52:57 +00:00
Julian Pawlowski
fb70f29ac9 feat(services): rewrite ApexCharts service for modern workflow
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.
2025-11-16 23:52:36 +00:00
Julian Pawlowski
3a9ba55dd3 feat(sensors): improve price trend sensors with temporal context
Enhanced current_price_trend and next_price_trend_change sensors with
consistent temporal information and fixed trend calculation logic.

Changes:
- Fixed trend calculation order: Calculate final trend state (momentum +
  future outlook) BEFORE scanning for next change, ensuring consistency
  between current_price_trend state and next_price_trend_change from_direction
- Added TIME_SENSITIVE_ENTITY_KEYS registration for both trend sensors
  to enable automatic 15-minute boundary updates (Timer #2)
- Removed redundant timestamp field from _trend_change_attributes (was
  duplicate of sensor state)
- Added timestamp attribute (current interval) to both sensors as first
  attribute for temporal reference
- Implemented _find_trend_start_time() to scan backward and determine
  when current trend began
- Added trend_duration_minutes to current_price_trend showing how long
  current trend has been active
- Added from_direction to current_price_trend showing previous trend
  state (enables detection of valleys/plateaus)
- Added minutes_until_change to next_price_trend_change showing time
  until trend changes
- Removed redundant attributes: valid_until, duration_hours,
  duration_minutes from current_price_trend (can be derived from
  next_price_trend_change sensor)
- Removed redundant next_direction from current_price_trend (available
  in next_price_trend_change sensor)

current_price_trend attributes:
- timestamp: Current interval (calculation basis)
- from_direction: Previous trend state (e.g., "stable" → "falling" = starting decline)
- trend_duration_minutes: How long current trend has been active

next_price_trend_change attributes:
- timestamp: Current interval (calculation basis)
- from_direction: Current trend state (should match current_price_trend state)
- direction: Target trend state
- minutes_until_change: Time until change occurs
- current_price_now, price_at_change, avg_after_change, trend_diff_%

Impact: Users can now detect important transitions (valleys: falling→stable,
plateaus: rising→stable) and understand trend context. Both sensors update
automatically every 15 minutes with consistent information.
2025-11-16 17:09:16 +00:00
Julian Pawlowski
76dc488bb5 feat(sensors): add momentum-based trend detection with two new sensors
Added intelligent price trend analysis combining historical momentum
(weighted 1h lookback) with future outlook for more accurate trend
recognition. Introduced two complementary sensors for comprehensive
trend monitoring.

New sensors:
- current_price_trend: Shows active trend direction with duration
- next_price_trend_change: Predicts when trend will reverse

Momentum analysis (historical perspective):
- Weighted 1h lookback (4 × 15-min intervals)
- Linear weight progression [0.5, 0.75, 1.0, 1.25]
- ±3% threshold for momentum classification
- Recognizes ongoing trends earlier than future-only analysis

Two-phase trend calculation:
- Phase 1: Calculate momentum from weighted trailing average
- Phase 2: Validate with volatility-adaptive future comparison
- Combines both for final trend determination (rising/falling/stable)
- Centralized in _calculate_trend_info() with 60s cache

Volatility-adaptive thresholds:
- Existing trend sensors (1h-12h) now use adaptive thresholds
- calculate_price_trend() adjusted by market volatility:
  * LOW volatility (<15% CV): factor 0.6 → more sensitive (e.g., 3%→1.8%)
  * MODERATE volatility (15-30%): factor 1.0 → baseline (3%)
  * HIGH volatility (≥30%): factor 1.4 → less sensitive (e.g., 3%→4.2%)
- Uses same coefficient of variation as volatility sensors
- Ensures mathematical consistency across integration

Default threshold reduction:
- Rising/falling thresholds: 5% → 3% (more responsive)
- Momentum-based detection enables lower thresholds without noise
- Adaptive adjustment compensates during high volatility

Architectural improvements:
- Centralized calculation: Single source of truth for both sensors
- Eliminates Henne-Ei problem (duplicate calculations)
- 60-second cache per coordinator update
- Shared helper methods: _calculate_momentum(), _combine_momentum_with_future()

Translation updates (all 5 languages):
- Documented momentum feature in custom_translations (de/en/nb/nl/sv)
- Explained "recognizes ongoing trends earlier" advantage
- Added sensor names and state options to standard translations
- Updated volatility threshold descriptions (clarify usage by trend sensors)

Files changed:
- custom_components/tibber_prices/sensor/core.py (930 lines added)
  * New: _calculate_momentum(), _combine_momentum_with_future()
  * New: _calculate_trend_info() (centralized with cache)
  * New: _get_current_trend_value(), _get_next_trend_change_value()
  * Modified: _get_price_trend_value() (volatility-adaptive thresholds)
- custom_components/tibber_prices/sensor/definitions.py
  * Added: current_price_trend (ENUM sensor)
  * Added: next_price_trend_change (TIMESTAMP sensor)
- custom_components/tibber_prices/sensor/attributes.py
  * New: _add_cached_trend_attributes() helper
  * Support for current_trend_attributes, trend_change_attributes
- custom_components/tibber_prices/price_utils.py (178 lines added)
  * New: _calculate_lookahead_volatility_factor()
  * Modified: calculate_price_trend() with volatility adjustment
  * Added: VOLATILITY_FACTOR_* constants (0.6/1.0/1.4)
- custom_components/tibber_prices/entity_utils/icons.py
  * Added: Dynamic icon handling for next_price_trend_change
- custom_components/tibber_prices/const.py
  * Changed: DEFAULT_PRICE_TREND_THRESHOLD_RISING/FALLING (5→3%)
- custom_components/tibber_prices/translations/*.json (5 files)
  * Added: Sensor names, state options, descriptions
- custom_components/tibber_prices/custom_translations/*.json (5 files)
  * Added: Long descriptions with momentum feature explanation

Impact: Users get significantly more accurate trend detection that
understands they're ALREADY in a trend, not just predicting future
changes. Momentum-based approach recognizes ongoing movements 15-60
minutes earlier. Adaptive thresholds prevent false signals during
volatile periods. Two complementary sensors enable both status display
(current trend) and event-based automation (when will it change).
Perfect for use cases like "charge EV when next trend change shows
falling prices" or dashboard badges showing "Rising for 2.5h".
2025-11-16 12:49:43 +00:00
Julian Pawlowski
6389249020 refactor(translations): update terminology for price period settings in German translations 2025-11-16 10:29:30 +00:00
Julian Pawlowski
4508e69ffd chore(release): bump version to 0.9.0 2025-11-16 00:12:20 +00:00
Julian Pawlowski
63442dae1d feat(api): add multi-home support and diagnostic sensors
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.
2025-11-16 00:11:56 +00:00
Julian Pawlowski
4e64cf7598 refactor(sensors): optimize default entity activation for better UX
Adjusted entity_registry_enabled_default flags to reduce initial entity
count while keeping most useful sensors active by default.

Changes:
- Disabled rating sensors (current/next interval, hourly, daily) - Level
  sensors provide better granularity (5 levels vs 3) for automations
- Disabled leading 24h window sensors (avg/min/max) - Advanced use case,
  overlaps with tomorrow statistics
- Disabled additional volatility sensors (tomorrow, next_24h,
  today_tomorrow) - Today's volatility sufficient for typical use cases

Rationale:
- Price level sensors (5 states: very_cheap → very_expensive) are more
  commonly used than rating sensors (3 states: low → high)
- Leading 24h windows overlap with tomorrow daily statistics which have
  clearer boundaries
- Single volatility indicator (today) covers most automation needs

Impact: Reduces default active entities from ~65 to ~50 while maintaining
all essential functionality. Advanced users can enable additional sensors
as needed. Improves initial setup experience by focusing on most relevant
sensors.
2025-11-15 21:47:09 +00:00
Julian Pawlowski
dae0b43971 refactor(translations): enhance clarity of price labels in German, Norwegian, Dutch, and Swedish 2025-11-15 21:35:44 +00:00
Julian Pawlowski
c3c98a4b63 refactor(translations): simplify price start time labels in multiple languages 2025-11-15 21:23:26 +00:00
Julian Pawlowski
a2c1edb876 refactor(translations): improve clarity of price labels in multiple languages 2025-11-15 21:18:19 +00:00
Julian Pawlowski
ac2ce5d9cf refactor(translations): update price labels for clarity and consistency across multiple languages 2025-11-15 21:01:26 +00:00
Julian Pawlowski
7250667434 chore(release): bump version to 0.8.0 2025-11-15 20:39:10 +00:00
Julian Pawlowski
d06ae63075 feat(sensors): add Energy Dashboard price sensor and period duration sensors
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.
2025-11-15 20:38:21 +00:00
Julian Pawlowski
9c00d985c8 chore(release): bump version to 0.7.0 2025-11-15 18:22:08 +00:00
Julian Pawlowski
a75b8ffdb3 perf(coordinator): optimize API calls and data processing
Implemented comprehensive performance optimizations to eliminate asyncio
warnings and reduce unnecessary API calls:

Performance Improvements:
- Two-tier caching: raw API data + transformed data
- Transformation caching prevents re-processing on every coordinator update
- Only retransform when config changes, new data arrives, or midnight turnover
- Reduced coordinator update time from ~120ms to <10ms (cache hits)

API Call Optimization:
- Removed periodic 6-hour "safety" API calls (trust cache validation)
- Random delay (0-30s) for tomorrow data checks (prevents thundering herd)
- API calls only when truly needed (no cache, invalid cache, tomorrow missing)
- Expected reduction: ~50% fewer API calls (from ~4-5/day to ~2/day)

Enhanced Logging:
- Clear [Timer #1/2/3] prefixes for multi-timer system
- Distinguish "Fetching from API" vs "Using cache"
- Separate "Transforming data" vs "Using cached transformed data"
- Hierarchical logging shows which timer triggered which action

Documentation:
- Comprehensive TIMER SYSTEM block explaining three independent timers
- Enhanced docstrings for timer handlers (synchronous callbacks vs async def)
- Clarified why @callback handlers don't use async/await (no I/O operations)
- Updated UPDATE_INTERVAL documentation (removed periodic check reference)

Technical Details:
- _get_current_transformation_config(): Captures all config affecting transformation
- _should_retransform_data(): Intelligent cache invalidation logic
- _should_update_price_data(): Returns bool|"tomorrow_check" to signal delay needed
- Timer handlers use @callback decorator (synchronous, no I/O, fast execution)

Impact: Eliminates asyncio warnings (tasks >0.1s), reduces API load by 50%,
maintains data accuracy through robust cache validation. No user-visible changes.
2025-11-15 18:21:50 +00:00
Julian Pawlowski
503075c443 refactor(config_flow): restructure package to satisfy hassfest validation
Home Assistant's hassfest validation requires config flows to be defined
in a file named config_flow.py (not a package directory).

Changes:
- Renamed custom_components/tibber_prices/config_flow/ → config_flow_handlers/
- Created config_flow.py as bridge file re-exporting from config_flow_handlers/
- Updated all import paths across 5 files (user_flow, options_flow, subentry_flow, etc.)
- Added ./scripts/hassfest for local validation (JSON/Python syntax, required files)
- Added ./scripts/clean with three modes (--minimal, normal, --deep)
- Refactored develop/lint/lint-check to use centralized cleanup (DRY principle)
- Updated documentation in AGENTS.md and docs/development/

Technical details:
- Bridge file uses __all__ exports to maintain clean public API
- hassfest script uses ast.parse() for syntax validation (no disk artifacts)
- clean --minimal removes .egg-info only (silent, for automated scripts)
- Dual pip/uv pip compatibility for package uninstallation

Impact: Integration now passes hassfest validation. Local validation available
via ./scripts/hassfest before pushing to GitHub. Cleanup logic centralized and
DRY across all development scripts.
2025-11-15 17:40:53 +00:00
Julian Pawlowski
decca432df feat(sensors): add timing sensors for best_price and peak_price periods
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.
2025-11-15 17:12:55 +00:00
Julian Pawlowski
22165d038d feat(sensors): add timestamp attributes and enhance icon system
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
2025-11-15 15:31:43 +00:00
Julian Pawlowski
b32679ba75 feat(translations): add price level and rating states for multiple languages 2025-11-15 14:18:41 +00:00
Julian Pawlowski
e18d653233 feat(sensors): add daily aggregated price level and rating sensors
Added 6 new sensors for yesterday/today/tomorrow aggregated price
levels and ratings, following the same calculation logic as existing
current/next interval sensors.

New sensors:
- yesterday_price_level, today_price_level, tomorrow_price_level
- yesterday_price_rating, today_price_rating, tomorrow_price_rating

Implementation details:
- Added DAILY_LEVEL_SENSORS and DAILY_RATING_SENSORS in sensor/definitions.py
- Implemented _get_daily_aggregated_value() in sensor/core.py using
  existing aggregate_level_data() and aggregate_rating_data() helpers
- Extended icon support in entity_utils/icons.py for dynamic icons
- Added icon_color attributes in sensor/attributes.py with helper
  functions _get_day_key_from_sensor_key() and _add_fallback_timestamp()
- Complete translations in all 5 languages (de, en, nb, nl, sv):
  * Standard translations: sensor names
  * Custom translations: description, long_description, usage_tips

Impact: Users can now see aggregated daily price levels and ratings
for yesterday, today, and tomorrow at a glance, making it easier to
compare overall price situations across days and plan energy consumption
accordingly. Sensors use same aggregation logic as hourly sensors for
consistency.
2025-11-15 13:31:44 +00:00
Julian Pawlowski
1e51b0485b refactor(const): update icon for 'off_no_future' state in BINARY_SENSOR_ICON_MAPPING 2025-11-15 13:08:54 +00:00
Julian Pawlowski
e35970dfcb refactor(const): update icon for 'off_no_future' state in BINARY_SENSOR_ICON_MAPPING 2025-11-15 13:07:04 +00:00
Julian Pawlowski
d90266e1ad refactor(config_flow): split monolithic file into modular package structure
Refactored config_flow.py (995 lines) into focused modules within config_flow/
package to improve maintainability and code organization.

Changes:
- Created config_flow/ package with 6 specialized modules (1,260 lines total)
- Extracted validators to validators.py (95 lines) - pure, testable functions
- Extracted schemas to schemas.py (577 lines) - centralized vol.Schema definitions
- Split flow handlers into separate files:
  * user_flow.py (274 lines) - Main config flow (setup + reauth)
  * subentry_flow.py (124 lines) - Subentry flow (add homes)
  * options_flow.py (160 lines) - Options flow (6-step configuration wizard)
- Package exports via __init__.py (50 lines) for backward compatibility
- Deleted config_flow_legacy.py (no longer needed)

Technical improvements:
- Used Mapping[str, Any] for config_entry.options compatibility
- Proper TYPE_CHECKING imports for circular dependency management
- All 10 inline vol.Schema definitions replaced with reusable functions
- Validators are pure functions (no side effects, easily testable)
- Clear separation of concerns (validation, schemas, flows)

Documentation:
- Updated AGENTS.md with new package structure
- Updated config flow patterns and examples
- Added "Add a new config flow step" guide to Common Tasks
- Marked refactoring plan as COMPLETED with lessons learned

Verification:
- All linting checks pass (./scripts/lint-check)
- All flow handlers import successfully
- Home Assistant loads integration without errors
- All flow types functional (user, subentry, options, reauth)
- No user-facing changes (backward compatible)

Impact: Improves code maintainability by organizing 995 lines into 6 focused
modules (avg 210 lines/module). Enables easier testing, future modifications,
and onboarding of new contributors.
2025-11-15 13:03:13 +00:00
Julian Pawlowski
efda22f7ad refactor(binary_sensor): split into package matching sensor/ structure
Split binary_sensor.py (645 lines) into binary_sensor/ package with
4 modules following the established sensor/ pattern for consistency
and maintainability.

Package structure:
- binary_sensor/__init__.py (32 lines): Platform setup
- binary_sensor/definitions.py (46 lines): ENTITY_DESCRIPTIONS, constants
- binary_sensor/attributes.py (443 lines): Attribute builder functions
- binary_sensor/core.py (282 lines): TibberPricesBinarySensor class

Changes:
- Created binary_sensor/ package with __init__.py importing from .core
- Extracted ENTITY_DESCRIPTIONS and constants to definitions.py
- Moved 13 attribute builders to attributes.py (get_price_intervals_attributes,
  build_async/sync_extra_state_attributes, add_* helpers)
- Moved TibberPricesBinarySensor class to core.py with state logic and
  icon handling
- Used keyword-only parameters to satisfy Ruff PLR0913 (too many args)
- Applied absolute imports (custom_components.tibber_prices.*) in modules

All 4 binary sensors tested and working:
- peak_price_period
- best_price_period
- connection
- tomorrow_data_available

Documentation updated:
- AGENTS.md: Architecture Overview, Component Structure, Common Tasks
- binary-sensor-refactoring-plan.md: Marked  COMPLETED with summary

Impact: Symmetric platform structure (sensor/ ↔ binary_sensor/). Easier
to add new binary sensors following documented pattern. No user-visible
changes.
2025-11-15 12:35:02 +00:00
Copilot
78498a9aec
Fix AttributeError when homes lack active subscriptions (#28)
* Initial plan

* Fix AttributeError for homes without active subscription

Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jpawlowski <75446+jpawlowski@users.noreply.github.com>
2025-11-15 13:08:15 +01:00
Julian Pawlowski
fa40c00f67 refactor(sensors): Transform sensor platform into package 2025-11-15 11:46:54 +00:00
Julian Pawlowski
d2d07d5e16 refactor(sensors): Refactor price sensor calculations and remove unused methods
- Removed the `calculate_current_rolling_5interval_avg` and `calculate_next_hour_rolling_5interval_avg` functions from `average_utils.py` to streamline the codebase.
- Introduced unified methods for retrieving interval values and rolling hour calculations in `sensor.py`, enhancing code reusability and readability.
- Organized sensor definitions into categories based on calculation methods for better maintainability.
- Updated handler methods to utilize the new unified methods, ensuring consistent data retrieval across different sensor types.
- Improved documentation and comments throughout the code to clarify the purpose and functionality of various methods.
2025-11-15 09:29:33 +00:00
Julian Pawlowski
7737dccd49 refactor(sensors): rename current price sensors for clarity
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.
2025-11-15 08:30:25 +00:00
Julian Pawlowski
c4f36d04de feat(icons): add dynamic icons and colors for all sensor types
Implemented comprehensive dynamic icon and color system across all sensor types:

Price Sensors (5 sensors):
- Current/hour prices: Dynamic cash-family icons based on price level
  (cash-multiple/plus/cash/minus/remove)
- Next/previous: Static contextual icons (cash-fast, cash-refund, clock-fast)
- All have icon_color attribute for card-mod styling

Price Level Sensors (5 sensors):
- Dynamic gauge-family icons: gauge-empty → gauge-low → gauge → gauge-full → alert
- icon_color attribute with CSS variables (green/gray/orange/red)

Price Rating Sensors (5 sensors):
- Dynamic thumb-family icons: thumb-up → thumbs-up-down → thumb-down
- icon_color attribute for LOW/NORMAL/HIGH ratings

Volatility Sensors (4 sensors):
- Dynamic chart-family icons: chart-line-variant → chart-timeline-variant →
  chart-bar → chart-scatter-plot
- icon_color attribute for LOW/MODERATE/HIGH/VERY_HIGH levels

Trend Sensors (8 sensors):
- Dynamic trend icons: trending-up/down/neutral based on price movement
- icon_color attribute (red=rising, green=falling, gray=stable)

Binary Sensors (2 sensors):
- Best Price Period: piggy-bank (ON) / timer-sand or timer-sand-complete (OFF)
- Peak Price Period: alert-circle (ON) / shield-check or shield-check-outline (OFF)
- 6-hour lookahead window for intelligent OFF state icons
- icon_color attribute for all states

Technical implementation:
- PRICE_LEVEL_CASH_ICON_MAPPING in const.py for price sensor icons
- PRICE_SENSOR_ICON_MAPPING removed (static icons now in entity descriptions)
- Centralized icon logic in sensor.py icon property
- All color mappings use CSS variables for theme compatibility
- Binary sensors detect future periods within 6-hour window

Impact: Users now have visual indicators for all price-related states without
requiring card-mod. Optional card-mod styling available via icon_color attribute
for advanced customization. Icons update dynamically as price levels, ratings,
volatility, and trends change throughout the day.
2025-11-14 11:31:25 +00:00
Julian Pawlowski
fe5af68f8e feat(sensor): add icon color determination based on price trend state 2025-11-14 09:52:28 +00:00
Julian Pawlowski
6521aa7bdd feat(sensor): add dynamic icon support for price trend sensors 2025-11-14 09:39:37 +00:00
Julian Pawlowski
3a9234ffbf chore(release): bump version to 0.6.1 2025-11-14 01:14:05 +00:00
Julian Pawlowski
07517660e3 refactor(volatility): migrate to coefficient of variation calculation
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.
2025-11-14 01:12:47 +00:00
Julian Pawlowski
6dc49becb1 chore(release): bump version to 0.6.0 2025-11-14 00:18:32 +00:00
Julian Pawlowski
67270d8fe2 chore(release): bump version to 0.5.1 2025-11-14 00:07:50 +00:00
Julian Pawlowski
5a5c8ca3cc feat(relaxation): make tail handling smarter and attempts configurable
- 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.
2025-11-14 00:07:12 +00:00
Julian Pawlowski
d3c02568ee fix(build_periods): improve comment clarity for smoothing impact on interval qualification 2025-11-13 23:00:26 +00:00
Julian Pawlowski
a39eb66f49 fix(const): clarify comments on peak price flexibility threshold 2025-11-13 23:00:18 +00:00
Julian Pawlowski
2e7dd64db0 chore(release): bump version to 0.5.0 2025-11-13 22:53:48 +00:00
Julian Pawlowski
383b495545
Feature/adaptive defaults (#22)
* 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>
2025-11-13 23:51:29 +01:00
Julian Pawlowski
3f43bb4bc0 chore(release): bump version to 0.4.1 2025-11-11 21:22:36 +00:00
Julian Pawlowski
aa58c6718f refactor(period_utils): implement per-day relaxation with 4×4 matrix strategy
Restructured relaxation mechanism to process each day independently instead
of globally, enabling different days to relax at different levels.

Key changes:
- Added hierarchical logging with INDENT_L0-L5 constants
- Replaced global relaxation loop with per-day relaxation (_relax_single_day)
- Implemented 4×4 matrix strategy (4 flex levels × 4 filter combinations)
- Enhanced _resolve_period_overlaps with replacement and extension logic
- Added helper functions: _group_periods_by_day, _group_prices_by_day,
  _check_min_periods_per_day

Relaxation strategy:
- Each flex level tries 4 filter combinations before increasing flex
- Early exit after EACH successful combination (minimal relaxation)
- Extensions preserve baseline metadata, replacements use relaxed metadata
- Only standalone periods count toward min_periods requirement

Impact: Users get more accurate period detection per day. Days with clear
cheap/expensive patterns use strict filters while difficult days relax as
needed. Reduces over-relaxation - finds 'good enough' solutions faster.
2025-11-11 21:21:56 +00:00