hass.tibber_prices/docs/developer/versioned_docs/version-v0.21.0/timer-architecture.md
Julian Pawlowski dff0faeef5 docs(dev): update GitHub links to use main branch
Changed all documentation links from version-specific tags (v0.20.0) to
main branch references. This makes documentation maintenance-free - links
stay current as code evolves.

Updated 38 files across:
- docs/developer/docs/ (7 files)
- docs/developer/versioned_docs/version-v0.21.0/ (8 files)
- docs/developer/versioned_docs/version-v0.22.0/ (8 files)

Impact: Documentation links no longer break when new versions are released.
Links always point to current code implementation.
2025-12-18 15:15:18 +00:00

433 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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