Files
ems/docs/ev-improvement-plan-2026-06-14.md
Dusan Vojacek 8ffe5460f1 fix(planner): živé EV SoC z integrálu power_w — konec phantom 11 kW oken
needed_wh i headroom z live_soc (soc_at_connect + integrál power_w), ne ze
zamrzlého soc_at_connect. energy_delivered_wh se během session nikdy nezapisoval
(→ needed konstantní, plánovač slepý k pokroku), counter energy_kwh (Telto reg 39)
je rozbitý (17.4 kWh nabito → counter 0.18). Nový fn_ev_session_delivered_wh
integruje power_w (dt cap 120 s), clamp 99 %, fallback drží staré chování bez
telemetrie. Ověřeno živě: needed_wh 18750→1329, live_soc 97.9 %.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:33:08 +02:00

214 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EV plán zlepšení — konsolidace (2026-06-14)
Triáž z živého provozu home-01 (Tesla na TeltoCharge). Sjednocuje pět propojených
pozorování do jednoho prioritizovaného plánu. Cílem je odstranit phantom nabíjení,
fragmentaci a slepotu plánovače k reálnému stavu auta — **bez nové „přežvykovací"
vrstvy nad plánem** (bolístka z v1). Veškerá logika zůstává v solveru / SQL,
control vrstva je hloupý vykonavatel.
## Průřezové zásady (platí pro všechny body)
- **Nebudit auto.** Tesla `vehicle_data` (SoC/odometer/poloha) se čte jen když je
auto vzhůru z vlastní vůle — `get_vehicle_api_state` (`tesla_client.py:183`,
online/asleep/offline, nebudí) gatuje budicí `get_charge_state`
(`tesla_client.py:125`). Výjimka bez buzení: **auto, které aktivně nabíjí, je
vzhůru** → během session lze SoC číst bezpečně.
- **SQL-first.** Živé SoC i „full" stav musí být v DB sloupci/tabulce; čte je
`fn_ev_session_planning_json` / `fn_ev_session_defaults`. Žádné skládání v Pythonu.
- **Golden gate.** Cokoliv mění `planning_interval` (solver, needed_wh) musí projít
golden gate. Solver změny za DB flagem s default = no-op, pak kalibrace harnessem.
Pozor: golden fixtures dnes EV **nulují** → nutná EV fixture z home-01 (chce DB).
- **Žádné hardcoded kódy zařízení.** Vybírat podle `site_id` + `id`.
- **Import/export tvrdé limity** (§7/§19): control nesmí jednostranně zvednout EV
výkon nad plán (rozbilo by garanci `import ≤ max_import`).
---
## 1. Živé SoC auta do plánovače — ✅ IMPLEMENTOVÁNO na dev (2026-06-14)
> **KOREKCE po ověření na živé DB:** plánovaný coulomb z `vw_latest_ev_charger.energy_kwh`
> NEFUNGUJE — ten counter (Telto reg 39) je **rozbitý** (17.4 kWh reálně nabito →
> counter stál na 0.18). Fix proto integruje **`power_w`** (spolehlivý signál), ne counter.
> Hotovo v `R__038`: nový `fn_ev_session_delivered_wh` (time-weighted integrál power_w,
> dt cap 120 s) + přepočet needed_wh/headroom z `live_soc = soc_at_connect + delivered/cap`
> (clamp 99 %), fallback `coalesce(live, energy_delivered_wh, 0)`. **Ověřeno živě:**
> needed_wh 18750 → **1329 Wh**, live_soc 97.9 %. **Nenasazeno na prod** (čeká deploy).
> Detail: `docs/planning-changelog.md` 2026-06-14.
**Pozorování:** auto na 99 %, ale plán do rána ukazuje 4× 11 kW okna (phantom).
**Příčina (ostrá, z workflow `wqikxa47f` — horší, než vypadala):** plánovač počítá
`needed_wh` i headroom **výhradně ze zamrzlého `soc_at_connect_pct`** (zapsán jednou
při příjezdu) mínus `energy_delivered_wh` — JENŽE **`energy_delivered_wh` se během
session NIKDY nezapíše** (V006:53, NOT NULL DEFAULT 0; `fn_ev_session_transition` jen
otevře/zavře, `telemetry_collector` píše kumulativní energii jen do
`telemetry_ev_charger.energy_kwh`, ne do session). Takže delivered je **trvale 0**
**`needed_wh = (target soc_at_connect)/100 × cap` je KONSTANTA po celou session,
neklesá jak auto nabíjí.** Plánovač je k pokroku nabíjení **úplně slepý** → rolling
replan každých 15 min znovu emituje plný deficit (4× 11 kW okno). (R__038:3761.)
**Klíč: živý progres už v DB MÁME**`telemetry_ev_charger.energy_kwh` (Teltonika
reg 39, kumulativní kWh per session, reset na novou session, poll 60s), vystavený přes
`vw_latest_ev_charger.energy_kwh`. **Hardwarově měřený, funguje pro všechna auta
(i Zoe), bez Tesla API, bez buzení.**
**Fix #1 (primární — čistě SQL, žádná nová vrstva/tabulka/job, žádné buzení):**
v `fn_ev_session_planning_json` (R__038) nahradit zamrzlý `coalesce(es.energy_delivered_wh, 0)`
**živým** `coalesce((select energy_kwh from vw_latest_ev_charger … otevřená session) × 1000,
es.energy_delivered_wh, 0)`. Odvodit `live_soc = soc_at_connect + delivered/(cap_wh)×100`,
**clamp 99 %** (taper ignorujeme). `needed_wh = greatest(0, (target live_soc)/100 × cap_wh)`
a `headroom = greatest(0, (99 live_soc)/100 × cap_wh)` z živého SoC. → needed i headroom
klesají s nabíjením a **kolabují na 0 při plném autě** → phantom okna zmizí.
- **Fallback `coalesce(vw, es.energy_delivered_wh, 0)`** drží staré golden fixtures
beze změny (bez WB telemetrie = delivered 0 = soc_at_connect = dnešní chování) →
**golden gate zůstane zelená by-construction.** Tj. #1 lze nakódovat a lokálně
dokázat ne-regresi **bez živé DB**; DB chce jen živé ověření na home-01.
**Komplement — ne-Tesla (Zoe):** dnes se session při `soc_at_connect_pct IS NULL`
**úplně vyřadí z LP** (R__038:29). Změkčit: startovní SoC z kaskády (ruční UI patch
přes R__015 → zděděný `soc_at_disconnect` minulé session → konzervativní default ~20 %),
pak coulomb delta dá použitelné absolutní SoC. *(samostatný krok po ověření #1.)*
**Odloženo (Fix #3, opt-in):** mid-session Tesla refresh živého SoC — JEN když coulomb
counter nestačí (auto nabito mimo WB / chybí WB telemetrie). Budí auto (vampire drain,
proti dnešní zásadě) → nedělat, dokud se coulomb fix neukáže jako nedostatečný. Wallbox
`charging_state` „full" je univerzální brzda zdarma navrch (auto přestalo brát → needed
spadne i kdyby coulomb plaval).
**Soubory:** `R__038_fn_ev_session_planning_json.sql` (jádro), `R__015` (Zoe patch),
`docs/04-modules/ev-charging.md`, `docs/planning-changelog.md`. **Golden:** ANO (mění
needed_wh) — fallback drží fixtures bez EV telemetrie identické; fixtures s nenulovou WB
telemetrií se přegenerují (phantom byl bug, nová čísla správná). **Roll-forward deficitu**
vyjde emergentně: nenabito dnes → SoC nízký → další deadline dožene.
**Ověřit na živé DB (chce IP):** že `vw_latest_ev_charger.energy_kwh` sedí na AKTUÁLNÍ
session (counter per connector — spolehlivě resetuje na session?); reálná AC→DC účinnost
(~812 % ztrát → live SoC mírně optimistické, žádoucí směr — méně phantom); porovnat
odvozené `live_soc` vs 99 % na displeji auta a že needed_wh/headroom spadnou na ~0.
---
## 2. Předehřev / 0 A logika — PRIORITA Č. 2 (control, bez golden)
**Princip:** wallbox neumí oddělit „proud na topení" od „proudu na nabití".
- **SoC ≥ target → NEřezat na 0 A.** Pusť proud → Tesla se předehřeje z WB (levná
síť/baterka) místo z vlastní (vožené, drahé) baterie; protože je na targetu,
baterku stejně nenabije → nulové riziko. Hlavní zimní případ.
- **SoC < target → řídí plán** (nabít v levných, 0 A v drahých). Konflikt
předehřevu v drahém slotu je vzácný (auto obvykle dosáhne targetu přes noc) →
nepřekomplikovávat.
- **Operačně:** odpojení → **jednorázové 0 A** (auto pryč, failsafe je jedno,
žádné periodické psaní); připojení → notifikace → plán + amps; po dobu připojení
→ re-assert amps každý tick (Fáze-0 oprava proti driftu WB watchdogu na failsafe).
- **Fáze z DB** (`asset_ev_charger.phases`), ne hardcoded 3/1 (`setpoints.py:185-186`).
**Závisí na #1** (potřebuje znát SoC ≥ target). **Soubory:** `setpoints.py`,
`outputs.py`, `docs/04-modules/ev-charging.md`. **Golden:** NE (jen překlad
watt→amp při zápisu; control nečte/nepíše planning_interval).
---
## 3. Anti-fragmentace + plný výkon v solveru — PRIORITA Č. 3 (za flagem)
**Pozorování:** nabíjení rozsekané přes 21:15 / 1:30 / 1:45 / 5:30 / 6:00, navíc
dílčí 1,31,4 kW. „Z baterky je solveru jedno kdy" (uživatel) = indiference →
náhodný scatter.
**Příčina:** EV je v solveru spojitá energie po slotech bez jakéhokoliv časového
členu — žádná start/stop ani commitment penalta (tu má jen baterie). Pro LP je
souvislý i roztříštěný profil ekonomicky identický (`solver_v2.py:292-337`).
Dílčí výkon = marginální slot dolitý na zbytek (spojitá proměnná, `:162-175`).
**Fix (jeden člen v objektivu, žádná nová vrstva):**
- **Block-start penalta:** `ev_start[t] ≥ on[t] on[t-1]`, objektiv
`+ Σ ev_start × planner_ev_start_penalty_czk`. Min počet startů = jedna várka.
Protože scatter z baterie je čistá remíza, **malá penalta slepí blok zadarmo** a
**nikdy nepřebije reálný cenový spread** (auto-splnění obavy „ať to nehrne přes
extrémní cenu"). DB param na `asset_ev_charger`, default 0 = no-op.
- **`min_power_w` → třífázový floor** (6 A × 3 × 230 ≈ 4140 W) místo jednofázových
1380 → zruší sub-6 A drobky i tiché shození pod minimem (`outputs.py:49` je
správně, problém je nefyzikální setpoint z plánu).
**Soubory:** `solver_v2.py`, `db/migration/V1xx__asset_ev_charger_ev_start_penalty.sql`,
`R__039`, `db_io.py`, golden fixtures, `docs/04-modules/planning.md`,
`docs/planning-changelog.md`. **Golden:** ANO (za flagem default 0 → no-op).
**Odloženo zvlášť:** explicitní round-trip cena EV-z-baterie v LP (citlivé na
arbitráž §8; na scatter nemá vliv).
---
## 4. Trip/usage forecast — aktivace (PRIORITA Č. 4, většinou jen config)
**Stav: postaveno** (V089 + R__096), chytřejší než původní nápad:
- `ev_vehicle_obs` (Tesla obs při příjezdu/odjezdu), `ev_trip` (km z odometru,
kWh z ΔSoC, `charged_away` vyloučí nabíjení cestou), `ev_usage_stats` (týdenní
DOW rytmus), job 00:50 (`lifespan.py:276 fn_update_ev_usage_stats`).
- `fn_ev_required_soc` = **P80 spotřeby toho DOW + 10 p.b.**, clamp
`[min_target_soc_pct, 100]`; `fn_ev_next_departure` = typický odjezd.
- Model je **DOW-based, ne GPS-route** — GPS okruhy zatím nedělá (refinement,
nízká priorita; DOW na dojíždění většinou stačí).
**Co dotáhnout:**
- Ověřit **objem dat** (≥4 vzorky/DOW; telemetrie od ~3/2026 → po 3 měsících by
mělo stačit) — chce živou DB.
- Zvážit zapnutí `asset_vehicle.target_soc_forecast_enabled` (default false =
sbírá, ale session jede na defaultech).
**Golden:** NE (jen nastaví target/deadline session). **Soubory:** žádné nové,
jen verifikace + flag.
---
## 5. Geofence arrival trigger — PRIORITA Č. 5 (schváleno uživatelem)
**Motivace:** dnes je celý arrival/trip ukotvený na **píchnutí do wallboxu**.
Když uživatel nepíchne (zaparkuje doma bez nabíjení), wallbox nevidí nic → žádný
trip, žádná obs. Presence cesta (V095) přitom „je doma" **detekuje přes GPS
geofence** i bez píchnutí (`telemetry_collector.py:840-849 at_home`).
**Fix:** přechod `at_home` false→true (auto vzhůru, nepíchnuté) brát jako
**arrival home event**:
- zapsat obs pro trip-building (i bez píchnutí), s `trigger` rozlišujícím zdroj
(wallbox vs geofence);
- umožnit proaktivní notifikaci (bod #6) i bez píchnutí.
**Caveaty:** oportunistické (jen když je auto vzhůru → ne instantní); **debounce**
(23 vzorky); **dedup s wallbox arrival** (když píchneš, wallbox je autoritativní,
geofence se nepočítá dvakrát); trip se páruje s nejbližším relevantním
odjezd-eventem. **Soubory:** `telemetry_collector.py` (presence cesta),
`R__096` (`fn_ev_build_trips` přijme geofence arrivals), případně nový `trigger`
enum ve `V089` schématu (nová migrace). **Golden:** NE (jen sběr dat/notifikace).
---
## 6. Proaktivní notifikace „doma + nenabito + levné" — PRIORITA Č. 6
**Datový základ existuje** (`ev_presence_obs`: `at_home`, `charging_state`, SoC,
vše bez buzení). Logika: `at_home=true` ∧ nepíchnuto (`charging_state` disconnected)
∧ SoC < target ∧ (přebytek PV NEBO záporná/levná cena) → Discord nudge „píchni ho,
je levno". Oportunistické (čeká, až je auto vzhůru). Napojí se na #5 (geofence
arrival) a stávající `ev_notify` / `discord_bot`. **Golden:** NE.
---
## Pořadí nasazení
1. **#1 živé SoC** — odstraní phantom okna a plýtvání; enabler pro #2. (golden)
2. **#2 předehřev/0 A** — control, hned po #1, bez golden.
3. **#3 anti-fragmentace** — za flagem default 0, kalibrace harnessem + EV fixture.
4. **#5 geofence arrival** + **#6 notifikace** — sběr/notifikace, samostatné PR.
5. **#4 forecast aktivace** — až je dat dost (verifikace na DB).
**Blokery:** #1, #3, #4 chtějí **živou DB** (EV fixture, objem dat, ověření) —
potřebuju IP serveru (`frank` se neresolvuje). Lokálně umím dokázat jen
ne-regresi (golden default off) + unit testy.
## Rozhodnutí (z rozhovoru 2026-06-14)
- 3 fáze (ne 1f surplus — pokryje velká baterka).
- Anti-fragmentace = malá ekonomická penalta v solveru, ne tvrdá priorita ani nová
vrstva; control zůstává hloupý (žádný max-amps override — rozbil by §7).
- Geofence arrival ANO (robustnost bez píchnutí).
- DOW forecast stačí; GPS-route clustering odloženo.