# 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 pro `read_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`](control.md) a
[`modbus-registers.md`](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)/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
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`.

```sql
-- 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é)

```env
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

- [x] 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_registers` blok místo jednotlivých registrů
