Initial commit
Made-with: Cursor
This commit is contained in:
216
docs/04-modules/telemetry.md
Normal file
216
docs/04-modules/telemetry.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# 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ů
|
||||
Reference in New Issue
Block a user