Files
ems/docs/04-modules/modbus-registers.md

12 KiB
Raw Blame History

Deye Modbus Registry EMS řízení

Důležité pravidlo

  • Registry 60499: POUZE FC 0x10 (write_registers)
  • Registry 059: FC 0x03 čtení, FC 0x06 zápis
  • Registry 500+: FC 0x03 pouze čtení

EMS zapisuje řídící hodnoty přes journal (modbus_command) a write_registers (FC 0x10), nikdy write_register (FC 0x06) pro rozsah 60499.

Řídící registry (R/W, FC 0x10)

Reg Název Rozsah Jednotka Použití v EMS
108 Max charge current 0 … max dle modelu (manuál Deye) 1 A Limit nabíjení baterie; horní mez není napříč modely stejná (nižší výkonové řady mívají jiný strop než např. SUN-20K)
109 Max discharge current 0 … max dle modelu (manuál Deye) 1 A Limit vybíjení baterie; viz výše
128 Grid charge current 0 … max dle modelu (manuál Deye) 1 A Nabíjení ze sítě
130 Grid charge enable 0/1 1 = povolit nabíjení ze sítě
141 Energy mgmt mode bitmask EMS vždy 0 (neměnit jinak)
142 Limit control 0/1/2 0 = selling first, 1 = zero export (built-in CT); EMS přepíná export vs. idle/nabíjení
143 Export limit W závisí na typu (SUN-20K až ~13 500) 1 W Max export do sítě; hodnota z site_grid_connection.max_export_power_w
178 Grid peak shaving switch bitmask EMS zapisuje pevnou hodnotu (bez read-modify-write kvůli kolizím s paralelním čtením z Loxone): 32 (0b00100000, bit45 = 10) v režimu SELL; 48 (0b00110000, bit45 = 11) v PASSIVE a CHARGE.
190 GEN peak shaving 016000 1 W Peak shaving na GEN portu
191 Grid peak shaving power 016000 1 W EMS NEZAPISUJE nastavit manuálně v SolarmanApp. Hodnota určuje výkon peak shavingu v W.

control_exporter.write_inverter_setpoints zapisuje přes modbus_command (journal; jeden řádek na registr) a execute_modbus_commands odesílá souvislé bloky jedním FC 0x10 (např. 6264, 148159, 166177, 108109, 141142 podle toho, co je ve frontě). Pořadí v journalu: 6264 (čas, viz níže), time points 148177 (jen řádky zařazené do daného běhu), 108, 109, 141, 142, 178, 143. Popisné názvy v DB bere DEYE_REGISTER_NAMES. Reg 191 EMS nezapisuje.

Reg 191 (výkon grid peak shaving)

  • EMS NEZAPISUJE nastavit manuálně v SolarmanApp.
  • Hodnota určuje výkon peak shavingu v W (typicky 016 000).

Reg 178 hodnoty podle fyzického režimu

  • SELL: 32 bit45 = 10, grid peak shaving disable (export do sítě).
  • PASSIVE a CHARGE: 48 bit45 = 11, grid peak shaving enable.

EMS nezapisuje read-modify-write (paralelní čtení jinými klienty může způsobit nesoulad).

Klíčové registry podle fyzického režimu Deye

Provozní režimy EMS (AUTO, SELF_SUSTAIN, SELL, …) se mapují na tři fyzické režimy střídače: PASSIVE, SELL, CHARGE. Ostatní je politika solveru / EMS, ne samostatný „režim“ invertoru.

Reg PASSIVE SELL CHARGE
142 1 (zero export to load) 0 (selling first) 1
108 max_charge_a z DB max_charge_a z DB battery_watts_to_amps(battery_w, max_charge_a)
109 max_discharge_a z DB max_discharge_a z DB 0
178 48 32 48
143 max export W z DB max export W z DB max export W z DB
141 0 0 0

Důležité: V PASSIVE i SELL jsou registry 108 a 109 vždy na plném limitu z DB. Deye si tok energie reguluje sám; snížení 108/109 pod maximum brání reakci na nepředvídatelnou spotřebu nebo přebytky FVE.

Detekce fyzického režimu (get_deye_mode v control_exporter.py)

