Files
ems/docs/04-modules/modbus-registers.md
Dusan Vojacek 342483b885
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped
invert logic cutoff register
2026-04-29 13:24:28 +02:00

21 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 Strop A z DB (COALESCE(deye_register_max_charge_a, odvod z kW) + clamp 350 A). Ve PASSIVE (AUTO) podle _deye_zero_export_amps_for_passive: výchozí max, u exportu v plánu bez vybíjení 0. CHARGE: proud z battery_w přes battery_watts_to_amps. SELL: 0.
109 Max discharge current 0 … max dle modelu (manuál Deye) 1 A Ve PASSIVE (AUTO): výchozí max, u importu bez nabíjení 0; SELL max vybíjení; CHARGE typicky 0.
128 Grid charge current 0 … max dle modelu (manuál Deye) 1 A Nabíjení ze sítě. Firmware automaticky sníží reálný proud tak, aby load + battery_charge nepřekročil velikost jističe — proto LP v planning_engine.py plánuje gi[t]do max_import_power_w + BMS_max_charge, aby uměl využít cenově nejlepší 15min okna pro nabíjení na plný BMS strop (viz planning.md sekce „Plánovací strop gi[t] vs. fyzický jistič"). Fyzické dodržení jističe drží reg 128 + firmware.
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 (System work mode) 0/1/2 0 = selling first, 1 = zero export to load, 2 = zero export to CT. Hodnota v non-SELL režimech pochází z asset_inverter.deye_zero_export_mode (závisí na instalaci viz tabulka níže). V režimu SELL vždy 0.
145 Solar sell 0/1 0 = disabled (přebytek FVE na straně měniče se nesmí vést do sítě — curtailment vůči síti), 1 = enabled. Platí jen pro FVE pod kontrolou Deye (controllable = true); druhá pole (např. pv-b u home-01) EMS tímto registerem neřídí. EMS dnes vždy zapisuje 1; při 108 = 0 a 145 = 1 přebytky z řiditelného stringu typicky tečou do sítě (viz pass-through níže).
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.
179 Control board special 1 bitmask BA81: bits 01 ovládají „MI export to Grid cutoff“ (AC coupling / GEN): 2 (10b) = disable (cutoff ON), 3 (11b) = enable. EMS zapisuje masked RMW (zachová ostatní bity) jen pokud asset_inverter.deye_gen_microinverter_cutoff_enabled = true.
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.

exporter_monolith.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, 141143, 145 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, 143, 145, 178. 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 + idempotence

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

Ověření v journalu (modbus_command): u zápisu 178 se při verify porovnávají jen bity 45 maskou 0x0030 s očekávanou hodnotou (32/48); value_verified zůstává plný readback. Při nesouladu masky následuje druhé FC3 čtení reg. 178 (mitigace RS485 glitchů). U TOU výkonu W (154159) verify akceptuje i readback max_charge_a × 51.2 nebo max_discharge_a × 51.2, pokud firmware hodnotu přepíše na interní maximum (skutečný výkon je stejně omezen reg. 108/109). Detail: modbus-command-journal.md.

Idempotence (proti spamu zápisů): pokud poslední verified hodnota už má správně nastavené bity 45 (maska 0x0030), EMS zápis reg. 178 v dalším běhu přeskočí (i když value_verified obsahuje jiné bity).

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. Solver navíc rozlišuje čtyři typy slotů každý typ určuje specifickou kombinaci registrů.

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

Vychází z grid_setpoint_w a battery_w z ControlSetpoints (aktivní plán / politika EMS), ne z telemetrie. Bez wattových prahů — jen znaménka.

Režim Podmínka
SELL grid_setpoint_w < 0 a battery_w < 0
CHARGE battery_w > 0 a grid_setpoint_w > 0
PASSIVE vše ostatní (včetně pass-through, self-consumption, SELF_SUSTAIN, IDLE, …)

Režim CHARGE_CHEAP nastaví oba setpointy na stejný kladný výkon (min. 1 W), aby byl výsledek CHARGE.

PASSIVE (ZERO): reg. 108/109 podle _deye_zero_export_amps_for_passive — při exportu v plánu bez vybíjení je 108 = 0 (přetok FVE); při importu bez nabíjení je 109 = 0 (držet baterii). Jinak oba max (AUTO). Detail: operating-modes.md.

BA81: GEN port cut-off (reg 179) z plánu

Pro instalace s AC coupling na GEN portu (mikroinvertory) může solver uložit do planning_interval flag deye_gen_cutoff_enabled.\n

  • true → exporter nastaví reg 179 bits01 na 3 (11b, enable = cut-off ON / export blokován)
  • false → exporter nastaví bits01 na 2 (10b, disable = cut-off OFF / export povolen)

Zápis do reg. 179 se v praxi provádí jako „clean write“ hodnoty 2 nebo 3 (bez read-modify-write), protože některé firmware/UI varianty nevyhodnocují jen bity 01 maskou, ale očekávají přímo hodnotu 2/3. Ověření v journalu (verify_modbus_commands) přesto porovnává jen bits01 maskou 0x0003 (odolnost vůči paralelním změnám jiných bitů / verzím FW).

Idempotence: EMS zápis reg. 179 přeskočí jen tehdy, když poslední verified hodnota je už clean 2/3. Masková shoda s hodnotami typu 0xfffe / 0xffff se záměrně nepovažuje za “už zapsáno”, aby se zařízení dostalo do stabilního stavu, který odpovídá UI i chování firmware. Pozn.: Flag se v solveru vůbec nevytváří ani neukládá tam, kde není povolen feature asset_inverter.deye_gen_microinverter_cutoff_enabled takové lokality ho nemají ani v UI.

Provozní režim EMS SELF_SUSTAIN

Z hlediska get_deye_mode je SELF_SUSTAIN stále PASSIVE (battery_w z LP je None). Exportér ale nastaví ControlSetpoints.self_sustain_local_use=True a v write_inverter_setpoints:

  • 108 / 109 = max z invertoru (DB) — plný rozsah nabíjení i vybíjení, aby přebytek FVE mohl do baterie.
  • 142 = asset_inverter.deye_zero_export_mode (1 = zero export to load, 2 = zero export to CT), stejně jako u ostatního PASSIVE mimo SELL.
  • TOU SOC (reg 166+) = vždy min_soc_percent (typicky 12 %) — _deye_passive_tou_battery_soc_pct při tomto příznaku ne přepíná na 100 % podle vykupní ceny, protože LP se v SELF_SUSTAIN nepoužívá.

Čtyři typy slotů a mapování na registry

Solver předvybírá sloty pro nabíjení a export-vybíjení (_select_charge_slots, _select_discharge_export_slots). Nabíjení: vždy povoleno v slotech s PV-surplus; zbytek rozpočtu (charge_slot_buffer × (soc_max current_soc) PV přínos) doplněn nejlevnějšími sloty podle buy_price (nákupní cena ze sítě). Export-vybíjení: top-N slotů podle nejvyšší sell_price. Výsledné setpointy pak určují typ slotu:

Charge Pass-through / FVE přetok Battery→grid (SELL) Self-consumption
Kdy bat_w > 0, grid_w > 0 typicky grid_w < 0, bat_w ≥ 0 grid_w < 0, bat_w < 0 import, bat_w ≤ 0 či mix
Deye mode CHARGE PASSIVE SELL PASSIVE
108 charge A dle bat_w 0 při exportu bez vybíjení 0 max nebo 0 dle varianty
109 discharge A 0 max max 0 při importu bez nabíjení, jinak max
142 limit control deye_zero_export_mode (1 nebo 2) deye_zero_export_mode (1 nebo 2) 0 (selling first) deye_zero_export_mode (1 nebo 2)
145 solar sell 1 (enabled) 1 (enabled) 1 (enabled) 1 (enabled)
178 peak shaving 48 (PASSIVE) 48 (PASSIVE) 32 (SELL) 48 (PASSIVE)
143 export limit max export W z DB max export W z DB max export W z DB max export W z DB
141 energy mode 0 0 0 0
TOU SOC (reg 166+) viz níže min_soc_pct reserve_soc_pct min_soc_pct

PASSIVE TOU SOC % (home-01 / Deye): EMS ukládá do řádku time pointu procento, které na zařízení řídí prioritu baterie vs. přetok FVE do sítě (viz firmware / instalace). Je-li zapsané procento níž než skutečný SoC, přebytek tíhne do sítě; při záporné efektivní vykupní (effective_sell_price ze slotu) nebo při kladném battery_setpoint_w (plánované nabíjení) EMS nastaví 100 % (signál „využij baterii naplno“) — ne v režimu SELF_SUSTAIN (self_sustain_local_use), tam je vždy min_soc_percent. asset_battery.max_soc_percent (typicky 95) je jiný účel: horní limit pro plánovač / denní provoz v % SoC (komfort, degradace, rezerva výrobce), ne časové „do kdy“ ani hodnota zapisovaná do tohoto TOU při této priorité. Jinak zůstane min_soc_percent.

Jak funguje pass-through fyzicky:

  1. Reg 108 = 0 → baterie se fyzicky nemůže nabíjet (Deye ji považuje za „plnou")
  2. Reg 142 = 1/2 → zero export mode (Deye nebude aktivně prodávat z baterie)
  3. Reg 145 = 1 → solar sell enabled: protože baterie je „plná" (108 = 0), PV přebytky tečou do sítě
  4. Reg 109 = max → pokud spotřeba překročí FVE, baterie může vybíjet (ochrana self-consumption)

deye_zero_export_mode per inverter

Hodnota registru 142 v non-SELL režimech závisí na fyzické instalaci. Uložena v asset_inverter.deye_zero_export_mode:

Site Inverter ID deye_zero_export_mode Důvod
home-01 (id=2) 3 1 (zero export to load) Nemá CT
BA81 (id=3) 5 2 (zero export to CT) CT osazeno
KV1 (id=4) 7 2 (zero export to CT) CT osazeno

Varování: Záměna způsobí chybné měření pokud site nemá CT a nastaví se „to CT" (2), střídač nevidí skutečný odběr. Naopak pokud má CT ale nastaví se „to load" (1), zátěže mimo load port (např. wallbox) nebudou vidět.

Efektivní max_charge_a / max_discharge_a pro řízení načítá _load_inverter_config z DB jedním výrazem COALESCE(strop v A, FLOOR z W)) (migrace V044 + dotaz v control_exporter.py). max_export_power_w / reg 143 také z DB.

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 / 141 / 142 / 143 / 145 / 178 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 _deye_passive_tou_battery_soc_pct: při neg. vykupní / plánovaném nabíjení = 100 %, jinak min_soc_percent 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 syncu neuplynulo 24 h. Sloupce asset_inverter.deye_last_system_time_sync_at a deye_last_system_time_sync_minute se doplňují po úspěšném FC 0x10 zápisu batche obsahujícího 6264 (write_inverter_setpoints) a znovu po úspěšné toleranční verifikaci (_verify_deye_clock_written_bundle) — obojí drží řidší zápis i když verify občas selže. Je-li deye_last_system_time_sync_at NULL (první provoz), zápis času se nevynechá. Při selhání čtení 6264 před rozhodnutím se čas zařadí do journalu (bezpečný fallback). Při scénáři „žádný řádek journalu, všechny hodnoty jako poslední verified“ se čas v DB neaktualizuje (žádný fiktivní sync).

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

Verifikace (journal): registry 6264 se nikdy neověřují striktním porovnáním po jednotlivých registrech (reg 64 by kvůli běžícím sekundám padal do mismatch a spouštěl SELF_SUSTAIN). Verifikační job vždy přečte FC 0x03 souvisle 6264 a použije toleranci 120 s na dekódovaný čas (_deye_clock_registers_verify_match). Je-li ve stavu written jen podmnožina řádků (např. jen 64), očekávané hodnoty pro chybějící registry se doplní z posledního verified nebo z aktuálního přečtení na sběrnici (_deye_expected_clock_triplet_for_verify). Po třech neúspěšných ověřeních bloku 6264 EMS nepřepíná provozní režim na SELF_SUSTAIN; pošle kritický Discord (notify_modbus_clock_verify_exhausted) — viz modbus-command-journal.md.

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 S16 FVE pole B; signed — záporné při zpětném toku / bez výroby
672 PV1 power 1 W S16 signed; EMS ukládá raw signed W, do pv_power_w jen max(0, kanál)
673 PV2 power 1 W S16 jako PV1

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),
        ('Solar sell',    145),
        ('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, 145)
  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)