fix cutoff gen port
Some checks failed
CI and deploy / migration-check (push) Failing after 11s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-29 12:51:53 +02:00
parent e35110cb87
commit afee62ba4e
4 changed files with 43 additions and 1 deletions

View File

@@ -0,0 +1,15 @@
---
description: When changing implementation, update relevant docs
alwaysApply: true
---
# Documentation update discipline
- When you make an **implementation change** (Python/SQL/frontend), you must also update the **relevant documentation**
in `docs/` (and/or `CLAUDE.md` if its normative guidance) in the same change set.
- The docs update must cover:
- what behavior changed (externally visible / operational impact),
- where it is implemented (file/function names),
- how to verify it (DB table/view, API endpoint, or operational check).
If there is no existing relevant document, add a short section to the closest module doc under `docs/04-modules/`.

View File

@@ -1515,6 +1515,7 @@ async def write_inverter_setpoints(
if inv is None: if inv is None:
return "FAIL inverter: no controllable Modbus endpoint" return "FAIL inverter: no controllable Modbus endpoint"
unit_id = int(inv.unit_id if inv.unit_id is not None else 1)
raw_bat = setpoints_now.battery_w raw_bat = setpoints_now.battery_w
grid_w = int(setpoints_now.grid_setpoint_w or 0) grid_w = int(setpoints_now.grid_setpoint_w or 0)
no_export = inv.no_export no_export = inv.no_export
@@ -1647,7 +1648,7 @@ async def write_inverter_setpoints(
) )
try: try:
mb179 = await get_modbus_client(inv.host, inv.port) mb179 = await get_modbus_client(inv.host, inv.port)
r179 = await mb179.read_holding_registers(179, 1, unit) r179 = await mb179.read_holding_registers(179, 1, unit_id)
if r179 and len(r179) >= 1: if r179 and len(r179) >= 1:
current_179 = int(r179[0]) current_179 = int(r179[0])
new_179 = (current_179 & ~REG179_MI_EXPORT_MASK) | int(target_bits) new_179 = (current_179 & ~REG179_MI_EXPORT_MASK) | int(target_bits)
@@ -1792,11 +1793,13 @@ async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict
b108 = await mb.read_holding_registers(108, 2) b108 = await mb.read_holding_registers(108, 2)
b141 = await mb.read_holding_registers(141, 5) b141 = await mb.read_holding_registers(141, 5)
r178 = await mb.read_holding_registers(178, 1) r178 = await mb.read_holding_registers(178, 1)
r179 = await mb.read_holding_registers(179, 1)
r191 = await mb.read_holding_registers(191, 1) r191 = await mb.read_holding_registers(191, 1)
r108, r109 = b108[0], b108[1] r108, r109 = b108[0], b108[1]
r141, r142, r143 = b141[0], b141[1], b141[2] r141, r142, r143 = b141[0], b141[1], b141[2]
r145 = b141[4] r145 = b141[4]
r178 = r178[0] r178 = r178[0]
r179 = r179[0]
r191 = r191[0] r191 = r191[0]
except Exception: except Exception:
logger.exception("read_deye_registers_live site=%s failed", site_id) logger.exception("read_deye_registers_live site=%s failed", site_id)
@@ -1810,6 +1813,9 @@ async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict
"reg143_export_limit_w": int(r143), "reg143_export_limit_w": int(r143),
"reg145_solar_sell": int(r145), "reg145_solar_sell": int(r145),
"reg178_peak_shaving_switch": int(r178), "reg178_peak_shaving_switch": int(r178),
"reg179_control_board_special_1": int(r179),
"reg179_mi_export_cutoff_bits": int(r179) & int(REG179_MI_EXPORT_MASK),
"reg179_mi_export_cutoff_is_on": (int(r179) & int(REG179_MI_EXPORT_MASK)) == int(REG179_MI_EXPORT_DISABLE),
"reg191_peak_shaving_w": int(r191), "reg191_peak_shaving_w": int(r191),
"read_at": read_at.isoformat(), "read_at": read_at.isoformat(),
} }

View File

