Files
ems/docs/04-modules/modbus-registers.md
Dusan Vojacek 52e4b68789
Some checks failed
CI and deploy / migration-check (push) Failing after 26s
CI and deploy / deploy (push) Has been skipped
ski battery charge u sell
2026-05-28 23:22:57 +02:00

257 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**). **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:** EMS **nezapisuje** (selling first = reg **142**; zbytečné nulování/obnova). |
| 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]`**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 **45** pro peak shaving switch: **32** (`0b00100000`, bit45 = **10**) v režimu **SELL**; **48** (`0b00110000`, bit45 = **11**) v **PASSIVE/CHARGE**. (b) **BA81:** bits **01** 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 | 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, 340 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, 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` (řiditelné pole A + nenulový strop střídače z `deye_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ř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.
- **Bez zápisu 340 (2026-05):** pokud plán má **bez exportu** (`export_mode = NONE` nebo `grid_setpoint_w ≥ 0` a `export_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). Funkce `plan_skips_deye_reg340_write` v `setpoints.py`. **Plánovač v32:** škrcení A v okně `sell < 0` jde přes `pv_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_w` se dopočítá / vynuluje jako dřív.
- **Ž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 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 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 01 a 45** (`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 (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):** 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 bits01) 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** 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 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í | **nezapisuje EMS** | 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 10100), 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 595). 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 10100**) 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 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 / 340 (podmíněně) / 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` | **`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 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)
```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)