Files
ems/db/routines/R__fn_fill_audit_interval.sql
Dusan Vojacek 5fcc47bce2
All checks were successful
test / smoke-test (push) Successful in 5s
deploy / deploy (push) Successful in 11s
implementace Ekonomiky
2026-04-05 20:10:43 +02:00

240 lines
8.9 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.
-- =============================================================
-- R__fn_fill_audit_interval.sql
-- EMS Platform plnění audit_interval ze skutečné telemetrie
-- Repeatable migration
-- =============================================================
CREATE OR REPLACE FUNCTION ems.fn_fill_audit_interval(
p_site_id INT,
p_interval_start TIMESTAMPTZ
)
RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
v_interval_end TIMESTAMPTZ := p_interval_start + INTERVAL '15 minutes';
v_run_id INT;
v_avg_pv_power_w INT;
v_avg_battery_power_w INT;
v_avg_grid_power_w INT;
v_avg_load_power_w INT;
v_last_soc NUMERIC(5,2);
v_sum_ev_power_w INT;
v_avg_hp_power_w INT;
v_plan ems.planning_interval%ROWTYPE;
v_buy_price NUMERIC;
v_sell_price NUMERIC;
v_actual_cost NUMERIC := NULL;
v_green_bonus_czk NUMERIC := 0;
v_pv_b_production_wh NUMERIC;
v_array_prod_wh NUMERIC;
r_bonus RECORD;
BEGIN
-- Najít aktivní plán pro tento interval
SELECT pi.* INTO v_plan
FROM ems.planning_interval pi
JOIN ems.planning_run pr ON pr.id = pi.run_id
WHERE pr.site_id = p_site_id
AND pi.interval_start = p_interval_start
AND pr.status IN ('active', 'superseded')
ORDER BY pr.created_at DESC
LIMIT 1;
v_run_id := v_plan.run_id;
-- Agregovat telemetrii střídače (průměr za 15min; agregace bez GROUP BY vrací vždy 1 řádek)
SELECT
AVG(pv_power_w)::INT,
AVG(battery_power_w)::INT,
AVG(grid_power_w)::INT,
AVG(load_power_w)::INT,
LAST(battery_soc_percent, measured_at)
INTO
v_avg_pv_power_w,
v_avg_battery_power_w,
v_avg_grid_power_w,
v_avg_load_power_w,
v_last_soc
FROM ems.telemetry_inverter
WHERE site_id = p_site_id
AND measured_at >= p_interval_start
AND measured_at < v_interval_end;
-- Agregovat EV nabíječky (součet průměrů po charger_id)
SELECT COALESCE(SUM(avg_power), 0)::INT
INTO v_sum_ev_power_w
FROM (
SELECT AVG(power_w) AS avg_power
FROM ems.telemetry_ev_charger
WHERE site_id = p_site_id
AND measured_at >= p_interval_start
AND measured_at < v_interval_end
GROUP BY charger_id
) sub;
-- Agregovat tepelné čerpadlo
SELECT AVG(power_w)::INT
INTO v_avg_hp_power_w
FROM ems.telemetry_heat_pump
WHERE site_id = p_site_id
AND measured_at >= p_interval_start
AND measured_at < v_interval_end;
-- Efektivní cena pro výpočet skutečných nákladů
v_buy_price := ems.fn_effective_buy_price(p_site_id, p_interval_start);
v_sell_price := ems.fn_effective_sell_price(p_site_id, p_interval_start);
-- Skutečné náklady (kladný grid = nákup, záporný = prodej)
IF v_avg_grid_power_w IS NOT NULL THEN
v_actual_cost := (v_avg_grid_power_w::NUMERIC / 1000.0 / 4.0)
* CASE WHEN v_avg_grid_power_w >= 0
THEN COALESCE(v_buy_price, 0)
ELSE COALESCE(v_sell_price, 0) END;
END IF;
-- Zelený bonus: výroba bonusových polí z reálné telemetrie (Wh = průměr W × 0,25 h)
v_pv_b_production_wh := NULL;
FOR r_bonus IN
SELECT pa.id, pa.inverter_id, pa.telemetry_source
FROM ems.asset_pv_array pa
WHERE pa.site_id = p_site_id
AND pa.green_bonus_czk_kwh IS NOT NULL
AND pa.green_bonus_valid_from <= p_interval_start::DATE
AND (pa.green_bonus_valid_to IS NULL
OR pa.green_bonus_valid_to > p_interval_start::DATE)
LOOP
v_array_prod_wh := NULL;
IF r_bonus.telemetry_source IS NOT NULL AND r_bonus.inverter_id IS NOT NULL THEN
SELECT AVG(
CASE r_bonus.telemetry_source
WHEN 'gen_port' THEN ti.gen_port_power_w
WHEN 'pv_strings' THEN COALESCE(ti.pv1_power_w, 0)
+ COALESCE(ti.pv2_power_w, 0)
WHEN 'pv_total' THEN ti.pv_power_w
ELSE NULL
END
)::NUMERIC * 0.25
INTO v_array_prod_wh
FROM ems.telemetry_inverter ti
WHERE ti.inverter_id = r_bonus.inverter_id
AND ti.measured_at >= p_interval_start
AND ti.measured_at < v_interval_end;
END IF;
-- Fallback na forecast pokud telemetrie není k dispozici
IF v_array_prod_wh IS NULL THEN
SELECT fpi.power_w * 0.25
INTO v_array_prod_wh
FROM ems.forecast_pv_interval fpi
JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id
WHERE fpr.site_id = p_site_id
AND fpr.pv_array_id = r_bonus.id
AND fpi.interval_start = p_interval_start
AND fpr.status = 'ok'
ORDER BY fpr.created_at DESC
LIMIT 1;
END IF;
v_array_prod_wh := COALESCE(v_array_prod_wh, 0);
IF v_pv_b_production_wh IS NULL THEN
v_pv_b_production_wh := 0;
END IF;
v_pv_b_production_wh := v_pv_b_production_wh + v_array_prod_wh;
v_green_bonus_czk := v_green_bonus_czk + ems.fn_green_bonus_revenue(
r_bonus.id,
p_interval_start,
v_array_prod_wh
);
END LOOP;
-- Upsert do audit_interval
INSERT INTO ems.audit_interval (
site_id, interval_start, planning_run_id,
actual_pv_power_w, actual_battery_power_w,
actual_grid_power_w, actual_load_power_w,
actual_battery_soc_pct,
actual_ev_power_w,
actual_heat_pump_power_w,
actual_cost_czk,
pv_b_production_wh,
green_bonus_czk,
deviation_grid_w,
deviation_cost_czk
) VALUES (
p_site_id, p_interval_start, v_run_id,
v_avg_pv_power_w,
v_avg_battery_power_w,
v_avg_grid_power_w,
v_avg_load_power_w,
v_last_soc,
v_sum_ev_power_w,
v_avg_hp_power_w,
ROUND(v_actual_cost, 4),
v_pv_b_production_wh,
ROUND(v_green_bonus_czk, 4),
CASE WHEN v_plan.run_id IS NOT NULL
THEN v_avg_grid_power_w - v_plan.grid_setpoint_w
ELSE NULL END,
CASE WHEN v_plan.run_id IS NOT NULL
THEN ROUND(v_actual_cost - COALESCE(v_plan.expected_cost_czk, 0), 4)
ELSE NULL END
)
ON CONFLICT (site_id, interval_start) DO UPDATE SET
planning_run_id = EXCLUDED.planning_run_id,
actual_pv_power_w = EXCLUDED.actual_pv_power_w,
actual_battery_power_w = EXCLUDED.actual_battery_power_w,
actual_grid_power_w = EXCLUDED.actual_grid_power_w,
actual_load_power_w = EXCLUDED.actual_load_power_w,
actual_battery_soc_pct = EXCLUDED.actual_battery_soc_pct,
actual_ev_power_w = EXCLUDED.actual_ev_power_w,
actual_heat_pump_power_w = EXCLUDED.actual_heat_pump_power_w,
actual_cost_czk = EXCLUDED.actual_cost_czk,
pv_b_production_wh = EXCLUDED.pv_b_production_wh,
green_bonus_czk = EXCLUDED.green_bonus_czk,
deviation_grid_w = EXCLUDED.deviation_grid_w,
deviation_cost_czk = EXCLUDED.deviation_cost_czk;
END;
$$;
COMMENT ON FUNCTION ems.fn_fill_audit_interval(INT, TIMESTAMPTZ) IS
'Naplní nebo aktualizuje jeden řádek v audit_interval pro danou lokalitu a 15min interval.
Agreguje průměry z telemetrie (střídač, EV, TČ), porovná se skutečným plánem a spočítá odchylky.
Zelený bonus: součet přes pole s green_bonus_czk_kwh; výroba primárně z reálné telemetrie
(dle asset_pv_array.telemetry_source), fallback na forecast_pv_interval pokud telemetrie chybí.
Volat každých 15 minut pro interval který právě skončil.';
-- ============================================================
-- Hromadné plnění auditu za historické období
-- ============================================================
CREATE OR REPLACE FUNCTION ems.fn_fill_audit_range(
p_site_id INT,
p_from TIMESTAMPTZ,
p_to TIMESTAMPTZ
)
RETURNS INT
LANGUAGE plpgsql
AS $$
DECLARE
v_slot TIMESTAMPTZ;
v_count INT := 0;
BEGIN
v_slot := date_trunc('hour', p_from)
+ INTERVAL '15 min' * FLOOR(EXTRACT(MINUTE FROM p_from) / 15);
WHILE v_slot < p_to LOOP
PERFORM ems.fn_fill_audit_interval(p_site_id, v_slot);
v_slot := v_slot + INTERVAL '15 minutes';
v_count := v_count + 1;
END LOOP;
RETURN v_count;
END;
$$;
COMMENT ON FUNCTION ems.fn_fill_audit_range(INT, TIMESTAMPTZ, TIMESTAMPTZ) IS
'Hromadně naplní audit_interval pro celé historické období.
Volá fn_fill_audit_interval pro každý 15min slot v rozsahu p_fromp_to.
Vrátí počet zpracovaných intervalů. Použít pro backfill po výpadku nebo prvním nasazení.';