second version

This commit is contained in:
Dusan Vojacek
2026-04-03 14:23:16 +02:00
parent 897b95f728
commit 9f4126946d
105 changed files with 9738 additions and 1470 deletions

View File

@@ -35,21 +35,23 @@ Samostatná Python služba. Běží jako smyčka, nezávislá na FastAPI.
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.
> Mapování v kódu: `backend/services/telemetry_collector.py` (holding registry, decimal adresa = offset pro `read_holding_registers`).
| Registr (hex) | Typ | Popis | Jednotka | Přepočet |
| Dec (hex) | Typ | Popis | Jednotka | Poznámka |
|---|---|---|---|---|
| 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 |
| 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: **+ vybíjení, nabíjení** |
| 625 (0x0271) | int16 | Výkon sítě | W | signed: **+ import, export** |
| 653 (0x028D) | uint16 | Celková spotřeba | W | `load_power_w` |
| 667 (0x029B) | uint16 | Výkon GEN portu (FVE pole B) | W | `gen_port_power_w`, nelze curtailovat |
| 672 (0x02A0) | uint16 | Výkon PV1 | W | `pv1_power_w` |
| 673 (0x02A1) | uint16 | Výkon PV2 | W | `pv2_power_w` |
`pv_power_w` v DB = **PV1 + PV2 + GEN port** (celková výroba na instalaci home-01).
`gen_port_power_w` zůstává i nadále uložen samostatně pro audit a detailní diagnostiku.
**Zápis setpointů (plánování → Deye):**
@@ -60,8 +62,7 @@ Komunikace: Modbus TCP, Unit ID dle DIP přepínače na střídači (typicky 1).
| 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ů.
Rychlá kontrola komunikace: `scripts/test_modbus_deye.py`.
---
@@ -108,66 +109,7 @@ Komunikace: Modbus TCP přes Waveshare.
## 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))
```
Implementace: `backend/services/telemetry_collector.py``poll_inverter()` používá konstanty `DEYE_REG_*` a třídu `ModbusDevice`; hlavní smyčka je `run_telemetry_loop` / `run_telemetry_loop_wrapper`.
---
@@ -209,7 +151,7 @@ MODBUS_READ_TIMEOUT_SEC=3
## Otevřené body
- [ ] Doplnit přesné Modbus registry Deye z PDF protokolu
- [x] Základní mapování Deye (holding registry 500673) 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