-- ============================================================= -- 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 poslední ok predikce pro slot (Wh = W × 0,25 h) v_pv_b_production_wh := NULL; FOR r_bonus IN SELECT id FROM ems.asset_pv_array WHERE site_id = p_site_id AND green_bonus_czk_kwh IS NOT NULL LOOP 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; 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 všechna pole s nastaveným bonusem, výroba z poslední ok forecast_pv_interval. 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í.';