@@ -111,6 +111,19 @@ def apply_overrides(plan, overrides) -> Setpoints:
**Princip:** držet mapování plán → Deye **jednoduché**; detail a zdůvodnění v [`operating-modes.md`](operating-modes.md) (sekce *Keep it simple*). **Princip:** držet mapování plán → Deye **jednoduché**; detail a zdůvodnění v [`operating-modes.md`](operating-modes.md) (sekce *Keep it simple*).
### BA81: GEN port cut-off (mikroinvertory na GEN) přes reg 179
U instalací typu **BA81** (AC coupling / mikroinvertory na GEN portu) může solver uložit do plánu flag
`planning_interval.deye_gen_cutoff_enabled` (true/false). Pokud je na střídači zapnutý feature flag
`asset_inverter.deye_gen_microinverter_cutoff_enabled = true`, exporter provede **masked read-modify-write**
registru **179**:
- `deye_gen_cutoff_enabled = true` → reg **179** bits **01** = **2** (`10b`, disable = cut-off **ON**)
- `deye_gen_cutoff_enabled = false` → reg **179** bits **01** = **3** (`11b`, enable = cut-off **OFF**)
Zápisy se ukládají do `ems.modbus_command` a ověřují v `verify_modbus_commands` (porovnává se pouze maska
bits 01). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg 179).
### Fyzický režim (`get_deye_mode` v `exporter_monolith.py`) ### Fyzický režim (`get_deye_mode` v `exporter_monolith.py`)
| Fyzický režim | Podmínka z `ControlSetpoints` | | Fyzický režim | Podmínka z `ControlSetpoints` |

View File

