Files
ems/db/routines/R__018_fn_extended_planning.sql
Dusan Vojacek f71bc944b4 fix(db): čtecí cesty telemetrie robustní vůči řídkým řádkům (idle-skip)
- fn_fill_audit_interval: EV a TČ agregace sum(power_w)/15 místo avg přes
  přítomné řádky — avg by při řídké telemetrii nadhodnotil aktivitu části
  slotu; chybějící minuta = 0 W (idle). TČ drží NULL bez power_w (MIM-B19N).
- fn_update_tuv_usage_stats: delta TUV normalizovaná na °C/min délkou mezery
  mezi řádky (gap_min), mezery > 30 min vyloučeny; pro hustá 1min data
  numericky identické s původním LAG.
- vw_pool_pump_day_energy: komentář — on_minutes drží invariant „zapnuté
  čerpadlo se ukládá každou minutu".

Pro hustá 1min data beze změny výsledků; připravuje idle-skip zápisů
v telemetry_collector (navazující commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:06:23 +02:00

162 lines
5.7 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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
-- 114 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).';