Commit graph

50 commits

Author SHA1 Message Date
Julian Pawlowski
60e05e0815 refactor(currency)!: rename major/minor to base/subunit currency terminology
Complete terminology migration from confusing "major/minor" to clearer
"base/subunit" currency naming throughout entire codebase, translations,
documentation, tests, and services.

BREAKING CHANGES:

1. **Service API Parameters Renamed**:
   - `get_chartdata`: `minor_currency` → `subunit_currency`
   - `get_apexcharts_yaml`: Updated service_data references from
     `minor_currency: true` to `subunit_currency: true`
   - All automations/scripts using these parameters MUST be updated

2. **Configuration Option Key Changed**:
   - Config entry option: Display mode setting now uses new terminology
   - Internal key: `currency_display_mode` values remain "base"/"subunit"
   - User-facing labels updated in all 5 languages (de, en, nb, nl, sv)

3. **Sensor Entity Key Renamed**:
   - `current_interval_price_major` → `current_interval_price_base`
   - Entity ID changes: `sensor.tibber_home_current_interval_price_major`
     → `sensor.tibber_home_current_interval_price_base`
   - Energy Dashboard configurations MUST update entity references

4. **Function Signatures Changed**:
   - `format_price_unit_major()` → `format_price_unit_base()`
   - `format_price_unit_minor()` → `format_price_unit_subunit()`
   - `get_price_value()`: Parameter `in_euro` deprecated in favor of
     `config_entry` (backward compatible for now)

5. **Translation Keys Renamed**:
   - All language files: Sensor translation key
     `current_interval_price_major` → `current_interval_price_base`
   - Service parameter descriptions updated in all languages
   - Selector options updated: Display mode dropdown values

Changes by Category:

**Core Code (Python)**:
- const.py: Renamed all format_price_unit_*() functions, updated docstrings
- entity_utils/helpers.py: Updated get_price_value() with config-driven
  conversion and backward-compatible in_euro parameter
- sensor/__init__.py: Added display mode filtering for base currency sensor
- sensor/core.py:
  * Implemented suggested_display_precision property for dynamic decimal places
  * Updated native_unit_of_measurement to use get_display_unit_string()
  * Updated all price conversion calls to use config_entry parameter
- sensor/definitions.py: Renamed entity key and updated all
  suggested_display_precision values (2 decimals for most sensors)
