CREATE OR REPLACE FUNCTION ems.fn_update_ev_arrival_stats( p_site_id INT, p_charger_id INT, p_vehicle_id INT, p_arrived_at TIMESTAMPTZ ) RETURNS VOID LANGUAGE sql AS $$ INSERT INTO ems.ev_arrival_stats (site_id, charger_id, vehicle_id, day_of_week, arrival_hour, sample_count, last_updated) VALUES ( p_site_id, p_charger_id, p_vehicle_id, EXTRACT(DOW FROM p_arrived_at AT TIME ZONE 'Europe/Prague')::INT, EXTRACT(HOUR FROM p_arrived_at AT TIME ZONE 'Europe/Prague')::INT, 1, now() ) ON CONFLICT (site_id, charger_id, day_of_week, arrival_hour) DO UPDATE SET sample_count = ems.ev_arrival_stats.sample_count + 1, last_updated = now(); $$; COMMENT ON FUNCTION ems.fn_update_ev_arrival_stats(INT, INT, INT, TIMESTAMPTZ) IS 'Přidá jeden příjezd do statistiky. Volat při otevření nové EV session (telemetry_collector: přechod status available → preparing/charging).'; CREATE OR REPLACE FUNCTION ems.fn_ev_expected_arrival( p_site_id INT, p_charger_id INT, p_for_date DATE DEFAULT ( (CURRENT_TIMESTAMP AT TIME ZONE 'Europe/Prague')::date + 1 ) ) RETURNS TABLE ( expected_hour INT, confidence_pct INT, sample_count INT ) LANGUAGE sql STABLE AS $$ SELECT s.arrival_hour, ROUND( s.sample_count::NUMERIC / NULLIF(SUM(s.sample_count) OVER (PARTITION BY s.day_of_week), 0) * 100 )::INT, s.sample_count FROM ems.ev_arrival_stats s WHERE s.site_id = p_site_id AND s.charger_id = p_charger_id AND s.day_of_week = EXTRACT(DOW FROM p_for_date)::INT AND s.sample_count >= 2 ORDER BY s.sample_count DESC LIMIT 3; $$; COMMENT ON FUNCTION ems.fn_ev_expected_arrival(INT, INT, DATE) IS 'Top 3 nejčastější hodiny příjezdu EV pro den v týdnu odpovídající kalendářnímu datu p_for_date. Backend předává „zítřek“ v časové zóně lokality. Použití: notifikace, později solver.';