"use strict";(globalThis.webpackChunkdocs_split_developer=globalThis.webpackChunkdocs_split_developer||[]).push([[7806],{5908:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>h,frontMatter:()=>l,metadata:()=>i,toc:()=>o});const i=JSON.parse('{"id":"timer-architecture","title":"Timer Architecture","description":"This document explains the timer/scheduler system in the Tibber Prices integration - what runs when, why, and how they coordinate.","source":"@site/docs/timer-architecture.md","sourceDirName":".","slug":"/timer-architecture","permalink":"/hass.tibber_prices/developer/timer-architecture","draft":false,"unlisted":false,"editUrl":"https://github.com/jpawlowski/hass.tibber_prices/tree/main/docs/developer/docs/timer-architecture.md","tags":[],"version":"current","lastUpdatedAt":1764985026000,"frontMatter":{"comments":false},"sidebar":"tutorialSidebar","previous":{"title":"Architecture","permalink":"/hass.tibber_prices/developer/architecture"},"next":{"title":"Caching Strategy","permalink":"/hass.tibber_prices/developer/caching-strategy"}}');var s=r(4848),t=r(8453);const l={comments:!1},d="Timer Architecture",c={},o=[{value:"Overview",id:"overview",level:2},{value:"Timer #1: DataUpdateCoordinator (HA Built-in)",id:"timer-1-dataupdatecoordinator-ha-built-in",level:2},{value:"Timer #2: Quarter-Hour Refresh (Custom)",id:"timer-2-quarter-hour-refresh-custom",level:2},{value:"Timer #3: Minute Refresh (Custom)",id:"timer-3-minute-refresh-custom",level:2},{value:"Listener Pattern (Python/HA Terminology)",id:"listener-pattern-pythonha-terminology",level:2},{value:"Timer Coordination Scenarios",id:"timer-coordination-scenarios",level:2},{value:"Scenario 1: Normal Operation (No Midnight)",id:"scenario-1-normal-operation-no-midnight",level:3},{value:"Scenario 2: Midnight Turnover",id:"scenario-2-midnight-turnover",level:3},{value:"Scenario 3: Tomorrow Data Check (After 13:00)",id:"scenario-3-tomorrow-data-check-after-1300",level:3},{value:"Why We Keep HA's Timer (Timer #1)",id:"why-we-keep-has-timer-timer-1",level:2},{value:"Reason 1: Load Distribution on Tibber API",id:"reason-1-load-distribution-on-tibber-api",level:3},{value:"Reason 2: What Timer #1 Actually Checks",id:"reason-2-what-timer-1-actually-checks",level:3},{value:"Reason 3: HA Integration Best Practices",id:"reason-3-ha-integration-best-practices",level:3},{value:"What We DO Synchronize",id:"what-we-do-synchronize",level:3},{value:"Performance Characteristics",id:"performance-characteristics",level:2},{value:"Timer #1 (DataUpdateCoordinator)",id:"timer-1-dataupdatecoordinator",level:3},{value:"Timer #2 (Quarter-Hour Refresh)",id:"timer-2-quarter-hour-refresh",level:3},{value:"Timer #3 (Minute Refresh)",id:"timer-3-minute-refresh",level:3},{value:"Debugging Timer Issues",id:"debugging-timer-issues",level:2},{value:"Check Timer #1 (HA Coordinator)",id:"check-timer-1-ha-coordinator",level:3},{value:"Check Timer #2 (Quarter-Hour)",id:"check-timer-2-quarter-hour",level:3},{value:"Check Timer #3 (Minute)",id:"check-timer-3-minute",level:3},{value:"Common Issues",id:"common-issues",level:3},{value:"Related Documentation",id:"related-documentation",level:2},{value:"Summary",id:"summary",level:2}];function a(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,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.header,{children:(0,s.jsx)(n.h1,{id:"timer-architecture",children:"Timer Architecture"})}),"\n",(0,s.jsx)(n.p,{children:"This document explains the timer/scheduler system in the Tibber Prices integration - what runs when, why, and how they coordinate."}),"\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:"three independent timer mechanisms"})," for different purposes:"]}),"\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:"Timer"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Interval"}),(0,s.jsx)(n.th,{children:"Purpose"}),(0,s.jsx)(n.th,{children:"Trigger Method"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Timer #1"})}),(0,s.jsx)(n.td,{children:"HA built-in"}),(0,s.jsx)(n.td,{children:"15 minutes"}),(0,s.jsx)(n.td,{children:"API data updates"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"DataUpdateCoordinator"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Timer #2"})}),(0,s.jsx)(n.td,{children:"Custom"}),(0,s.jsx)(n.td,{children:":00, :15, :30, :45"}),(0,s.jsx)(n.td,{children:"Entity state refresh"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"async_track_utc_time_change()"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.strong,{children:"Timer #3"})}),(0,s.jsx)(n.td,{children:"Custom"}),(0,s.jsx)(n.td,{children:"Every minute"}),(0,s.jsx)(n.td,{children:"Countdown/progress"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"async_track_utc_time_change()"})})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Key principle:"})," Timer #1 (HA) controls ",(0,s.jsx)(n.strong,{children:"data fetching"}),", Timer #2 controls ",(0,s.jsx)(n.strong,{children:"entity updates"}),", Timer #3 controls ",(0,s.jsx)(n.strong,{children:"timing displays"}),"."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"timer-1-dataupdatecoordinator-ha-built-in",children:"Timer #1: DataUpdateCoordinator (HA Built-in)"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"File:"})," ",(0,s.jsx)(n.code,{children:"coordinator/core.py"})," \u2192 ",(0,s.jsx)(n.code,{children:"TibberPricesDataUpdateCoordinator"})]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Type:"})," Home Assistant's built-in ",(0,s.jsx)(n.code,{children:"DataUpdateCoordinator"})," with ",(0,s.jsx)(n.code,{children:"UPDATE_INTERVAL = 15 minutes"})]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"What it is:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["HA provides this timer system automatically when you inherit from ",(0,s.jsx)(n.code,{children:"DataUpdateCoordinator"})]}),"\n",(0,s.jsxs)(n.li,{children:["Triggers ",(0,s.jsx)(n.code,{children:"_async_update_data()"})," method every 15 minutes"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Not"})," synchronized to clock boundaries (each installation has different start time)"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Purpose:"})," Check if fresh API data is needed, fetch if necessary"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"What it does:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'async def _async_update_data(self) -> TibberPricesData:\n # Step 1: Check midnight turnover FIRST (prevents race with Timer #2)\n if self._check_midnight_turnover_needed(dt_util.now()):\n await self._perform_midnight_data_rotation(dt_util.now())\n # Notify ALL entities after midnight turnover\n return self.data # Early return\n\n # Step 2: Check if we need tomorrow data (after 13:00)\n if self._should_update_price_data() == "tomorrow_check":\n await self._fetch_and_update_data() # Fetch from API\n return self.data\n\n # Step 3: Use cached data (fast path - most common)\n return self.data\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Load Distribution:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Each HA installation starts Timer #1 at different times \u2192 natural distribution"}),"\n",(0,s.jsx)(n.li,{children:'Tomorrow data check adds 0-30s random delay \u2192 prevents "thundering herd" on Tibber API'}),"\n",(0,s.jsx)(n.li,{children:"Result: API load spread over ~30 minutes instead of all at once"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Midnight Coordination:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Atomic check: ",(0,s.jsx)(n.code,{children:"_check_midnight_turnover_needed(now)"})," compares dates only (no side effects)"]}),"\n",(0,s.jsx)(n.li,{children:"If midnight turnover needed \u2192 performs it and returns early"}),"\n",(0,s.jsx)(n.li,{children:"Timer #2 will see turnover already done and skip gracefully"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Why we use HA's timer:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Automatic restart after HA restart"}),"\n",(0,s.jsx)(n.li,{children:"Built-in retry logic for temporary failures"}),"\n",(0,s.jsx)(n.li,{children:"Standard HA integration pattern"}),"\n",(0,s.jsx)(n.li,{children:"Handles backpressure (won't queue up if previous update still running)"}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"timer-2-quarter-hour-refresh-custom",children:"Timer #2: Quarter-Hour Refresh (Custom)"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"File:"})," ",(0,s.jsx)(n.code,{children:"coordinator/listeners.py"})," \u2192 ",(0,s.jsx)(n.code,{children:"ListenerManager.schedule_quarter_hour_refresh()"})]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Type:"})," Custom timer using ",(0,s.jsx)(n.code,{children:"async_track_utc_time_change(minute=[0, 15, 30, 45], second=0)"})]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Purpose:"})," Update time-sensitive entity states at interval boundaries ",(0,s.jsx)(n.strong,{children:"without waiting for API poll"})]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Problem it solves:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Timer #1 runs every 15 minutes but NOT synchronized to clock (:03, :18, :33, :48)"}),"\n",(0,s.jsx)(n.li,{children:"Current price changes at :00, :15, :30, :45 \u2192 entities would show stale data for up to 15 minutes"}),"\n",(0,s.jsx)(n.li,{children:"Example: 14:00 new price, but Timer #1 ran at 13:58 \u2192 next update at 14:13 \u2192 users see old price until 14:13"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"What it does:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:"async def _handle_quarter_hour_refresh(self, now: datetime) -> None:\n # Step 1: Check midnight turnover (coordinates with Timer #1)\n if self._check_midnight_turnover_needed(now):\n # Timer #1 might have already done this \u2192 atomic check handles it\n await self._perform_midnight_data_rotation(now)\n # Notify ALL entities after midnight turnover\n return\n\n # Step 2: Normal quarter-hour refresh (most common path)\n # Only notify time-sensitive entities (current_interval_price, etc.)\n self._listener_manager.async_update_time_sensitive_listeners()\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Smart Boundary Tolerance:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Uses ",(0,s.jsx)(n.code,{children:"round_to_nearest_quarter_hour()"})," with \xb12 second tolerance"]}),"\n",(0,s.jsx)(n.li,{children:"HA may schedule timer at 14:59:58 \u2192 rounds to 15:00:00 (shows new interval)"}),"\n",(0,s.jsx)(n.li,{children:"HA restart at 14:59:30 \u2192 stays at 14:45:00 (shows current interval)"}),"\n",(0,s.jsxs)(n.li,{children:["See ",(0,s.jsx)(n.a,{href:"/hass.tibber_prices/developer/architecture#3-quarter-hour-precision",children:"Architecture"})," for details"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Absolute Time Scheduling:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"async_track_utc_time_change()"})," plans for ",(0,s.jsx)(n.strong,{children:"all future boundaries"})," (15:00, 15:15, 15:30, ...)"]}),"\n",(0,s.jsx)(n.li,{children:'NOT relative delays ("in 15 minutes")'}),"\n",(0,s.jsx)(n.li,{children:"If triggered at 14:59:58 \u2192 next trigger is 15:15:00, NOT 15:00:00 (prevents double updates)"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Which entities listen:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:['All sensors that depend on "current interval" (e.g., ',(0,s.jsx)(n.code,{children:"current_interval_price"}),", ",(0,s.jsx)(n.code,{children:"next_interval_price"}),")"]}),"\n",(0,s.jsxs)(n.li,{children:['Binary sensors that check "is now in period?" (e.g., ',(0,s.jsx)(n.code,{children:"best_price_period_active"}),")"]}),"\n",(0,s.jsx)(n.li,{children:"~50-60 entities out of 120+ total"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Why custom timer:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"HA's built-in coordinator doesn't support exact boundary timing"}),"\n",(0,s.jsxs)(n.li,{children:["We need ",(0,s.jsx)(n.strong,{children:"absolute time"})," triggers, not periodic intervals"]}),"\n",(0,s.jsx)(n.li,{children:"Allows fast entity updates without expensive data transformation"}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"timer-3-minute-refresh-custom",children:"Timer #3: Minute Refresh (Custom)"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"File:"})," ",(0,s.jsx)(n.code,{children:"coordinator/listeners.py"})," \u2192 ",(0,s.jsx)(n.code,{children:"ListenerManager.schedule_minute_refresh()"})]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Type:"})," Custom timer using ",(0,s.jsx)(n.code,{children:"async_track_utc_time_change(second=0)"})," (every minute)"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Purpose:"})," Update countdown and progress sensors for smooth UX"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"What it does:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:"async def _handle_minute_refresh(self, now: datetime) -> None:\n # Only notify minute-update entities\n # No data fetching, no transformation, no midnight handling\n self._listener_manager.async_update_minute_listeners()\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Which entities listen:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"best_price_remaining_minutes"})," - Countdown timer"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"peak_price_remaining_minutes"})," - Countdown timer"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"best_price_progress"})," - Progress bar (0-100%)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"peak_price_progress"})," - Progress bar (0-100%)"]}),"\n",(0,s.jsx)(n.li,{children:"~10 entities total"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Why custom timer:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Users want smooth countdowns (not jumping 15 minutes at a time)"}),"\n",(0,s.jsx)(n.li,{children:"Progress bars need minute-by-minute updates"}),"\n",(0,s.jsx)(n.li,{children:"Very lightweight (no data processing, just state recalculation)"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Why NOT every second:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Minute precision sufficient for countdown UX"}),"\n",(0,s.jsx)(n.li,{children:"Reduces CPU load (60\xd7 fewer updates than seconds)"}),"\n",(0,s.jsx)(n.li,{children:"Home Assistant best practice (avoid sub-minute updates)"}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"listener-pattern-pythonha-terminology",children:"Listener Pattern (Python/HA Terminology)"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Your question:"})," \"Sind Timer f\xfcr dich eigentlich 'Listener'?\""]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Answer:"})," In Home Assistant terminology:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Timer"})," = The mechanism that triggers at specific times (",(0,s.jsx)(n.code,{children:"async_track_utc_time_change"}),")"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Listener"})," = A callback function that gets called when timer triggers"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Observer Pattern"})," = Entities register callbacks, coordinator notifies them"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"How it works:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:"# Entity registers a listener callback\nclass TibberPricesSensor(CoordinatorEntity):\n async def async_added_to_hass(self):\n # Register this entity's update callback\n self._remove_listener = self.coordinator.async_add_time_sensitive_listener(\n self._handle_coordinator_update\n )\n\n# Coordinator maintains list of listeners\nclass ListenerManager:\n def __init__(self):\n self._time_sensitive_listeners = [] # List of callbacks\n\n def async_add_time_sensitive_listener(self, callback):\n self._time_sensitive_listeners.append(callback)\n\n def async_update_time_sensitive_listeners(self):\n # Timer triggered \u2192 notify all listeners\n for callback in self._time_sensitive_listeners:\n callback() # Entity updates itself\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Why this pattern:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Decouples timer logic from entity logic"}),"\n",(0,s.jsx)(n.li,{children:"One timer can notify many entities efficiently"}),"\n",(0,s.jsx)(n.li,{children:"Entities can unregister when removed (cleanup)"}),"\n",(0,s.jsx)(n.li,{children:"Standard HA pattern for coordinator-based integrations"}),"\n"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"timer-coordination-scenarios",children:"Timer Coordination Scenarios"}),"\n",(0,s.jsx)(n.h3,{id:"scenario-1-normal-operation-no-midnight",children:"Scenario 1: Normal Operation (No Midnight)"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"14:00:00 \u2192 Timer #2 triggers\n \u2192 Update time-sensitive entities (current price changed)\n \u2192 60 entities updated (~5ms)\n\n14:03:12 \u2192 Timer #1 triggers (HA's 15-min cycle)\n \u2192 Check if tomorrow data needed (no, still cached)\n \u2192 Return cached data (fast path, ~2ms)\n\n14:15:00 \u2192 Timer #2 triggers\n \u2192 Update time-sensitive entities\n \u2192 60 entities updated (~5ms)\n\n14:16:00 \u2192 Timer #3 triggers\n \u2192 Update countdown/progress entities\n \u2192 10 entities updated (~1ms)\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Key observation:"})," Timer #1 and Timer #2 run ",(0,s.jsx)(n.strong,{children:"independently"}),", no conflicts."]}),"\n",(0,s.jsx)(n.h3,{id:"scenario-2-midnight-turnover",children:"Scenario 2: Midnight Turnover"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"23:45:12 \u2192 Timer #1 triggers\n \u2192 Check midnight: current_date=2025-11-17, last_check=2025-11-17\n \u2192 No turnover needed\n \u2192 Return cached data\n\n00:00:00 \u2192 Timer #2 triggers FIRST (synchronized to midnight)\n \u2192 Check midnight: current_date=2025-11-18, last_check=2025-11-17\n \u2192 Turnover needed! Perform rotation, save cache\n \u2192 _last_midnight_check = 2025-11-18\n \u2192 Notify ALL entities\n\n00:03:12 \u2192 Timer #1 triggers (its regular cycle)\n \u2192 Check midnight: current_date=2025-11-18, last_check=2025-11-18\n \u2192 Turnover already done \u2192 skip\n \u2192 Return existing data (fast path)\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Key observation:"})," Atomic date comparison prevents double-turnover, whoever runs first wins."]}),"\n",(0,s.jsx)(n.h3,{id:"scenario-3-tomorrow-data-check-after-1300",children:"Scenario 3: Tomorrow Data Check (After 13:00)"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"13:00:00 \u2192 Timer #2 triggers\n \u2192 Normal quarter-hour refresh\n \u2192 Update time-sensitive entities\n\n13:03:12 \u2192 Timer #1 triggers\n \u2192 Check tomorrow data: missing or invalid\n \u2192 Fetch from Tibber API (~300ms)\n \u2192 Transform data (~200ms)\n \u2192 Calculate periods (~100ms)\n \u2192 Notify ALL entities (new data available)\n\n13:15:00 \u2192 Timer #2 triggers\n \u2192 Normal quarter-hour refresh (uses newly fetched data)\n \u2192 Update time-sensitive entities\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Key observation:"})," Timer #1 does expensive work (API + transform), Timer #2 does cheap work (entity notify)."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"why-we-keep-has-timer-timer-1",children:"Why We Keep HA's Timer (Timer #1)"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Your question:"}),' "warum wir den HA timer trotzdem weiter benutzen, da er ja f\xfcr uns unkontrollierte aktualisierte \xe4nderungen triggert"']}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Answer:"})," You're correct that it's not synchronized, but that's actually ",(0,s.jsx)(n.strong,{children:"intentional"}),":"]}),"\n",(0,s.jsx)(n.h3,{id:"reason-1-load-distribution-on-tibber-api",children:"Reason 1: Load Distribution on Tibber API"}),"\n",(0,s.jsx)(n.p,{children:"If all installations used synchronized timers:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"\u274c Everyone fetches at 13:00:00 \u2192 Tibber API overload"}),"\n",(0,s.jsx)(n.li,{children:"\u274c Everyone fetches at 14:00:00 \u2192 Tibber API overload"}),"\n",(0,s.jsx)(n.li,{children:'\u274c "Thundering herd" problem'}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"With HA's unsynchronized timer:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"\u2705 Installation A: 13:03:12, 13:18:12, 13:33:12, ..."}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Installation B: 13:07:45, 13:22:45, 13:37:45, ..."}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Installation C: 13:11:28, 13:26:28, 13:41:28, ..."}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Natural distribution over ~30 minutes"}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Plus: Random 0-30s delay on tomorrow checks"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Result:"})," API load spread evenly, no spikes."]}),"\n",(0,s.jsx)(n.h3,{id:"reason-2-what-timer-1-actually-checks",children:"Reason 2: What Timer #1 Actually Checks"}),"\n",(0,s.jsx)(n.p,{children:"Timer #1 does NOT blindly update. It checks:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'def _should_update_price_data(self) -> str:\n # Check 1: Do we have tomorrow data? (only relevant after ~13:00)\n if tomorrow_missing or tomorrow_invalid:\n return "tomorrow_check" # Fetch needed\n\n # Check 2: Is cache still valid?\n if cache_valid:\n return "cached" # No fetch needed (most common!)\n\n # Check 3: Has enough time passed?\n if time_since_last_update < threshold:\n return "cached" # Too soon, skip fetch\n\n return "update_needed" # Rare case\n'})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Most Timer #1 cycles:"})," Fast path (~2ms), no API call, just returns cached data."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"API fetch only when:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Tomorrow data missing/invalid (after 13:00)"}),"\n",(0,s.jsx)(n.li,{children:"Cache expired (midnight turnover)"}),"\n",(0,s.jsx)(n.li,{children:"Explicit user refresh"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"reason-3-ha-integration-best-practices",children:"Reason 3: HA Integration Best Practices"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\u2705 Standard HA pattern: ",(0,s.jsx)(n.code,{children:"DataUpdateCoordinator"})," is recommended by HA docs"]}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Automatic retry logic for temporary API failures"}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Backpressure handling (won't queue updates if previous still running)"}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Developer tools integration (users can manually trigger refresh)"}),"\n",(0,s.jsx)(n.li,{children:"\u2705 Diagnostics integration (shows last update time, success/failure)"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"what-we-do-synchronize",children:"What We DO Synchronize"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\u2705 ",(0,s.jsx)(n.strong,{children:"Timer #2:"})," Entity state updates at exact boundaries (user-visible)"]}),"\n",(0,s.jsxs)(n.li,{children:["\u2705 ",(0,s.jsx)(n.strong,{children:"Timer #3:"})," Countdown/progress at exact minutes (user-visible)"]}),"\n",(0,s.jsxs)(n.li,{children:["\u274c ",(0,s.jsx)(n.strong,{children:"Timer #1:"})," API fetch timing (invisible to user, distribution wanted)"]}),"\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:"timer-1-dataupdatecoordinator",children:"Timer #1 (DataUpdateCoordinator)"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Triggers:"})," Every 15 minutes (unsynchronized)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Fast path:"})," ~2ms (cache check, return existing data)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Slow path:"})," ~600ms (API fetch + transform + calculate)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Frequency:"})," ~96 times/day"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"API calls:"})," ~1-2 times/day (cached otherwise)"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"timer-2-quarter-hour-refresh",children:"Timer #2 (Quarter-Hour Refresh)"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Triggers:"})," 96 times/day (exact boundaries)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Processing:"})," ~5ms (notify 60 entities)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"No API calls:"})," Uses cached/transformed data"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"No transformation:"})," Just entity state updates"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"timer-3-minute-refresh",children:"Timer #3 (Minute Refresh)"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Triggers:"})," 1440 times/day (every minute)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Processing:"})," ~1ms (notify 10 entities)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"No API calls:"})," No data processing at all"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Lightweight:"})," Just countdown math"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Total CPU budget:"})," ~15 seconds/day for all timers combined."]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"debugging-timer-issues",children:"Debugging Timer Issues"}),"\n",(0,s.jsx)(n.h3,{id:"check-timer-1-ha-coordinator",children:"Check Timer #1 (HA Coordinator)"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# Enable debug logging\n_LOGGER.setLevel(logging.DEBUG)\n\n# Watch for these log messages:\n"Fetching data from API (reason: tomorrow_check)" # API call\n"Using cached data (no update needed)" # Fast path\n"Midnight turnover detected (Timer #1)" # Turnover\n'})}),"\n",(0,s.jsx)(n.h3,{id:"check-timer-2-quarter-hour",children:"Check Timer #2 (Quarter-Hour)"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# Watch coordinator logs:\n"Updated 60 time-sensitive entities at quarter-hour boundary" # Normal\n"Midnight turnover detected (Timer #2)" # Turnover\n'})}),"\n",(0,s.jsx)(n.h3,{id:"check-timer-3-minute",children:"Check Timer #3 (Minute)"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",children:'# Watch coordinator logs:\n"Updated 10 minute-update entities" # Every minute\n'})}),"\n",(0,s.jsx)(n.h3,{id:"common-issues",children:"Common Issues"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Timer #2 not triggering:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Check: ",(0,s.jsx)(n.code,{children:"schedule_quarter_hour_refresh()"})," called in ",(0,s.jsx)(n.code,{children:"__init__"}),"?"]}),"\n",(0,s.jsxs)(n.li,{children:["Check: ",(0,s.jsx)(n.code,{children:"_quarter_hour_timer_cancel"})," properly stored?"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Double updates at midnight:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Should NOT happen (atomic coordination)"}),"\n",(0,s.jsx)(n.li,{children:"Check: Both timers use same date comparison logic?"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"API overload:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Check: Random delay working? (0-30s jitter on tomorrow check)"}),"\n",(0,s.jsx)(n.li,{children:"Check: Cache validation logic correct?"}),"\n"]}),"\n"]}),"\n"]}),"\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/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:"/hass.tibber_prices/developer/caching-strategy",children:"Caching Strategy"})})," - Cache lifetimes, invalidation, midnight turnover"]}),"\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"]}),"\n",(0,s.jsx)(n.hr,{}),"\n",(0,s.jsx)(n.h2,{id:"summary",children:"Summary"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Three independent timers:"})}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Timer #1"})," (HA built-in, 15 min, unsynchronized) \u2192 Data fetching (when needed)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Timer #2"})," (Custom, :00/:15/:30/:45) \u2192 Entity state updates (always)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Timer #3"})," (Custom, every minute) \u2192 Countdown/progress (always)"]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Key insights:"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Timer #1 unsynchronized = good (load distribution on API)"}),"\n",(0,s.jsx)(n.li,{children:"Timer #2 synchronized = good (user sees correct data immediately)"}),"\n",(0,s.jsx)(n.li,{children:"Timer #3 synchronized = good (smooth countdown UX)"}),"\n",(0,s.jsx)(n.li,{children:"All three coordinate gracefully (atomic midnight checks, no conflicts)"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:'"Listener" terminology:'})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Timer = mechanism that triggers"}),"\n",(0,s.jsx)(n.li,{children:"Listener = callback that gets called"}),"\n",(0,s.jsx)(n.li,{children:"Observer pattern = entities register, coordinator notifies"}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}}}]);