Commit graph

311 commits

Author SHA1 Message Date
Julian Pawlowski
b92becdf8f chore(release): bump version to 0.27.0 2026-03-29 18:49:21 +00:00
Julian Pawlowski
0381749e6f fix(interval_pool): fix DST spring-forward causing missing tomorrow intervals
_get_cached_intervals() used fixed-offset datetimes from fromisoformat()
for iteration. When start and end boundaries span a DST transition (e.g.,
+01:00 CET → +02:00 CEST), the loop's end check compared UTC values,
stopping 1 hour early on spring-forward days.

This caused the last 4 quarter-hourly intervals of "tomorrow" to be
missing, making the binary sensor "Tomorrow data available" show Off
even when full data was present.

Changed iteration to use naive local timestamps, matching the index key
format (timezone stripped via [:19]). The end boundary comparison now
works correctly regardless of DST transitions.

Impact: Binary sensor "Tomorrow data available" now correctly shows On
on DST spring-forward days. Affects all European users on the last
Sunday of March each year.
2026-03-29 18:42:27 +00:00
Julian Pawlowski
00a653396c fix(translations): update API token instructions to use placeholder for Tibber URL 2026-03-29 18:19:42 +00:00
Julian Pawlowski
1bf031ba19 fix(options_flow): enhance translation handling for config fields and update language fallback 2026-01-21 18:35:19 +00:00
Julian Pawlowski
89880c7755 chore(release): bump version to 0.27.0b0 2026-01-21 17:37:35 +00:00
Julian Pawlowski
631cebeb55 feat(config_flow): show override warnings when config entities control settings
When runtime config override entities (number/switch) are enabled,
the Options Flow now displays warning indicators at the top of each
affected section. Users see which fields are being managed by config
entities and can still edit the base values if needed.

Changes:
- Add ConstantSelector warnings in Best Price/Peak Price sections
- Implement multi-language support for override warnings (de, en, nb, nl, sv)
- Add _get_override_translations() to load translated field labels
- Add _get_active_overrides() to detect enabled override entities
- Extend get_best_price_schema/get_peak_price_schema with translations param
- Add 14 number/switch config entities for runtime period tuning
- Document runtime configuration entities in user docs

Warning format adapts to overridden fields:
- Single: "⚠️ Flexibility controlled by config entity"
- Multiple: "⚠️ Flexibility and Minimum Distance controlled by config entity"

Impact: Users can now dynamically adjust period calculation parameters
via Home Assistant automations, scripts, or dashboards without entering
the Options Flow. Clear UI indicators show which settings are currently
overridden.
2026-01-21 17:36:51 +00:00
Julian Pawlowski
cc75bc53ee feat(services): add average indicator for hourly resolution in charts
Add visual indicators to distinguish hourly aggregated data from
original 15-minute interval data in ApexCharts output.

Changes:
- Chart title: Append localized suffix like "(Ø hourly)" / "(Ø stündlich)"
- Y-axis label: Append "(Ø)" suffix, e.g., "øre/kWh (Ø)"

The suffix pattern avoids confusion with Scandinavian currency symbols
(øre/öre) which look similar to the average symbol (Ø) when used as prefix.

Added hourly_suffix translations for all 5 languages (en, de, sv, nb, nl).

Impact: Users can now clearly see when a chart displays averaged hourly
data rather than original 15-minute prices.
2026-01-20 16:44:18 +00:00
Julian Pawlowski
b541f7b15e feat(apexcharts): add legend toggle for best/peak price overlays
Implement clickable legend items to show/hide best/peak price period
overlays in generated ApexCharts YAML configuration.

Legend behavior by configuration:
- Only best price: No legend (overlay always visible)
- Only peak price: Legend shown, peak toggleable (starts hidden)
- Both enabled: Legend shown, both toggleable (best visible, peak hidden)

Changes:
- Best price overlay: in_legend only when peak also enabled
- Peak price overlay: always in_legend with hidden_by_default: true
- Enable experimental.hidden_by_default when peak price active
- Price level series (LOW/NORMAL/HIGH): hidden from legend when
  overlays active, visible otherwise (preserves easy legend enable)
- Add triangle icons (▼/▲) before overlay names for visual distinction
- Custom legend markers (size: 0) only when overlays active
- Increased itemMargin for better visual separation

Impact: Users can toggle best/peak price period visibility directly
in the chart via legend click. Without overlays, legend behavior
unchanged - users can still enable it by setting show: true.
2026-01-20 16:27:14 +00:00
Julian Pawlowski
2f36c73c18 feat(services): add hourly resolution option for chart data services
Add resolution parameter to get_chartdata and get_apexcharts_yaml services,
allowing users to choose between original 15-minute intervals or aggregated
hourly values for chart visualization.

Implementation uses rolling 5-interval window aggregation (-2, -1, 0, +1, +2
around :00 of each hour = 60 minutes total), matching the sensor rolling
hour methodology. Respects user's CONF_AVERAGE_SENSOR_DISPLAY setting for
mean vs median calculation.

