-- 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).';