23 KiB
Deye Modbus Registry – EMS řízení
Důležité pravidlo
- Registry 60–499: POUZE FC 0x10 (
write_registers) - Registry 0–59: 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 60–499.
Ří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). PASSIVE + plán chce nabíjet (battery_w>0): 108 = max (špička FVE nesmí být omezená průměrem slotu). PASSIVE + export bez nabíjení: 0. CHARGE: 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; při PASSIVE + battery_w>0 + export zůstává max (domácnost z baterie při výpadku PV). 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] až 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; směr přebytku (baterie vs. síť) řeší energie management měniče a 142, ne umělé 108 = 0 (viz pass-through níže). |
| 340 | Max solar power | min … cap (W) | 1 W | Strop výkonu DC PV řízeného střídačem (pole A). Cap z fn_inverter_pv_a_max_w (deye_reg340_max_solar_w, typ. 32 000 home-01, 65 000 větší hybridy), ne součet Wp — studené panely mohou překročit nominál. Min z deye_reg340_min_solar_w (home-01 400 W, jinde 0 dle firmware). EMS zapisuje jen při zeleném bonusu a cap > 0. Není v DEYE_CRITICAL_REGS_SELF_SUSTAIN. |
| 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. EMS ji neodvozuje z forecastu ani z grid_setpoint_w; pro exportní sloty je to tvrdý site/inverter cap. |
| 178 | Control board special 1 | bitmask | — | Bitové pole pro více funkcí. EMS používá: (a) bits 4–5 pro peak shaving switch: 32 (0b00100000, bit4–5 = 10) v režimu SELL; 48 (0b00110000, bit4–5 = 11) v PASSIVE/CHARGE. (b) BA81: bits 0–1 pro „MI export to Grid cutoff“ (AC coupling / GEN): 2 = disable (cutoff OFF), 3 = enable (cutoff ON). EMS zapisuje jako read-modify-write (zachová ostatní bity). V některých manuálech/UI je to označené jako „register 179“ (1-based). |
| 190 | GEN peak shaving | 0–16000 | 1 W | Peak shaving na GEN portu |
| 191 | Grid peak shaving power | 0–16000 | 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ř. 62–64, 148–159, 166–177, 108–109, 141–143, 145, 340 podle toho, co je ve frontě). Pořadí v journalu: 62–64 (čas, viz níže), time points 148–177 (jen řádky zařazené do daného běhu), 108, 109, 141, 142, 143, 145, 340 (podmíněně), 178. Popisné názvy v DB bere DEYE_REGISTER_NAMES. Reg 191 EMS nezapisuje.
Reg 340 (max solar power)
- FC 0x10, jednotka W; firmware omezuje strop výroby z řiditelných stringů (pole A na hybridu).
- Kdy EMS zapisuje:
ems.fn_site_has_active_green_bonus_pv(site_id)aems.fn_inverter_pv_a_max_w(inverter_id) > 0(řiditelné pole A + nenulový strop střídače zdeye_reg340_max_solar_w/max_dc_input_w). Bez bonusu nebo cap 0 EMS reg 340 nezapisuje. - Hodnota: z
ControlSetpoints.pv_a_allowed_w(AUTO): bez curtailmentu = plný cap; připv_a_curtailed_w > 0viz tabulka výše. Režimy SELF_SUSTAIN / PRESERVE / CHARGE_CHEAP majípv_a_allowed_w = None→ žádný zápis 340 z EMS v daném ticku. - Bez zápisu 340 (2026-05): pokud plán má bez exportu (
export_mode = NONEnebogrid_setpoint_w ≥ 0aexport_limit_w = 0), bez nabíjení baterie (battery_setpoint_w ≤ 0) a bez curtailu A (pv_a_curtailed_w = 0), EMS reg 340 neposílá — Deye řídí PV A přes 108/109/142 a při plné baterii typicky solar sell off (hardware). Funkceplan_skips_deye_reg340_writevsetpoints.py. Plánovač v32: škrcení A v okněsell < 0jde přespv_a_curtailed_w→ reg 340; registry 108/109 se kvůli fázím nemění. - Výjimka: explicitní curtail v plánu nebo záporné buy+sell s PV B →
pv_a_allowed_wse dopočítá / vynuluje jako dřív. - Živé čtení:
read_deye_registers_livevracíreg340_max_solar_power_w(integer) jen pokud je přepínač zapnutý; jinaknull(bez extra FC3 čtení reg 340).
Reg 191 (výkon grid peak shaving)
- EMS NEZAPISUJE – nastavit manuálně v SolarmanApp.
- Hodnota určuje výkon peak shavingu v W (typicky 0–16 000).
Reg 178 – hodnoty podle fyzického režimu + idempotence
- SELL: 32 – bit4–5 = 10, grid peak shaving disable (export do sítě).
- PASSIVE a CHARGE: 48 – bit4–5 = 11, grid peak shaving enable.
EMS zapisuje read-modify-write a zachovává ostatní bity (reg 178 obsahuje více funkcí).
Ověření v journalu (modbus_command): u zápisu 178 se při verify porovnává maska bits 0–1 a 4–5 (0x0033) s očekávanou hodnotou; 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 (154–159) 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 4–5 (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): u slotu export_mode = PV_SURPLUS exporter nastaví 108 = 0 (nabíjecí proud), 109 = max — baterie nemá kam brát přebytek FVE, jde do sítě při 145 = 1; 142 zůstává deye_zero_export_mode (u CT často 2 = zero export k měření zátěže, ne selling first z baterie). Detail: operating-modes.md.
BA81: GEN port cut-off (reg 178 bits0–1) 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 178 bits0–1 na 3 (11b, enable = cut-off ON / export blokován)false→ exporter nastaví bits0–1 na 2 (10b, disable = cut-off OFF / export povolen)
Zápis se provádí jako read-modify-write nad reg 178 (zachová ostatní bity registru).
Idempotence: pokud poslední verified hodnota už má správně nastavené relevantní bity (maska 0x0033), EMS zápis reg. 178 v dalším běhu přeskočí.
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 %) — stejně jako u běžného AUTO PASSIVE: akumulace vs. síť řeší plán a 145 / 178, ne výška TOU %.
Č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/2 = zero export k load/CT; ne „blokace do sítě“). Přetok FVE do sítě: 108=0, 145=1 |
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+) | max_soc_percent (clamp 10–100), grid charge ON |
min_soc_percent z DB |
reserve_soc_pct | min_soc_pct |
PASSIVE – TOU SOC % (Deye): EMS zapisuje vždy min_soc_percent z asset_battery (clamp jako u všech TOU SOC 5–95). Slouží jako spodní pásmový signál pro firmware; výšku nepoužíváme k řízení „honit akumulaci na 100 %“ ve PASSIVE — to u levného importu řeší 108/109 (viz operating-modes.md), u záporné vykupní BLOCK_EXPORT přes export_ban → 145, případně 178 na GEN.
CHARGE: TOU řádek nese max_soc_percent z DB (clamp 10–100) jako cíl při grid charge (spolu s příznakem grid charge v time pointu). Energy pattern („load first“ / „battery first“) v UI střídače zůstává v kompetenci instalace — EMS ho přes Modbus nenastavuje.
Jak funguje pass-through (logicky):
- 108 / 109 typicky max z invertoru — horní limity, ne příkaz „nabíjej / vybíjej“.
- Reg 142 = 1/2 → zero export to load / CT (instalace závislá).
- Reg 145 = 1 → solar sell enabled; přebytek řiditelné FVE po zátěži a limitech směřuje do sítě podle firmware.
- Plán (
battery_w,grid_setpoint_w) a CHARGE / SELL větev vdeye_battery_charge_discharge_ampsdál určují asymetrie (např. CHARGE: 109 = 0).
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 1–2 (TOU index 0–1) při každém control_export. Bloky 3–6 (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 62–64). 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 |
| 3–6 | 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 / 340 (podmíněně) / 178 odpovídají aktuálnímu plánu (okamžitý výstup; setpoints_now v write_inverter_setpoints). TOU řádky 1–2 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:15–14:30), po 14:30 blok 2 (plán 14:30–14:45). Po dalším exportu se oba časy posunou (např. 14:30 / 14:45).
Fyzické režimy Deye – parametry jednoho time pointu (bloky 1–2)
| Režim | Výkon (W) | SOC min (reg 166+) | Grid charge |
|---|---|---|---|
| PASSIVE | max_discharge_a × 51,2 |
min_soc_percent z DB (_deye_passive_tou_battery_soc_pct) |
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 |
clamp(10 … 100, asset_battery.max_soc_percent) |
ANO |
Bloky 3–6 používají čas 2355 a stejnou SOC hodnotu jako PASSIVE (min_soc_percent, grid charge = NE).
Synchronizace času
Registry 62–64 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 62–64 (FC 0x03). Do journalu 62–64 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 62–64 (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í 62–64 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 62–64 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 62–64 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 62–64 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ší 62–64 výše, denní TOU 3–6 + 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))), kdemax_ampsje 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 verifikacebackend/services/control_exporter.py– zápisybackend/services/modbus_client.py–write_registers(FC 0x10)