@@ -25,6 +25,8 @@ Indexy: podle `(site_id, status, created_at)` a částečný index pro `pending`
1. Po `mismatch` se odešle **Discord** alert (`notify_modbus_mismatch`), pokud je nastaven `DISCORD_WEBHOOK_URL`. 1. Po `mismatch` se odešle **Discord** alert (`notify_modbus_mismatch`), pokud je nastaven `DISCORD_WEBHOOK_URL`.
2. **Retry** zápisu max. **3×** (počítáno přes `attempt_count` po zápisech). 2. **Retry** zápisu max. **3×** (počítáno přes `attempt_count` po zápisech).
3. **Reg 178** (grid peak shaving switch): journal ukládá **celé 16bit** `value_to_write` (32 nebo 48). Při ověření se za **shodu** považuje shoda **bitů 45** maskou **`0x0030`** s očekáváním; `value_verified` = přečtená surová hodnota. Při nesouladu masky se **jednou** znovu přečte reg. 178 (druhé FC3) kvůli glitchům na RS485 — pokud druhé čtení maskou sedí, stav je **`verified`**. 3. **Reg 178** (grid peak shaving switch): journal ukládá **celé 16bit** `value_to_write` (32 nebo 48). Při ověření se za **shodu** považuje shoda **bitů 45** maskou **`0x0030`** s očekáváním; `value_verified` = přečtená surová hodnota. Při nesouladu masky se **jednou** znovu přečte reg. 178 (druhé FC3) kvůli glitchům na RS485 — pokud druhé čtení maskou sedí, stav je **`verified`**.
4. **Reg 179** (control board special 1, BA81 GEN cut-off): exporter zapisuje masked RMW (zachová ostatní bity).
Při ověření se za shodu považuje jen maska **bits 01** (`0x0003`) vůči očekávání (2 = cutoff ON, 3 = cutoff OFF).
4. **TOU výkon W (154159):** firmware často vrátí **max. výkon z reg. 108/109 × 51.2 V** místo přesně zapsaného W; verify to akceptuje jako **shodu** (skutečný výkon je stejně omezen proudy 108/109). 4. **TOU výkon W (154159):** firmware často vrátí **max. výkon z reg. 108/109 × 51.2 V** místo přesně zapsaného W; verify to akceptuje jako **shodu** (skutečný výkon je stejně omezen proudy 108/109).
5. **Pojistka 6264**: pokud by se řádek registru **62, 63 nebo 64** omylem dostal do striktní větve po jednom registru, verify to zachytí a zpracuje **jako toleranční celek 6264** (stejně jako primární clock větev) — bez přepnutí do SELF_SUSTAIN jen kvůli tomu. 5. **Pojistka 6264**: pokud by se řádek registru **62, 63 nebo 64** omylem dostal do striktní větve po jednom registru, verify to zachytí a zpracuje **jako toleranční celek 6264** (stejně jako primární clock větev) — bez přepnutí do SELF_SUSTAIN jen kvůli tomu.
6. Po třech neúspěšných cyklech ověření: 6. Po třech neúspěšných cyklech ověření:
@@ -46,6 +48,9 @@ Implementace: `services/control_exporter.py` — `verify_modbus_commands`, `_ver
`write_inverter_setpoints` přidá do journalu podle potřeby **6264** (čas — po čtení z invertoru jen při driftu / 24h intervalu; viz `modbus-registers.md`) a **time pointy 148177** (bloky 36 typicky jednou denně; viz `modbus-registers.md`), dále **108**, **109**, **141**, **142**, **178**, **143**. Každý řádek daného exportního běhu má **`deye_physical_mode`** (**PASSIVE** / **SELL** / **CHARGE**). **Reg 191** EMS nezapisuje (SolarmanApp). Převod výkonu: `battery_watts_to_amps` v `modbus-registers.md`. `write_inverter_setpoints` přidá do journalu podle potřeby **6264** (čas — po čtení z invertoru jen při driftu / 24h intervalu; viz `modbus-registers.md`) a **time pointy 148177** (bloky 36 typicky jednou denně; viz `modbus-registers.md`), dále **108**, **109**, **141**, **142**, **178**, **143**. Každý řádek daného exportního běhu má **`deye_physical_mode`** (**PASSIVE** / **SELL** / **CHARGE**). **Reg 191** EMS nezapisuje (SolarmanApp). Převod výkonu: `battery_watts_to_amps` v `modbus-registers.md`.
Pokud je zapnutý feature `asset_inverter.deye_gen_microinverter_cutoff_enabled = true`, exporter navíc zapisuje
**reg 179** (masked RMW) podle `planning_interval.deye_gen_cutoff_enabled` (BA81 GEN port cut-off).
**Dávky:** `execute_modbus_commands` slučuje souvislé adresy do jednoho **`write_registers`** (FC **0x10**). `verify_modbus_commands` čte zpět po souvislých blocích (`read_holding_registers`, FC 0x03). Detail režimů: `modbus-registers.md`. **Dávky:** `execute_modbus_commands` slučuje souvislé adresy do jednoho **`write_registers`** (FC **0x10**). `verify_modbus_commands` čte zpět po souvislých blocích (`read_holding_registers`, FC 0x03). Detail režimů: `modbus-registers.md`.
## APScheduler ## APScheduler
@@ -67,6 +72,9 @@ Vrátí počty `checked` / `verified` / `mismatch` a seznam dotčených příkaz
Tabulka pro budoucí logování **cut-off** přepínačů (mikroinvertory / GEN při záporné prodejní ceně). Záznam při **změně** stavu: `asset_code`, `new_state`, `previous_state`, `reason`, `sell_price_czk`, `triggered_by`. Zatím jen schéma; logika napojení v `control_exporter` je v TODO. Tabulka pro budoucí logování **cut-off** přepínačů (mikroinvertory / GEN při záporné prodejní ceně). Záznam při **změně** stavu: `asset_code`, `new_state`, `previous_state`, `reason`, `sell_price_czk`, `triggered_by`. Zatím jen schéma; logika napojení v `control_exporter` je v TODO.
Poznámka: **GEN port cut-off na BA81** se aktuálně provádí přímo přes Deye **reg 179** a loguje se v `ems.modbus_command`.
`cutoff_switch_log` je oddělená tabulka pro budoucí obecnější “cut-off” akce (nezávisle na konkrétním Modbus registru).
## Konfigurace ## Konfigurace
- `.env`: `DISCORD_WEBHOOK_URL` — prázdné = notifikace vypnuté (jen log). - `.env`: `DISCORD_WEBHOOK_URL` — prázdné = notifikace vypnuté (jen log).