255 lines
22 KiB
Markdown
255 lines
22 KiB
Markdown
# 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 | 0 … cap (W) | 1 W | Strop výkonu DC PV řízeného střídačem (pole A). EMS zapisuje jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` (zelený bonus na PV poli) **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0`. Hodnota z aktivního `planning_interval`: bez curtailmentu = cap; s `pv_a_curtailed_w > 0` = `clamp(0, cap, pv_a_forecast_solver_w − pv_a_curtailed_w)`. **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do 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)` **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (součet `nominal_power_wp` z `asset_pv_array` kde `controllable = true`). Při součtu **0** nebo bez aktivního zeleného bonusu EMS reg 340 **nezapisuje** (ruční hodnota v invertoru zůstane).
|
||
- **Hodnota:** z `ControlSetpoints.pv_a_allowed_w` (AUTO): bez curtailmentu = plný cap; při `pv_a_curtailed_w > 0` viz 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.
|
||
- **Živé čtení:** `read_deye_registers_live` vrací **`reg340_max_solar_power_w`** (integer) jen pokud je přepínač zapnutý; jinak **`null`** (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):** reg. **108/109** podle `_deye_zero_export_amps_for_passive` / `deye_battery_charge_discharge_amps` — **108** a **109** jsou typicky **max** z DB; výjimka jen **import bez nabíjení** (`109 = 0`). Export bez kladného `battery_w` už **108 nenuluje** (přebytek do sítě řeší režim / 142 / 145, ne falešné „baterie plná“). 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 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+) | **`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):**
|
||
|
||
1. **108 / 109** typicky **max** z invertoru — horní limity, ne příkaz „nabíjej / vybíjej“.
|
||
2. Reg **142** = 1/2 → zero export to load / CT (instalace závislá).
|
||
3. Reg **145** = 1 → solar sell enabled; přebytek řiditelné FVE po zátěži a limitech směřuje do sítě podle firmware.
|
||
4. Plán (`battery_w`, `grid_setpoint_w`) a **CHARGE** / **SELL** větev v `deye_battery_charge_discharge_amps` dá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)))`, 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)
|
||
|
||
```bash
|
||
docker compose up -d --build backend
|
||
```
|
||
|
||
```python
|
||
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())
|
||
```
|
||
|
||
```bash
|
||
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)
|