fix(planner): EV session viditelna i bez deadline / nad targetem (BUG2)

Zivy incident home-01: aktivni plan mel ev_sessions:0, ac session bezela
(target 70 %). Planovac neviděl ~6 kW zatez auta a spatne rozvrhl baterii
(zbytecny vecerni import).

Root cause (dve pasti):
- fn_planning_site_context vracela session jako null, kdyz needed_wh=0
  (auto nad targetem) i kdyz target_deadline is null.
- _ev_session_from_json (Python) zahazovala session bez deadline.

Fix:
- R__038 fn_ev_session_planning_json: session se vyradi (null) JEN bez tvrdych
  dat (kapacita vozidla / soc_at_connect). target_deadline smi byt NULL --
  solver hard deadline constraint aplikuje jen pri needed>0; oportunisticka
  vrstva bezi i bez deadline. Auto nad targetem zustava v planu jako znama
  zatez i s headroomem k levnemu doplneni. R__039 vola helper (deduplikace
  dvou inline poddotazu, SQL-first).
- _ev_session_from_json si NULL deadline ponecha (energy_needed_wh default 0).
- testy test_ev_session_parse.py; docs ev-charging + planning-changelog;
  CLAUDE.md funkce.

Navrh agresivnejsiho oportunistickeho algoritmu (P50 levnych oken z
market_price_stats misto konstanty 1 Kc/kWh) -- NEnasazeno, k rozhodnuti,
sepsano v docs/04-modules/planning.md (EV oportunismus); riziko regrese
golden ekonomiky, nutny EV fixture + eval.

Overeni: pytest -q 362 passed; golden replay gate 7 passed; solver_v2_eval
beze zmeny (fixtures bez EV session).

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

View File

@@ -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

View File

@@ -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).

View File

@@ -5,6 +5,14 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
---
## 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.