Files
ems/docs/04-modules/telemetry.md
Dusan Vojacek 8b4af663d8 Initial commit
Made-with: Cursor
2026-03-20 13:27:44 +01:00

217 lines
8.0 KiB
Markdown
Raw 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.
# Modul: Telemetry (Sběr dat ze zařízení)
## Co modul dělá
- Čte data ze střídače Deye, EV nabíječek Teltonika a tepelného čerpadla Samsung přes Modbus TCP
- 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 | |
| Teltonika EV nabíječka 2 | 60 s | |
| Samsung tepelné čerpadlo | 60 s | |
### Chování při chybě
- Chyba komunikace: záznam se nezapíše, chyba se loguje
- 3 po sobě jdoucí chyby = alert (log WARNING)
- 10 po sobě jdoucích chyb = log ERROR + pokus o reconnect
- 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).
> Registry jsou specifické pro Deye SUN-20K-SG01LP1-EU.
> Finální hodnoty ověřit z Deye Modbus protokolu / Loxone šablony.
| Registr (hex) | Typ | Popis | Jednotka | Přepočet |
|---|---|---|---|---|
| 0x0215 | Read Holding | PV celkový výkon | W | ×1 |
| 0x0103 | Read Holding | Battery SoC | % | ×1 |
| 0x0105 | Read Holding | Battery power | W | signed, kladné=nabíjení |
| 0x0101 | Read Holding | Battery voltage | 0.1V | ×0.1 |
| 0x0169 | Read Holding | Grid power | W | signed, kladné=import |
| 0x016F | Read Holding | Grid voltage L1 | 0.1V | ×0.1 |
| 0x0213 | Read Holding | Load power | W | ×1 |
| 0x0220 | Read Holding | Inverter temperature | 0.1°C | ×0.1 |
| 0x0168 | Read Holding | Operating mode | enum | viz tabulka módů |
| 0x0180 | Read Holding | Fault code | bitfield | 0=ok |
**Zápis setpointů (plánování → Deye):**
| 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) |
> **TODO:** Přesné registry doplnit z Deye SUN-20K Modbus protokolu PDF.
> Loxone šablona pro Deye je dobrý výchozí bod pro mapování registrů.
---
## 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 (632A) |
| 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)
```python
# backend/services/telemetry_collector.py
import asyncio
from pymodbus.client import AsyncModbusTcpClient
from datetime import datetime, timezone
async def poll_inverter(site_id: int, inverter: AssetInverter, endpoint: SiteEndpoint, db):
"""Přečte všechny registry Deye a uloží záznam do telemetry_inverter."""
async with AsyncModbusTcpClient(endpoint.host, port=endpoint.port) as client:
try:
# Čtení bloku registrů (optimalizovat jako jeden read multiple)
pv_power = await read_register(client, 0x0215, endpoint.unit_id)
batt_soc = await read_register(client, 0x0103, endpoint.unit_id)
batt_power = await read_register_signed(client, 0x0105, endpoint.unit_id)
batt_voltage = await read_register(client, 0x0101, endpoint.unit_id) / 10.0
grid_power = await read_register_signed(client, 0x0169, endpoint.unit_id)
grid_voltage = await read_register(client, 0x016F, endpoint.unit_id) / 10.0
load_power = await read_register(client, 0x0213, endpoint.unit_id)
inv_temp = await read_register(client, 0x0220, endpoint.unit_id) / 10.0
op_mode = await read_register(client, 0x0168, endpoint.unit_id)
fault_code = await read_register(client, 0x0180, endpoint.unit_id)
await db.execute("""
INSERT INTO ems.telemetry_inverter
(site_id, inverter_id, measured_at,
pv_power_w, battery_soc_percent, battery_power_w, battery_voltage_v,
grid_power_w, grid_voltage_v, load_power_w,
inverter_temp_c, operating_mode, fault_code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
ON CONFLICT (inverter_id, measured_at) DO NOTHING
""",
site_id, inverter.id, datetime.now(timezone.utc),
pv_power, batt_soc, batt_power, batt_voltage,
grid_power, grid_voltage, load_power,
inv_temp, str(op_mode), fault_code
)
except Exception as e:
logger.warning(f"Inverter poll failed [{inverter.code}]: {e}")
raise
async def run_collector(db):
"""Hlavní smyčka každých 60s sbírá data ze všech aktivních zařízení."""
while True:
start = asyncio.get_event_loop().time()
sites = await db.fetch("SELECT id FROM ems.site WHERE active = true")
for site in sites:
await asyncio.gather(
poll_all_inverters(site.id, db),
poll_all_ev_chargers(site.id, db),
poll_all_heat_pumps(site.id, db),
return_exceptions=True # jeden výpadek nezastaví ostatní
)
elapsed = asyncio.get_event_loop().time() - start
await asyncio.sleep(max(0, 60 - elapsed))
```
---
## Agregace 1min → 15min
Prováděna PostgreSQL funkcí `ems.fn_fill_audit_interval()` a `ems.fn_fill_baseline_consumption()`.
Spouštěna každých 15 minut jako scheduled task (Python APScheduler nebo pg_cron).
```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);
```
---
## Konfigurace (env proměnné)
```env
TELEMETRY_POLL_INTERVAL_SEC=60
TELEMETRY_ERROR_WARN_THRESHOLD=3 # počet chyb před WARNING logem
TELEMETRY_ERROR_RECONNECT_THRESHOLD=10
MODBUS_CONNECT_TIMEOUT_SEC=5
MODBUS_READ_TIMEOUT_SEC=3
```
---
## Otevřené body
- [ ] Doplnit přesné Modbus registry Deye z PDF protokolu
- [ ] 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ů