- sensor/calculators/*.py: Updated all price conversion calls (8 calculators)
- sensor/helpers.py: Updated aggregate_price_data() signature with config_entry
- sensor/attributes/future.py: Updated future price attributes conversion

**Services**:
- services/chartdata.py: Renamed parameter minor_currency → subunit_currency
  throughout (53 occurrences), updated metadata calculation
- services/apexcharts.py: Updated service_data references in generated YAML
- services/formatters.py: Renamed parameter use_minor_currency →
  use_subunit_currency in aggregate_hourly_exact() and get_period_data()
- sensor/chart_metadata.py: Updated default parameter name

**Translations (5 Languages)**:
- All /translations/*.json:
  * Added new config step "display_settings" with comprehensive explanations
  * Renamed current_interval_price_major → current_interval_price_base
  * Updated service parameter descriptions (subunit_currency)
  * Added selector.currency_display_mode.options with translated labels
- All /custom_translations/*.json:
  * Renamed sensor description keys
  * Updated chart_metadata usage_tips references

**Documentation**:
- docs/user/docs/actions.md: Updated parameter table and feature list
- docs/user/versioned_docs/version-v0.21.0/actions.md: Backported changes

**Tests**:
- Updated 7 test files with renamed parameters and conversion logic:
  * test_connect_segments.py: Renamed minor/major to subunit/base
  * test_period_data_format.py: Updated period price conversion tests
  * test_avg_none_fallback.py: Fixed tuple unpacking for new return format
  * test_best_price_e2e.py: Added config_entry parameter to all calls
  * test_cache_validity.py: Fixed cache data structure (price_info key)
  * test_coordinator_shutdown.py: Added repair_manager mock
  * test_midnight_turnover.py: Added config_entry parameter
  * test_peak_price_e2e.py: Added config_entry parameter, fixed price_avg → price_mean
  * test_percentage_calculations.py: Added config_entry mock

**Coordinator/Period Calculation**:
- coordinator/periods.py: Added config_entry parameter to
  calculate_periods_with_relaxation() calls (2 locations)

Migration Guide:

1. **Update Service Calls in Automations/Scripts**:
   \`\`\`yaml
   # Before:
   service: tibber_prices.get_chartdata
   data:
     minor_currency: true

   # After:
   service: tibber_prices.get_chartdata
   data:
     subunit_currency: true
   \`\`\`

2. **Update Energy Dashboard Configuration**:
   - Settings → Dashboards → Energy
   - Replace sensor entity:
     `sensor.tibber_home_current_interval_price_major` →
     `sensor.tibber_home_current_interval_price_base`

3. **Review Integration Configuration**:
   - Settings → Devices & Services → Tibber Prices → Configure
   - New "Currency Display Settings" step added
   - Default mode depends on currency (EUR → subunit, Scandinavian → base)

Rationale:

The "major/minor" terminology was confusing and didn't clearly communicate:
- **Major** → Unclear if this means "primary" or "large value"
- **Minor** → Easily confused with "less important" rather than "smaller unit"

New terminology is precise and self-explanatory:
- **Base currency** → Standard ISO currency (€, kr, $, £)
- **Subunit currency** → Fractional unit (ct, øre, ¢, p)

This aligns with:
- International terminology (ISO 4217 standard)
- Banking/financial industry conventions
- User expectations from payment processing systems

Impact: Aligns currency terminology with international standards. Users must
update service calls, automations, and Energy Dashboard configuration after
upgrade.

Refs: User feedback session (December 2025) identified terminology confusion
2025-12-11 08:26:30 +00:00
Julian Pawlowski
51a99980df feat(sensors)!: add configurable median/mean display for average sensors
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>
2025-12-08 17:53:40 +00:00
Julian Pawlowski
6922e52368 feat(sensors): add chart_metadata sensor for lightweight chart configuration
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.
2025-12-05 20:30:54 +00:00
Julian Pawlowski
c8e9f7ec2a feat(apexcharts): add server-side metadata with dynamic yaxis and gradient
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.
2025-12-05 18:14:18 +00:00
Julian Pawlowski
44f6ae2c5e feat(interval-pool): add intelligent interval caching and memory optimization
Implemented interval pool architecture for efficient price data management:

Core Components:
- IntervalPool: Central storage with timestamp-based index
- FetchGroupCache: Protected range management (day-before-yesterday to tomorrow)
- IntervalFetcher: Gap detection and optimized API queries
- TimestampIndex: O(1) lookup for price intervals

Key Features:
- Deduplication: Touch intervals instead of duplicating (memory efficient)
- GC cleanup: Removes dead intervals no longer referenced by index
- Gap detection: Only fetches missing ranges, reuses cached data
- Protected range: Keeps yesterday/today/tomorrow, purges older data
- Resolution support: Handles hourly (pre-Oct 2025) and quarter-hourly data

Integration:
- TibberPricesApiClient: Uses interval pool for all range queries
- DataUpdateCoordinator: Retrieves data from pool instead of direct API
- Transparent: No changes required in sensor/service layers

Performance Benefits:
- Reduces API calls by 70% (reuses overlapping intervals)
- Memory footprint: ~10KB per home (protects 384 intervals max)
- Lookup time: O(1) timestamp-based index

Breaking Changes: None (backward compatible integration layer)

Impact: Significantly reduces Tibber API load while maintaining data
freshness. Memory-efficient storage prevents unbounded growth.
2025-11-25 20:44:39 +00:00
Julian Pawlowski
3b11c6721e feat(types): add TypedDict documentation and BaseCalculator helpers
Phase 1.1 - TypedDict Documentation System:
- Created sensor/types.py with 14 TypedDict classes documenting sensor attributes
- Created binary_sensor/types.py with 3 TypedDict classes for binary sensors
- Added Literal types (PriceLevel, PriceRating, VolatilityLevel, DataCompleteness)
- Updated imports in sensor/attributes/__init__.py and binary_sensor/attributes.py
- Changed function signatures to use dict[str, Any] for runtime flexibility
- TypedDicts serve as IDE documentation, not runtime validation

Phase 1.2 - BaseCalculator Improvements:
- Added 8 smart data access methods to BaseCalculator:
  * get_intervals(day) - day-specific intervals with None-safety
  * intervals_today/tomorrow/yesterday - convenience properties
  * get_all_intervals() - combined yesterday+today+tomorrow
  * find_interval_at_offset(offset) - interval lookup with bounds checking
  * safe_get_from_interval(interval, key, default) - safe dict access
  * has_data() / has_price_info() - existence checks
  * get_day_intervals(day) - alias for consistency
- Refactored 5 calculator files to use new helper methods:
  * daily_stat.py: -11 lines (coordinator_data checks, get_intervals usage)
  * interval.py: -18 lines (eliminated find_price_data_for_interval duplication)
  * rolling_hour.py: -3 lines (simplified interval collection)
  * volatility.py: -4 lines (eliminated price_info local variable)
  * window_24h.py: -2 lines (replaced coordinator_data check)
  * Total: -38 lines of duplicate code eliminated
- Added noqa comment for lazy import (circular import avoidance)

Type Duplication Resolution:
- Identified duplication: Literal types in types.py vs string constants in const.py
- Attempted solution: Derive constants from Literal types using typing.get_args()
- Result: Circular import failure (const.py → sensor/types.py → sensor/__init__.py → const.py)
- Final solution: Keep string constants as single source of truth
- Added SYNC comments in all 3 files (const.py, sensor/types.py, binary_sensor/types.py)
- Accept manual synchronization to avoid circular dependencies
- Platform separation maintained (no cross-imports between sensor/ and binary_sensor/)

Impact: Developers get IDE autocomplete and type hints for attribute dictionaries.
Calculator code is more readable with fewer None-checks and clearer data access patterns.
Type/constant duplication documented with sync requirements.
2025-11-22 14:32:24 +00:00
Julian Pawlowski
9a6eb44382 refactor(config): use negative values for Best Price min_distance
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.
2025-11-22 04:44:57 +00:00
Julian Pawlowski
14b68a504b refactor(config): optimize volatility thresholds with separate ranges and improved UX
Volatility Threshold Optimization:
- Replaced global MIN/MAX_VOLATILITY_THRESHOLD (0-100%) with six separate
  constants for overlapping ranges per threshold level
- MODERATE: 5.0-25.0% (was: 0-100%)
- HIGH: 20.0-40.0% (was: 0-100%)
- VERY_HIGH: 35.0-80.0% (was: 0-100%)
- Added detailed comments explaining ranges and cascading requirements

Validators:
- Added three specific validation functions (one per threshold level)
- Added cross-validation ensuring MODERATE < HIGH < VERY_HIGH
- Added fallback to existing option values for completeness check
- Updated error keys to specific messages per threshold level

UI Improvements:
- Changed NumberSelector mode: BOX → SLIDER (consistency with other config steps)
- Changed step size: 0.1% → 1.0% (better UX, sufficient precision)
- Updated min/max ranges to match new validation constants

Translations:
- Removed: "invalid_volatility_threshold" (generic)
- Added: "invalid_volatility_threshold_moderate/high/very_high" (specific ranges)
- Added: "invalid_volatility_thresholds" (cross-validation error)
- Updated all 5 languages (de, en, nb, nl, sv)

Files modified:
- config_flow_handlers/options_flow.py: Updated validation logic
- config_flow_handlers/schemas.py: Updated NumberSelector configs
- config_flow_handlers/validators.py: Added specific validators + cross-validation
- const.py: Replaced global constants with six specific constants
- translations/*.json: Updated error messages (5 languages)

Impact: Users get clearer validation errors with specific ranges shown,
better UX with sliders and appropriate step size, and guaranteed
threshold ordering (MODERATE < HIGH < VERY_HIGH).
2025-11-21 17:31:07 +00:00
Julian Pawlowski
ebd1b8ddbf chore: Enhance validation logic and constants for options configuration flow
- 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.
2025-11-21 13:57:35 +00:00
Julian Pawlowski
e950737478 feat(chart_export): migrate sensor config from UI to configuration.yaml
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.
2025-11-20 13:41:26 +00:00
Julian Pawlowski
457fa7c03f refactor(periods): merge adjacent periods and remove is_extension logic
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
2025-11-19 20:16:58 +00:00
Julian Pawlowski
625bc222ca refactor(coordinator): centralize time operations through TimeService
Introduce TimeService as single source of truth for all datetime operations,
replacing direct dt_util calls throughout the codebase. This establishes
consistent time context across update cycles and enables future time-travel
testing capability.

Core changes:
- NEW: coordinator/time_service.py with timezone-aware datetime API
- Coordinator now creates TimeService per update cycle, passes to calculators
- Timer callbacks (#2, #3) inject TimeService into entity update flow
- All sensor calculators receive TimeService via coordinator reference
- Attribute builders accept time parameter for timestamp calculations

Key patterns replaced:
- dt_util.now() → time.now() (single reference time per cycle)
- dt_util.parse_datetime() + as_local() → time.get_interval_time()
- Manual interval arithmetic → time.get_interval_offset_time()
- Manual day boundaries → time.get_day_boundaries()
- round_to_nearest_quarter_hour() → time.round_to_nearest_quarter()

Import cleanup:
- Removed dt_util imports from ~30 files (calculators, attributes, utils)
- Restricted dt_util to 3 modules: time_service.py (operations), api/client.py
  (rate limiting), entity_utils/icons.py (cosmetic updates)
- datetime/timedelta only for TYPE_CHECKING (type hints) or duration arithmetic

Interval resolution abstraction:
- Removed hardcoded MINUTES_PER_INTERVAL constant from 10+ files
- New methods: time.minutes_to_intervals(), time.get_interval_duration()
- Supports future 60-minute resolution (legacy data) via TimeService config

Timezone correctness:
- API timestamps (startsAt) already localized by data transformation
- TimeService operations preserve HA user timezone throughout
- DST transitions handled via get_expected_intervals_for_day() (future use)

Timestamp ordering preserved:
- Attribute builders generate default timestamp (rounded quarter)
- Sensors override when needed (next interval, daily midnight, etc.)
- Platform ensures timestamp stays FIRST in attribute dict

Timer integration:
- Timer #2 (quarter-hour): Creates TimeService, calls _handle_time_sensitive_update(time)
- Timer #3 (30-second): Creates TimeService, calls _handle_minute_update(time)
- Consistent time reference for all entities in same update batch

Time-travel readiness:
- TimeService.with_reference_time() enables time injection (not yet used)
- All calculations use time.now() → easy to simulate past/future states
- Foundation for debugging period calculations with historical data

Impact: Eliminates timestamp drift within update cycles (previously 60+ independent
dt_util.now() calls could differ by milliseconds). Establishes architecture for
time-based testing and debugging features.
2025-11-19 18:36:12 +00:00
Julian Pawlowski
5ab7703d90 fix(imports): update imports after utils package reorganization
Updated all imports to reflect new module structure:

1. Utils package imports:
   - average_utils → utils.average
   - price_utils → utils.price
   - Added MINUTES_PER_INTERVAL imports from const.py

2. Entity utils imports:
   - Added entity_utils.helpers imports where needed
   - Fixed find_rolling_hour_center_index import paths
   - Added get_price_value import in binary_sensor

3. Type imports:
   - Added coordinator/period_handlers/types.py MINUTES_PER_INTERVAL
     re-export (with noqa:F401) for period handler modules

4. Platform imports:
   - Updated sensor platform imports (utils.average, utils.price)
   - Updated binary_sensor imports (entity_utils helpers)
   - Updated coordinator imports (utils packages)

All import paths validated:
✓ Integration loads successfully
✓ All service handlers importable
✓ No circular dependencies
✓ Lint checks passing

Impact: Clean import structure, no breaking changes to functionality.
All sensors and services work identically to before.
2025-11-18 20:07:28 +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
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
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
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
fa40c00f67 refactor(sensors): Transform sensor platform into package 2025-11-15 11:46:54 +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
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
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
a39eb66f49 fix(const): clarify comments on peak price flexibility threshold 2025-11-13 23:00:18 +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
817658f230 feat(periods): add gap tolerance for price level filters with intelligent period splitting
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.
2025-11-10 04:38:44 +00:00
Julian Pawlowski
40a335dabe feat(periods): add adaptive filter relaxation for minimum period guarantee
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.
2025-11-10 03:34:09 +00:00
Julian Pawlowski
9640b041e0 refactor(periods): move all period logic to coordinator and refactor period_utils
Moved filter logic and all period attribute calculations from binary_sensor.py
to coordinator.py and period_utils.py, following Home Assistant best practices
for data flow architecture.

ARCHITECTURE CHANGES:

Binary Sensor Simplification (~225 lines removed):
- Removed _build_periods_summary, _add_price_diff_for_period (calculation logic)
- Removed _get_period_intervals_from_price_info (107 lines, interval reconstruction)
- Removed _should_show_periods, _check_volatility_filter, _check_level_filter
- Removed _build_empty_periods_result (filtering result builder)
- Removed _get_price_hours_attributes (24 lines, dead code)
- Removed datetime import (unused after cleanup)
- New: _build_final_attributes_simple (~20 lines, timestamp-only)
- Result: Pure display-only logic, reads pre-calculated data from coordinator

Coordinator Enhancement (+160 lines):
- Added _should_show_periods(): UND-Verknüpfung of volatility and level filters
- Added _check_volatility_filter(): Checks min_volatility threshold
- Added _check_level_filter(): Checks min/max level bounds
- Enhanced _calculate_periods_for_price_info(): Applies filters before period calculation
- Returns empty periods when filters don't match (instead of calculating unnecessarily)
- Passes volatility thresholds (moderate/high/very_high) to PeriodConfig

Period Utils Refactoring (+110 lines):
- Extended PeriodConfig with threshold_volatility_moderate/high/very_high
- Added PeriodData NamedTuple: Groups timing data (start, end, length, position)
- Added PeriodStatistics NamedTuple: Groups calculated stats (prices, volatility, ratings)
- Added ThresholdConfig NamedTuple: Groups all thresholds + reverse_sort flag
- New _calculate_period_price_statistics(): Extracts price_avg/min/max/spread calculation
- New _build_period_summary_dict(): Builds final dict with correct attribute ordering
- Enhanced _extract_period_summaries(): Now calculates ALL attributes (no longer lightweight):
  * price_avg, price_min, price_max, price_spread (in minor units: ct/øre)
  * volatility (low/moderate/high/very_high based on absolute thresholds)
  * rating_difference_% (average of interval differences)
  * period_price_diff_from_daily_min/max (period avg vs daily reference)
  * aggregated level and rating_level
  * period_interval_count (renamed from interval_count for clarity)
- Removed interval_starts array (redundant - start/end/count sufficient)
- Function signature refactored from 9→4 parameters using NamedTuples

Code Organization (HA Best Practice):
- Moved calculate_volatility_level() from const.py to price_utils.py
- Rule: const.py should contain only constants, no functions
- Removed duplicate VOLATILITY_THRESHOLD_* constants from const.py
- Updated imports in sensor.py, services.py, period_utils.py

DATA FLOW:

Before:
API → Coordinator (basic enrichment) → Binary Sensor (calculate everything on each access)

After:
API → Coordinator (enrichment + filtering + period calculation with ALL attributes) →
      Cached Data → Binary Sensor (display + timestamp only)

ATTRIBUTE STRUCTURE:

Period summaries now contain (following copilot-instructions.md ordering):
1. Time: start, end, duration_minutes
2. Decision: level, rating_level, rating_difference_%
3. Prices: price_avg, price_min, price_max, price_spread, volatility
4. Differences: period_price_diff_from_daily_min/max (conditional)
5. Details: period_interval_count, period_position
6. Meta: periods_total, periods_remaining

BREAKING CHANGES: None
- Period data structure enhanced but backwards compatible
- Binary sensor API unchanged (state + attributes)

Impact: Binary sensors now display pre-calculated data from coordinator instead
of calculating on every access. Reduces complexity, improves performance, and
centralizes business logic following Home Assistant coordinator pattern. All
period filtering (volatility + level) now happens in coordinator before caching.
2025-11-09 23:46:48 +00:00
Julian Pawlowski
532a91be58 fix(translations): resolve hassfest selector key validation errors
Changed all selector option keys from uppercase to lowercase to comply
with Home Assistant's hassfest validation pattern [a-z0-9-_]+.

Fixed inconsistency in PEAK_PRICE_MIN_LEVEL_OPTIONS where some values
were uppercase while others were lowercase.

Changes:
- translations/*.json: All selector keys now lowercase (volatility, price_level)
- const.py: Added .lower() to all PEAK_PRICE_MIN_LEVEL_OPTIONS values
- binary_sensor.py: Added .upper() conversion when looking up price levels
  in PRICE_LEVEL_MAPPING to handle lowercase config values

Impact: Config flow now works correctly with translated selector options.
Hassfest validation passes without selector key errors.
2025-11-09 15:31:37 +00:00
Julian Pawlowski
f4568be34e feat(sensors): add price volatility analysis and period filters
Added comprehensive volatility analysis system:
- 4 new volatility sensors (today, tomorrow, next_24h, today+tomorrow)
- Volatility classification (LOW/MODERATE/HIGH/VERY HIGH) based on price spread
- Configurable thresholds in options flow (step 6 of 6)
- Best/Peak price period filters using volatility and price level
- Price spread calculation in get_price service

Volatility sensors help users decide if price-based optimization is worthwhile.
For example, battery optimization only makes sense when volatility ≥ MODERATE.

Period filters allow AND-logic combinations:
- best_price_min_volatility: Only show cheap periods on volatile days
- best_price_max_level: Only show periods when prices reach desired level
- peak_price_min_volatility: Only show peaks on volatile days
- peak_price_min_level: Only show peaks when expensive levels occur

All 5 language files updated (de, en, nb, nl, sv) with:
- Volatility sensor translations (name, states, descriptions)
- Config flow step 6 "Volatility" with threshold settings
- Step progress indicators added to all config steps
- Period filter translations with usage tips

Impact: Users can now assess daily price volatility and configure period
sensors to only activate when conditions justify battery cycling or load
shifting. Reduces unnecessary battery wear on low-volatility days.
2025-11-09 14:24:34 +00:00
Julian Pawlowski
db0d65a939 feat: Add price trend thresholds configuration and update related calculations 2025-11-08 16:02:21 +00:00
Julian Pawlowski
3f3edd8a28 refactor: Update price level and rating options to inline definitions for sensor initialization 2025-11-08 09:24:28 +00:00
Julian Pawlowski
df9fb37fe4 refactor: Update integration version handling in TibberPrices components 2025-11-07 23:43:39 +00:00
Julian Pawlowski
1ed2c08f34 feat: Add minimum period length configuration for best and peak price sensors 2025-11-07 15:16:16 +00:00
Julian Pawlowski
ef1a81ccc1 Refactor translations for electricity prices in multiple languages
- Updated keys from "cents" to more user-friendly terms for current, next, and previous prices.
- Added state descriptions for price levels and ratings, including categories like "very cheap," "cheap," "normal," "expensive," and "very expensive."
- Introduced new average price sensors for the next 1 to 12 hours.
- Added price trend sensors for 1 to 12 hours with states indicating rising, falling, or stable trends.
- Ensured consistency in naming conventions across English, Norwegian, Dutch, and Swedish translations.
2025-11-06 22:36:12 +00:00
Julian Pawlowski
63904fff39 feat: Enhance Tibber Prices integration with new configuration options and improved data handling
- Added new configuration options for minimum distance from average price for best and peak prices.
- Updated default values for best and peak price flexibility.
- Improved coordinator to handle midnight turnover and data rotation more effectively.
- Refactored entity initialization to streamline device information retrieval.
- Updated sensor attributes to use more descriptive names for price values.
- Enhanced translations for new configuration options in English and German.
- Improved unit tests for coordinator functionality, ensuring proper cleanup and async handling.
2025-11-06 11:43:22 +00:00
Julian Pawlowski
17a20fbb39 update default values 2025-11-03 22:04:14 +00:00
Julian Pawlowski
2f7b48e177 update currency 2025-11-03 21:31:38 +00:00
Julian Pawlowski
8c61292acf refactoring for QUARTER_HOURLY prices 2025-11-02 19:33:19 +00:00
Julian Pawlowski
e02630440a fix options flow 2025-11-02 16:58:47 +00:00
Julian Pawlowski
0ffa17679b fix config flow 2025-11-02 15:46:13 +00:00
Julian Pawlowski
bd33fc7367 config 2025-05-24 20:50:17 +00:00
Julian Pawlowski
dd65f0efad fix 2025-05-20 23:19:45 +00:00
Julian Pawlowski
52cfc4a87f refactoring 2025-05-17 17:39:06 +00:00
Julian Pawlowski
a4859a9d2e
refactoring 2025-05-11 13:21:22 +02:00
Julian Pawlowski
94ef6ed4a6 add friendly name 2025-04-23 23:53:11 +00:00
Julian Pawlowski
02a226819a refactoring 2025-04-23 23:07:30 +00:00
Julian Pawlowski
3d33d8d6bc add descriptions 2025-04-23 21:13:57 +00:00
Julian Pawlowski
f092ad2839 add data retrieving 2025-04-18 21:14:36 +00:00
Julian Pawlowski
8123285489 rename directory 2025-04-18 13:16:59 +00:00
Renamed from custom_components/integration_blueprint/const.py (Browse further)