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.
Overviewâ
The integration uses 4 distinct caching layers with different purposes and lifetimes:
- Persistent API Data Cache (HA Storage) - Hours to days
- Translation Cache (Memory) - Forever (until HA restart)
- Config Dictionary Cache (Memory) - Until config changes
- Period Calculation Cache (Memory) - Until price data or config changes
1. Persistent API Data Cacheâ
Location: coordinator/cache.py â HA Storage (.storage/tibber_prices.<entry_id>)
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 GraphQLviewerquery - 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:
-
Midnight turnover (Timer #2 in coordinator):
# 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() -
Cache validation on load:
# 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 -
Tomorrow data check (after 13:00):
# 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:
# 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â
{
"thresholds": {"low": 15, "high": 35},
"volatility_thresholds": {"moderate": 15.0, "high": 25.0, "very_high": 40.0},
# ... 20+ more config fields
}
PeriodCalculator Config Cacheâ
{
"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):
# 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:
{
"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
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:
-
Config change (explicit):
def invalidate_config_cache() -> None:
self._cached_periods = None
self._last_periods_hash = None -
Price data change (automatic via hash mismatch):
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:
{
"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
MainThreadonly (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:
- Is
_handle_options_update()called? (should see "Options updated" log) - Are
invalidate_config_cache()methods executed? - Does
async_request_refresh()trigger?
Fix: Ensure config_entry.add_update_listener() is registered in coordinator init.
Symptom: Period calculation not updatingâ
Check:
- Verify hash changes when data changes:
_compute_periods_hash() - Check
_last_periods_hashvscurrent_hash - 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:
is_cache_valid()logic incoordinator/cache.py- Midnight turnover execution (Timer #2)
- Cache clear confirmation in logs
Fix: Timer may not be firing. Check _schedule_midnight_turnover() registration.
Symptom: Missing translationsâ
Check:
async_load_translations()called at startup?- Translation files exist in
/translations/and/custom_translations/? - Cache population:
_TRANSLATIONS_CACHEkeys
Fix: Re-install integration or restart HA to reload translation files.
Related Documentationâ
- Timer Architecture - Timer system, scheduling, midnight coordination
- Architecture - Overall system design, data flow
- AGENTS.md - Complete reference for AI development