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:
@@ -179,113 +179,12 @@ begin
|
||||
where v.site_id = p_site_id
|
||||
and ch.code in ('ev-charger-1', 'ev-charger-2');
|
||||
|
||||
-- EV session per wallbox — logika v ems.fn_ev_session_planning_json
|
||||
-- (R__038): 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ěž.
|
||||
v_ev := jsonb_build_array(
|
||||
(
|
||||
select case
|
||||
when es.target_deadline is null then null::jsonb
|
||||
when v.battery_capacity_kwh is null then null::jsonb
|
||||
when es.soc_at_connect_pct is null then null::jsonb
|
||||
when coalesce(es.target_soc_pct, v.default_target_soc_pct) is null then null::jsonb
|
||||
when 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
|
||||
) <= 0
|
||||
and (
|
||||
coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) <= 0
|
||||
or (100 - greatest(
|
||||
coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
|
||||
es.soc_at_connect_pct::numeric
|
||||
)) <= 0
|
||||
) then null::jsonb
|
||||
else jsonb_build_object(
|
||||
'target_deadline', es.target_deadline,
|
||||
'energy_needed_wh', 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
|
||||
),
|
||||
-- headroom od max(target, SoC při připojení): „nenabíjet" (nízký
|
||||
-- target) nesmí paradoxně ZVĚTŠIT oportunistickou vrstvu; auto může
|
||||
-- fyzicky vzít jen energii nad svým aktuálním SoC.
|
||||
'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)::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 = 'ev-charger-1'
|
||||
limit 1
|
||||
),
|
||||
(
|
||||
select case
|
||||
when es.target_deadline is null then null::jsonb
|
||||
when v.battery_capacity_kwh is null then null::jsonb
|
||||
when es.soc_at_connect_pct is null then null::jsonb
|
||||
when coalesce(es.target_soc_pct, v.default_target_soc_pct) is null then null::jsonb
|
||||
when 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
|
||||
) <= 0
|
||||
and (
|
||||
coalesce(es.opportunistic_value_czk_kwh, v.opportunistic_value_czk_kwh, 0) <= 0
|
||||
or (100 - greatest(
|
||||
coalesce(es.target_soc_pct, v.default_target_soc_pct)::numeric,
|
||||
es.soc_at_connect_pct::numeric
|
||||
)) <= 0
|
||||
) then null::jsonb
|
||||
else jsonb_build_object(
|
||||
'target_deadline', es.target_deadline,
|
||||
'energy_needed_wh', 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
|
||||
),
|
||||
-- headroom od max(target, SoC při připojení): „nenabíjet" (nízký
|
||||
-- target) nesmí paradoxně ZVĚTŠIT oportunistickou vrstvu; auto může
|
||||
-- fyzicky vzít jen energii nad svým aktuálním SoC.
|
||||
'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)::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 = 'ev-charger-2'
|
||||
limit 1
|
||||
)
|
||||
ems.fn_ev_session_planning_json(p_site_id, 'ev-charger-1'),
|
||||
ems.fn_ev_session_planning_json(p_site_id, 'ev-charger-2')
|
||||
);
|
||||
|
||||
select ti.battery_soc_percent
|
||||
@@ -351,4 +250,4 @@ end;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_planning_site_context is
|
||||
'Kontext pro planning_engine / LP (bez samotného solveru). EV session: opportunistic_value = coalesce(session, vehicle); headroom_wh od max(target, soc_at_connect), 0 při vypnutém oportunismu; vehicles nesou min_power_w wallboxu.';
|
||||
'Kontext pro planning_engine / LP (bez samotného solveru). EV session přes fn_ev_session_planning_json: session se nevyřazuje při needed_wh=0 (auto nad targetem) — zůstává v plánu kvůli oportunismu i jako známá zátěž; opportunistic_value = coalesce(session, vehicle); headroom_wh od max(target, soc_at_connect), 0 při vypnutém oportunismu; vehicles nesou min_power_w wallboxu.';
|
||||
|
||||
Reference in New Issue
Block a user