diff --git a/backend/services/control/exporter_monolith.py b/backend/services/control/exporter_monolith.py index 357f32a..7c559af 100644 --- a/backend/services/control/exporter_monolith.py +++ b/backend/services/control/exporter_monolith.py @@ -332,9 +332,18 @@ def _drop_registers_matching_last_verified( skipped: list[int] = [] for reg, meta, val in registers: lv = last_verified.get(int(reg)) - if lv is not None and lv == int(val): - skipped.append(int(reg)) - continue + if lv is not None: + # reg178: porovnáváme jen masku bitů 4–5 (Deye si v dalších bitech drží vlastní stav). + if int(reg) == 178 and _deye_reg178_verify_match(int(val), int(lv)): + skipped.append(int(reg)) + continue + # reg179: porovnáváme jen bits0–1 maskou 0x0003 (masked RMW zachovává ostatní bity). + if int(reg) == 179 and _deye_reg179_verify_match(int(val), int(lv)): + skipped.append(int(reg)) + continue + if int(lv) == int(val): + skipped.append(int(reg)) + continue out.append((reg, meta, val)) return out, skipped diff --git a/backend/tests/test_drop_registers_matching_last_verified.py b/backend/tests/test_drop_registers_matching_last_verified.py new file mode 100644 index 0000000..304139c --- /dev/null +++ b/backend/tests/test_drop_registers_matching_last_verified.py @@ -0,0 +1,38 @@ +from services.control.exporter_monolith import ( + REG178_PASSIVE, + _drop_registers_matching_last_verified, +) + + +def test_drop_registers_skips_reg178_when_mask_matches(): + # last_verified contains extra bits beyond 0x0030; we still want to skip if bits 4–5 match. + registers = [(178, "grid_peak_shaving_switch", REG178_PASSIVE)] + last_verified = {178: 12030} # real-world example from home-01 (bits4-5 still == 0b11) + out, skipped = _drop_registers_matching_last_verified(registers, last_verified) + assert out == [] + assert skipped == [178] + + +def test_drop_registers_keeps_reg178_when_mask_differs(): + registers = [(178, "grid_peak_shaving_switch", REG178_PASSIVE)] + last_verified = {178: 32} # SELL mask 0b10 + out, skipped = _drop_registers_matching_last_verified(registers, last_verified) + assert out == registers + assert skipped == [] + + +def test_drop_registers_skips_reg179_when_mask_matches(): + registers = [(179, "control_board_special_1", 2)] # bits0–1 = 2 (cutoff ON) + last_verified = {179: 0x1236} # ...0110b => bits0–1 still == 2 + out, skipped = _drop_registers_matching_last_verified(registers, last_verified) + assert out == [] + assert skipped == [179] + + +def test_drop_registers_keeps_reg179_when_mask_differs(): + registers = [(179, "control_board_special_1", 2)] # want cutoff ON + last_verified = {179: 0x1237} # ...0111b => bits0–1 == 3 (cutoff OFF) + out, skipped = _drop_registers_matching_last_verified(registers, last_verified) + assert out == registers + assert skipped == [] + diff --git a/docs/04-modules/modbus-registers.md b/docs/04-modules/modbus-registers.md index f1870b3..d1e2b08 100644 --- a/docs/04-modules/modbus-registers.md +++ b/docs/04-modules/modbus-registers.md @@ -32,7 +32,7 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi - **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 +### 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**. @@ -41,6 +41,8 @@ EMS **nezapisuje** read-modify-write (paralelní čtení jinými klienty může **Ověření v journalu (`modbus_command`):** u zápisu **178** se při verify porovnávají jen **bity 4–5** maskou **`0x0030`** s očekávanou hodnotou (32/48); `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ů. @@ -62,7 +64,12 @@ Režim **CHARGE_CHEAP** nastaví oba setpointy na stejný kladný výkon (min. 1 ### BA81: GEN port cut-off (reg 179) 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 **179** bits0–1 na **2** (`10b`, disable = cut-off ON)\n+- `false` → exporter nastaví bits0–1 na **3** (`11b`, enable = cut-off OFF)\n+\n+Zápis je **masked read-modify-write** (zachová ostatní bity reg. 179). Ověření v journalu (`verify_modbus_commands`) porovnává jen bits0–1 maskou `0x0003`.\n+ +- `true` → exporter nastaví reg **179** bits0–1 na **2** (`10b`, disable = cut-off ON) +- `false` → exporter nastaví bits0–1 na **3** (`11b`, enable = cut-off OFF) + +Zápis je **masked read-modify-write** (zachová ostatní bity reg. 179). Ověření v journalu (`verify_modbus_commands`) porovnává jen bits0–1 maskou `0x0003`. + +**Idempotence:** pokud poslední `verified` hodnota už má správně nastavené bits0–1 (maska `0x0003`), EMS zápis reg. 179 v dalším běhu přeskočí (ostatní bity se ignorují). **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