create or replace function ems.fn_site_notifications_context(p_site_id int) returns jsonb language sql stable as $fn$ select case when not exists (select 1 from ems.site s0 where s0.id = p_site_id) then jsonb_build_object('error', 'not_found') else jsonb_build_object( 'timezone', coalesce( nullif(trim((select s.timezone from ems.site s where s.id = p_site_id)), ''), 'Europe/Prague' ), 'mode_code', (select m.mode_code from ems.site_operating_mode m where m.site_id = p_site_id), 'has_plan', exists ( select 1 from ems.planning_run pr where pr.site_id = p_site_id and pr.status = 'active' ), 'tomorrow_slots', ( select count(*)::int from ems.vw_site_effective_price v where v.site_id = p_site_id and (v.interval_start at time zone coalesce( nullif(trim((select s2.timezone from ems.site s2 where s2.id = p_site_id)), ''), 'Europe/Prague' ))::date = ( ( current_timestamp at time zone coalesce( nullif(trim((select s3.timezone from ems.site s3 where s3.id = p_site_id)), ''), 'Europe/Prague' ) )::date + 1 ) ), 'reserve_soc', (select min(ab.reserve_soc_percent)::float from ems.asset_battery ab where ab.site_id = p_site_id), 'min_soc', (select min(ab.min_soc_percent)::float from ems.asset_battery ab where ab.site_id = p_site_id), 'soc_pct', (select li.battery_soc_percent::float from ems.vw_latest_inverter li where li.site_id = p_site_id order by li.measured_at desc nulls last limit 1), 'inv_measured_at', (select li.measured_at from ems.vw_latest_inverter li where li.site_id = p_site_id order by li.measured_at desc nulls last limit 1), 'hb_last_seen', (select hb.last_seen from ems.site_heartbeat hb where hb.site_id = p_site_id limit 1), 'price_slots', coalesce( ( select jsonb_agg( jsonb_build_object( 'interval_start', v.interval_start, 'effective_buy_price_czk_kwh', v.effective_buy_price_czk_kwh, 'effective_sell_price_czk_kwh', v.effective_sell_price_czk_kwh ) order by v.interval_start ) from ems.vw_site_effective_price v where v.site_id = p_site_id and v.interval_start >= now() and v.interval_start < now() + interval '48 hours' ), '[]'::jsonb ), 'avg_buy', ( select avg(v.effective_buy_price_czk_kwh)::float from ems.vw_site_effective_price v where v.site_id = p_site_id and v.interval_start::date in (current_date, current_date + 1) ), 'usable_wh', ( select coalesce(sum(ab.usable_capacity_wh), 0)::float from ems.asset_battery ab join ems.asset_inverter ai on ai.id = ab.inverter_id where ai.site_id = p_site_id ), 'ev_sessions', coalesce( ( select jsonb_agg( jsonb_build_object( 'id', es.id, 'charger_id', es.charger_id, 'energy_delivered_wh', es.energy_delivered_wh, 'target_soc_pct', es.target_soc_pct, 'session_start', es.session_start, 'soc_at_connect_pct', es.soc_at_connect_pct, 'battery_capacity_kwh', coalesce(av_id.battery_capacity_kwh, av_def.battery_capacity_kwh), 'make', coalesce(av_id.make, av_def.make), 'model', coalesce(av_id.model, av_def.model), 'default_target_soc_pct', coalesce(av_id.default_target_soc_pct, av_def.default_target_soc_pct), 'charger_code', ac.code ) order by es.id ) from ems.ev_session es join ems.asset_ev_charger ac on ac.id = es.charger_id left join ems.asset_vehicle av_id on av_id.id = es.vehicle_id left join ems.asset_vehicle av_def on av_def.default_charger_id = ac.id and es.vehicle_id is null where es.site_id = p_site_id and es.session_end is null ), '[]'::jsonb ), 'neg_windows', coalesce( ( select jsonb_agg( jsonb_build_object( 'predicted_date', p.predicted_date, 'window_start_hour', p.window_start_hour, 'window_end_hour', p.window_end_hour, 'probability_pct', p.probability_pct ) order by p.predicted_date, p.window_start_hour ) from ems.predicted_negative_price_window p where p.site_id = p_site_id and p.predicted_date between current_date and current_date + 2 and p.probability_pct >= 50 ), '[]'::jsonb ) ) end; $fn$; comment on function ems.fn_site_notifications_context(int) is 'Vstupy pro build_smart_notifications + infra pravidla (GET /notifications).';