fix(modbus): reg 15 re-asert kazdy tick + per-charger failsafe (BUG1)

Zivy incident home-01 (TeltoCharge .16): od ~22:45 UTC 12.6. nevznikl zadny
telto journal radek (ani failed), auto jelo failsafe 8 A misto planovanych 0 A.

Root cause: reg 15 (amps) byl write-on-change proti journalu
(fn_modbus_device_state_map). Jakmile mel reg 15 radek "0 verified" a plan
dal chtel 0, NIKDY nevznikl novy prikaz -- a TeltoCharge si po vypadku
komunikace sam prepsal reg 15 na failsafe (reg 20) BEZ journal radku. Verify
cte zpet jen 'written' radky, takze tichy drift 0 -> 8 A nikdo nevidel ani
neopravil.

- reg 15 (amps to use) se zapisuje VZDY (re-asert) -- volatilni ridici
  registr, ne EEPROM; drzi verify jobu cerstvy written radek -> drift se
  zachyti a hned opravi. _split_amps_and_watchdog odděluje 15 od 19/20.
- reg 19/20 (watchdog config, EEPROM) zustavaji write-on-change.
- per-charger failsafe/timeout: asset_ev_charger.watchdog_failsafe_a /
  watchdog_comm_timeout_s (V106; default 8 A / 300 s). "Zakaz nabijeni" =
  reg 15 = 0 (protokol rev 0.5 nema samostatny enable registr).
- testy test_ev_write_on_change.py; docs teltocharge + journal + data-model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-13 22:03:11 +02:00
parent 03b7396676
commit 54288ee2fd
6 changed files with 229 additions and 100 deletions

View File