Vychází z grid_setpoint_w a battery_w z ControlSetpoints (aktivní plán / politika EMS), ne z telemetrie.

Režim Podmínka
SELL grid_setpoint_w < 200
CHARGE battery_w > 500 a grid_setpoint_w > 200
PASSIVE vše ostatní (včetně SELF_SUSTAIN, IDLE, …)

Režim CHARGE_CHEAP v EMS nastaví grid_setpoint_w tak, aby platila podmínka importu (> 200 W), jinak by fyzicky zůstal PASSIVE.

Všechny limity (max_charge_a, max_discharge_a, max_export_power_w / reg 143) pocházejí výhradně z DB (_load_inverter_config).

Time Points řízení podle fyzického režimu

Deye má 6 časových bloků. EMS přepisuje bloky 12 (TOU index 01) při každém control_export. Bloky 36 (neaktivní výplň, čas 2355) zapisuje nejednou častěji než jednou za kalendářní den v Europe/Prague a okamžitě znovu, pokud se změní podpis deye_tou_inactive_signature (HHMM|min_soc|reserve_soc|tp_discharge_w) — metadata v asset_inverter (V028 + V029 komentář).

Výběr aktivního segmentu na invertoru: platí poslední časový bod, jehož HH:MM ≤ aktuálnímu času na hodinách střídače (po synchronizaci 6264). Proto nesmí zůstat jako jediný „minulý“ bod např. 00:00 s pasivním profilem, zatímco profil s nabíjením ze sítě je až u budoucího času mezi půlnocí a tím budoucím časem by invertor celou dobu používal špatný segment.

Blok Čas (HHMM, Europe/Prague) Zdroj plánu Účel SOC min Grid charge
1 current_slot_hhmm() začátek probíhajícího 15min slotu planning_interval pro aktuální slot (_fetch_plan_row_for_slot_offset(..., 0)) PASSIVE / SELL / CHARGE dle _deye_tou_params viz tabulka níže viz tabulka níže
2 next_slot_hhmm() začátek následujícího 15min slotu planning_interval pro další slot (_fetch_plan_row_for_slot_offset(..., 1)) Přechod na další čtvrthodinu viz tabulka níže viz tabulka níže
36 23:55 (2355) Neaktivní (pasivní profil); ne 23:59 — firmware Deye často 2359 neuloží → verify mismatch min_soc_percent (DB) NE

Registry 108 / 109 / 142 / 178 / 143 odpovídají aktuálnímu plánu (okamžitý výstup; setpoints_now v write_inverter_setpoints). TOU řádky 12 doplňují stejnou logiku pro časové segmenty (_deye_tou_params).

Příklad v 14:18: blok 1 má čas 1415, blok 2 čas 1430 mezi 14:15 a 14:29 je aktivní segment z bloku 1 (sladěný s plánem pro 14:1514:30), po 14:30 blok 2 (plán 14:3014:45). Po dalším exportu se oba časy posunou (např. 14:30 / 14:45).

Fyzické režimy Deye parametry jednoho time pointu (bloky 12)

Režim Výkon (W) SOC min (reg 166+) Grid charge
PASSIVE max_discharge_a × 51,2 min_soc_percent z DB NE
SELL max_discharge_a × 51,2 reserve_soc_percent z DB NE
CHARGE battery_watts_to_amps(battery_w, max_charge_a) × 51,2 min(95, cíl SoC z plánu nebo 80) ANO

Bloky 36 používají čas 2355 a stejnou SOC hodnotu jako PASSIVE (min_soc_percent, grid charge = NE).

Synchronizace času

Registry 6264 nastavují invertoru čas v Europe/Prague.

  • reg 62: (rok - 2000) << 8 | měsíc
  • reg 63: den << 8 | hodina
  • reg 64: minuta << 8 | sekunda — při zápisu z EMS jsou sekundy vždy 0 (stabilnější hodnota; na zařízení pak sekundy dál běží).

