172 lines
6.1 KiB
PL/PgSQL
172 lines
6.1 KiB
PL/PgSQL
-- =============================================================
|
||
-- 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;
|
||
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;
|
||
|
||
-- 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,
|
||
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),
|
||
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,
|
||
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.
|
||
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_from–p_to.
|
||
Vrátí počet zpracovaných intervalů. Použít pro backfill po výpadku nebo prvním nasazení.';
|