Merge branch 'fix/ev-teltocharge-reg15-and-session-visibility' into dev
# Conflicts: # docs/planning-changelog.md
This commit is contained in:
@@ -175,6 +175,10 @@ CREATE TABLE asset_ev_charger (
|
||||
phases INT DEFAULT 3,
|
||||
connector_count INT DEFAULT 1,
|
||||
schedulable BOOLEAN DEFAULT true,
|
||||
-- TeltoCharge watchdog (V106): reg 19/20. Failsafe = proud po výpadku
|
||||
-- komunikace EMS; běžný provoz řídí reg 15 z plánu, ne failsafe.
|
||||
watchdog_failsafe_a INT NOT NULL DEFAULT 8, -- reg 20: 0–32 A (0 = po výpadku nenabíjet)
|
||||
watchdog_comm_timeout_s INT NOT NULL DEFAULT 300, -- reg 19: s bez komunikace → failsafe
|
||||
notes TEXT
|
||||
);
|
||||
```
|
||||
|
||||
@@ -394,6 +394,26 @@ oportunismus). Session zůstává v plánu i po dosažení targetu, dokud má he
|
||||
**oportunistická vrstva není omezená deadline** (auto bývá doma dál, odjezd
|
||||
řeší rolling replan — rozhodnutí 2026-06-12).
|
||||
|
||||
### Session se NEvyřazuje při needed_wh=0 (fix 2026-06-13)
|
||||
|
||||
Dřív `fn_planning_site_context` vracela `ev_sessions[e] = null`, když
|
||||
`needed_wh = 0` (auto už nad targetem) **a** oportunismus byl vypnutý/headroom
|
||||
nulový — a navíc úplně, když `target_deadline is null`. Druhá past byla v
|
||||
Pythonu: `_ev_session_from_json` zahazovala session bez deadline. Důsledek
|
||||
incidentu: aktivní plán měl `ev_sessions:0`, ač session běžela; **plánovač
|
||||
neviděl ~6 kW zátěž auta** a špatně rozvrhl baterii (zbytečný večerní import).
|
||||
|
||||
Oprava (R__038 `ems.fn_ev_session_planning_json` + `db_io._ev_session_from_json`):
|
||||
|
||||
- Session se vyřadí (`null`) **jen** bez tvrdých dat — neznámá kapacita vozidla
|
||||
nebo `soc_at_connect_pct` (nelze spočítat Wh). Jinak vždy objekt.
|
||||
- **`target_deadline` smí být NULL** (žádný tvrdý cíl) — solver_v2 hard
|
||||
deadline constraint aplikuje jen při `energy_needed_wh > 0`; oportunistická
|
||||
vrstva běží i bez deadline. Auto nad targetem nebo bez cíle tak zůstává v
|
||||
plánu jako známá zátěž i s headroomem k případnému levnému doplnění.
|
||||
- `energy_needed_wh` = 0 bez deadline / cíle; headroom a opportunistic_value
|
||||
beze změny (coalesce session → vozidlo).
|
||||
|
||||
### Min. výkon wallboxu a účtování via-bat (2026-06-12, dev)
|
||||
|
||||
- **`asset_ev_charger.min_power_w`** (1380 W = 6 A IEC 61851) jde přes
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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, 6–32 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) | 0–600 s (0 = vypnuto), default 30 | `TELTO_WATCHDOG_TIMEOUT_S` = **300** |
|
||||
| 20 | R/W | Failsafe current | 0, 6–32 A, default 6 | `TELTO_WATCHDOG_FAILSAFE_A` = **8** |
|
||||
| 15 | R/W | **Amps to use** (limit proudu) | 0 = stop, 6–32 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) | 0–600 s (0 = vypnuto), default 30 | per charger `asset_ev_charger.watchdog_comm_timeout_s` (default **300**) |
|
||||
| 20 | R/W | Failsafe current | 0, 6–32 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 ↔ 6–32 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 0–40 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)
|
||||
|
||||
|
||||
@@ -342,6 +342,47 @@ if ev_session[e].target_deadline and ev_session[e].soc_at_connect_pct is not Non
|
||||
# energy_needed = (default_target_soc - estimated_soc_from_session) * capacity
|
||||
```
|
||||
|
||||
### EV oportunismus — návrh agresivnějšího ocenění z cen (K ROZHODNUTÍ, 2026-06-13)
|
||||
|
||||
**Stav (nasazeno):** měkký cíl = dekompozice `Σ(EV) == needed − unmet + opp`,
|
||||
`opp ∈ [0, headroom]`, hodnota `opportunistic_value_czk_kwh` (default vozidla
|
||||
**1 Kč/kWh**, konstanta). Session zůstává v plánu i bez deadline / nad targetem
|
||||
(fix 2026-06-13). Filozofie v2: ceny, ne heuristiky priorit — solver srovná
|
||||
oportunistický bonus s reálným nákladem nabití (slotový buy + degradace), takže
|
||||
auto se opp vrstvou doplní **jen** když je energie levnější než bonus: typicky
|
||||
**záporná cena** (auto vydělá / lepší než curtail) nebo velmi levné okno.
|
||||
|
||||
**Problém uživatele:** „když je auto k dispozici, chci ho nabíjet hlavně při
|
||||
ZÁPORNÉ ceně (vydělám), ne ať si to šetří na bůhvíkdy." Konstanta 1 Kč/kWh je
|
||||
sice korektní (= ušetřené budoucí nabití, auto neumí prodat zpět), ale je tupá:
|
||||
neodráží, jak levné jsou skutečně budoucí okna daného horizontu.
|
||||
|
||||
**Návrh (NEnasazeno — ověřit ekonomikou + golden):**
|
||||
1. **`opportunistic_value` odvozený z cen, ne konstanta.** Místo fixní 1 Kč/kWh
|
||||
vzít **P50 budoucích levných nákupních oken** z `market_price_stats`
|
||||
(`fn_get_predicted_price` / kvantil za OTE horizont) — „kolik bych typicky
|
||||
zaplatil, kdybych to NEnabil teď". Drahá budoucnost → vyšší bonus (nabít teď
|
||||
se vyplatí), levná budoucnost → nízký bonus (počká si). Spočítat v SQL
|
||||
(`fn_planning_site_context` / nový `fn_ev_opportunistic_value`), ne v Pythonu.
|
||||
2. **Záporná cena = agresivní strop = plné auto.** Při `buy < 0` (a v rozumné
|
||||
míře i hluboce levných slotech) je nabití auta **zisk**: solver to už vidí
|
||||
přes zápornou cenu v objective, ale headroom musí sahat k **100 %**, ne jen
|
||||
k targetu — to dnes platí (headroom = 100 − max(target, soc_at_connect)),
|
||||
takže stačí, aby opp vrstva nebyla zbytečně škrcená nízkým bonusem. Pro
|
||||
záporné ceny lze bonus „zvednout" implicitně (cena sama < 0 stačí), explicitní
|
||||
navýšení netřeba.
|
||||
3. **Sladění s baterií (přirozeně z cen):** záporná cena → nabíjet auto i
|
||||
baterii (oba mají kladnou hodnotu uložení / zisk); vysoká cena → ani auto,
|
||||
ani export z baterie do sítě (degradace + ušlý budoucí prodej to zaplatí).
|
||||
**Žádné explicitní priority** — správné účtování (slotová cena, degradace,
|
||||
terminal/arbitrage hodnota) to vyřeší samo (pravidlo 8 / arbitrage-accounting).
|
||||
|
||||
**Rozhodnout:** zda nahradit konstantu cenovým kvantilem (riziko: rozkmitá
|
||||
golden ekonomiku — nutný eval na fixtures s EV session, které zatím nejsou).
|
||||
Minimum, co je nasazeno bezpečně: session viditelná + headroom k plnému; bonus
|
||||
zůstává konfigurovatelný per vozidlo/session. Až bude EV golden fixture, doplnit
|
||||
bod 1 za flagem a změřit Kč.
|
||||
|
||||
### SoC kontinuita
|
||||
```python
|
||||
# battery_discharge = bd (W z baterie na AC sběrnici z bilance pv+gi+bd = load+bc+ge).
|
||||
|
||||
@@ -13,6 +13,14 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
- **Soubory:** backend/services/control/setpoints.py, test_control_export_plan_guard.py (test_neg_sell_grid_charge_not_blocked), docs/audits/planner-neg-buy-charge-not-executed-2026-06-13.md.
|
||||
- **Ověření:** guard testy 47 passed; živě — záporný den → grid_w roste, SoC k cíli. Mimo solver → golden gate / solver_v2_eval beze změny.
|
||||
|
||||
## 2026-06-13 — EV session viditelná i bez deadline; reg 15 re-asert (2 bugy home-01)
|
||||
|
||||
- **BUG1 (Modbus zápis EV rozbitý):** od ~22:45 UTC 12.6. nevznikl žádný telto journal řádek (ani failed), auto jelo failsafe 8 A místo plánovaných 0 A. **Příčina:** reg 15 (amps) byl write-on-change proti journalu (`fn_modbus_device_state_map`). Jakmile měl reg 15 řádek „0 verified", a plán dál chtěl 0, **nikdy nevznikl nový příkaz** — a TeltoCharge si po výpadku komunikace sám přepsal reg 15 na failsafe (reg 20) **bez journal řádku**. Verify čte zpět jen `written` řádky, takže drift 0 → 8 A nikdo neviděl ani neopravil (tichá divergence). **Fix:** reg 15 se zapisuje **každý tick** (re-asert), reg 19/20 zůstávají write-on-change (EEPROM); per-charger failsafe/timeout (V106 `asset_ev_charger.watchdog_failsafe_a` / `watchdog_comm_timeout_s`). „Zákaz nabíjení" = reg 15 = 0 (protokol rev 0.5 nemá samostatný enable registr).
|
||||
- **BUG2 (plánovač slepý k autu):** aktivní plán měl `ev_sessions:0`, ač session běžela (target 70 %) → plán neviděl ~6 kW zátěž, špatně rozvrhl baterii (zbytečný večerní import). **Příčina:** `fn_planning_site_context` vracela session jako `null`, když `needed_wh=0` (auto nad targetem) i když `target_deadline is null`; navíc `_ev_session_from_json` zahazovala session bez deadline (Python). **Fix:** R__038 `fn_ev_session_planning_json` — session se vyřadí jen bez tvrdých dat (kapacita / soc_at_connect); `target_deadline` smí být NULL (solver hard constraint aplikuje jen při needed>0; oportunistická vrstva běží i bez deadline). `_ev_session_from_json` si NULL deadline ponechá.
|
||||
- **Soubory:** V106, R__038, R__039 (volá helper), `services/control/outputs.py`, `services/planning/db_io.py`; testy `test_ev_write_on_change.py`, `test_ev_session_parse.py`; docs teltocharge / journal / ev-charging.
|
||||
- **Ověření:** `pytest -q` 362 passed; golden replay gate 7 passed; solver_v2_eval beze změny (fixtures bez EV session — golden potvrzuje žádnou regresi na neEV cestě).
|
||||
- **K ROZHODNUTÍ (nenasazeno):** agresivnější oportunistický algoritmus z cen (P50 levných oken z `market_price_stats` místo konstanty 1 Kč/kWh) — návrh v `docs/04-modules/planning.md` sekce „EV oportunismus — návrh".
|
||||
|
||||
## 2026-06-13 — degradační cena dle skutečných cen packů (V103)
|
||||
|
||||
- **Problém:** seedy nesly default 0.50 Kč/kWh u KV1/BA81/HU1 — u malých packů zabíjel mělké arbitráže, u HU1 zkresloval studii spotové smlouvy.
|
||||
|
||||
Reference in New Issue
Block a user