Files
ems/db/routines/R__022_fn_fill_forecast_accuracy.sql
Dusan Vojacek 1dfab8c7a1
Some checks failed
CI and deploy / migration-check (push) Failing after 17s
CI and deploy / deploy (push) Has been skipped
dalsi uprava vypoctu delty (ignorujeme orezane vyroby)
2026-04-22 22:42:12 +02:00

154 lines
6.7 KiB
PL/PgSQL

CREATE OR REPLACE FUNCTION ems.fn_fill_forecast_accuracy(
p_site_id INT,
p_lookback_hours INT DEFAULT 48
)
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
v_count INT := 0;
BEGIN
INSERT INTO ems.forecast_accuracy (
site_id, pv_array_id, interval_start, run_id,
forecast_power_w, forecast_created_at, lead_time_hours,
actual_power_w, actual_filled_at,
error_w, error_pct,
learning_eligible, learning_exclude_reason
)
SELECT
fpr.site_id,
fpr.pv_array_id,
fpi.interval_start,
fpi.run_id,
fpi.power_w AS forecast_power_w,
fpr.created_at AS forecast_created_at,
ROUND(
EXTRACT(EPOCH FROM (fpi.interval_start - fpr.created_at))
/ 3600.0, 2
) AS lead_time_hours,
CASE
WHEN v.exclude_actual_for_learning THEN NULL
ELSE slot.avg_actual_w::INT
END AS actual_power_w,
now() AS actual_filled_at,
CASE
WHEN v.exclude_actual_for_learning THEN NULL
ELSE fpi.power_w - COALESCE(slot.avg_actual_w::INT, 0)
END AS error_w,
CASE
WHEN v.exclude_actual_for_learning THEN NULL
WHEN slot.avg_actual_w IS NOT NULL
AND slot.avg_actual_w > 0
THEN ROUND(
(fpi.power_w::NUMERIC - slot.avg_actual_w::NUMERIC)
/ slot.avg_actual_w::NUMERIC * 100,
4
)
ELSE NULL
END AS error_pct,
v.learning_eligible,
v.learning_exclude_reason
FROM ems.forecast_pv_interval fpi
JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id
JOIN ems.asset_pv_array pa ON pa.id = fpr.pv_array_id
LEFT JOIN ems.site_pv_forecast_calibration cal
ON cal.site_id = fpr.site_id
LEFT JOIN LATERAL (
SELECT
coalesce(cal.delta_learn_min_ts, timestamptz '2026-04-11T22:00:00Z') AS delta_learn_min_ts,
cal.pv_curtailment_policy_effective_from AS policy_from
) cal_eff ON true
LEFT JOIN LATERAL (
SELECT pi.pv_a_curtailed_w, pi.deye_gen_cutoff_enabled
FROM ems.planning_interval pi
JOIN ems.planning_run pr ON pr.id = pi.run_id
WHERE pr.site_id = fpr.site_id
AND pr.status = 'active'
AND pi.interval_start = fpi.interval_start
LIMIT 1
) ap ON true
LEFT JOIN LATERAL (
SELECT
(fpi.interval_start < cal_eff.delta_learn_min_ts) AS before_learn_cutoff,
(
cal_eff.policy_from IS NOT NULL
AND fpi.interval_start >= cal_eff.policy_from
AND (
coalesce(ap.pv_a_curtailed_w, 0) > 50
OR coalesce(ap.deye_gen_cutoff_enabled, false) IS TRUE
OR EXISTS (
SELECT 1
FROM ems.cutoff_switch_log l
WHERE l.site_id = fpr.site_id
AND l.switched_at >= fpi.interval_start
AND l.switched_at < fpi.interval_start + INTERVAL '15 minutes'
AND l.new_state IS FALSE
)
)
) AS is_curtailed_learning_slot,
EXISTS (
SELECT 1
FROM ems.telemetry_inverter ti_d
WHERE ti_d.site_id = fpr.site_id
AND ti_d.measured_at >= fpi.interval_start
AND ti_d.measured_at < fpi.interval_start + INTERVAL '15 minutes'
AND (
coalesce(ti_d.is_export_limited, false) IS TRUE
OR (ti_d.pv_derating_flags IS NOT NULL AND ti_d.pv_derating_flags <> 0)
)
) AS is_telemetry_derated_slot
) flags ON true
LEFT JOIN LATERAL (
SELECT
CASE
WHEN flags.before_learn_cutoff THEN false
WHEN flags.is_curtailed_learning_slot THEN false
WHEN flags.is_telemetry_derated_slot THEN false
ELSE true
END AS learning_eligible,
CASE
WHEN flags.before_learn_cutoff THEN 'before_delta_learn_min'
WHEN flags.is_telemetry_derated_slot THEN 'telemetry_derating'
WHEN flags.is_curtailed_learning_slot THEN 'curtailment_or_export_cutoff'
ELSE NULL
END AS learning_exclude_reason,
(flags.is_curtailed_learning_slot OR flags.is_telemetry_derated_slot) AS exclude_actual_for_learning
) v ON true
LEFT JOIN LATERAL (
SELECT AVG(
CASE
WHEN pa.controllable = false THEN ti.gen_port_power_w::NUMERIC
ELSE (COALESCE(ti.pv1_power_w, 0) + COALESCE(ti.pv2_power_w, 0))::NUMERIC
END
) AS avg_actual_w
FROM ems.telemetry_inverter ti
WHERE ti.site_id = fpr.site_id
AND ti.measured_at >= fpi.interval_start
AND ti.measured_at < fpi.interval_start + INTERVAL '15 minutes'
) slot ON true
WHERE fpr.site_id = p_site_id
AND fpr.status = 'ok'
AND fpi.interval_start < now() - INTERVAL '15 minutes'
AND fpi.interval_start >= now() - make_interval(hours => p_lookback_hours)
ON CONFLICT (run_id, interval_start) DO UPDATE SET
actual_power_w = EXCLUDED.actual_power_w,
actual_filled_at = EXCLUDED.actual_filled_at,
error_w = EXCLUDED.error_w,
error_pct = EXCLUDED.error_pct,
learning_eligible = EXCLUDED.learning_eligible,
learning_exclude_reason = EXCLUDED.learning_exclude_reason;
GET DIAGNOSTICS v_count = ROW_COUNT;
RETURN v_count;
END;
$$;
COMMENT ON FUNCTION ems.fn_fill_forecast_accuracy(INT, INT) IS
'Doplní skutečné hodnoty výroby do forecast_accuracy z telemetrie.
learning_eligible / learning_exclude_reason: před delta_learn_min_ts (kalibrace site) se nepočítá do učení delty;
po pv_curtailment_policy_effective_from sloty s curtailment / gen cutoff / cutoff_switch_log (export off) mají NULL actual a jsou vyloučeny z učení;
telemetrie: is_export_limited nebo pv_derating_flags <> 0 v okně slotu → stejné vyloučení (telemetry_derating).
Volat každých 15 minut (spolu s audit_filler) pro inkrementální plnění.
p_lookback_hours: kolik hodin zpět zpracovat (default 48h pro catch-up).
Pro první backfill: SELECT ems.fn_fill_forecast_accuracy(2, 8760) -- 1 rok';