--- comments: false --- # Caching Strategy This document explains all caching mechanisms in the Tibber Prices integration, their purpose, invalidation logic, and lifetime. For timer coordination and scheduling details, see [Timer Architecture](./timer-architecture.md). ## Overview The integration uses **4 distinct caching layers** with different purposes and lifetimes: 1. **Persistent API Data Cache** (HA Storage) - Hours to days 2. **Translation Cache** (Memory) - Forever (until HA restart) 3. **Config Dictionary Cache** (Memory) - Until config changes 4. **Period Calculation Cache** (Memory) - Until price data or config changes ## 1. Persistent API Data Cache **Location:** `coordinator/cache.py` → HA Storage (`.storage/tibber_prices.`) **Purpose:** Reduce API calls to Tibber by caching user data and price data between HA restarts. **What is cached:** - **Price data** (`price_data`): Day before yesterday/yesterday/today/tomorrow price intervals with enriched fields (384 intervals total) - **User data** (`user_data`): Homes, subscriptions, features from Tibber GraphQL `viewer` query - **Timestamps**: Last update times for validation **Lifetime:** - **Price data**: Until midnight turnover (cleared daily at 00:00 local time) - **User data**: 24 hours (refreshed daily) - **Survives**: HA restarts via persistent Storage **Invalidation triggers:** 1. **Midnight turnover** (Timer #2 in coordinator): ```python # coordinator/day_transitions.py def _handle_midnight_turnover() -> None: self._cached_price_data = None # Force fresh fetch for new day self._last_price_update = None await self.store_cache() ``` 2. **Cache validation on load**: ```python # coordinator/cache.py def is_cache_valid(cache_data: CacheData) -> bool: # Checks if price data is from a previous day if today_date < local_now.date(): # Yesterday's data return False ``` 3. **Tomorrow data check** (after 13:00): ```python # coordinator/data_fetching.py if tomorrow_missing or tomorrow_invalid: return "tomorrow_check" # Update needed ``` **Why this cache matters:** Reduces API load on Tibber (~192 intervals per fetch), speeds up HA restarts, enables offline operation until cache expires. --- ## 2. Translation Cache **Location:** `const.py` → `_TRANSLATIONS_CACHE` and `_STANDARD_TRANSLATIONS_CACHE` (in-memory dicts) **Purpose:** Avoid repeated file I/O when accessing entity descriptions, UI strings, etc. **What is cached:** - **Standard translations** (`/translations/*.json`): Config flow, selector options, entity names - **Custom translations** (`/custom_translations/*.json`): Entity descriptions, usage tips, long descriptions **Lifetime:** - **Forever** (until HA restart) - No invalidation during runtime **When populated:** - At integration setup: `async_load_translations(hass, "en")` in `__init__.py` - Lazy loading: If translation missing, attempts file load once **Access pattern:** ```python # Non-blocking synchronous access from cached data description = get_translation("binary_sensor.best_price_period.description", "en") ``` **Why this cache matters:** Entity attributes are accessed on every state update (~15 times per hour per entity). File I/O would block the event loop. Cache enables synchronous, non-blocking attribute generation. --- ## 3. Config Dictionary Cache **Location:** `coordinator/data_transformation.py` and `coordinator/periods.py` (per-instance fields) **Purpose:** Avoid ~30-40 `options.get()` calls on every coordinator update (every 15 minutes). **What is cached:** ### DataTransformer Config Cache ```python { "thresholds": {"low": 15, "high": 35}, "volatility_thresholds": {"moderate": 15.0, "high": 25.0, "very_high": 40.0}, # ... 20+ more config fields } ``` ### PeriodCalculator Config Cache ```python { "best": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60}, "peak": {"flex": 0.15, "min_distance_from_avg": 5.0, "min_period_length": 60} } ``` **Lifetime:** - Until `invalidate_config_cache()` is called - Built once on first use per coordinator update cycle **Invalidation trigger:** - **Options change** (user reconfigures integration): ```python # coordinator/core.py async def _handle_options_update(...) -> None: self._data_transformer.invalidate_config_cache() self._period_calculator.invalidate_config_cache() await self.async_request_refresh() ``` **Performance impact:** - **Before:** ~30 dict lookups + type conversions per update = ~50μs - **After:** 1 cache check = ~1μs - **Savings:** ~98% (50μs → 1μs per update) **Why this cache matters:** Config is read multiple times per update (transformation + period calculation + validation). Caching eliminates redundant lookups without changing behavior. --- ## 4. Period Calculation Cache **Location:** `coordinator/periods.py` → `PeriodCalculator._cached_periods` **Purpose:** Avoid expensive period calculations (~100-500ms) when price data and config haven't changed. **What is cached:** ```python { "best_price": { "periods": [...], # Calculated period objects "intervals": [...], # All intervals in periods "metadata": {...} # Config snapshot }, "best_price_relaxation": {"relaxation_active": bool, ...}, "peak_price": {...}, "peak_price_relaxation": {...} } ``` **Cache key:** Hash of relevant inputs ```python hash_data = ( today_signature, # (startsAt, rating_level) for each interval tuple(best_config.items()), # Best price config tuple(peak_config.items()), # Peak price config best_level_filter, # Level filter overrides peak_level_filter ) ``` **Lifetime:** - Until price data changes (today's intervals modified) - Until config changes (flex, thresholds, filters) - Recalculated at midnight (new today data) **Invalidation triggers:** 1. **Config change** (explicit): ```python def invalidate_config_cache() -> None: self._cached_periods = None self._last_periods_hash = None ``` 2. **Price data change** (automatic via hash mismatch): ```python current_hash = self._compute_periods_hash(price_info) if self._last_periods_hash != current_hash: # Cache miss - recalculate ``` **Cache hit rate:** - **High:** During normal operation (coordinator updates every 15min, price data unchanged) - **Low:** After midnight (new today data) or when tomorrow data arrives (~13:00-14:00) **Performance impact:** - **Period calculation:** ~100-500ms (depends on interval count, relaxation attempts) - **Cache hit:** `<`1ms (hash comparison + dict lookup) - **Savings:** ~70% of calculation time (most updates hit cache) **Why this cache matters:** Period calculation is CPU-intensive (filtering, gap tolerance, relaxation). Caching avoids recalculating unchanged periods 3-4 times per hour. --- ## 5. Transformation Cache (Price Enrichment Only) **Location:** `coordinator/data_transformation.py` → `_cached_transformed_data` **Status:** ✅ **Clean separation** - enrichment only, no redundancy **What is cached:** ```python { "timestamp": ..., "homes": {...}, "priceInfo": {...}, # Enriched price data (trailing_avg_24h, difference, rating_level) # NO periods - periods are exclusively managed by PeriodCalculator } ``` **Purpose:** Avoid re-enriching price data when config unchanged between midnight checks. **Current behavior:** - Caches **only enriched price data** (price + statistics) - **Does NOT cache periods** (handled by Period Calculation Cache) - Invalidated when: - Config changes (thresholds affect enrichment) - Midnight turnover detected - New update cycle begins **Architecture:** - DataTransformer: Handles price enrichment only - PeriodCalculator: Handles period calculation only (with hash-based cache) - Coordinator: Assembles final data on-demand from both caches **Memory savings:** Eliminating redundant period storage saves ~10KB per coordinator (14% reduction). --- ## Cache Invalidation Flow ### User Changes Options (Config Flow) ``` User saves options ↓ config_entry.add_update_listener() triggers ↓ coordinator._handle_options_update() ↓ ├─> DataTransformer.invalidate_config_cache() │ └─> _config_cache = None │ _config_cache_valid = False │ _cached_transformed_data = None │ └─> PeriodCalculator.invalidate_config_cache() └─> _config_cache = None _config_cache_valid = False _cached_periods = None _last_periods_hash = None ↓ coordinator.async_request_refresh() ↓ Fresh data fetch with new config ``` ### Midnight Turnover (Day Transition) ``` Timer #2 fires at 00:00 ↓ coordinator._handle_midnight_turnover() ↓ ├─> Clear persistent cache │ └─> _cached_price_data = None │ _last_price_update = None │ └─> Clear transformation cache └─> _cached_transformed_data = None _last_transformation_config = None ↓ Period cache auto-invalidates (hash mismatch on new "today") ↓ Fresh API fetch for new day ``` ### Tomorrow Data Arrives (~13:00) ``` Coordinator update cycle ↓ should_update_price_data() checks tomorrow ↓ Tomorrow data missing/invalid ↓ API fetch with new tomorrow data ↓ Price data hash changes (new intervals) ↓ Period cache auto-invalidates (hash mismatch) ↓ Periods recalculated with tomorrow included ``` --- ## Cache Coordination **All caches work together:** ``` Persistent Storage (HA restart) ↓ API Data Cache (price_data, user_data) ↓ ├─> Enrichment (add rating_level, difference, etc.) │ ↓ │ Transformation Cache (_cached_transformed_data) │ └─> Period Calculation ↓ Period Cache (_cached_periods) ↓ Config Cache (avoid re-reading options) ↓ Translation Cache (entity descriptions) ``` **No cache invalidation cascades:** - Config cache invalidation is **explicit** (on options update) - Period cache invalidation is **automatic** (via hash mismatch) - Transformation cache invalidation is **automatic** (on midnight/config change) - Translation cache is **never invalidated** (read-only after load) **Thread safety:** - All caches are accessed from `MainThread` only (Home Assistant event loop) - No locking needed (single-threaded execution model) --- ## Performance Characteristics ### Typical Operation (No Changes) ``` Coordinator Update (every 15 min) ├─> API fetch: SKIP (cache valid) ├─> Config dict build: ~1μs (cached) ├─> Period calculation: ~1ms (cached, hash match) ├─> Transformation: ~10ms (enrichment only, periods cached) └─> Entity updates: ~5ms (translation cache hit) Total: ~16ms (down from ~600ms without caching) ``` ### After Midnight Turnover ``` Coordinator Update (00:00) ├─> API fetch: ~500ms (cache cleared, fetch new day) ├─> Config dict build: ~50μs (rebuild, no cache) ├─> Period calculation: ~200ms (cache miss, recalculate) ├─> Transformation: ~50ms (re-enrich, rebuild) └─> Entity updates: ~5ms (translation cache still valid) Total: ~755ms (expected once per day) ``` ### After Config Change ``` Options Update ├─> Cache invalidation: `<`1ms ├─> Coordinator refresh: ~600ms │ ├─> API fetch: SKIP (data unchanged) │ ├─> Config rebuild: ~50μs │ ├─> Period recalculation: ~200ms (new thresholds) │ ├─> Re-enrichment: ~50ms │ └─> Entity updates: ~5ms └─> Total: ~600ms (expected on manual reconfiguration) ``` --- ## Summary Table | Cache Type | Lifetime | Size | Invalidation | Purpose | |------------|----------|------|--------------|---------| | **API Data** | Hours to 1 day | ~50KB | Midnight, validation | Reduce API calls | | **Translations** | Forever (until HA restart) | ~5KB | Never | Avoid file I/O | | **Config Dicts** | Until options change | `<`1KB | Explicit (options update) | Avoid dict lookups | | **Period Calculation** | Until data/config change | ~10KB | Auto (hash mismatch) | Avoid CPU-intensive calculation | | **Transformation** | Until midnight/config change | ~50KB | Auto (midnight/config) | Avoid re-enrichment | **Total memory overhead:** ~116KB per coordinator instance (main + subentries) **Benefits:** - 97% reduction in API calls (from every 15min to once per day) - 70% reduction in period calculation time (cache hits during normal operation) - 98% reduction in config access time (30+ lookups → 1 cache check) - Zero file I/O during runtime (translations cached at startup) **Trade-offs:** - Memory usage: ~116KB per home (negligible for modern systems) - Code complexity: 5 cache invalidation points (well-tested, documented) - Debugging: Must understand cache lifetime when investigating stale data issues --- ## Debugging Cache Issues ### Symptom: Stale data after config change **Check:** 1. Is `_handle_options_update()` called? (should see "Options updated" log) 2. Are `invalidate_config_cache()` methods executed? 3. Does `async_request_refresh()` trigger? **Fix:** Ensure `config_entry.add_update_listener()` is registered in coordinator init. ### Symptom: Period calculation not updating **Check:** 1. Verify hash changes when data changes: `_compute_periods_hash()` 2. Check `_last_periods_hash` vs `current_hash` 3. Look for "Using cached period calculation" vs "Calculating periods" logs **Fix:** Hash function may not include all relevant data. Review `_compute_periods_hash()` inputs. ### Symptom: Yesterday's prices shown as today **Check:** 1. `is_cache_valid()` logic in `coordinator/cache.py` 2. Midnight turnover execution (Timer #2) 3. Cache clear confirmation in logs **Fix:** Timer may not be firing. Check `_schedule_midnight_turnover()` registration. ### Symptom: Missing translations **Check:** 1. `async_load_translations()` called at startup? 2. Translation files exist in `/translations/` and `/custom_translations/`? 3. Cache population: `_TRANSLATIONS_CACHE` keys **Fix:** Re-install integration or restart HA to reload translation files. --- ## Related Documentation - **[Timer Architecture](./timer-architecture.md)** - Timer system, scheduling, midnight coordination - **[Architecture](./architecture.md)** - Overall system design, data flow - **[AGENTS.md](https://github.com/jpawlowski/hass.tibber_prices/blob/v0.20.0/AGENTS.md)** - Complete reference for AI development