merge dev → main: EV živé SoC fix (phantom okna) + amps round() + docs
- fix(planner): živé EV SoC z integrálu power_w (R__038) — needed_wh 18750→1329, konec phantom 11 kW oken; reg 39 counter rozbitý → integrál power_w - fix: watts_to_amps round() (11 kW = 16 A místo 15 A) - docs: arbitráž sell strana, changelog hardcoded wallbox kódy Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,9 +146,11 @@ def _deye_reg178_verify_with_double_read(
|
||||
|
||||
|
||||
def watts_to_amps(power_w: int | None, phases: int = 3, voltage: int = 230) -> int:
|
||||
# round(), NE int(): 11 kW / (3×230) = 15.94 A → int useklo na 15 A (~10.35 kW,
|
||||
# −6 % výkonu); round dá správných 16 A (~11 kW). Strop 32 A drží horní mez.
|
||||
if not power_w or power_w <= 0:
|
||||
return 0
|
||||
return min(32, max(0, int(power_w / (phases * voltage))))
|
||||
return min(32, max(0, round(power_w / (phases * voltage))))
|
||||
|
||||
|
||||
def battery_watts_to_amps(power_w: int, max_amps: int) -> int:
|
||||
|
||||
@@ -8,12 +8,53 @@
|
||||
-- dokud má oportunistický headroom (cena rozhodne, jestli se nabíjí) — měkký
|
||||
-- cíl řeší solver dekompozicí Σ == needed − unmet + opp.
|
||||
--
|
||||
-- ŽIVÉ SoC (fix 2026-06-14, phantom okna): needed_wh i headroom se počítají z
|
||||
-- ŽIVÉHO SoC = soc_at_connect + integrovaná dodaná energie (fn_ev_session_delivered_wh),
|
||||
-- ne ze zamrzlého soc_at_connect. Dřív se odečítalo es.energy_delivered_wh, JENŽE
|
||||
-- ten sloupec se během session NIKDY nezapisoval (trvale 0) → needed_wh konstantní
|
||||
-- → plánovač slepý k pokroku nabíjení → 11 kW phantom okna i u plného auta.
|
||||
-- NEpoužíváme energy_kwh counter (Telto reg 39 na TeltoCharge neakumuluje —
|
||||
-- ověřeno: 17.4 kWh nabito, counter stál na 0.18 kWh), proto integrál power_w.
|
||||
-- live_soc clamp 99 % (finální taper k 100 % ignorujeme). Fallback na
|
||||
-- energy_delivered_wh drží staré fixtures bez telemetrie identické.
|
||||
--
|
||||
-- Vyřazení (null) jen když chybí tvrdá data:
|
||||
-- - žádná otevřená session na wallboxu, nebo
|
||||
-- - neznámá kapacita vozidla / SoC při připojení (nelze spočítat Wh).
|
||||
-- target_deadline SMÍ být NULL (žádný tvrdý cíl) — solver to zvládá
|
||||
-- (deadline constraint se aplikuje jen při needed_wh > 0).
|
||||
|
||||
-- Dodaná energie do auta za session = time-weighted integrál power_w z
|
||||
-- telemetry_ev_charger (1min). dt cap 120 s ať výpadek telemetrie nezkresluje.
|
||||
-- Wh (AC, bez korekce na AC→DC ztráty — mírně optimistické = méně phantom,
|
||||
-- žádoucí směr). Vrací 0 bez telemetrie (drží staré chování).
|
||||
drop function if exists ems.fn_ev_session_delivered_wh;
|
||||
|
||||
create or replace function ems.fn_ev_session_delivered_wh(
|
||||
p_charger_id int,
|
||||
p_since timestamptz
|
||||
)
|
||||
returns numeric
|
||||
language sql
|
||||
stable
|
||||
as $fn$
|
||||
select coalesce(sum(
|
||||
power_w * least(coalesce(dt, 60), 120)
|
||||
) / 3600.0, 0)::numeric
|
||||
from (
|
||||
select power_w,
|
||||
extract(epoch from (
|
||||
measured_at - lag(measured_at) over (order by measured_at)
|
||||
)) as dt
|
||||
from ems.telemetry_ev_charger
|
||||
where charger_id = p_charger_id
|
||||
and measured_at >= p_since
|
||||
) q;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_ev_session_delivered_wh is
|
||||
'Dodaná energie do EV za session (Wh, AC) = time-weighted integrál power_w z telemetry_ev_charger (dt cap 120 s). NEpoužívá energy_kwh counter (Telto reg 39 neakumuluje). Vstup živého SoC ve fn_ev_session_planning_json. 0 bez telemetrie.';
|
||||
|
||||
drop function if exists ems.fn_ev_session_planning_json;
|
||||
|
||||
create or replace function ems.fn_ev_session_planning_json(
|
||||
@@ -24,53 +65,74 @@ returns jsonb
|
||||
language sql
|
||||
stable
|
||||
as $fn$
|
||||
select case
|
||||
when v.battery_capacity_kwh is null then null::jsonb
|
||||
when es.soc_at_connect_pct is null then null::jsonb
|
||||
else jsonb_build_object(
|
||||
-- tvrdý cíl: jen pokud je nastaven deadline I cílový SoC (jinak null →
|
||||
-- solver hard constraint vynechá, energy_needed_wh = 0).
|
||||
'target_deadline', case
|
||||
when coalesce(es.target_soc_pct, v.default_target_soc_pct) is null then null
|
||||
else es.target_deadline
|
||||
end,
|
||||
'energy_needed_wh', case
|
||||
when es.target_deadline is null then 0::numeric
|
||||
when coalesce(es.target_soc_pct, v.default_target_soc_pct) is null then 0::numeric
|
||||
else greatest(
|
||||
0,
|
||||
(coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric
|
||||
- es.soc_at_connect_pct::numeric) / 100.0
|
||||
* (v.battery_capacity_kwh * 1000)
|
||||
- coalesce(es.energy_delivered_wh, 0)::numeric
|
||||
)
|
||||
end,
|
||||
-- headroom do 100 % od max(target, SoC při připojení): „nenabíjet" (nízký
|
||||
-- target) nesmí ZVĚTŠIT oportunistickou vrstvu; auto fyzicky bere jen
|
||||
-- energii nad svým aktuálním SoC. Při vypnutém oportunismu (value <= 0)
|
||||
-- headroom = 0 — session zůstane v plánu, ale solver ji nebude doplňovat.
|
||||
'headroom_wh', case
|
||||
when coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) > 0 then greatest(
|
||||
0,
|
||||
(100 - greatest(
|
||||
coalesce(es.target_soc_pct, v.default_target_soc_pct, es.soc_at_connect_pct)::numeric,
|
||||
es.soc_at_connect_pct::numeric
|
||||
)) / 100.0 * (v.battery_capacity_kwh * 1000)
|
||||
)
|
||||
else 0
|
||||
end,
|
||||
'opportunistic_value_czk_kwh',
|
||||
coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0)
|
||||
)
|
||||
end
|
||||
with s as (
|
||||
select
|
||||
es.soc_at_connect_pct,
|
||||
es.target_soc_pct,
|
||||
es.target_deadline,
|
||||
es.energy_delivered_wh,
|
||||
es.opportunistic_value_czk_kwh,
|
||||
v.battery_capacity_kwh,
|
||||
v.default_target_soc_pct,
|
||||
v.opportunistic_value_czk_kwh as v_opp,
|
||||
ems.fn_ev_session_delivered_wh(es.charger_id, es.session_start) as live_delivered_wh
|
||||
from ems.ev_session es
|
||||
join ems.asset_ev_charger ch on ch.id = es.charger_id
|
||||
left join ems.asset_vehicle v on v.id = es.vehicle_id
|
||||
where es.site_id = p_site_id
|
||||
and es.session_end is null
|
||||
and ch.code = p_charger_code
|
||||
limit 1;
|
||||
limit 1
|
||||
),
|
||||
c as (
|
||||
select s.*,
|
||||
-- živé SoC: SoC při připojení + integrovaná dodaná energie, clamp 99 %.
|
||||
-- coalesce(live, energy_delivered_wh, 0): bez telemetrie = staré chování.
|
||||
least(99.0, s.soc_at_connect_pct::numeric
|
||||
+ coalesce(s.live_delivered_wh, s.energy_delivered_wh, 0)::numeric
|
||||
/ (s.battery_capacity_kwh * 1000) * 100.0) as live_soc_pct
|
||||
from s
|
||||
)
|
||||
select case
|
||||
when c.battery_capacity_kwh is null then null::jsonb
|
||||
when c.soc_at_connect_pct is null then null::jsonb
|
||||
else jsonb_build_object(
|
||||
-- tvrdý cíl: jen pokud je nastaven deadline I cílový SoC (jinak null →
|
||||
-- solver hard constraint vynechá, energy_needed_wh = 0).
|
||||
'target_deadline', case
|
||||
when coalesce(c.target_soc_pct, c.default_target_soc_pct) is null then null
|
||||
else c.target_deadline
|
||||
end,
|
||||
'energy_needed_wh', case
|
||||
when c.target_deadline is null then 0::numeric
|
||||
when coalesce(c.target_soc_pct, c.default_target_soc_pct) is null then 0::numeric
|
||||
else greatest(
|
||||
0,
|
||||
(coalesce(c.target_soc_pct, c.default_target_soc_pct)::numeric
|
||||
- c.live_soc_pct) / 100.0
|
||||
* (c.battery_capacity_kwh * 1000)
|
||||
)
|
||||
end,
|
||||
-- headroom do 99 % od max(target, ŽIVÉ SoC): „nenabíjet" (nízký target)
|
||||
-- nesmí ZVĚTŠIT oportunistickou vrstvu; auto fyzicky bere jen energii nad
|
||||
-- aktuálním SoC. Plné auto (live_soc → 99) → headroom 0. Při vypnutém
|
||||
-- oportunismu (value <= 0) headroom = 0.
|
||||
'headroom_wh', case
|
||||
when coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0) > 0 then greatest(
|
||||
0,
|
||||
(99 - greatest(
|
||||
coalesce(c.target_soc_pct, c.default_target_soc_pct, c.live_soc_pct)::numeric,
|
||||
c.live_soc_pct
|
||||
)) / 100.0 * (c.battery_capacity_kwh * 1000)
|
||||
)
|
||||
else 0
|
||||
end,
|
||||
'opportunistic_value_czk_kwh',
|
||||
coalesce(c.opportunistic_value_czk_kwh, c.v_opp, 0)
|
||||
)
|
||||
end
|
||||
from c;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_ev_session_planning_json is
|
||||
'EV session objekt pro LP (fn_planning_site_context). Session se NEvyřazuje při needed_wh=0 (auto nad targetem) — zůstává v plánu kvůli oportunistickému headroomu i jako známá zátěž. Null jen bez použitelných dat (kapacita / soc_at_connect). target_deadline smí být NULL (bez tvrdého cíle).';
|
||||
'EV session objekt pro LP (fn_planning_site_context). needed_wh i headroom z ŽIVÉHO SoC = soc_at_connect + fn_ev_session_delivered_wh (integrál power_w), clamp 99 % — ne ze zamrzlého soc_at_connect (energy_delivered_wh se nikdy nezapisoval → phantom 11 kW okna). Session se NEvyřazuje při needed_wh=0 (zůstává jako známá zátěž + oportunistický headroom). Null jen bez použitelných dat (kapacita / soc_at_connect). target_deadline smí být NULL.';
|
||||
|
||||
@@ -237,7 +237,18 @@ Uložit do `ev_session` při připojení/odpojení.
|
||||
### Renault Zoe
|
||||
|
||||
Žádné API. Stav připojení čteme výhradně z WB Modbus (`status != 'available'`).
|
||||
SoC Zoe neznáme přesně – použijeme energii dodanou v session (kumulativní kWh z WB).
|
||||
SoC Zoe neznáme přesně – použijeme energii dodanou v session.
|
||||
|
||||
### Živé SoC během session (needed_wh, fix 2026-06-14)
|
||||
|
||||
`fn_ev_session_planning_json` (R__038) počítá `energy_needed_wh` i `headroom_wh` z
|
||||
**živého SoC** = `soc_at_connect + dodaná_energie/kapacita`, clamp 99 % (finální taper
|
||||
ignorujeme) — ne ze zamrzlého `soc_at_connect`. Dodaná energie je **time-weighted integrál
|
||||
`power_w`** (`ems.fn_ev_session_delivered_wh`, dt cap 120 s), NE counter `energy_kwh`:
|
||||
ten je na TeltoCharge (Telto reg 39) **rozbitý** — neakumuluje (ověřeno: 17.4 kWh nabito,
|
||||
counter 0.18). Bez toho byl `energy_delivered_wh` trvale 0 → needed_wh konstantní →
|
||||
plánovač slepý k pokroku → phantom 11 kW okna i u plného auta. Funguje pro Teslu i Zoe
|
||||
(power-based, bez API). Pozn.: reg 39 rozbitý ⇒ i EV audit/ekonomika z něj jede naslepo.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -77,6 +77,51 @@ Použití `min(buy)` jako „acquisition cost“ pro večerní export:
|
||||
|
||||
---
|
||||
|
||||
## 3b. Výprodejní strana: marginální cena, ne špička (proč baterie nedrží jen na nejdražší slot)
|
||||
|
||||
Zrcadlo sekce 3 na **prodejní** straně. Častá intuice: „baterie drží, protože večer
|
||||
je jeden drahý slot (sell 2,76 Kč) — prodá se za něj“. **Ne.** Stejně jako se 64 kWh
|
||||
**nenabije** v jedné čtvrthodině, tak se ani **nevyprodá** do jednoho slotu — strop
|
||||
exportu (home-01 **13,5 kW**) pustí jen ~**3,4 kWh / 15 min**. Vybití celé baterie do
|
||||
sítě tedy zabere **hodiny** a rozloží se přes **více** prodejních slotů s **klesající**
|
||||
cenou.
|
||||
|
||||
**Klíčový princip — MILP je marginální:** účelová funkce `Σ (ge[t] × sell[t])` přes
|
||||
**všechny** sloty s **per-slot stropem** `ge[t] ≤ max_export` znamená, že solver
|
||||
prodává **nejdřív nejdražší sloty** a každou další kWh až za **nižší** dosažitelnou
|
||||
cenu. Rozhodnutí **„držet baterii vs. vybít teď“** se tedy poměřuje proti
|
||||
**MARGINÁLNÍ ceně** = nejnižší z **reálně využitých** prodejních slotů, **ne** proti
|
||||
špičce. Baterie drží jen tolik, dokud i ta **marginální** vyvezená kWh (prodaná v tom
|
||||
nejlevnějším z použitých oken) bije alternativu (krytí loadu / další držení); nad to
|
||||
prodá / spotřebuje dřív.
|
||||
|
||||
**Konkrétní příklad (home-01, večer 2026-06-14, plán):** baterie nabitá za odpolední
|
||||
**záporné** ceny (SoC ~100 %) se vyváží na **stropu 13,5 kW napříč 20:30–23:00**, ne jen
|
||||
do špičky 22:00:
|
||||
|
||||
| slot | sell Kč/kWh | export | pozn. |
|
||||
|------|-------------|--------|-------|
|
||||
| 20:30 | 2,21 | 13,5 kW | |
|
||||
| 21:00 | 2,20 | 13,5 kW | marginální (nejnižší použitý) |
|
||||
| 21:30 | 2,25 | 13,5 kW | |
|
||||
| **22:00** | **2,76** | 13,5 kW | **špička — jen jeden slot** |
|
||||
| 22:30 | 2,47 | 13,5 kW | |
|
||||
| 23:00 | 2,32 | 13,5 kW | |
|
||||
|
||||
SoC klesne 92 → 32 % (~38 kWh) za **6** slotů. Do špičky 2,76 jde jen **jeden** slot;
|
||||
marginální kWh se prodá za **~2,2**. Kdyby solver oceňoval **celou** baterii špičkou
|
||||
2,76, **držel by víc**, než je optimální. Že vyváží napříč oknem = důkaz, že počítá
|
||||
**marginálně se stropem**, ne naivně na špičku.
|
||||
|
||||
**Caveat — kde to marginální NENÍ:** platí jen pro sloty **uvnitř horizontu** (viditelná
|
||||
prodejní okna). Energie zbylá **za horizontem** (`soc[T−1]`) se ocení **jedním skalárem**
|
||||
(`terminal_soc_value` = průměr buy prvních 24 h × faktor, pravidlo §16), **ne** pořádnou
|
||||
marginální budoucí prodejní křivkou. Pro „dnešní večer v horizontu“ je to správně
|
||||
marginální; zpřesnění terminal value (počasím/cenou vážené, „fáze 3e“) je
|
||||
v `docs/06-open-questions.md` a `docs/04-modules/planning-extended-horizon.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Co používat místo toho (směr návrhu)
|
||||
|
||||
| Pojem | Význam | Poznámka |
|
||||
|
||||
213
docs/ev-improvement-plan-2026-06-14.md
Normal file
213
docs/ev-improvement-plan-2026-06-14.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 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:37–61.)
|
||||
|
||||
**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
|
||||
(~8–12 % 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,3–1,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**
|
||||
(2–3 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.
|
||||
@@ -5,6 +5,22 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-14 — phantom 11 kW okna: plánovač slepý k pokroku nabíjení EV (živé SoC)
|
||||
|
||||
- **Problém:** Tesla připojená na 70 %, dotankovaná na ~98 %, ale plán emitoval **15 oken po 11 kW** (20:15–23:45) — phantom. `fn_ev_session_planning_json` vracela `energy_needed_wh = 18750 Wh` konstantně po celou session.
|
||||
- **Příčina:** needed_wh = (target − soc_at_connect)/100 × cap − `energy_delivered_wh`, JENŽE `energy_delivered_wh` se během session **NIKDY nezapisuje** (V006 DEFAULT 0, žádný updater) → needed_wh konstantní, plánovač slepý k pokroku nabíjení; headroom navíc ze zamrzlého soc_at_connect. **Counter `energy_kwh` (Telto reg 39) je ROZBITÝ** — ověřeno živě: 17.4 kWh reálně nabito, counter stál na 0.18 kWh → coulomb z něj nejde.
|
||||
- **Mechanismus (fix):** nový `ems.fn_ev_session_delivered_wh(charger_id, since)` = time-weighted integrál **`power_w`** z telemetry_ev_charger (dt cap 120 s; power_w je spolehlivý). R__038 počítá `live_soc = soc_at_connect + delivered/cap`, clamp 99 %; needed_wh i headroom z živého SoC místo zamrzlého soc_at_connect. Fallback `coalesce(live, energy_delivered_wh, 0)` drží staré chování bez telemetrie. Žádné buzení Tesly, funguje i pro Zoe (power-based, bez API).
|
||||
- **Soubory:** `db/routines/R__038_fn_ev_session_planning_json.sql` (helper fn + přepočet), `docs/04-modules/ev-charging.md`, `docs/ev-improvement-plan-2026-06-14.md`.
|
||||
- **Ověření (živá DB, read-only psql):** session #6 home-01 — integrál power_w = 17.42 kWh → live_soc 97.9 % (sedí na realitu i na „99 %" z displeje); nová fn `energy_needed_wh` 18750 → **1329 Wh**, headroom 0. Golden gate testuje Python solver downstream R__038 (frozen JSON fixtures), takže SQL změna se ho netýká; fallback drží případné re-extrakce identické.
|
||||
- **Zbývá (backlog, plán bod 2–6):** předehřev/0 A (nepouštět 0 A při SoC≥target), anti-fragmentace v solveru (block-start penalta), geofence arrival, proaktivní notifikace, aktivace usage forecast. **Counter reg 39** rozbitý = i audit/ekonomika EV jede naslepo — zvážit fix čtení nebo přepnout audit na integrál power_w.
|
||||
|
||||
## 2026-06-14 — HOTFIX: plánovač oslepl k autu po přejmenování wallboxu (hardcoded kódy)
|
||||
|
||||
- **Problém:** uživatel přejmenoval wallboxy `ev-charger-1/2` → `vt-ev-charger-1/2`. fn_planning_site_context (R__039) a fn_load_planning_slots_full (R__063) měly kódy NATVRDO → ctx.vehicles=[], ev_sessions=[null,null], ev1/ev2_connected vždy false → plánovač auto NEVIDĚL → žádné nabíjení ani v záporných cenách (Tesla 70%, okno −0.32 Kč nevyužito).
|
||||
- **Mechanismus (fix):** výběr wallboxu DYNAMICKY podle site_id, ev1=nejnižší ch.id, ev2=druhý (stabilní, odolné přejmenování). Inverter pro gen_cutoff přes `controllable=true` místo `code='deye-main'`. Konzistentní R__039 (vehicles order by id, sessions dynamické kódy) + R__063 (ev1/ev2 connected).
|
||||
- **Soubory:** R__039, R__063 (pure SQL). **Ověření:** po deployi ctx vehicles=2, ev_sessions=[True,False], plán nabíjí 14.6 kWh v záporném okně 14:15–16:00. 363 testů zelených.
|
||||
- **Zbývá (backlog):** outputs.py `_current_limit_for_charger` (endswith '-1'/'-2' fallback — funguje, ale křehké u kódů bez suffixu), frontend Settings.tsx hardcoded kódy, notifikace mismatch/clock = asset_code bez site. Doporučení: oddělit `code` (identifikátor) od `name` (zobrazení).
|
||||
|
||||
## 2026-06-13 — exekuce: baterie se nedobila v záporných cenách (guard carve-out)
|
||||
|
||||
- **Problém:** buy záporný 13:00–15:45 (−0.47…−0.95 Kč), plán ordinoval CHARGE +17 kW import, SoC cíl ~96 %, ale realita SoC 71 % (nabíjení jen z PV, grid≈0). Večer se dokupovalo ze sítě místo z plné baterie.
|
||||
|
||||
Reference in New Issue
Block a user