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

8.0 KiB
Raw Blame History

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)

# 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).

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

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ů