create or replace function ems.fn_plan_current_bundle(p_site_id int) returns jsonb language plpgsql stable as $fn$ declare v_run jsonb; v_run_id int; v_batt_wh float; v_intervals jsonb; v_total_cost numeric; v_curtailed numeric; v_charge bigint; v_discharge bigint; v_export bigint; v_pv_kwh numeric; v_cap numeric; v_cov numeric; v_scarcity numeric; begin select to_jsonb(pr) into v_run from ems.planning_run pr where pr.site_id = p_site_id and pr.status = 'active' order by pr.created_at desc limit 1; if v_run is null then return jsonb_build_object('error', 'no_active_plan'); end if; v_run_id := (v_run->>'id')::int; select coalesce(sum(ab.usable_capacity_wh), 0)::float into v_batt_wh from ems.asset_battery ab where ab.site_id = p_site_id; with fc_slot as ( select u.interval_start, coalesce(sum(u.power_w), 0)::bigint as pv_forecast_total_w from ( select distinct on (fpi.interval_start, fpr.pv_array_id) fpi.interval_start, fpi.power_w from ems.forecast_pv_interval fpi join ems.forecast_pv_run fpr on fpr.id = fpi.run_id join ems.asset_pv_array apa on apa.id = fpr.pv_array_id and apa.site_id = fpr.site_id where fpr.site_id = p_site_id and fpr.status = 'ok' order by fpi.interval_start, fpr.pv_array_id, fpr.created_at desc ) u group by u.interval_start ), joined as ( select to_jsonb(pi.*) || jsonb_build_object( 'pv_power_w', ai.actual_pv_power_w, 'pv_forecast_total_w', fs.pv_forecast_total_w ) as j, pi.interval_start, pi.expected_cost_czk, pi.pv_a_curtailed_w, pi.battery_setpoint_w, pi.grid_setpoint_w, fs.pv_forecast_total_w from ems.planning_interval pi left join ems.audit_interval ai on ai.site_id = p_site_id and ai.interval_start = pi.interval_start left join fc_slot fs on fs.interval_start = pi.interval_start where pi.run_id = v_run_id ), agg as ( select coalesce(jsonb_agg(j order by interval_start), '[]'::jsonb) as intervals, coalesce( sum( case when expected_cost_czk is not null then expected_cost_czk::numeric else 0::numeric end ), 0::numeric ) as total_cost, coalesce( sum(coalesce(pv_a_curtailed_w, 0)::numeric * 0.25 / 1000.0), 0::numeric ) as curtailed_kwh, coalesce( sum( case when battery_setpoint_w is not null and battery_setpoint_w > 0 then 1 else 0 end ), 0::bigint ) as charge_slots, coalesce( sum( case when battery_setpoint_w is not null and battery_setpoint_w < 0 then 1 else 0 end ), 0::bigint ) as discharge_slots, coalesce( sum( case when grid_setpoint_w is not null and grid_setpoint_w < 0 then 1 else 0 end ), 0::bigint ) as export_slots from joined ), pv96 as ( select coalesce( sum( greatest(0::numeric, coalesce(pv_forecast_total_w, 0)::numeric) * 0.25 / 1000.0 ), 0::numeric ) as pv_kwh from ( select pv_forecast_total_w from joined order by interval_start limit 96 ) z ) select a.intervals, a.total_cost, a.curtailed_kwh, a.charge_slots, a.discharge_slots, a.export_slots, p.pv_kwh into strict v_intervals, v_total_cost, v_curtailed, v_charge, v_discharge, v_export, v_pv_kwh from agg a cross join pv96 p; v_cap := greatest(1::numeric, coalesce(v_batt_wh, 0::float)::numeric / 1000.0); v_cov := least(1::numeric, greatest(0::numeric, coalesce(v_pv_kwh, 0) / v_cap)); v_scarcity := round(0.65::numeric + 0.35 * v_cov, 4); return jsonb_build_object( 'run', v_run, 'intervals', v_intervals, 'summary', jsonb_build_object( 'total_expected_cost_czk', round(v_total_cost, 4), 'total_pv_curtailed_kwh', round(v_curtailed, 6), 'charge_slots', v_charge, 'discharge_slots', v_discharge, 'export_slots', v_export, 'pv_scarcity_factor', v_scarcity ) ); end; $fn$; comment on function ems.fn_plan_current_bundle(int) is 'AktivnĂ­ planning_run + intervaly + souhrn (GET /plan/current).';