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