-- jeden jsonb snapshot pro LP: režim, baterie, síť, EV, TČ, tuv stats create or replace function ems.fn_planning_site_context(p_site_id int) returns jsonb language plpgsql stable as $fn$ declare v_mode text; v_b jsonb; v_hp jsonb; v_grid jsonb; v_veh jsonb; v_ev jsonb; v_soc_pct numeric; v_soc_wh numeric; v_tuv numeric; v_tuv_stats jsonb; v_uc numeric; v_min_soc_wh numeric; v_arb_wh numeric; v_soc_max_wh numeric; begin if not exists (select 1 from ems.site s where s.id = p_site_id) then return jsonb_build_object('error', 'unknown_site'); end if; select som.mode_code into v_mode from ems.site_operating_mode som where som.site_id = p_site_id; select jsonb_build_object( 'usable_capacity_wh', ab.usable_capacity_wh, 'min_soc_wh', (ab.min_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric, 'arb_floor_wh', (ab.reserve_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric, 'reserve_soc_wh', (ab.reserve_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric, 'soc_max_wh', (ab.max_soc_percent / 100.0 * ab.usable_capacity_wh)::numeric, 'charge_efficiency', ab.charge_efficiency, 'discharge_efficiency', ab.discharge_efficiency, 'degradation_cost_czk_kwh', ab.degradation_cost_czk_kwh, 'max_charge_power_w', least( coalesce(ai.max_battery_charge_w, ai.max_charge_power_w), coalesce( ab.bms_max_charge_w, case when ab.max_charge_c_rate is not null then (ab.max_charge_c_rate * ab.usable_capacity_wh)::bigint end, coalesce(ai.max_battery_charge_w, ai.max_charge_power_w) ) )::int, 'max_discharge_power_w', least( coalesce(ai.max_battery_discharge_w, ai.max_discharge_power_w), coalesce( ab.bms_max_discharge_w, case when ab.max_discharge_c_rate is not null then (ab.max_discharge_c_rate * ab.usable_capacity_wh)::bigint end, coalesce(ai.max_battery_discharge_w, ai.max_discharge_power_w) ) )::int, 'charge_slot_buffer', ab.charge_slot_buffer, 'discharge_slot_buffer', ab.discharge_slot_buffer ) into v_b from ems.asset_battery ab join ems.asset_inverter ai on ai.id = ab.inverter_id and ai.site_id = ab.site_id where ab.site_id = p_site_id order by ab.id limit 1; if v_b is null then raise exception 'No asset_battery for site_id=%', p_site_id; end if; v_uc := (v_b->>'usable_capacity_wh')::numeric; v_min_soc_wh := (v_b->>'min_soc_wh')::numeric; v_soc_max_wh := (v_b->>'soc_max_wh')::numeric; if (v_b->>'max_charge_power_w')::int <= 0 or (v_b->>'max_discharge_power_w')::int <= 0 then raise exception 'Invalid battery effective limits for site_id=%', p_site_id; end if; select jsonb_build_object( 'rated_heating_power_w', greatest(coalesce(hp.rated_heating_power_w, 8000), 0)::int, 'tuv_min_temp_c', coalesce(hp.tuv_min_temp_c, 45)::numeric, 'tuv_target_temp_c', coalesce(hp.tuv_target_temp_c, 55)::numeric ) into v_hp from ems.asset_heat_pump hp where hp.site_id = p_site_id order by hp.id limit 1; if v_hp is null then v_hp := jsonb_build_object( 'rated_heating_power_w', 0, 'tuv_min_temp_c', 0, 'tuv_target_temp_c', 55 ); end if; select jsonb_build_object( 'max_import_power_w', sgc.max_import_power_w, 'max_export_power_w', sgc.max_export_power_w, 'deye_gen_microinverter_cutoff_enabled', coalesce( ( select ai.deye_gen_microinverter_cutoff_enabled from ems.asset_inverter ai where ai.site_id = p_site_id and ai.code = 'deye-main' limit 1 ), false ) ) into v_grid from ems.site_grid_connection sgc where sgc.site_id = p_site_id order by sgc.id limit 1; if v_grid is null then raise exception 'No site_grid_connection for site_id=%', p_site_id; end if; select coalesce( jsonb_agg( jsonb_build_object( 'max_charge_power_w', v.max_charge_power_w, 'battery_capacity_kwh', v.battery_capacity_kwh, 'default_target_soc_pct', v.default_target_soc_pct ) order by ch.code ), '[]'::jsonb ) into v_veh from ems.asset_vehicle v join ems.asset_ev_charger ch on ch.id = v.default_charger_id where v.site_id = p_site_id and ch.code in ('ev-charger-1', 'ev-charger-2'); 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 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 ) ) 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 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 ) ) 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 ) ); select ti.battery_soc_percent into v_soc_pct from ems.telemetry_inverter ti where ti.site_id = p_site_id order by ti.measured_at desc limit 1; if v_soc_pct is null then v_soc_wh := v_uc * 0.5; else v_soc_wh := v_soc_pct::numeric / 100.0 * v_uc; end if; v_soc_wh := greatest(v_min_soc_wh, least(v_soc_wh, v_soc_max_wh)); select thp.tuv_tank_temp_c into v_tuv from ems.telemetry_heat_pump thp where thp.site_id = p_site_id order by thp.measured_at desc limit 1; v_tuv := coalesce(v_tuv::numeric, 50::numeric); select coalesce( jsonb_agg( jsonb_build_object( 'dow', tu.day_of_week, 'hour', tu.hour_of_day, 'delta', tu.avg_temp_delta_c ) ), '[]'::jsonb ) into v_tuv_stats from ems.tuv_usage_stats tu where tu.site_id = p_site_id; return jsonb_build_object( 'operating_mode', v_mode, 'battery', v_b, 'heat_pump', v_hp, 'grid', v_grid, 'vehicles', v_veh, 'ev_sessions', v_ev, 'soc_wh', v_soc_wh, 'tuv_temp', v_tuv, 'tuv_delta_stats', v_tuv_stats, 'planning_config', coalesce( ( select pc.config from ems.planning_config pc where pc.site_id = p_site_id limit 1 ), '{}'::jsonb ) ); end; $fn$; comment on function ems.fn_planning_site_context(int) is 'Kontext pro planning_engine / LP (bez samotného solveru).';