-- 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. -- -- Ž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( p_site_id int, p_charger_code text ) returns jsonb language sql stable as $fn$ 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 ), 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). 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.';