13 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 | 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, bit4–5 = 10) v režimu SELL; 48 (0b00110000, bit4–5 = 11) v PASSIVE a CHARGE. |
| 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. |
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ř. 62–64, 148–159, 166–177, 108–109, 141–142 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, 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 0–16 000).
Reg 178 – hodnoty podle fyzického režimu
- 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 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 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 / 142 / 178 / 143 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 |
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 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 | 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))), 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),
('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 verifikacebackend/services/control_exporter.py– zápisybackend/services/modbus_client.py–write_registers(FC 0x10)