11 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. EMS je do fronty nezařadí, pokud ještě neuplynula nová kalendářní minuta oproti poslednímu úspěšnému zápisu (sloupec asset_inverter.deye_last_system_time_sync_minute). Jinak platí:
- reg 62:
(rok - 2000) << 8 | měsíc - reg 63:
den << 8 | hodina - reg 64:
minuta << 8 | sekunda
Zápis prochází journal jako každý jiný registr; na sběrnici jde souvislý blok FC 0x10.
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 (nová minuta u 62–64, 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)