Changes:
- formatters.py: Add aggregate_to_hourly() function preserving original
  field names (startsAt, total, level, rating_level) for unified processing
- get_chartdata.py: Pre-aggregate data before processing when resolution is
  'hourly', enabling same code path for filters/insert_nulls/connect_segments
- get_apexcharts_yaml.py: Add resolution parameter, pass to all 4 get_chartdata
  service calls in generated JavaScript
- services.yaml: Add resolution field with interval/hourly selector
- icons.json: Add section icons for get_apexcharts_yaml fields
- translations: Add highlight_peak_price and resolution field translations
  for all 5 languages (en, de, sv, nb, nl)

Impact: Users can now generate cleaner charts with 24 hourly data points
instead of 96 quarter-hourly intervals. The unified processing approach
ensures all chart features (filters, null insertion, segment connection)
work identically for both resolutions.
2026-01-20 15:51:34 +00:00
Julian Pawlowski
1b22ce3f2a feat(config_flow): add entity status checks to options flow pages
Added dynamic warnings when users configure settings for sensors that
are currently disabled. This improves UX by informing users that their
configuration changes won't have any visible effect until they enable
the relevant sensors.

Changes:
- Created entity_check.py helper module with sensor-to-step mappings
- Added check_relevant_entities_enabled() to detect disabled sensors
- Integrated warnings into 6 options flow steps (price_rating,
  price_level, best_price, peak_price, price_trend, volatility)
- Made Chart Data Export info page content-aware: shows configuration
  guide when sensor is enabled, shows enablement instructions when disabled
- Updated all 5 translation files (de, en, nb, nl, sv) with dynamic
  placeholders {entity_warning} and {sensor_status_info}

Impact: Users now receive clear feedback when configuring settings for
disabled sensors, reducing confusion about why changes aren't visible.
Chart Data Export page now provides context-appropriate guidance.
2026-01-20 13:59:07 +00:00
Julian Pawlowski
5fc1f4db33 feat(sensors): add 5-level price trend scale with configurable thresholds
Extends trend sensors from 3-level (rising/stable/falling) to 5-level scale
(strongly_rising/rising/stable/falling/strongly_falling) for finer granularity.

Changes:
- Add PRICE_TREND_MAPPING with integer values (-2, -1, 0, +1, +2) matching
  PRICE_LEVEL_MAPPING pattern for consistent automation comparisons
- Add configurable thresholds for strongly_rising (default: 6%) and
  strongly_falling (default: -6%) independent from base thresholds
- Update calculate_price_trend() to return 3-tuple: (trend_state, diff_pct, trend_value)
- Add trend_value attribute to all trend sensors for numeric comparisons
- Update sensor entity descriptions with 5-level options
- Add validation with cross-checks (strongly_rising > rising, etc.)
- Update icons: chevron-double-up/down for strong trends, trending-up/down for normal

