fix rs485 s eror self_sustain
All checks were successful
CI and deploy / migration-check (push) Successful in 6s
CI and deploy / deploy (push) Successful in 29s

This commit is contained in:
Dusan Vojacek
2026-04-19 15:29:58 +02:00
parent efc2cbfded
commit f8e1eed127
6 changed files with 179 additions and 22 deletions

View File

@@ -139,6 +139,15 @@ Hodnota `deye_zero_export_mode` (1 = zero export to load, 2 = zero export to CT)
**TOU (time points, reg. 166+):** SOC závisí na fyzickém režimu z `get_deye_mode`**SELL** zapisuje ekonomickou rezervu (`reserve_soc_percent`), **PASSIVE** a neaktivní řádky **36** provozní minimum (`min_soc_percent`). Viz [`modbus-registers.md`](modbus-registers.md).
### Verifikace zápisů (journal) a SELF_SUSTAIN
Po zápisu na Modbus se hodnoty ověřují v `verify_modbus_commands` (`control_exporter.py`). Po **3 neúspěšných** cyklech zápis+verify:
- **Kritické registry** (**108, 109, 142, 143, 145**) → přepnutí lokality do **SELF_SUSTAIN** (`system:mismatch`).
- **Ostatní** (včetně **178** a **TOU power W 154159** po vyčerpání soft pravidel) → zůstane **AUTO** (nebo aktuální režim), řádek journalu **`mismatch`**, Discord upozornění.
Při přechodu **SELF_SUSTAIN → AUTO** (`run_fn_set_mode_with_discord`) se na pozadí spustí **rolling replan**, aby aktivní plán odpovídal plné optimalizaci. Viz [`modbus-command-journal.md`](modbus-command-journal.md).
```python
async def write_inverter_setpoints(site_id: int, setpoints: Setpoints, db):
inverters = await db.fetch(

View File

@@ -24,19 +24,23 @@ 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`.
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`** — readback může mít jiné ostatní bity (firmware / paralelní čtení). `value_verified` = přečtená surová hodnota; stav **`verified`**, pokud maska sedí s očekáváním.
4. **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. Po třech neúspěšných cyklech ověření:
- **Obyčejné registry** (mimo souvislý blok Deye **6264**): přepnutí lokality na **SELF_SUSTAIN** přes `run_fn_set_mode_with_discord``ems.fn_set_mode` (`activated_by` = `system:mismatch`, poznámka = důvod). Při skutečné změně `mode_code` jde na Discord **kritická** zpráva (stejný formát jako u ostatních přepnutí režimu).
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. **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.
6. Po třech neúspěšných cyklech ověření:
- **Kritické Deye registry** (**108, 109, 142, 143, 145**): přepnutí lokality na **SELF_SUSTAIN** přes `run_fn_set_mode_with_discord``ems.fn_set_mode` (`activated_by` = `system:mismatch`, poznámka = důvod). Při skutečné změně `mode_code` jde na Discord **kritická** zpráva (stejný formát jako u ostatních přepnutí režimu).
- **Nekritické / soft registry** (např. **178** po vyčerpání druhého čtení, **154159** bez akceptovaného clampu, ostatní mimo výše uvedené kritické): po 3 pokusech zůstane řádek v **`mismatch`**, jde **Discord** (`notify_modbus_mismatch`), **režim se nemění**.
- **Výjimka — systémový čas 6264:** přepnutí režimu **se neprovádí**. Po 3 neúspěšných ověřeních jde **kritický** Discord (`notify_modbus_clock_verify_exhausted`); střídač a EMS režim zůstávají v aktuálním stavu (čas na sběrnici může vyžadovat ruční kontrolu / firmware).
**Po návratu SELF_SUSTAIN → AUTO** (přes `fn_set_mode`): `notification_service` naplánuje na pozadí **rolling replan** (`run_plan_api`, `triggered_by=mode:self_sustain_exit`), aby aktivní plán odpovídal znovu plné optimalizaci v AUTO.
**Baseline po deployi (operativa):** např. počet přepnutí na SELF_SUSTAIN z verify za poslední 2 dny:
`SELECT count(*) FROM ems.site_operating_mode_log WHERE mode_code = 'SELF_SUSTAIN' AND activated_by = 'system:mismatch' AND activated_at >= now() - interval '2 days';`
Pro diagnostiku času Deye po opravě clock logiky používej u `modbus_command` krátké okno (např. `verified_at >= now() - interval '2 days'`).
**Discord při jakékoli změně režimu** (nejen Modbus): `notification_service.run_fn_set_mode_with_discord` volá `ems.fn_set_mode` a při změně `mode_code` oproti stavu před voláním pošle zprávu (`notify_operating_mode_changed`). Úroveň: `user:api` → info, obecné `system:*` → warning, `system:mismatch` → critical. Použití: HTTP `POST /api/v1/sites/{site_id}/mode`, `_switch_to_self_sustain` v `control_exporter`. Vypršení `valid_until`: `ems.fn_expire_modes()` vrací řádky `(site_id, site_code, old_mode, new_mode)` pro každé provedené přepnutí; scheduler v `main.py` (a lazy expire v `_fetch_operating_mode`) z nich pošle Discord.
Implementace: `services/control_exporter.py``verify_modbus_commands`, `_verify_deye_clock_written_bundle`, `_fetch_written_deye_clock_commands`, `_switch_to_self_sustain`; `services/notification_service.py``run_fn_set_mode_with_discord`, `notify_operating_mode_changed`.
Implementace: `services/control_exporter.py``verify_modbus_commands`, `_verify_deye_clock_written_bundle`, `_fetch_written_deye_clock_commands`, `_switch_to_self_sustain`, `DEYE_CRITICAL_REGS_SELF_SUSTAIN`, `_deye_tou_power_verify_match`; `services/notification_service.py``run_fn_set_mode_with_discord`, `_auto_rolling_replan_after_self_sustain_exit`, `notify_operating_mode_changed`.
## Střídač (Deye)

View File

@@ -12,8 +12,8 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi
| Reg | Název | Rozsah | Jednotka | Použití v EMS |
|-----|-------|--------|----------|---------------|
| 108 | Max charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | EMS počítá proud v **SQL**: `COALESCE(deye_register_max_charge_a, FLOOR(LEAST(W)/51.2))` — sloupec stropu v **A** je volitelný (NULL = jen odvod z kW); při vyplnění např. 350 při W→351 A se použije 350. |
| 109 | Max discharge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Stejně: `COALESCE(deye_register_max_discharge_a, FLOOR(LEAST(W)/51.2))`. |
| 108 | Max charge current | 0 … **max dle modelu** (manuál Deye) | 1 A | EMS počítá proud v **SQL**: `COALESCE(deye_register_max_charge_a, FLOOR(LEAST(W)/51.2))` — sloupec stropu v **A** je volitelný (NULL = jen odvod z kW); při vyplnění např. 350 při W→351 A se použije 350. V Pythonu se navíc **clampuje horní strop 350 A** (`DEYE_LV_BATTERY_MAX_CHARGE_DISCHARGE_A`), aby firmware nevracel 350 při zápisu 351. |
| 109 | Max discharge current | 0 … **max dle modelu** (manuál Deye) | 1 A | Stejně: `COALESCE(deye_register_max_discharge_a, FLOOR(LEAST(W)/51.2))` + **clamp 350 A** jako u 108. |
| 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) |
@@ -38,7 +38,7 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi
EMS **nezapisuje** read-modify-write (paralelní čtení jinými klienty může způsobit nesoulad).
**Ověření v journalu (`modbus_command`):** u zápisu **178** se při verify porovnávají jen **bity 45** maskou **`0x0030`** s očekávanou hodnotou (32/48); `value_verified` zůstává plný readback. Detail: `modbus-command-journal.md`.
**Ověření v journalu (`modbus_command`):** u zápisu **178** se při verify porovnávají jen **bity 45** 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 (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`.
## Klíčové registry podle fyzického režimu Deye