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>
77 lines
3.5 KiB
SQL
77 lines
3.5 KiB
SQL
-- jeden EV session objekt pro LP (fn_planning_site_context).
|
||
-- Vrací jsonb objekt session na daném wallboxu, nebo null::jsonb pokud session
|
||
-- není nebo nemá použitelná data (kapacita vozidla, SoC při připojení).
|
||
--
|
||
-- KLÍČOVÝ ROZDÍL oproti dřívější inline logice (bug 2026-06-13): session se
|
||
-- NEVYŘAZUJE jen proto, že needed_wh = 0 (auto už nad targetem). Plánovač pak
|
||
-- neviděl ~6 kW zátěž auta a špatně rozvrhl baterii. Session zůstává v plánu,
|
||
-- dokud má oportunistický headroom (cena rozhodne, jestli se nabíjí) — měkký
|
||
-- cíl řeší solver dekompozicí Σ == needed − unmet + opp.
|
||
--
|
||
-- 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).
|
||
|
||
drop function if exists ems.fn_ev_session_planning_json;
|
||
|
||
create or replace function ems.fn_ev_session_planning_json(
|
||
p_site_id int,
|
||
p_charger_code text
|
||
)
|
||
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
|
||
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;
|
||
$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).';
|