@@ -48,20 +48,29 @@ Implementace: `services/control_exporter.py` — `verify_modbus_commands`, `_ver
## EV wallbox (TeltoCharge)
`write_ev_setpoints` (každý export tick) a `write_ev_arrival_hold` (po detekci
příjezdu EV) zapisují registry **15** (amps to use), **19** (comm timeout 300 s)
a **20** (failsafe 8 A) — vždy přes journal (`asset_type = 'ev_charger'`).
příjezdu EV) zapisují registry **15** (amps to use), **19** (comm timeout) a
**20** (failsafe) — vždy přes journal (`asset_type = 'ev_charger'`). Timeout
a failsafe jsou per charger (`asset_ev_charger.watchdog_comm_timeout_s` /
`watchdog_failsafe_a`, V106; default 300 s / 8 A).
- **Verify job ověřuje všechny asset typy** — `fn_modbus_written_command_ids`
nefiltruje podle `asset_type` a registry 15/19/20 jsou dle protokolu R/W
(čtou se zpět standardní FC 3 větví).
- **Write-on-change:** před zápisem se registry filtrují proti
**`ems.fn_modbus_device_state_map`** (nejnovější řádek journalu per registr;
hodnota jen pro stav `written`/`verified`). Shodná hodnota ⇒ zápis se
přeskočí. Na rozdíl od `fn_modbus_last_verified_map` (Deye drop-unchanged)
nečeká na verify — `written` stačí, takže pomalý/neúspěšný verify read
nevede k opakovaným zápisům každý tick (EEPROM wear). Nejnovější řádek
`failed`/`mismatch` ⇒ registr v mapě chybí ⇒ po výpadku zařízení se
konfigurace (vč. watchdog 19/20) obnoví jedním zápisem.
- **Reg 15 (amps) se zapisuje KAŽDÝ tick** (re-asert), **NE write-on-change.**
Incident 2026-06-13: TeltoCharge si po výpadku komunikace sám přepíše reg 15
na failsafe (reg 20) bez journal řádku; write-on-change proti journalu
(poslední „0 verified") by tichý drift **0 → 8 A** nikdy nezahlédlo (verify
čte zpět jen `written`) a nikdy neopravilo. Re-asert každý tick drift opraví
a drží verify jobu čerstvý `written` reg-15 řádek. Reg 15 je volatilní řídicí
registr (ne EEPROM).
- **Reg 19/20 (watchdog config) zůstávají write-on-change:** před zápisem se
filtrují proti **`ems.fn_modbus_device_state_map`** (nejnovější řádek journalu
per registr; hodnota jen pro stav `written`/`verified`). Shodná hodnota ⇒
zápis se přeskočí. Na rozdíl od `fn_modbus_last_verified_map` (Deye
drop-unchanged) nečeká na verify — `written` stačí, takže pomalý/neúspěšný
verify read nevede k opakovaným zápisům každý tick (EEPROM wear). Nejnovější
řádek `failed`/`mismatch` ⇒ registr v mapě chybí ⇒ po výpadku zařízení se
watchdog 19/20 obnoví jedním zápisem.
- **Mismatch po 3 pokusech NEpřepíná SELF_SUSTAIN** — fallback režim je Deye
politika (`asset_type = 'inverter'`); u wallboxu zůstane řádek `mismatch`
+ Discord (`notify_modbus_mismatch`).

View File

@@ -32,42 +32,67 @@ ukončil session a EV výkon 0 by špinil bazál (pravidlo 15).
| Reg | R/W | Význam | Hodnoty | EMS zapisuje |
|-----|-----|--------|---------|--------------|
| 15 | R/W | **Amps to use** (limit proudu) | 0 = stop, 632 A | hodnota z plánu (`ev{1,2}_current_a`); příjezd EV → hold 0 A |
| 16 | R/W | Start/Stop session | 0 nic · 1 stop · 2 start | ne |
| 19 | R/W | Communication timeout (watchdog) | 0600 s (0 = vypnuto), default 30 | `TELTO_WATCHDOG_TIMEOUT_S` = **300** |
| 20 | R/W | Failsafe current | 0, 632 A, default 6 | `TELTO_WATCHDOG_FAILSAFE_A` = **8** |
| 15 | R/W | **Amps to use** (limit proudu) | 0 = stop, 632 A | hodnota z plánu (`ev{1,2}_current_a`); příjezd EV → hold 0 A. **Zapisuje se KAŽDÝ tick** (re-asert, ne write-on-change — viz níže) |
| 16 | R/W | Start/Stop session | 0 nic · 1 stop · 2 start | ne (tvrdé zastavení řešíme reg 15 = 0) |
| 19 | R/W | Communication timeout (watchdog) | 0600 s (0 = vypnuto), default 30 | per charger `asset_ev_charger.watchdog_comm_timeout_s` (default **300**) |
| 20 | R/W | Failsafe current | 0, 632 A, default 6 | per charger `asset_ev_charger.watchdog_failsafe_a` (default **8**) |
Všechny čtyři registry jsou dle oficiálního protokolu (wiki *External control
RS485* / protokol rev 0.5) **R/W** — verify job je čte zpět standardní FC 3
větví (žádný write-only registr v této sadě).
### Write-on-change — POVINNÉ (EEPROM wear)
**„Zákaz nabíjení" = reg 15 = 0.** Protokol rev 0.5 v této sadě **nemá**
samostatný boolean „charging enable/disable" registr — řízení je proudovým
limitem (reg 15: 0 = stop) plus volitelně reg 16 (1 = stop session). EMS
používá **reg 15 = 0** jako řízené zastavení (arrival-hold i běžný plán);
reg 16 se nezapisuje. Failsafe (reg 20) je hodnota PŘI výpadku komunikace,
ne při běžném provozu — běžně auto stojí na 0 A, dokud plán neřekne jinak.
### Reg 15 (amps) — VŽDY re-asert; reg 19/20 — write-on-change (EEPROM)
Export tick běží ~8×/hod (control_export `:14,:29,:44,:59` + rolling replan
`*/15` s exportem). Zápis do wallboxu se proto provádí **jen při skutečné
změně hodnoty**: `write_ev_setpoints` i `write_ev_arrival_hold` filtrují
registry přes `_drop_registers_matching_last_verified` proti
**`ems.fn_modbus_device_state_map`** (nejnovější řádek journalu per registr
se stavem `written` **nebo** `verified`). Důsledky:
`*/15` s exportem).
- **reg 15** se zapíše jen při změně plánovaného proudu (0 ↔ 632 A) — to je
legitimní zápis;
- **reg 19/20** se zapíší jednou po nasazení / po výpadku zařízení (nejnovější
řádek `failed`/`mismatch` ⇒ registr v mapě chybí ⇒ znovu se zapíše) a pak
už nikdy, dokud se hodnota nezmění;
- čekání na verify **neblokuje** skip — `written` (TCP ack) stačí, mismatch
z verify stav mapy zneplatní a vynutí nový zápis.
- **reg 15 (amps to use) se zapisuje při KAŽDÉM ticku** (`write_ev_setpoints`
i `write_ev_arrival_hold`). **Důvod (incident 2026-06-13):** TeltoCharge si
po výpadku komunikace sám přepíše reg 15 na failsafe (reg 20) — bez journal
řádku. Kdyby byl reg 15 write-on-change proti journalu (poslední
„0 verified"), EMS by tichý drift **0 → 8 A** na zařízení **NIKDY
nezahlédlo** (verify čte zpět jen `written` řádky) a nikdy ho neopravilo:
auto po každém krátkém výpadku spojení tiše jelo 8 A místo plánovaných 0 A.
Reg 15 je volatilní řídicí registr (ne EEPROM), opakovaný zápis je v pořádku;
re-asert každý tick zároveň drží verify jobu čerstvý `written` reg-15 řádek
→ případný drift se zachytí a hned opraví.
- **reg 19/20 (watchdog config) zůstávají write-on-change** přes
`_drop_registers_matching_last_verified` proti **`ems.fn_modbus_device_state_map`**
(nejnovější řádek journalu per registr, stav `written` **nebo** `verified`):
zapíší se jednou po nasazení / po výpadku zařízení (nejnovější řádek
`failed`/`mismatch` ⇒ registr v mapě chybí ⇒ znovu se zapíše) a pak už ne,
dokud se hodnota nezmění — šetří EEPROM. Čekání na verify skip neblokuje,
`written` (TCP ack) stačí.
### Watchdog — sytí ho i čtení
Implementace: `_telto_setpoint_registers` (per-charger failsafe/timeout),
`_split_amps_and_watchdog` (reg 15 vs 19/20) v `services/control/outputs.py`.
### Watchdog — sytí ho i čtení; failsafe konfigurovatelný
Protokol definuje timeout jako *„if no **valid communication** is present
after a configurable time interval…"* — timer resetuje **jakákoli** validní
Modbus komunikace s unit ID wallboxu, **včetně FC 3 čtení**. Telemetrie čte
blok 040 každých **60 s**, takže watchdog 300 s je trvale sycen čtením a
**periodické zápisy k udržení spojení nejsou potřeba**. Failsafe (omezení na
8 A, reg 20 „max allowed current on comm timeout") nastane až po 5 min bez
jakéhokoli pollingu = skutečný výpadek EMS; auto se pak přes noc dobije
pomalu místo stání na 0 A.
**periodické zápisy k udržení spojení nejsou potřeba**. Failsafe (reg 20
„max allowed current on comm timeout") nastane až po `watchdog_comm_timeout_s`
bez jakéhokoli pollingu = skutečný výpadek EMS.
**Failsafe je per charger** (`asset_ev_charger.watchdog_failsafe_a`, default
8 A; `watchdog_comm_timeout_s`, default 300 s; migrace V106):
- default **8 A** = po skutečném výpadku EMS se auto přes noc pomalu dobije
místo stání na 0 A;
- snížit lze na **6 A** (IEC 61851 minimum) nebo **0** (po výpadku nenabíjet),
dle dotačních / komfortních požadavků;
- **běžný provoz po zapojení řídí reg 15 z plánu** (0 A drží arrival-hold +
sycení watchdogu čtením telemetrie), failsafe se uplatní jen při výpadku —
rozpor „chci řízený default 0 A, ale po výpadku malý proud" je tím vyřešen.
## WB2 mimo EMS (V105, 2026-06-13)