-- ============================================================= -- 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 '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í.';