414 lines
16 KiB
PL/PgSQL
414 lines
16 KiB
PL/PgSQL
-- =============================================================
|
||
-- 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;
|
||
|
||
-- 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;
|
||
|
||
-- 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.
|
||
Nově: per-minutový split pro 6 energetických veličin (import/export/batt/PV/load Wh);
|
||
grid import/export primárně z delta Deye total counterů (reg 522-525), fallback per-minute.
|
||
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_from–p_to.
|
||
Vrátí počet zpracovaných intervalů. Použít pro backfill po výpadku nebo prvním nasazení.';
|