Commit graph

8 commits

Author SHA1 Message Date
Julian Pawlowski
8f05f8cac7 perf(interval_pool): hoist fetch_groups and precompute period criteria
- Move UTC import from inline (inside _has_real_gaps_in_range) to
  module-level in manager.py
- Hoist get_fetch_groups() out of while loop in _get_cached_intervals:
  eliminates ~384 function calls per invocation
- Pre-compute criteria_by_day dict in build_periods before the for-loop:
  eliminates ~381 redundant NamedTuple constructions per call; only
  ref_price/avg_price vary by day (max 3 entries), flex/min_distance/
  reverse_sort are constant throughout

Impact: Reduces unnecessary object creation during the hot paths called
every 15 minutes and during all relaxation phases.
2026-04-06 14:35:33 +00:00
Julian Pawlowski
8975aef900 fix(interval_pool): preserve DST fall-back duplicate intervals
On DST fall-back nights the clocks repeat an hour (e.g. 02:00 CET/CEST).
Tibber delivers quarter-hourly intervals for both the CEST (+02:00) and
CET (+01:00) copies of that hour.  Both share the same 19-char naive local
key 'YYYY-MM-DDTHH:MM:SS', so _add_intervals treated the CET arrivals as
unwanted duplicates and sent them to _touch_intervals, which kept the CEST
data and silently discarded the CET price data.  The fall-back hour's prices
were permanently lost from the pool.

Fix:
- Add module constant _DST_COLLISION_MAX_SAME_UTC_S (60 s) to distinguish
  true duplicate arrivals (same UTC, ≤60 s apart) from DST collision pairs
  (~3600 s apart).
- Add _handle_index_collision() helper that compares the UTC datetimes of
  the existing and incoming interval.  If they differ by more than the
  threshold it stores the new interval in self._dst_extras keyed by the
  normalised local timestamp and returns True.
- _add_intervals delegates every collision to _handle_index_collision and
  only routes to touch when it returns False (true duplicate).
- _get_cached_intervals yields the saved extras after the main interval.
- After each GC run, stale entries are pruned from _dst_extras.
- to_dict / from_dict persist and restore _dst_extras across HA restarts.

Impact: The full fall-back hour (e.g. 02:00-02:45 CET) now appears in the
interval pool alongside the CEST copies, so sensors that query that hour
return correct prices instead of stale or missing data.
2026-04-06 14:05:10 +00:00
Julian Pawlowski
ce049e48b1 fix(interval_pool): use UTC-aware gap detection to prevent spring-forward false positives
_get_sensor_interval_stats() computed expected_count via UTC time
arithmetic ((end - start).total_seconds() / 900 = 480 for 5 days), then
iterated through fixed-offset local timestamps adding timedelta(minutes=15).

On DST spring-forward days (e.g. last Sunday March in EU), clocks skip
from 02:00 to 03:00. The 4 local quarter-hour slots 02:00-02:45 never
exist, so the Tibber API never returns intervals for them. The iteration
still visits those 4 keys, finds them absent from the index, and reports
has_gaps=True (expected=480, actual=476). Since no API call can ever
fill those non-existent slots, the pool triggers an unnecessary gap-fill
fetch every 15 minutes for the entire spring-forward day.

Fix: keep the nominal expected_count for diagnostics, but determine
has_gaps via the new _has_real_gaps_in_range() helper that sorts
cached intervals by UTC time and checks consecutive UTC differences.
The 01:45+01:00 -> 03:00+02:00 transition is exactly 15 minutes in
UTC, so no gap is reported. Start/end boundary comparisons use naive
19-char local timestamps to stay consistent with the fixed-offset
arithmetic used by get_protected_range().

Impact: No spurious API fetches on DST spring-forward Sunday. Gap
detection for real missing data (API failures, first startup) remains
fully functional.
2026-04-06 13:57:03 +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
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
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
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