-- fn_update_market_price_stats, fn_update_tuv_usage_stats, fn_get_predicted_price -- (rozšířený horizont plánování – predikce cen a TUV) CREATE OR REPLACE FUNCTION ems.fn_update_market_price_stats( p_site_id INT, p_lookback_days INT DEFAULT 90 ) RETURNS INT LANGUAGE plpgsql AS $$ DECLARE v_count INT; BEGIN INSERT INTO ems.market_price_stats (site_id, day_of_week, hour_of_day, avg_price, stddev_price, p25_price, p75_price, sample_count, last_updated) SELECT p_site_id, EXTRACT(DOW FROM interval_start AT TIME ZONE 'Europe/Prague')::INT, EXTRACT(HOUR FROM interval_start AT TIME ZONE 'Europe/Prague')::INT, AVG(buy_raw_price_czk_kwh), STDDEV(buy_raw_price_czk_kwh), PERCENTILE_CONT(0.25) WITHIN GROUP ( ORDER BY buy_raw_price_czk_kwh), PERCENTILE_CONT(0.75) WITHIN GROUP ( ORDER BY buy_raw_price_czk_kwh), COUNT(*)::INT, now() FROM ems.market_interval_price WHERE market_source IN ('OTE_CZ', 'OTE_CZ_DAM') AND interval_start >= now() - make_interval(days => p_lookback_days) GROUP BY EXTRACT(DOW FROM interval_start AT TIME ZONE 'Europe/Prague'), EXTRACT(HOUR FROM interval_start AT TIME ZONE 'Europe/Prague') HAVING COUNT(*) >= 4 ON CONFLICT (site_id, day_of_week, hour_of_day) DO UPDATE SET avg_price = 0.7 * ems.market_price_stats.avg_price + 0.3 * EXCLUDED.avg_price, stddev_price = COALESCE( 0.7 * ems.market_price_stats.stddev_price + 0.3 * EXCLUDED.stddev_price, EXCLUDED.stddev_price), p25_price = EXCLUDED.p25_price, p75_price = EXCLUDED.p75_price, sample_count = ems.market_price_stats.sample_count + EXCLUDED.sample_count, last_updated = now(); GET DIAGNOSTICS v_count = ROW_COUNT; RETURN v_count; END; $$; COMMENT ON FUNCTION ems.fn_update_market_price_stats IS 'Aktualizuje historické průměry spotové ceny per DOW+hodina. EMA 70/30 pro postupné zpřesňování. Volat denně po importu OTE. Pro první naplnění: SELECT ems.fn_update_market_price_stats(2, 180);'; CREATE OR REPLACE FUNCTION ems.fn_update_tuv_usage_stats( p_site_id INT, p_lookback_days INT DEFAULT 30 ) RETURNS INT LANGUAGE plpgsql AS $$ DECLARE v_count INT; BEGIN -- Telemetrie TČ je idle-skip (telemetry_collector): rozestup řádků je -- 1–14 min. Delta se proto normalizuje na °C/min (gap_min); hustá 1min -- data dávají identické hodnoty jako dřív. Mezery > 30 min (výpadek -- sběru) se z učení vylučují. insert into ems.tuv_usage_stats (site_id, day_of_week, hour_of_day, avg_temp_delta_c, stddev_temp_delta, sample_count, last_updated) with deltas as ( select measured_at, extract(dow from measured_at at time zone 'Europe/Prague')::int as dow, extract(hour from measured_at at time zone 'Europe/Prague')::int as hour, tuv_tank_temp_c - lag(tuv_tank_temp_c) over w as temp_delta_c, extract(epoch from measured_at - lag(measured_at) over w) / 60.0 as gap_min from ems.telemetry_heat_pump where site_id = p_site_id and measured_at >= now() - make_interval(days => p_lookback_days) and tuv_tank_temp_c is not null window w as (partition by site_id order by measured_at) ) select p_site_id, dow, hour, avg(temp_delta_c / gap_min), stddev(temp_delta_c / gap_min), count(*)::int, now() from deltas where temp_delta_c is not null and gap_min > 0 and gap_min <= 30 and abs(temp_delta_c) < 5 group by dow, hour having count(*) >= 4 ON CONFLICT (site_id, day_of_week, hour_of_day) DO UPDATE SET avg_temp_delta_c = 0.7 * ems.tuv_usage_stats.avg_temp_delta_c + 0.3 * EXCLUDED.avg_temp_delta_c, stddev_temp_delta = COALESCE( 0.7 * ems.tuv_usage_stats.stddev_temp_delta + 0.3 * EXCLUDED.stddev_temp_delta, EXCLUDED.stddev_temp_delta), sample_count = ems.tuv_usage_stats.sample_count + EXCLUDED.sample_count, last_updated = now(); GET DIAGNOSTICS v_count = ROW_COUNT; RETURN v_count; END; $$; COMMENT ON FUNCTION ems.fn_update_tuv_usage_stats IS 'Aktualizuje statistiku poklesu teploty TUV zásobníku per DOW+hodina. avg_temp_delta_c je v °C/min (delta normalizovaná délkou mezery mezi řádky — robustní vůči idle-skip telemetrii; mezery > 30 min vyloučeny). Záporné avg_temp_delta_c = zásobník se ochlazuje (spotřeba teplé vody). Potřeba min. 1 měsíc telemetrie TČ. Volat denně.'; CREATE OR REPLACE FUNCTION ems.fn_get_predicted_price( p_site_id INT, p_interval_start TIMESTAMPTZ ) RETURNS NUMERIC LANGUAGE plpgsql STABLE AS $$ DECLARE v_dow INT; v_hour INT; v_price NUMERIC; v_corr NUMERIC := 1.0; BEGIN v_dow := EXTRACT(DOW FROM p_interval_start AT TIME ZONE 'Europe/Prague')::INT; v_hour := EXTRACT(HOUR FROM p_interval_start AT TIME ZONE 'Europe/Prague')::INT; SELECT avg_price INTO v_price FROM ems.market_price_stats WHERE site_id = p_site_id AND day_of_week = v_dow AND hour_of_day = v_hour; IF v_price IS NULL THEN RETURN 2.50; END IF; RETURN ROUND(v_price * v_corr, 6); END; $$; COMMENT ON FUNCTION ems.fn_get_predicted_price IS 'Vrátí predikovanou cenu pro slot za horizontem OTE. Zatím používá jen sezónní průměr. Fáze 3d přidá korekci počasím. Fallback 2.50 Kč/kWh pokud nejsou historická data (min. 3 měsíce).';