Řidší zápis: před každým exportem setpointů EMS přečte 6264 (FC 0x03). Do journalu 6264 nezařadí, pokud je dekódovaný čas invertoru vůči aktuální Europe/Prague v odchylce ≤ 60 s a zároveň od posledního úspěšného zápisu 6264 neuplynulo 24 h (asset_inverter.deye_last_system_time_sync_at, mění se jen při zápisu). Je-li sloupec NULL (např. první provoz), zápis času se nevynechá. Při selhání čtení se čas zapíše (bezpečný fallback). Sloupec deye_last_system_time_sync_minute doplňuje začátek pražské minuty u úspěšného zápisu 6264.

Zápis prochází journal jako každý jiný registr; na sběrnici jde souvislý blok FC 0x10.

Verifikace (journal): u souvislého bloku 6264 není porovnání bajt po bajtu — invertor mezi zápisem a čtením posune sekundy. EMS považuje zápis za úspěšný, pokud se dekódované časy (z value_to_write trojice vs. přečtené hodnoty) liší nejvýše o 120 s (control_exporter._verify_deye_clock_command_run).

Před vytvořením journalu: pokud je navrhovaná hodnota shodná s posledním verified záznamem daného registru v modbus_command, EMS řádek nevytvoří a na Modbus neposílá (žádný „X → X“ zápis jen kvůli periodickému exportu). Výjimky řeší stávající logika (řidší 6264 výše, denní TOU 36 + meta sloupce na asset_inverter).

Mapování registrů (time point i, i = 0…5)

Účel Adresa
Čas HHMM 148 + i
Výkon (W) 154 + i
Min. SOC % 166 + i
Grid charge enable 0/1 172 + i

Limity nabíjení/vybíjení v ampérech a export z site_grid_connection / asset_inverter / asset_battery načítá _load_inverter_config() (max_charge_a / max_discharge_a jako LEAST(BMS, střídač) / 51,2). Python neřeže na univerzální číslo hodnoty v DB mají odpovídat skutečnému modelu střídače a BMS (maximální povolená hodnota v registru se liší podle typu; není to všude např. 185 A). Ověřit v dokumentaci k danému SUN-*K.

Telemetrické registry (R only, FC 0x03)

Reg Název Jednotka Poznámka
500 Run state 0 = standby, 2 = normal
588 Battery SOC 1 %
590 Battery power 1 W S16 + vybíjení / nabíjení
625 Grid total power 1 W S16 + import / export
653 Load total power 1 W S16
667 GEN port power 1 W FVE pole B
672 PV1 power 1 W
673 PV2 power 1 W

Přepočty

  • Výkon baterie → proud (LV 51,2 V): battery_watts_to_amps(power_w, max_amps) = min(max(0, max_amps), max(0, round(|power_w| / 51.2))), kde max_amps je z DB
  • max_export_power_w / max_import_power_w / limity baterie berou se z DB (_load_inverter_config), ne z natvrdo v Pythonu
  • Export do registru 143 = site_grid_connection.max_export_power_w (např. home-01 / SUN-20K 13 500 W)

Ověření (Modbus + DB)

docker compose up -d --build backend
import asyncio
from pymodbus.client import AsyncModbusTcpClient

async def check():
    c = AsyncModbusTcpClient('172.16.1.10', port=502, timeout=5)
    await c.connect()

    times = await c.read_holding_registers(148, count=2)
    for i in range(2):
        h, m = divmod(times.registers[i], 100)
        print(f'Time point {i+1}: {h:02d}:{m:02d}')

    for name, reg in [
        ('Limit control', 142),
        ('Peak sw (bit4-5)', 178),
        ('Export limit',  143),
        ('Discharge A',   109),
        ('Grid power',    625),
    ]:
        r = await c.read_holding_registers(reg, count=1)
        raw = r.registers[0]
        signed = raw - 65536 if raw > 32767 else raw
        print(f'{name} ({reg}): {signed}')

    c.close()

asyncio.run(check())
docker compose exec db psql -U ems_user -d ems -c "
  SELECT register_name, value_to_write, status,
         created_at AT TIME ZONE 'Europe/Prague' AS cas
  FROM ems.modbus_command
  WHERE site_id=2 AND register IN (108, 109, 142)
  ORDER BY created_at DESC LIMIT 9;"

Související

  • docs/04-modules/modbus-command-journal.md journal a verifikace
  • backend/services/control_exporter.py zápisy
  • backend/services/modbus_client.py write_registers (FC 0x10)