feat(telemetry): idle-skip zápisů — neukládat 1min řádky idle zařízení
Some checks failed
CI and deploy / migration-check (push) Successful in 28s
CI and deploy / deploy (push) Failing after 17m56s

Slabý server: dict (tabulka, asset_id) → (signature, last_stored_at);
_idle_skip ukládá vždy při změně signature, aktivitě, po startu procesu
a heartbeat po > 840 s (každý 15min bucket má ≥ 1 řádek).

- telemetry_ev_charger: aktivní = status != 'available' nebo power > 50 W;
  signature (status, výkon na 100 W)
- telemetry_pool_pump: aktivní = is_on nebo power > 5 W (ON řádky 1/min
  kvůli on_minutes); signature (is_on, výkon na 10 W)
- telemetry_loxone_sensor: jen změna hodnoty ≥ 0.1 / heartbeat
- telemetry_heat_pump: aktivní = mode != 'off' nebo defrost; signature
  (mode, teploty na 0.2 °C)
- telemetry_inverter: beze změny — NIKDY se nepřeskakuje (audit Wh split,
  baseline, SoC plánovače)

Detekce příjezdu/odjezdu EV: previous_status přesunut z posledního řádku DB
do in-memory _EV_LAST_STATUS (po startu seed z vw_latest_ev_charger —
přechod během výpadku se pozná, prázdná DB nevystřelí falešný příjezd);
fn_ev_session_transition se volá jen při změně statusu.

PoolCard: staleness práh 5 → 16 min (> heartbeat 840 s).
Docs: telemetry.md sekce „Idle-skip zápisů" (pravidla pro nové čtecí dotazy:
sumy/gapfill, ne avg přes řádky), planning-changelog (TUV °C/min).
Testy: tests/test_telemetry_idle_skip.py — _idle_skip jednotkově + EV
arrival/departure přežije skip i restart procesu (303 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-12 19:06:41 +02:00
parent f71bc944b4
commit 815a233049
5 changed files with 405 additions and 30 deletions

View File

@@ -127,6 +127,57 @@ Implementace: `backend/services/telemetry_collector.py` — `poll_inverter()` po
---
## Idle-skip zápisů (úspora zápisů na slabém serveru)
Zařízení v klidu negeneruje nový 1min řádek — vzorek se zahodí, pokud je
zařízení **idle** a **signature** (kvantizovaný stav) se nezměnila. Mechanismus:
`_idle_skip(key, signature, is_active, now, max_gap_s=840)` v
`telemetry_collector.py`, modulový stav `(tabulka, asset_id) → (signature,
last_stored_at)`.
**Uloží se vždy, když:** signature se změnila; zařízení je aktivní; od
posledního uložení uplynulo **> 840 s** (heartbeat — každý 15min bucket má
≥ 1 řádek); nebo jde o první vzorek po startu procesu.
| Tabulka | Aktivní = ukládá se 1/min | Signature |
|---|---|---|
| `telemetry_ev_charger` | `status != 'available'` nebo `power_w > 50` | (status, výkon na 100 W) |
| `telemetry_pool_pump` | `is_on` nebo `power_w > 5` | (is_on, výkon na 10 W) |
| `telemetry_loxone_sensor` | nikdy (čidlo) — jen změna/heartbeat | hodnota na 0.1 |
| `telemetry_heat_pump` | `operating_mode != 'off'` nebo defrost | (mode, teploty na 0.2 °C) |
| `telemetry_inverter` | **vždy** — NIKDY se nepřeskakuje | — |
Střídač se nepřeskakuje: je vstupem auditu (per-minute Wh split, 7 směrových
toků), baseline a SoC plánovače — hustá řada je nutná.
**Detekce příjezdu/odjezdu EV** už nečte předchozí status z posledního řádku
DB (ten je při idle-skip řidší), ale z in-memory `_EV_LAST_STATUS`; po startu
procesu se seeduje z `vw_latest_ev_charger` (přechod během výpadku backendu se
pozná, prázdná DB nevystřelí falešný příjezd). `fn_ev_session_transition` se
volá jen při změně statusu.
**Důsledky pro čtecí dotazy (POVINNÉ pravidlo):** nad idle-skip tabulkami
nesmí nový dotaz počítat `avg(power)` přes přítomné řádky — chybějící minuta
znamená „zařízení idle ≈ 0 W“ a avg by aktivitu části okna nadhodnotil.
Správně: **suma / počet minut okna** (`sum(power_w) / 15.0` pro 15min slot),
`time_bucket_gapfill`, nebo delta čítače energie. Poslední hodnota
(`vw_latest_*`, TUV teplota, teplota bazénu) je díky heartbeatu max. ~14 min
stará — staleness prahy musí být > 840 s (PoolCard používá 16 min).
Přizpůsobené čtecí cesty:
- `fn_fill_audit_interval` (R__019): EV a TČ `sum(power_w)/15` místo avg.
- `fn_update_tuv_usage_stats` (R__018): delta TUV normalizovaná na **°C/min**
délkou mezery mezi řádky (`gap_min`), mezery > 30 min vyloučeny.
- `fn_update_baseline_stats` (R__003): beze změny — `coalesce(avg, 0)`
v okně ±30 s; chybějící řádek = 0 W, což při idle-skip platí.
- `vw_pool_pump_day_energy` (R__097): `on_minutes` počítá ON řádky — drží,
protože zapnuté čerpadlo se ukládá každou minutu; kWh je delta čítače.
- `fn_pool_daily_runtime_min`, `fn_planning_site_context` (TUV),
`fn_load_planning_slots_full` (EV status): poslední hodnota — heartbeat stačí.
---
## Agregace 1min → 15min
Prováděna PostgreSQL funkcí `ems.fn_fill_audit_interval()` a navazujícími

View File

@@ -5,6 +5,28 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
---
## 2026-06-12 — idle-skip telemetrie: TUV delta normalizovaná na °C/min
**Problém:** telemetry_collector nově přeskakuje 1min zápisy idle zařízení
(heartbeat 840 s — viz `telemetry.md`, sekce Idle-skip zápisů). Vstupy
plánovače čtené z těchto tabulek nesmí předpokládat hustou 1min řadu.
**Mechanismus:** `fn_update_tuv_usage_stats` (R__018) počítá deltu TUV jako
`(temp lag(temp)) / gap_min` (°C/min, mezery > 30 min vyloučeny) — pro
hustá 1min data numericky identické s původním per-row LAG; po idle-skip bez
až 14× nadhodnocení delty. Ostatní vstupy solveru (poslední TUV teplota v
`fn_planning_site_context`, poslední EV status v `fn_load_planning_slots_full`,
baseline stats) pokrývá heartbeat beze změny. Audit: EV/TČ `sum/15` v R__019.
**Soubory:** `telemetry_collector.py`, `R__018_fn_extended_planning.sql`,
`R__019_fn_fill_audit_interval.sql`, `R__097_vw_pool_pump.sql`, `PoolCard.tsx`,
`docs/04-modules/telemetry.md`.
**Ověření:** `tests/test_telemetry_idle_skip.py` (změna/aktivita/heartbeat/
start; EV arrival přežije skip i restart procesu); celá sada 303 passed.
---
## 2026-06-12 — v2 AKTIVNÍ v produkci + robustnostní trojice „nejistota jako cena"
**Přepnutí (847015f):** `PLANNING_ENGINE_VERSION` default **v2** v deploy compose; v1 běží