From ed88ef8910c2455d6075580bcbf0b8d00b5b9f31 Mon Sep 17 00:00:00 2001 From: Dusan Vojacek Date: Fri, 1 May 2026 14:58:29 +0200 Subject: [PATCH] oprava import/export kwh --- db/routines/R__011_fn_effective_price.sql | 68 +++++++++++++++++++ .../R__012_fn_energy_flows_intervals_day.sql | 24 +++++-- db/routines/R__019_fn_fill_audit_interval.sql | 14 +++- .../R__062_fn_energy_flows_daily_month.sql | 14 ++-- .../R__068_fn_economics_daily_month.sql | 30 ++++---- db/views/R__056_vw_energy_flows.sql | 9 ++- ..._067_vw_site_effective_price_economics.sql | 37 +++++++--- docs/04-modules/energy-flows.md | 2 + 8 files changed, 158 insertions(+), 40 deletions(-) diff --git a/db/routines/R__011_fn_effective_price.sql b/db/routines/R__011_fn_effective_price.sql index 90058f9..3ab6acd 100644 --- a/db/routines/R__011_fn_effective_price.sql +++ b/db/routines/R__011_fn_effective_price.sql @@ -3,6 +3,74 @@ -- EMS Platform – funkce pro výpočet efektivní ceny per site -- Repeatable migration – nasazuje se při každé změně -- ============================================================= +-- Pomocné (audit/ekonomika): sjednocení import/export Wh — musí běžet před R__012/R__056. +create or replace function ems.fn_audit_grid_import_wh_for_economics( + p_import_wh numeric, + p_export_wh numeric, + p_grid_power_w int +) +returns numeric +language sql +stable +as $$ + select case + when coalesce(p_import_wh, 0) > 0 and coalesce(p_export_wh, 0) > 0 then + coalesce( + p_import_wh, + greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 + ) + when coalesce(p_export_wh, 0) = 0 then + greatest( + coalesce( + p_import_wh, + greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 + ), + greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 + ) + else + coalesce( + p_import_wh, + greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 + ) + end; +$$; + +create or replace function ems.fn_audit_grid_export_wh_for_economics( + p_import_wh numeric, + p_export_wh numeric, + p_grid_power_w int +) +returns numeric +language sql +stable +as $$ + select case + when coalesce(p_import_wh, 0) > 0 and coalesce(p_export_wh, 0) > 0 then + coalesce( + p_export_wh, + abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 + ) + when coalesce(p_import_wh, 0) = 0 then + greatest( + coalesce( + p_export_wh, + abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 + ), + abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 + ) + else + coalesce( + p_export_wh, + abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 + ) + end; +$$; + +comment on function ems.fn_audit_grid_import_wh_for_economics(numeric, numeric, int) is + 'Import Wh pro audit/ekonomiku: u čistého importu max(uložený čítač, max(0,P_grid)×¼ h).'; + +comment on function ems.fn_audit_grid_export_wh_for_economics(numeric, numeric, int) is + 'Export Wh pro audit/ekonomiku: u čistého exportu max(uložený čítač, |min(0,P_grid)|×¼ h).'; CREATE OR REPLACE FUNCTION ems.fn_effective_buy_price( p_site_id INT, diff --git a/db/routines/R__012_fn_energy_flows_intervals_day.sql b/db/routines/R__012_fn_energy_flows_intervals_day.sql index b050d78..0e3ccf4 100644 --- a/db/routines/R__012_fn_energy_flows_intervals_day.sql +++ b/db/routines/R__012_fn_energy_flows_intervals_day.sql @@ -14,13 +14,29 @@ as $fn$ end, 'grid_import_kwh', case - when ai.actual_grid_import_wh is null then null - else round(ai.actual_grid_import_wh::numeric / 1000, 4) + when ai.actual_grid_import_wh is null + and ai.actual_grid_export_wh is null + and ai.actual_grid_power_w is null + then null + else round( + ems.fn_audit_grid_import_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + )::numeric / 1000, + 4 + ) end, 'grid_export_kwh', case - when ai.actual_grid_export_wh is null then null - else round(ai.actual_grid_export_wh::numeric / 1000, 4) + when ai.actual_grid_import_wh is null + and ai.actual_grid_export_wh is null + and ai.actual_grid_power_w is null + then null + else round( + ems.fn_audit_grid_export_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + )::numeric / 1000, + 4 + ) end, 'batt_charge_kwh', case diff --git a/db/routines/R__019_fn_fill_audit_interval.sql b/db/routines/R__019_fn_fill_audit_interval.sql index d0f961b..63a50e5 100644 --- a/db/routines/R__019_fn_fill_audit_interval.sql +++ b/db/routines/R__019_fn_fill_audit_interval.sql @@ -45,6 +45,8 @@ DECLARE 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; @@ -141,6 +143,13 @@ BEGIN 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 @@ -371,8 +380,9 @@ $$; 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. +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. diff --git a/db/routines/R__062_fn_energy_flows_daily_month.sql b/db/routines/R__062_fn_energy_flows_daily_month.sql index ddb0ba9..51d7485 100644 --- a/db/routines/R__062_fn_energy_flows_daily_month.sql +++ b/db/routines/R__062_fn_energy_flows_daily_month.sql @@ -21,8 +21,10 @@ as $fn$ 'day', (date_trunc('day', ai.interval_start at time zone 'Europe/Prague'))::date, 'interval_count', count(*)::int, 'pv_production_kwh', round(sum(coalesce(ai.actual_pv_production_wh, 0)) / 1000, 3), - 'grid_import_kwh', round(sum(coalesce(ai.actual_grid_import_wh, 0)) / 1000, 3), - 'grid_export_kwh', round(sum(coalesce(ai.actual_grid_export_wh, 0)) / 1000, 3), + 'grid_import_kwh', round(sum(ems.fn_audit_grid_import_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w)) / 1000, 3), + 'grid_export_kwh', round(sum(ems.fn_audit_grid_export_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w)) / 1000, 3), 'batt_charge_kwh', round(sum(coalesce(ai.actual_batt_charge_wh, 0)) / 1000, 3), 'batt_discharge_kwh', round(sum(coalesce(ai.actual_batt_discharge_wh, 0)) / 1000, 3), 'load_kwh', round(sum(coalesce(ai.actual_load_consumption_wh, 0)) / 1000, 3), @@ -36,7 +38,9 @@ as $fn$ 'grid_import_cashflow_czk', round( sum( - coalesce(ai.actual_grid_import_wh, 0) / 1000.0 + ems.fn_audit_grid_import_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000.0 * coalesce(ep.effective_buy_price_czk_kwh, 0) ), 2 @@ -44,7 +48,9 @@ as $fn$ 'grid_export_revenue_czk', round( sum( - coalesce(ai.actual_grid_export_wh, 0) / 1000.0 + ems.fn_audit_grid_export_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000.0 * coalesce(ep.effective_sell_price_czk_kwh, 0) ), 2 diff --git a/db/routines/R__068_fn_economics_daily_month.sql b/db/routines/R__068_fn_economics_daily_month.sql index e725b91..8abcf15 100644 --- a/db/routines/R__068_fn_economics_daily_month.sql +++ b/db/routines/R__068_fn_economics_daily_month.sql @@ -59,7 +59,13 @@ as $fn$ coalesce(dt.vat_rate, 0.21) as vat_rate, mip.buy_raw_price_czk_kwh as buy_spot, mip.sell_raw_price_czk_kwh as sell_spot, - pi.expected_cost_czk as planned_cost_czk + pi.expected_cost_czk as planned_cost_czk, + ems.fn_audit_grid_import_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) as est_grid_import_wh, + ems.fn_audit_grid_export_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) as est_grid_export_wh from ems.audit_interval ai left join lateral ( select smc_inner.* @@ -202,29 +208,19 @@ as $fn$ select s5.site_id, s5.day_local, + round(s5.est_grid_import_wh / 1000, 4) as import_kwh, + round(s5.est_grid_export_wh / 1000, 4) as export_kwh, round( - coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000, - 4 - ) as import_kwh, - round( - coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000, - 4 - ) as export_kwh, - round( - coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000.0 - * coalesce(s5.buy_p, 0), + s5.est_grid_import_wh / 1000.0 * coalesce(s5.buy_p, 0), 4 ) as grid_import_cashflow_czk, round( - coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000.0 - * coalesce(s5.sell_p, 0), + s5.est_grid_export_wh / 1000.0 * coalesce(s5.sell_p, 0), 4 ) as grid_export_revenue_czk, round( - coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000.0 - * coalesce(s5.buy_p, 0) - - coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000.0 - * coalesce(s5.sell_p, 0), + s5.est_grid_import_wh / 1000.0 * coalesce(s5.buy_p, 0) + - s5.est_grid_export_wh / 1000.0 * coalesce(s5.sell_p, 0), 4 ) as dynamic_cost_czk, s5.green_bonus_czk, diff --git a/db/views/R__056_vw_energy_flows.sql b/db/views/R__056_vw_energy_flows.sql index 83851a3..1502cfc 100644 --- a/db/views/R__056_vw_energy_flows.sql +++ b/db/views/R__056_vw_energy_flows.sql @@ -11,8 +11,10 @@ SELECT site_id, (date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague'))::date AS day_local, ROUND(SUM(COALESCE(actual_pv_production_wh, 0)) / 1000, 3) AS pv_production_kwh, - ROUND(SUM(COALESCE(actual_grid_import_wh, 0)) / 1000, 3) AS grid_import_kwh, - ROUND(SUM(COALESCE(actual_grid_export_wh, 0)) / 1000, 3) AS grid_export_kwh, + ROUND(SUM(ems.fn_audit_grid_import_wh_for_economics( + actual_grid_import_wh, actual_grid_export_wh, actual_grid_power_w)) / 1000, 3) AS grid_import_kwh, + ROUND(SUM(ems.fn_audit_grid_export_wh_for_economics( + actual_grid_import_wh, actual_grid_export_wh, actual_grid_power_w)) / 1000, 3) AS grid_export_kwh, ROUND(SUM(COALESCE(actual_batt_charge_wh, 0)) / 1000, 3) AS batt_charge_kwh, ROUND(SUM(COALESCE(actual_batt_discharge_wh, 0)) / 1000, 3) AS batt_discharge_kwh, ROUND(SUM(COALESCE(actual_load_consumption_wh, 0)) / 1000, 3) AS load_kwh, @@ -29,4 +31,5 @@ GROUP BY (date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague'))::date; COMMENT ON VIEW ems.vw_energy_flows_daily IS -'Denní součty energie a modelovaných toků (prioritní alokace z fn_fill_audit_interval). kWh z Wh sloupců.'; +'Denní součty energie a toků (prioritní alokace z fn_fill_audit_interval). PV/baterie/load/flow z Wh sloupců; +grid import/export kWh používají fn_audit_grid_*_wh_for_economics – shodně jako audit po doplnění intervalu.'; diff --git a/db/views/R__067_vw_site_effective_price_economics.sql b/db/views/R__067_vw_site_effective_price_economics.sql index 574aee5..e837ec7 100644 --- a/db/views/R__067_vw_site_effective_price_economics.sql +++ b/db/views/R__067_vw_site_effective_price_economics.sql @@ -15,25 +15,43 @@ CREATE VIEW ems.vw_economics_interval AS SELECT ai.site_id, ai.interval_start, - -- Wh-based kWh (per-direction, zachytí bidirectional flow) - ROUND(COALESCE(ai.actual_grid_import_wh, GREATEST(ai.actual_grid_power_w, 0)::NUMERIC / 4) / 1000, 4) + -- Wh-based kWh (per-direction; u čistého importu/exportu max čítač vs. odhad z P_grid) + ROUND( + ems.fn_audit_grid_import_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000, + 4 + ) AS import_kwh, - ROUND(COALESCE(ai.actual_grid_export_wh, ABS(LEAST(ai.actual_grid_power_w, 0))::NUMERIC / 4) / 1000, 4) + ROUND( + ems.fn_audit_grid_export_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000, + 4 + ) AS export_kwh, -- Směrové cashflow: kolik Kč za import ze sítě / kolik Kč za export do sítě ROUND( - COALESCE(ai.actual_grid_import_wh, GREATEST(ai.actual_grid_power_w, 0)::NUMERIC / 4) + ems.fn_audit_grid_import_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000.0 * COALESCE(ep.effective_buy_price_czk_kwh, 0), 4 ) AS grid_import_cashflow_czk, ROUND( - COALESCE(ai.actual_grid_export_wh, ABS(LEAST(ai.actual_grid_power_w, 0))::NUMERIC / 4) + ems.fn_audit_grid_export_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000.0 * COALESCE(ep.effective_sell_price_czk_kwh, 0), 4 ) AS grid_export_revenue_czk, -- Net cost (zpětná kompatibilita): import_cashflow - export_revenue ROUND( - COALESCE(ai.actual_grid_import_wh, GREATEST(ai.actual_grid_power_w, 0)::NUMERIC / 4) + ems.fn_audit_grid_import_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000.0 * COALESCE(ep.effective_buy_price_czk_kwh, 0) - - COALESCE(ai.actual_grid_export_wh, ABS(LEAST(ai.actual_grid_power_w, 0))::NUMERIC / 4) + - ems.fn_audit_grid_export_wh_for_economics( + ai.actual_grid_import_wh, ai.actual_grid_export_wh, ai.actual_grid_power_w + ) / 1000.0 * COALESCE(ep.effective_sell_price_czk_kwh, 0), 4 ) AS dynamic_cost_czk, ai.actual_cost_czk AS stored_cost_czk, @@ -65,9 +83,8 @@ LEFT JOIN ems.planning_interval pi COMMENT ON VIEW ems.vw_economics_interval IS 'Dynamické ekonomické vyhodnocení per 15min slot. -import/export kWh primárně z per-direction Wh sloupců audit_interval (Deye counter / per-minute split), -fallback na průměrný výkon pro zpětnou kompatibilitu se starými daty. -grid_import_cashflow_czk / grid_export_revenue_czk = směrové cashflow podle skutečného toku energie.'; +import/export kWh díky fn_audit_grid_*_wh_for_economics: primárně Wh z auditu (čítač Deye), u jednosměrného toku max s odhadem z průměrného grid_power_w (¼ h). +grid_import_cashflow_czk / grid_export_revenue_czk = směrové cashflow podle stejného Wh odhadu × efektivní cena.'; CREATE VIEW ems.vw_economics_daily AS SELECT diff --git a/docs/04-modules/energy-flows.md b/docs/04-modules/energy-flows.md index f3dd8f1..b3b5e7e 100644 --- a/docs/04-modules/energy-flows.md +++ b/docs/04-modules/energy-flows.md @@ -12,6 +12,8 @@ Funkce `ems.fn_fill_audit_interval` pro každý 15min slot: 2. Pro každou minutu aplikuje alokaci (pořadí): PV → spotřeba → nabíjení baterie → export; pak vybití baterie → spotřeba / export; síť → zbytek spotřeby a nabíjení. 3. Součet výkonů × 1/60 h = Wh za slot; výsledek v sloupcích `flow_*_wh` v `ems.audit_interval`. +**Import / export ze sítě (`actual_grid_import_wh` / `actual_grid_export_wh`):** nejprve se bere delta z Deye registrů `grid_import_total_wh` / `grid_export_total_wh` (pokud je k dispozici), jinak součet kladných resp. záporných dílů z `grid_power_w` po minutách. Okamžitě poté `fn_fill_audit_interval` sjednotí hodnoty funkcemi `ems.fn_audit_grid_import_wh_for_economics` / `fn_audit_grid_export_wh_for_economics`: při **jednosměrném** toku v slotu (typicky jen import nebo jen export podle uložených Wh) vezme **maximum** z čítače a z odhadu `max(0, P_grid) × ¼ h` resp. `|min(0, P_grid)| × ¼ h` z průměrného `grid_power_w` — u některých instalací lifetime čítač Deye **rostl pomaleji** než integrál z výkonu, takže bez této úpravy ekonomika a záložka **Ekonomika** podhodnocovaly kWh. Při **obousměrném** toku ve stejném 15min slotu (obě strany > 0 Wh) se uložený split z čítače/per-minute **nemění**. + Sloupce: `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`. Základní 6 Wh veličin (import/export, PV, baterie, load) zůstává ve Fázi 1; toky jsou nadstavba.