# 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 (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) ```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ů