-- 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 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 ( PARTITION BY site_id ORDER BY measured_at ) AS temp_delta_c 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 ) SELECT p_site_id, dow, hour, AVG(temp_delta_c), STDDEV(temp_delta_c), COUNT(*)::INT, now() FROM deltas WHERE temp_delta_c IS NOT NULL 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. 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).';