Files
ems/db/routines/R__019_fn_fill_audit_interval.sql
Dusan Vojacek ed88ef8910
Some checks failed
CI and deploy / migration-check (push) Failing after 11s
CI and deploy / deploy (push) Has been skipped
oprava import/export kwh
2026-05-01 14:58:29 +02:00

424 lines
17 KiB
PL/PgSQL
Raw Permalink 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__019_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;
-- per-minute Wh veličiny
v_grid_import_wh NUMERIC;
v_grid_export_wh NUMERIC;
v_batt_charge_wh NUMERIC;
v_batt_discharge_wh NUMERIC;
v_pv_production_wh NUMERIC;
v_load_consumption_wh NUMERIC;
-- Deye counter delta
v_counter_import_first BIGINT;
v_counter_import_last BIGINT;
v_counter_export_first BIGINT;
v_counter_export_last BIGINT;
v_delta_import NUMERIC;
v_delta_export NUMERIC;
v_imp_before NUMERIC;
v_exp_before NUMERIC;
-- 7 směrových toků (prioritní alokace per minuta; součet W/60 = Wh)
r_flow RECORD;
v_flow_samples INT := 0;
v_acc_ptl NUMERIC := 0;
v_acc_ptb NUMERIC := 0;
v_acc_ptg NUMERIC := 0;
v_acc_btl NUMERIC := 0;
v_acc_btg NUMERIC := 0;
v_acc_gtl NUMERIC := 0;
v_acc_gtb NUMERIC := 0;
v_pv NUMERIC;
v_load_m NUMERIC;
v_gi NUMERIC;
v_ge NUMERIC;
v_bc NUMERIC;
v_bd NUMERIC;
v_ptl NUMERIC;
v_ptb NUMERIC;
v_ptg NUMERIC;
v_btl NUMERIC;
v_btg NUMERIC;
v_gtl NUMERIC;
v_gtb NUMERIC;
v_flow_pv_to_load_wh NUMERIC;
v_flow_pv_to_batt_wh NUMERIC;
v_flow_pv_to_grid_wh NUMERIC;
v_flow_batt_to_load_wh NUMERIC;
v_flow_batt_to_grid_wh NUMERIC;
v_flow_grid_to_load_wh NUMERIC;
v_flow_grid_to_batt_wh NUMERIC;
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ěry (pro zpětnou kompatibilitu) + per-minute split pro Wh
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),
-- Per-minute split: každý vzorek × 1/60 h = Wh
ROUND(SUM(GREATEST(grid_power_w, 0))::NUMERIC / 60, 1),
ROUND(SUM(ABS(LEAST(grid_power_w, 0)))::NUMERIC / 60, 1),
ROUND(SUM(ABS(LEAST(battery_power_w, 0)))::NUMERIC / 60, 1),
ROUND(SUM(GREATEST(battery_power_w, 0))::NUMERIC / 60, 1),
ROUND(SUM(GREATEST(pv_power_w, 0))::NUMERIC / 60, 1),
ROUND(SUM(GREATEST(load_power_w, 0))::NUMERIC / 60, 1),
-- Deye total energy counter delta
FIRST(grid_import_total_wh, measured_at),
LAST(grid_import_total_wh, measured_at),
FIRST(grid_export_total_wh, measured_at),
LAST(grid_export_total_wh, 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,
v_grid_import_wh,
v_grid_export_wh,
v_batt_charge_wh,
v_batt_discharge_wh,
v_pv_production_wh,
v_load_consumption_wh,
v_counter_import_first,
v_counter_import_last,
v_counter_export_first,
v_counter_export_last
FROM ems.telemetry_inverter
WHERE site_id = p_site_id
AND measured_at >= p_interval_start
AND measured_at < v_interval_end;
-- Deye counter delta (primární zdroj pro grid import/export, pokud jsou čítače dostupné)
IF v_counter_import_first IS NOT NULL AND v_counter_import_last IS NOT NULL
AND v_counter_import_last >= v_counter_import_first THEN
v_delta_import := v_counter_import_last - v_counter_import_first;
v_grid_import_wh := v_delta_import;
END IF;
IF v_counter_export_first IS NOT NULL AND v_counter_export_last IS NOT NULL
AND v_counter_export_last >= v_counter_export_first THEN
v_delta_export := v_counter_export_last - v_counter_export_first;
v_grid_export_wh := v_delta_export;
END IF;
v_imp_before := v_grid_import_wh;
v_exp_before := v_grid_export_wh;
v_grid_import_wh := ems.fn_audit_grid_import_wh_for_economics(
v_imp_before, v_exp_before, v_avg_grid_power_w);
v_grid_export_wh := ems.fn_audit_grid_export_wh_for_economics(
v_imp_before, v_exp_before, v_avg_grid_power_w);
-- 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 per-direction (import × buy - export × sell)
IF v_grid_import_wh IS NOT NULL OR v_grid_export_wh IS NOT NULL THEN
v_actual_cost := COALESCE(v_grid_import_wh, 0) / 1000.0 * COALESCE(v_buy_price, 0)
- COALESCE(v_grid_export_wh, 0) / 1000.0 * COALESCE(v_sell_price, 0);
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 'pv1' THEN ti.pv1_power_w
WHEN 'pv2' THEN ti.pv2_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;
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;
-- Prioritní alokace toků: PV → load → batt charge → export; pak batt discharge → load/export; grid → zbytek
FOR r_flow IN
SELECT pv_power_w, grid_power_w, battery_power_w, load_power_w
FROM ems.telemetry_inverter
WHERE site_id = p_site_id
AND measured_at >= p_interval_start
AND measured_at < v_interval_end
ORDER BY measured_at
LOOP
v_flow_samples := v_flow_samples + 1;
v_pv := GREATEST(COALESCE(r_flow.pv_power_w, 0)::NUMERIC, 0);
v_load_m := GREATEST(COALESCE(r_flow.load_power_w, 0)::NUMERIC, 0);
v_gi := GREATEST(COALESCE(r_flow.grid_power_w, 0)::NUMERIC, 0);
v_ge := ABS(LEAST(COALESCE(r_flow.grid_power_w, 0)::NUMERIC, 0));
v_bc := ABS(LEAST(COALESCE(r_flow.battery_power_w, 0)::NUMERIC, 0));
v_bd := GREATEST(COALESCE(r_flow.battery_power_w, 0)::NUMERIC, 0);
v_ptl := LEAST(v_pv, v_load_m);
v_ptb := LEAST(v_pv - v_ptl, v_bc);
v_ptg := LEAST(v_pv - v_ptl - v_ptb, v_ge);
v_btl := LEAST(v_bd, v_load_m - v_ptl);
v_btg := LEAST(v_bd - v_btl, GREATEST(0::NUMERIC, v_ge - v_ptg));
v_gtl := GREATEST(0::NUMERIC, v_load_m - v_ptl - v_btl);
v_gtb := GREATEST(0::NUMERIC, v_bc - v_ptb);
v_acc_ptl := v_acc_ptl + v_ptl;
v_acc_ptb := v_acc_ptb + v_ptb;
v_acc_ptg := v_acc_ptg + v_ptg;
v_acc_btl := v_acc_btl + v_btl;
v_acc_btg := v_acc_btg + v_btg;
v_acc_gtl := v_acc_gtl + v_gtl;
v_acc_gtb := v_acc_gtb + v_gtb;
END LOOP;
IF v_flow_samples = 0 THEN
v_flow_pv_to_load_wh := NULL;
v_flow_pv_to_batt_wh := NULL;
v_flow_pv_to_grid_wh := NULL;
v_flow_batt_to_load_wh := NULL;
v_flow_batt_to_grid_wh := NULL;
v_flow_grid_to_load_wh := NULL;
v_flow_grid_to_batt_wh := NULL;
ELSE
v_flow_pv_to_load_wh := ROUND(v_acc_ptl / 60, 1);
v_flow_pv_to_batt_wh := ROUND(v_acc_ptb / 60, 1);
v_flow_pv_to_grid_wh := ROUND(v_acc_ptg / 60, 1);
v_flow_batt_to_load_wh := ROUND(v_acc_btl / 60, 1);
v_flow_batt_to_grid_wh := ROUND(v_acc_btg / 60, 1);
v_flow_grid_to_load_wh := ROUND(v_acc_gtl / 60, 1);
v_flow_grid_to_batt_wh := ROUND(v_acc_gtb / 60, 1);
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,
pv_b_production_wh,
green_bonus_czk,
deviation_grid_w,
deviation_cost_czk,
actual_grid_import_wh,
actual_grid_export_wh,
actual_batt_charge_wh,
actual_batt_discharge_wh,
actual_pv_production_wh,
actual_load_consumption_wh,
flow_pv_to_load_wh,
flow_pv_to_batt_wh,
flow_pv_to_grid_wh,
flow_batt_to_load_wh,
flow_batt_to_grid_wh,
flow_grid_to_load_wh,
flow_grid_to_batt_wh
) 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,
v_grid_import_wh,
v_grid_export_wh,
v_batt_charge_wh,
v_batt_discharge_wh,
v_pv_production_wh,
v_load_consumption_wh,
v_flow_pv_to_load_wh,
v_flow_pv_to_batt_wh,
v_flow_pv_to_grid_wh,
v_flow_batt_to_load_wh,
v_flow_batt_to_grid_wh,
v_flow_grid_to_load_wh,
v_flow_grid_to_batt_wh
)
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,
actual_grid_import_wh = EXCLUDED.actual_grid_import_wh,
actual_grid_export_wh = EXCLUDED.actual_grid_export_wh,
actual_batt_charge_wh = EXCLUDED.actual_batt_charge_wh,
actual_batt_discharge_wh = EXCLUDED.actual_batt_discharge_wh,
actual_pv_production_wh = EXCLUDED.actual_pv_production_wh,
actual_load_consumption_wh = EXCLUDED.actual_load_consumption_wh,
flow_pv_to_load_wh = EXCLUDED.flow_pv_to_load_wh,
flow_pv_to_batt_wh = EXCLUDED.flow_pv_to_batt_wh,
flow_pv_to_grid_wh = EXCLUDED.flow_pv_to_grid_wh,
flow_batt_to_load_wh = EXCLUDED.flow_batt_to_load_wh,
flow_batt_to_grid_wh = EXCLUDED.flow_batt_to_grid_wh,
flow_grid_to_load_wh = EXCLUDED.flow_grid_to_load_wh,
flow_grid_to_batt_wh = EXCLUDED.flow_grid_to_batt_wh;
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.
Per-minutový split pro 6 energetických veličin (import/export/batt/PV/load Wh);
grid import/export nejprve z delta Deye total counterů (reg 522-525), fallback per-minute; poté sjednocení
fn_audit_grid_*_wh_for_economics (u jednosměrného toku max s odhadem z průměrného grid_power_w).
7 směrových toků (flow_*_wh): prioritní alokace per minuta z telemetrie (PV→load→batt→export; baterie→load/export; síť→zbytek).
actual_cost_czk = per-direction (import_wh × buy - export_wh × sell).
Zelený bonus: součet přes pole s green_bonus_czk_kwh.
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í.';