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>
11 KiB
Modul: Telemetry (Sběr dat ze zařízení)
Co modul dělá
- Čte data ze střídače Deye přes Modbus TCP
- EV nabíječky Teltonika a tepelné čerpadlo Samsung mají zatím placeholder vzorky; konkrétní registry jsou TODO
- Ukládá surová měření do DB (1min granularita)
- Detekuje výpadky komunikace a loguje chyby
- Agreguje 1min data na 15min průměry pro spotřebu, audit a plánování
Komponenta: telemetry_collector (Python service)
Samostatná Python služba. Běží jako smyčka, nezávislá na FastAPI.
Polling intervaly
| Zařízení | Interval | Důvod |
|---|---|---|
| Deye střídač | 60 s | 1min granularita telemetrie |
| Teltonika EV nabíječka 1 | 60 s | zatím placeholder available, 0 W |
| Teltonika EV nabíječka 2 | 60 s | zatím placeholder available, 0 W |
| Samsung tepelné čerpadlo | 60 s | zatím placeholder hodnoty |
Chování při chybě
- Chyba komunikace: záznam se nezapíše, chyba se loguje
- Kód zatím nedrží počítadlo po sobě jdoucích chyb podle zařízení; chyby se logují při jednotlivých poll pokusech
- Data se neinterpolují – chybějící minuty zůstanou prázdné (audit to pozná)
Deye SUN-20K – Modbus registry
Komunikace: Modbus TCP, Unit ID dle DIP přepínače na střídači (typicky 1).
Mapování v kódu:
backend/services/telemetry_collector.py(holding registry, decimal adresa = offset proread_holding_registers).
| Dec (hex) | Typ | Popis | Jednotka | Poznámka |
|---|---|---|---|---|
| 500 (0x01F4) | uint16 | Provozní stav střídače | enum | raw do run_state, ladění |
| 514 (0x0202) | uint16 | Dnešní nabití baterie | Wh | batt_charge_today_wh |
| 515 (0x0203) | uint16 | Dnešní vybití baterie | Wh | batt_discharge_today_wh |
| 588 (0x024C) | uint16 | Battery SoC | % | battery_soc_percent |
| 590 (0x024E) | int16 | Tok výkonu baterie | W | signed z Deye; v DB battery_power_w platí + nabíjení, − vybíjení |
| 625 (0x0271) | int16 | Výkon sítě | W | signed: + import, − export |
| 653 (0x028D) | uint16 | Celková spotřeba | W | load_power_w |
| 667 (0x029B) | int16 | Výkon GEN portu (FVE pole B) | W (signed) | gen_port_power_w; záporné při zpětném toku / bez výroby — číst signed |
| 672 (0x02A0) | int16 | Výkon PV1 | W (signed) | pv1_power_w; při špatném unsigned čtení se záporné hodnoty jeví jako desítky kW |
| 673 (0x02A1) | int16 | Výkon PV2 | W (signed) | pv2_power_w |
pv1_power_w / pv2_power_w / gen_port_power_w v DB = signed W z Modbus (mohou být záporné).
pv_power_w = max(0, PV1) + max(0, PV2) + max(0, GEN) — okamžitá kladná výroba FVE pro dashboard a souhrny; záporné kanály do součtu nepatří (typicky večer při exportu z baterie do sítě).
gen_port_power_w zůstává uložen samostatně pro audit zeleného bonusu a diagnostiku.
Ověření po změně sběru: za denního SVitu zkontrolovat, že pv_power_w a jednotlivé kanály odpovídají očekávanému max. výkonu instalace (logika: aggregate_pv_production_w v telemetry_collector.py, unit testy tests/test_telemetry_pv_signed.py).
Zápis setpointů (plánování → Deye):
Aktuální řízení Deye je popsané v control.md a
modbus-registers.md. Nepoužívá starý write_register
model, ale journal ems.modbus_command a FC 0x10 (write_registers) pro
registry 108/109/141/142/143/145/178/340 + TOU bloky.
Historická orientační mapa níže neplatí jako implementační kontrakt:
| Registr (hex) | Typ | Popis | Hodnota |
|---|---|---|---|
| 0x00F3 | Write Single | Battery charge power limit | W |
| 0x00F4 | Write Single | Battery discharge power limit | W |
| 0x00F6 | Write Single | Grid export power limit | W |
| 0x00F0 | Write Single | Work mode | enum (viz tabulka) |
Rychlá kontrola komunikace: scripts/test_modbus_deye.py.
Teltonika TeltoCharge – Modbus registry
Komunikace: Modbus TCP přes Waveshare, Unit ID = 1 (ověřit).
Registry doplnit z Teltonika TeltoCharge Modbus dokumentace / Loxone šablony.
| Registr | Typ | Popis | Jednotka |
|---|---|---|---|
| TBD | Read | Stav konektoru (OCPP status enum) | enum |
| TBD | Read | Aktuální výkon | W |
| TBD | Read | Kumulativní energie session | Wh |
| TBD | Read | Proud L1/L2/L3 | 0.1A |
| TBD | Read | Napětí | 0.1V |
| TBD | Read | Session ID | uint |
| TBD | Read | Error code | uint |
| TBD | Write | Max proud (charge limit) | A (6–32A) |
| TBD | Write | Povolení nabíjení (on/off) | bool |
Samsung tepelné čerpadlo – Modbus registry
Komunikace: Modbus TCP přes Waveshare.
Registry doplnit ze Samsung NASA Modbus dokumentace / Loxone šablony.
| Registr | Typ | Popis | Jednotka |
|---|---|---|---|
| TBD | Read | Venkovní teplota | 0.1°C |
| TBD | Read | Teplota vody vstup | 0.1°C |
| TBD | Read | Teplota vody výstup | 0.1°C |
| TBD | Read | Teplota zásobníku TUV | 0.1°C |
| TBD | Read | Příkon | W |
| TBD | Read | Provozní režim | enum |
| TBD | Read | Alarm kód | uint |
| TBD | Read | Odmrazování aktivní | bool |
| TBD | Write | Povolení provozu | bool |
| TBD | Write | Požadovaná teplota TUV | °C |
Kód telemetrie (Python)
Implementace: backend/services/telemetry_collector.py — poll_inverter() používá konstanty DEYE_REG_* a sdíleného Modbus klienta z services.modbus_client; hlavní smyčka je run_telemetry_loop / run_telemetry_loop_wrapper.
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)/15mí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_minutespočí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
funkcemi pro baseline/accuracy. Spouští ji Python APScheduler: audit filler v
minutách :01,:16,:31,:46, forecast accuracy v :02,:17,:32,:47.
-- Příklad agregace telemetrie na 15min průměr
-- (součást fn_fill_audit_interval)
SELECT
site_id,
time_bucket('15 minutes', measured_at) AS interval_start,
AVG(pv_power_w)::INT AS avg_pv_power_w,
AVG(battery_power_w)::INT AS avg_battery_power_w,
AVG(grid_power_w)::INT AS avg_grid_power_w,
AVG(load_power_w)::INT AS avg_load_power_w,
LAST(battery_soc_percent, measured_at) AS last_soc_pct
FROM ems.telemetry_inverter
WHERE measured_at >= $1 AND measured_at < $1 + INTERVAL '15 minutes'
AND site_id = $2
GROUP BY site_id, time_bucket('15 minutes', measured_at);
Timescale continuous aggregates (střídač → dashboard)
Nad ems.telemetry_inverter běží dva continuous aggregate (TimescaleDB); oba se periodicky obnovují (řádově každých 15 minut). Definice CA je ve verzovaných migracích (V011, V039); view nad CA držíme v repeatable souborech (db/views/R__*.sql), aby šla měnit jedna aktuální definice bez nové V migrace.
| Objekt | Bucket | View pro PostgREST / UI | Poznámka |
|---|---|---|---|
ems.telemetry_inverter_hourly |
1 hodina | ems.vw_telemetry_hourly_7d |
CA a view v V011; security_invoker v V031. Hodinové trendy. |
ems.telemetry_inverter_15m |
15 minut | ems.vw_telemetry_15m_7d |
db/views/R__071_vw_telemetry_15m_7d.sql – posledních 7 dní, zarovnání s 15min sloty přehledu. |
Frontend přehled (frontend/src/hooks/useDashboardData.ts): skutečné výkony a SoC po slotech bere z /vw_telemetry_15m_7d (klíč slotu = začátek 15min intervalu v UTC, stejně jako floorSlotUtcMs v grafu). Horní karty a aktuální SoC v grafu jsou dál z vw_site_status (poslední 1min vzorek) a z WebSocketu /ws/telemetry, aby „teď“ odpovídalo boxu i po refreshi agregátu.
Plánovač počáteční SoC nečte z těchto view – bere poslední řádek z ems.telemetry_inverter (planning_engine._load_site_context).
Konfigurace (env proměnné)
TELEMETRY_POLL_INTERVAL_SEC=60
MODBUS_CONNECT_TIMEOUT_SEC=5
MODBUS_READ_TIMEOUT_SEC=3
TELEMETRY_POLL_INTERVAL_SEC a chybové prahy zatím nejsou v kódu používány;
smyčka běží každých 60 s přímo v run_telemetry_loop_wrapper.
Otevřené body
- Základní mapování Deye (holding registry 500–673) v
telemetry_collector.py - Doplnit Modbus registry Teltonika z dokumentace / Loxone šablony
- Doplnit Modbus registry Samsung z dokumentace / Loxone šablony
- Ověřit Unit ID všech zařízení při instalaci
- Optimalizovat čtení Deye jako jeden
read_holding_registersblok místo jednotlivých registrů