Files changed:
- const.py: PRICE_TREND_* constants, PRICE_TREND_MAPPING, config constants
- utils/price.py: Extended calculate_price_trend() signature and return value
- sensor/calculators/trend.py: Pass new thresholds, handle 3-tuple return
- sensor/definitions.py: 5-level options for all 9 trend sensors
- sensor/core.py: 5-level icon mapping
- entity_utils/icons.py: 5-level trend icons
- config_flow_handlers/: validators, schemas, options_flow for new settings
- translations/*.json: Labels and error messages (en, de, nb, sv, nl)
- tests/test_percentage_calculations.py: Updated for 3-tuple return

Impact: Users get more nuanced trend information for automation decisions.
New trend_value attribute enables numeric comparisons (e.g., > 0 for any rise).
Existing automations using "rising"/"falling"/"stable" continue to work.
2026-01-20 13:36:01 +00:00
Julian Pawlowski
972cbce1d3 chore(release): bump version to 0.26.0 2026-01-20 12:40:37 +00:00
Julian Pawlowski
f88d6738e6 fix(validation): enhance user data validation to require active subscription and price info.
Fixes #73
2026-01-20 12:33:45 +00:00
Julian Pawlowski
3e6bcf2345 fix(sensor): synchronize current_interval_price_base with current_interval_price
Fixed inconsistency between "Current Electricity Price" and "Current Electricity Price
(Energy Dashboard)" sensors that were showing different prices and icons.

Changes:
- Add current_interval_price_base to TIME_SENSITIVE_ENTITY_KEYS so it updates at
  quarter-hour boundaries instead of only on API polls. This ensures both sensors
  update synchronously when a new 15-minute interval starts.
- Use interval_data["startsAt"] as timestamp for current interval price sensors
  (both variants) instead of rounded calculation time. This prevents timestamp
  divergence when sensors update at slightly different times.
- Include current_interval_price_base in icon color attribute mapping so both
  sensors display the same dynamic cash icon based on current price level.
- Include current_interval_price_base in dynamic icon function so it gets the
  correct icon based on current price level (VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE).

Impact: Both sensors now show identical prices, timestamps, and icons as intended.
They update synchronously at interval boundaries (00, 15, 30, 45 minutes) and
correctly represent the Energy Dashboard compatible variant without lag or
inconsistencies.
2025-12-26 16:23:05 +00:00
Julian Pawlowski
0a4af0de2f feat(sensor): convert timing sensors to hour-based display with minute attributes
Convert best_price and peak_price timing sensors to display in hours (UI-friendly)
while retaining minute values in attributes (automation-friendly). This improves
readability in dashboards by using Home Assistant's automatic duration formatting
"1 h 35 min" instead of decimal "1.58 h".

BREAKING CHANGE: State unit changed from minutes to hours for 6 timing sensors.

Affected sensors:
  * best_price_period_duration, best_price_remaining_minutes, best_price_next_in_minutes
  * peak_price_period_duration, peak_price_remaining_minutes, peak_price_next_in_minutes

Migration guide for users:
  - If your automations use {{ state_attr(..., 'remaining_time') }} or similar:
    No action needed - attribute values remain in minutes
  - If your automations use {{ states('sensor.best_price_remaining_minutes') }} directly:
    Update to use the minute attribute instead: {{ state_attr('sensor.best_price_remaining_minutes', 'remaining_minutes') }}
  - If your dashboards display the state value:
    Values now show as "1 h 35 min" instead of "95" - this is the intended improvement
  - If your templates do math with the state: multiply by 60 to convert hours back to minutes
    Before: remaining * 60
    After: remaining_minutes (use attribute directly)

Implementation details:
- Timing sensors now use device_class=DURATION, unit=HOURS, precision=2
- State values converted from minutes to hours via _minutes_to_hours()
- New minute-precision attributes added for automation compatibility:
  * period_duration_minutes (for checking if period is long enough)
  * remaining_minutes (for countdown-based automation logic)
  * next_in_minutes (for time-to-event automation triggers)
- Translation improvements across all 5 languages (en, de, nb, nl, sv):
  * Descriptions now clarify state in hours vs attributes in minutes
  * Long descriptions explain dual-format architecture
  * Usage tips updated to reference minute attributes for automations
  * All translation files synchronized (fixed order, removed duplicates)
- Type safety: Added type assertions (cast) for timing calculator results to
  satisfy Pyright type checking (handles both float and datetime return types)

Home Assistant now automatically formats these durations as "1 h 35 min" for improved
UX, matching the behavior of battery.remaining_time and other duration sensors.

Rationale for breaking change:
The previous minute-based state was unintuitive for users ("95 minutes" doesn't
immediately convey "1.5 hours") and didn't match Home Assistant's standard duration
formatting. The new hour-based state with minute attributes provides:
- Better UX: Automatic "1 h 35 min" formatting in UI
- Full automation compatibility: Minute attributes for all calculation needs
- Consistency: Matches HA's duration sensor pattern (battery, timer, etc.)

Impact: Timing sensors now display in human-readable hours with full backward
compatibility via minute attributes. Users relying on direct state access must
migrate to minute attributes (simple change, documented above).
2025-12-26 16:03:00 +00:00
Julian Pawlowski
09a50dccff fix(sensor): streamline lifecycle attrs and next poll visibility
- Remove pool stats/fetch-age from lifecycle sensor to avoid stale data under state-change filtering; add `next_api_poll` for transparency.
- Clean lifecycle calculator by dropping unused helpers/constants and delete the obsolete cache age test.
- Clarify lifecycle state is diagnostics-only in coordinator comments, keep state-change filtering in timer test, and retain quarter-hour precision notes in constants.
- Keep sensor core aligned with lifecycle state filtering.

Impact: Lifecycle sensor now exposes only state-relevant fields without recorder noise, next API poll is visible, and dead code/tests tied to removed attributes are gone.
2025-12-26 12:13:36 +00:00
Julian Pawlowski
665fac10fc feat(services): add peak price overlay toggle to ApexCharts YAML
Added `highlight_peak_price` (default: false) to `get_apexcharts_yaml` service
and implemented a subtle red overlay analogous to best price periods using
`period_filter: 'peak_price'`. Tooltips now dynamically exclude overlay
series to prevent overlay tooltips.

Impact: Users can visualize peak-price periods in ApexCharts cards
when desired, with default opt-out behavior.
2025-12-26 00:07:28 +00:00
Julian Pawlowski
3157c6f0df chore(release): bump version to 0.25.0b0 2025-12-25 22:48:07 +00:00
Julian Pawlowski
c6d6e4a5b2 fix(volatility): expose price coefficient variation attribute
Expose the `price_coefficient_variation_%` value across period statistics, binary sensor attributes, and the volatility calculator, and refresh the volatility descriptions/translations to mention the coefficient-of-variation metric.
2025-12-25 19:10:42 +00:00
Julian Pawlowski
23b4330b9a fix(coordinator): track API calls separately from cached data usage
The lifecycle sensor was always showing "fresh" state because
_last_price_update was set on every coordinator update, regardless of
whether data came from API or cache.

Changes:
- interval_pool/manager.py: get_intervals() and get_sensor_data() now
  return tuple[data, bool] where bool indicates actual API call
- coordinator/price_data_manager.py: All fetch methods propagate
  api_called flag through the call chain
- coordinator/core.py: Only update _last_price_update when api_called=True,
  added debug logging to distinguish API calls from cached data
- services/get_price.py: Updated to handle new tuple return type

Impact: Lifecycle sensor now correctly shows "cached" during normal
15-minute updates (using pool cache) and only "fresh" within 5 minutes
of actual API calls. This fixes the issue where the sensor would never
leave the "fresh" state during frequent HA restarts or normal operation.
2025-12-25 18:53:29 +00:00
Copilot
a437d22b7a
Fix flex filter excluding valid low-price intervals in best price periods (#68)
Fixed bug in best price flex filter that incorrectly excluded prices
when checking for periods. The filter was requiring price >= daily_min,
which is unnecessary and could theoretically exclude valid low prices.

Changed from:
  in_flex = price >= criteria.ref_price and price <= flex_threshold

To:
  in_flex = price <= flex_threshold

This ensures all low prices up to the threshold are included in best
price period consideration, matching the expected behavior described
in the period calculation documentation.

The fix addresses the user's observation that qualifying intervals
appearing after the daily minimum in chronological order should be
included if they meet the flex criteria.
2025-12-25 09:49:31 +01:00
Julian Pawlowski
9eea984d1f refactor(coordinator): remove price_data from cache, delegate to Pool
Cache now stores only user metadata and timestamps. Price data is
managed exclusively by IntervalPool (single source of truth).

Changes:
- cache.py: Remove price_data and last_price_update fields
- core.py: Remove _cached_price_data, update references to use Pool
- core.py: Rename _data_fetcher to _price_data_manager
- AGENTS.md: Update class naming examples (DataFetcher → PriceDataManager)

This completes the Pool integration architecture where IntervalPool
handles all price data persistence and coordinator cache handles
only user account metadata.
2025-12-23 14:15:26 +00:00
Julian Pawlowski
9b34d416bc feat(services): add debug_clear_tomorrow for testing refresh cycle
Add debug service to clear tomorrow data from interval pool, enabling
testing of tomorrow data refresh cycle without waiting for next day.

Service available only in DevContainer (TIBBER_PRICES_DEV=1 env var).
Removes intervals from both Pool index and coordinator.data["priceInfo"]
so sensors properly show "unknown" state.

Changes:
- Add debug_clear_tomorrow.py service handler
- Register conditionally based on TIBBER_PRICES_DEV env var
- Add service schema and translations
- Set TIBBER_PRICES_DEV=1 in devcontainer.json

Usage: Developer Tools → Services → tibber_prices.debug_clear_tomorrow

Impact: Enables rapid testing of tomorrow data refresh cycle during
development without waiting or restarting HA.
2025-12-23 14:13:51 +00:00
Julian Pawlowski
cfc7cf6abc refactor(coordinator): replace DataFetcher with PriceDataManager
Rename and refactor data_fetching.py → price_data_manager.py to reflect
actual responsibilities:
- User data: Fetches directly via API, validates, caches
- Price data: Delegates to IntervalPool (single source of truth)

Key changes:
- Add should_fetch_tomorrow_data() for intelligent API call decisions
- Add include_tomorrow parameter to prevent API spam before 13:00
- Remove cached_price_data property (Pool is source of truth)
- Update tests to use new class name

Impact: Clearer separation of concerns, reduced API calls through
intelligent tomorrow data fetching logic.
2025-12-23 14:13:43 +00:00
Julian Pawlowski
78df8a4b17 refactor(lifecycle): integrate with Pool for sensor metrics
Replace cache-based metrics with Pool as single source of truth:
- get_cache_age_minutes() → get_sensor_fetch_age_minutes() (from Pool)
- Remove get_cache_validity_status(), get_data_completeness_status()
- Add get_pool_stats() for comprehensive pool statistics
- Add has_tomorrow_data() using Pool as source

Attributes now show:
- sensor_intervals_count/expected/has_gaps (protected range)
- cache_intervals_total/limit/fill_percent/extra (entire pool)
- last_sensor_fetch, cache_oldest/newest_interval timestamps
- tomorrow_available based on Pool state

Impact: More accurate lifecycle status, consistent with Pool as source
of truth, cleaner diagnostic information.
2025-12-23 14:13:34 +00:00
Julian Pawlowski
7adc56bf79 fix(interval_pool): prevent external mutation of cached intervals
Return shallow copies from _get_cached_intervals() to prevent external
code (e.g., parse_all_timestamps()) from mutating Pool internal cache.
This fixes TypeError in check_coverage() caused by datetime objects in
cached interval dicts.

Additional improvements:
- Add TimeService support for time-travel testing in cache/manager
- Normalize startsAt to consistent format (handles datetime vs string)
- Rename detect_gaps() → check_coverage() for clarity
- Add get_sensor_data() for sensor data fetching with fetch/return separation
- Add get_pool_stats() for lifecycle sensor metrics

Impact: Fixes critical cache mutation bug, enables time-travel testing,
improves pool API for sensor integration.
2025-12-23 14:13:24 +00:00
Julian Pawlowski
94615dc6cd refactor(interval_pool): improve reliability and test coverage
Added async_shutdown() method for proper cleanup on unload - cancels
debounce and background tasks to prevent orphaned task leaks.

Added Phase 1.5 to GC: removes empty fetch groups after dead interval
cleanup, with index rebuild to maintain consistency.

Added update_batch() to TimestampIndex for efficient batch updates.
Touch operations now use batch updates instead of N remove+add calls.

Rewrote memory leak tests for modular architecture - all 9 tests now
pass using new component APIs (cache, index, gc).

Impact: Prevents task leaks on HA restart/reload, reduces memory
overhead from empty groups, improves touch operation performance.
2025-12-23 10:10:35 +00:00
Julian Pawlowski
db0de2376b chore(release): bump version to 0.24.0 2025-12-22 23:40:14 +00:00
Julian Pawlowski
4971ab92d6 fix(chartdata): use proportional padding for yaxis bounds
Changed from fixed padding (0.5ct below min, 1ct above max) to
proportional padding based on data range (8% below, 15% above).

This ensures consistent visual "airiness" across all price ranges,
whether prices are at 30ct or 150ct. Both subunit (ct/øre) and
base currency (€/kr) now use the same proportional logic.

Previous fixed padding looked too tight on charts with large price
ranges (e.g., 0.6€-1.5€) compared to charts with small ranges
(e.g., 28-35ct).

Impact: Chart metadata sensor provides better-scaled yaxis_min/yaxis_max
values for all chart cards, making price visualizations more readable
with appropriate whitespace around data regardless of price range.
2025-12-22 23:39:35 +00:00
Julian Pawlowski
49b8a018e7 fix(types): resolve Pyright type errors
- coordinator/core.py: Fix return type for _get_threshold_percentages()
- coordinator/data_transformation.py: Add type ignore for cached data return
- sensor/core.py: Initialize _state_info with required unrecorded_attributes
2025-12-22 23:22:02 +00:00
Julian Pawlowski
4158e7b1fd feat(periods): cross-day extension and supersession
Intelligent handling when tomorrow's price data arrives:

1. Cross-Day Extension
   - Late-night periods (starting ≥20:00) can extend past midnight
   - Extension continues while prices remain below daily_min × (1+flex)
   - Maximum extension to 08:00 next day (covers typical night low)

2. Period Supersession
   - Obsolete late-night today periods filtered when tomorrow is better
   - Tomorrow must be ≥10% cheaper to supersede (SUPERSESSION_PRICE_IMPROVEMENT_PCT)
   - Prevents stale relaxation periods from persisting

Impact: Late-night periods reflect tomorrow's data when available.
2025-12-22 23:21:57 +00:00
Julian Pawlowski
5ef0396c8b feat(periods): add quality gates for period homogeneity
Prevent relaxation from creating heterogeneous periods:

1. CV-based Quality Gate (PERIOD_MAX_CV = 25%)
   - Periods with internal CV >25% are rejected during relaxation
   - CV field added to period statistics for transparency

2. Period Overlap Protection
   - New periods cannot "swallow" existing smaller periods
   - CV-based merge blocking prevents heterogeneous combinations
   - Preserves good baseline periods from relaxation replacement

3. Constants in types.py
   - PERIOD_MAX_CV, CROSS_DAY_*, SUPERSESSION_* thresholds
   - TibberPricesPeriodStatistics extended with coefficient_of_variation field

Impact: Users get smaller, more homogeneous periods that better represent
actual cheap/expensive windows.
2025-12-22 23:21:51 +00:00
Julian Pawlowski
7ee013daf2 feat(outliers): adaptive confidence based on daily volatility
Outlier smoothing now adapts to daily price volatility (CV):
- Flat days (CV≤10%): conservative (confidence=2.5), fewer false positives
- Volatile days (CV≥30%): aggressive (confidence=1.5), catch more spikes
- Linear interpolation between thresholds

Uses calculate_coefficient_of_variation() for consistency with volatility sensors.

Impact: Better outlier detection that respects natural price variation patterns.
Flat days preserve more structure, volatile days get stronger smoothing.
2025-12-22 23:21:44 +00:00
Julian Pawlowski
325d855997 feat(utils): add coefficient of variation (CV) calculation
Add calculate_coefficient_of_variation() as central utility function:
- CV = (std_dev / mean) * 100 as standardized volatility measure
- calculate_volatility_with_cv() returns both level and numeric CV
- Volatility sensors now expose CV in attributes for transparency

Used as foundation for quality gates, adaptive smoothing, and period statistics.

Impact: Volatility sensors show numeric CV percentage alongside categorical level,
enabling users to see exact price variation.
2025-12-22 23:21:38 +00:00
Julian Pawlowski
70552459ce fix(periods): protect daily extremes from outlier smoothing
The outlier filter was incorrectly smoothing daily minimum/maximum prices,
causing best/peak price periods to miss their most important intervals.

Root cause: When the daily minimum (e.g., 0.5535 kr at 05:00) was surrounded
by higher prices, the trend-based prediction calculated an "expected" price
(0.6372 kr) that exceeded the flex threshold (0.6365 kr), causing the
interval to be excluded from the best price period.

Solution: Daily extremes are now protected from smoothing. Before applying
any outlier detection, we calculate daily min/max prices and skip smoothing
for any interval at or within 0.1% of these values.

Changes:
- Added _calculate_daily_extremes() to compute daily min/max
- Added _is_daily_extreme() to check if price should be protected
- Added EXTREMES_PROTECTION_TOLERANCE constant (0.1%)
- Updated filter_price_outliers() to skip extremes before analysis
- Enhanced logging to show protected interval count

Impact: Best price periods now correctly include daily minimum intervals,
and peak price periods correctly include daily maximum intervals. The
period for 2024-12-23 now extends from 03:15-05:30 (10 intervals) instead
of incorrectly stopping at 05:00 (7 intervals).
2025-12-22 21:05:30 +00:00
Julian Pawlowski
11d4cbfd09 feat(config_flow): add price level gap tolerance for Tibber API level field
Implement gap tolerance smoothing for Tibber's price level classification
(VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE/VERY_EXPENSIVE), separate from the existing
rating_level gap tolerance (LOW/NORMAL/HIGH).

New feature:
- Add CONF_PRICE_LEVEL_GAP_TOLERANCE config option with separate UI step
- Implement _apply_level_gap_tolerance() using same bidirectional gravitational
  pull algorithm as rating gap tolerance
- Add _build_level_blocks() and _merge_small_level_blocks() helper functions

Config flow changes:
- Add new "price_level" options step with dedicated schema
- Add menu entry "🏷️ Preisniveau" / "🏷️ Price Level"
- Include translations for all 5 languages (de, en, nb, nl, sv)

Bug fixes:
- Use copy.deepcopy() for price intervals before enrichment to prevent
  in-place modification of cached raw API data, which caused gap tolerance
  changes to not take effect when reverting settings
- Clear transformation cache in invalidate_config_cache() to ensure
  re-enrichment with new settings

Logging improvements:
- Reduce options update handler from 4 INFO messages to 1 DEBUG message
- Move level_filtering and period_overlap debug logs to .details logger
  for granular control via configuration.yaml

Technical details:
- level_gap_tolerance is tracked separately in transformation config hash
- Algorithm: Identifies small blocks (≤ tolerance) and merges them into
  the larger neighboring block using gravitational pull calculation
- Default: 1 (smooth single isolated intervals), Range: 0-4

Impact: Users can now stabilize Tibber's price level classification
independently from the internal rating_level calculation. Prevents
automation flickering caused by brief price level changes in Tibber's API.
2025-12-22 20:25:30 +00:00
Julian Pawlowski
f57997b119 feat(config_flow): add configurable hysteresis and gap tolerance for price ratings
Added UI controls for price rating stabilization parameters that were
previously hardcoded. Users can now fine-tune rating stability to match
their automation needs.

Changes:
- Added CONF_PRICE_RATING_HYSTERESIS constant (0-5%, step 0.5%, default 2%)
- Added CONF_PRICE_RATING_GAP_TOLERANCE constant (0-4 intervals, default 1)
- Extended get_price_rating_schema() with two new sliders
- Updated data_transformation.py to pass both parameters to enrichment function
- Improved descriptions in all 5 languages (de, en, nb, nl, sv) to focus on
  automation stability instead of chart appearance
- Both settings included in factory reset via get_default_options()

Hysteresis explanation: Prevents rapid state changes when prices hover near
thresholds (e.g., LOW requires price > threshold+hysteresis to leave).

Gap tolerance explanation: Merges small isolated rating blocks into dominant
neighboring blocks using "look through" algorithm (fixed in previous commit).

Impact: Users can now adjust rating stability for their specific use cases.
Lower hysteresis (0-1%) for responsive automations, higher (3-5%) for stable
long-running processes. Gap tolerance prevents brief rating spikes from
triggering unnecessary automation actions.
2025-12-22 13:54:10 +00:00
Julian Pawlowski
64cf842719 fix(rating): improve gap tolerance to find dominant large blocks
The gap tolerance algorithm now looks through small intermediate blocks
to find the first LARGE block (> gap_tolerance) in each direction.
This ensures small isolated rating intervals are merged into the
correct dominant block, not just the nearest neighbor.

Example: NORMAL(large) HIGH(1) NORMAL(1) HIGH(large)
Before: HIGH at 05:45 merged into NORMAL (wrong - nearest neighbor)
After:  NORMAL at 06:00 merged into HIGH (correct - dominant block)

Also collects all merge decisions BEFORE applying them, preventing
order-dependent outcomes when multiple small blocks are adjacent.

Impact: Rating transitions now appear at visually logical positions
where prices actually change direction, not at arbitrary boundaries.
2025-12-22 13:28:25 +00:00
Julian Pawlowski
ced9d8656b fix(chartdata): assign vertical transition lines to more expensive segment
Problem: In segmented price charts with connect_segments=true, vertical lines
at price level transitions were always drawn by the ending segment. This meant
a price INCREASE showed a cheap-colored line going UP, and a price DECREASE
showed an expensive-colored line going DOWN - counterintuitive for users.

Solution: Implement directional bridge-point logic using price level hierarchy:
- Add _is_transition_to_more_expensive() helper using PRICE_LEVEL_MAPPING and
  PRICE_RATING_MAPPING to determine transition direction
- Price INCREASE (cheap → expensive): The MORE EXPENSIVE segment draws the
  vertical line UP via new start-bridge logic (end-bridge at segment start)
- Price DECREASE (expensive → cheap): The MORE EXPENSIVE segment draws the
  vertical line DOWN via existing end-bridge logic (bridge at segment end)

Technical changes:
- Track prev_value and prev_price for segment start detection
- Add end-bridge points at segment starts for upward transitions
- Replace unconditional bridge points with directional hold/bridge logic
- Hold points extend segment horizontally when next segment handles transition

Impact: Vertical transition lines now consistently use the color of the more
expensive price level, making price movements more visually intuitive.
2025-12-21 17:40:13 +00:00
Julian Pawlowski
941f903a9c fix(apexcharts): synchronize y-axis tick intervals for consistent grid alignment
Problem: When using dual y-axes (price + hidden highlight for best-price overlay),
ApexCharts calculates tick intervals independently for each axis. This caused
misaligned horizontal grid lines - the grid follows the first y-axis ticks,
but if the hidden highlight axis had different tick calculations, visual
inconsistencies appeared (especially visible without best-price highlight).

Solution:
- Set tickAmount: 4 on BOTH y-axes to force identical tick intervals
- Add forceNiceScale: true to ensure rounded tick values despite fixed min/max
- Add showAlways: true to price axis in template modes to prevent axis
  disappearing when toggling series via legend

Also add tooltip.shared: true to combine tooltips from all series at the
same x-value into a single tooltip, reducing visual clutter at data points.

Impact: Grid lines now align consistently regardless of which series are
visible. Y-axis remains stable when toggling series in legend.
2025-12-21 17:39:12 +00:00
Julian Pawlowski
ada17f6d90 refactor(services): process chartdata intervals as unified timeline instead of per-day
Changed from iterating over each day separately to collecting all
intervals for selected days into one continuous list before processing.

Changes:
- Collect all intervals via get_intervals_for_day_offsets() with all
  day_offsets at once
- Remove outer `for day in days:` loop around interval processing
- Build date->day_key mapping during average calculation for lookup
- Add _get_day_key_for_interval() helper for average_field assignment
- Simplify midnight handling: only extend at END of entire selection
- Remove complex "next day lookup" logic at midnight boundaries

The segment boundary handling (bridge points, NULL insertion) now works
automatically across midnight since intervals are processed as one list.

Impact: Fixes bridge point rendering at midnight when rating levels
change between days. Simplifies code structure by removing ~60 lines
of per-day midnight-specific logic.
2025-12-21 14:55:52 +00:00
Julian Pawlowski
78b57241eb chore(release): bump version to 0.23.1 2025-12-21 10:46:00 +00:00
Julian Pawlowski
4e0c2b47b1 fix: conditionally enable tooltips for first series based on highlight_best_price
Fixes #63
2025-12-21 10:44:29 +00:00
Julian Pawlowski
9eb5c01c94 chore(release): bump version to 0.23.0 2025-12-18 15:16:55 +00:00
Julian Pawlowski
0a06e12afb i18n: update translations for average sensor display feature
Synchronized all translation files (de, en, nb, nl, sv) with:
1. Custom translations: Added 'configurable display format' messaging to
   sensor descriptions
2. Standard translations: Added detailed bullet-point descriptions for
   average_sensor_display config option

Changes affect both /custom_translations/ and /translations/ directories,
ensuring UI shows complete information about the new display configuration
option across all supported languages.
2025-12-18 15:14:41 +00:00
Julian Pawlowski
abb02083a7 feat(sensors): always show both mean and median in average sensor attributes
Implemented configurable display format (mean/median/both) while always
calculating and exposing both price_mean and price_median attributes.

Core changes:
- utils/average.py: Refactored calculate_mean_median() to always return both
  values, added comprehensive None handling (117 lines changed)
- sensor/attributes/helpers.py: Always include both attributes regardless of
  user display preference (41 lines)
- sensor/core.py: Dynamic _unrecorded_attributes based on display setting
  (55 lines), extracted helper methods to reduce complexity
- Updated all calculators (rolling_hour, trend, volatility, window_24h) to
  use new always-both approach

Impact: Users can switch display format in UI without losing historical data.
Automation authors always have access to both statistical measures.
2025-12-18 15:12:30 +00:00
Julian Pawlowski
29e934d66b chore(release): bump version to 0.22.1 2025-12-13 14:07:34 +00:00
Julian Pawlowski
87f0022baa fix(api): handle None values in API responses to prevent AttributeError
Fixed issue #60 where Tibber API temporarily returning incomplete data
(None values during maintenance) caused AttributeError crashes.

Root cause: `.get(key, default)` returns None when key exists with None value,
causing chained `.get()` calls to crash (None.get() → AttributeError).

Changes:
- api/helpers.py: Use `or {}` pattern in flatten_price_info() to handle
  None values (priceInfo, priceInfoRange, today, tomorrow)
- entity.py: Use `or {}` pattern in _get_fallback_device_info() for address dict
- coordinator/data_fetching.py: Add _validate_user_data() method (67 lines)
  to reject incomplete API responses before caching
- coordinator/data_fetching.py: Modify _get_currency_for_home() to raise
  exceptions instead of silent EUR fallback
- coordinator/data_fetching.py: Add home_id parameter to constructor
- coordinator/core.py: Pass home_id to TibberPricesDataFetcher
- tests/test_user_data_validation.py: Add 12 test cases for validation logic

Architecture improvement: Instead of defensive coding with fallbacks,
implement validation to reject incomplete data upfront. This prevents
caching temporary API errors and ensures currency is always known
(critical for price calculations).

Impact: Integration now handles API maintenance periods gracefully without
crashes. No silent EUR fallbacks - raises exceptions if currency unavailable,
ensuring data integrity. Users see clear errors instead of wrong calculations.

Fixes #60
2025-12-13 14:02:30 +00:00
Julian Pawlowski
6c741e8392 fix(config_flow): restructure options flow to menu-based navigation and fix settings persistence
Fixes configuration wizard not saving settings (#59):

Root cause was twofold:
1. Linear multi-step flow pattern didn't properly persist changes between steps
2. Best/peak price settings used nested sections format - values were saved
   in sections (period_settings, flexibility_settings, etc.) but read from
   flat structure, causing configured values to be ignored on subsequent runs

Solution:
- Replaced linear step-through flow with menu-based navigation system
- Each configuration area now has dedicated "Save & Back" buttons
- Removed nested sections from all steps except best/peak price (where they
  provide better UX for grouping related settings)
- Fixed best/peak price steps to correctly extract values from sections:
  period_settings, flexibility_settings, relaxation_and_target_periods
- Added reset-to-defaults functionality with confirmation dialog

UI/UX improvements:
- Menu structure: General Settings, Currency Display, Price Rating Thresholds,
  Volatility, Best Price Period, Peak Price Period, Price Trend,
  Chart Data Export, Reset to Defaults, Back
- Removed confusing step progress indicators ("{step_num} / {total_steps}")
- Changed all submit buttons from "Continue →" to "↩ Save & Back"
- Clear grouping of settings by functional area

Translation updates (nl.json + sv.json):
- Refined volatility threshold descriptions with CV formula explanations
- Clarified price trend thresholds (compares current vs. future N-hour average,
  not "per hour increase")
- Standardized terminology (e.g., "entry" → "item", compound word consistency)
- Consistently formatted all sensor names and descriptions
- Added new data lifecycle status sensor names

Technical changes:
- Options flow refactored from linear to menu pattern with menu_options dict
- New reset_to_defaults step with confirmation and abort handlers
- Section extraction logic in best_price/peak_price steps now correctly reads
  from nested structure (period_settings.*, flexibility_settings.*, etc.)
- Removed sections from general_settings, display_settings, volatility, etc.
  (simpler flat structure via menu navigation)

Impact: Configuration wizard now reliably saves all settings. Users can
navigate between setting areas without restarting the flow. Reset function
enables quick recovery when experimenting with thresholds. Previously
configured best/peak price settings are now correctly applied.
2025-12-13 13:33:31 +00:00
Julian Pawlowski
1c19cebff5 fix: support main and subunit currency 2025-12-11 23:07:06 +00:00