oprava import/export kwh
Some checks failed
CI and deploy / migration-check (push) Failing after 11s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-01 14:58:29 +02:00
parent 91ee8a6adf
commit ed88ef8910
8 changed files with 158 additions and 40 deletions

View File

@@ -3,6 +3,74 @@
-- EMS Platform funkce pro výpočet efektivní ceny per site -- EMS Platform funkce pro výpočet efektivní ceny per site
-- Repeatable migration nasazuje se při každé změně -- 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( CREATE OR REPLACE FUNCTION ems.fn_effective_buy_price(
p_site_id INT, p_site_id INT,

View File

@@ -14,13 +14,29 @@ as $fn$
end, end,
'grid_import_kwh', 'grid_import_kwh',
case case
when ai.actual_grid_import_wh is null then null when ai.actual_grid_import_wh is null
else round(ai.actual_grid_import_wh::numeric / 1000, 4) 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, end,
'grid_export_kwh', 'grid_export_kwh',
case case
when ai.actual_grid_export_wh is null then null when ai.actual_grid_import_wh is null
else round(ai.actual_grid_export_wh::numeric / 1000, 4) 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, end,
'batt_charge_kwh', 'batt_charge_kwh',
case case

View File

@@ -45,6 +45,8 @@ DECLARE
v_counter_export_last BIGINT; v_counter_export_last BIGINT;
v_delta_import NUMERIC; v_delta_import NUMERIC;
v_delta_export 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) -- 7 směrových toků (prioritní alokace per minuta; součet W/60 = Wh)
r_flow RECORD; r_flow RECORD;
@@ -141,6 +143,13 @@ BEGIN
v_grid_export_wh := v_delta_export; v_grid_export_wh := v_delta_export;
END IF; 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) -- Agregovat EV nabíječky (součet průměrů po charger_id)
SELECT COALESCE(SUM(avg_power), 0)::INT SELECT COALESCE(SUM(avg_power), 0)::INT
INTO v_sum_ev_power_w INTO v_sum_ev_power_w
@@ -371,8 +380,9 @@ $$;
COMMENT ON FUNCTION ems.fn_fill_audit_interval(INT, TIMESTAMPTZ) IS 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. '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. 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); 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. 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). 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). actual_cost_czk = per-direction (import_wh × buy - export_wh × sell).
Zelený bonus: součet přes pole s green_bonus_czk_kwh. Zelený bonus: součet přes pole s green_bonus_czk_kwh.

View File

@@ -21,8 +21,10 @@ as $fn$
'day', (date_trunc('day', ai.interval_start at time zone 'Europe/Prague'))::date, 'day', (date_trunc('day', ai.interval_start at time zone 'Europe/Prague'))::date,
'interval_count', count(*)::int, 'interval_count', count(*)::int,
'pv_production_kwh', round(sum(coalesce(ai.actual_pv_production_wh, 0)) / 1000, 3), '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_import_kwh', round(sum(ems.fn_audit_grid_import_wh_for_economics(
'grid_export_kwh', round(sum(coalesce(ai.actual_grid_export_wh, 0)) / 1000, 3), 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_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), '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), 'load_kwh', round(sum(coalesce(ai.actual_load_consumption_wh, 0)) / 1000, 3),
@@ -36,7 +38,9 @@ as $fn$
'grid_import_cashflow_czk', 'grid_import_cashflow_czk',
round( round(
sum( 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) * coalesce(ep.effective_buy_price_czk_kwh, 0)
), ),
2 2
@@ -44,7 +48,9 @@ as $fn$
'grid_export_revenue_czk', 'grid_export_revenue_czk',
round( round(
sum( 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) * coalesce(ep.effective_sell_price_czk_kwh, 0)
), ),
2 2

View File

@@ -59,7 +59,13 @@ as $fn$
coalesce(dt.vat_rate, 0.21) as vat_rate, coalesce(dt.vat_rate, 0.21) as vat_rate,
mip.buy_raw_price_czk_kwh as buy_spot, mip.buy_raw_price_czk_kwh as buy_spot,
mip.sell_raw_price_czk_kwh as sell_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 from ems.audit_interval ai
left join lateral ( left join lateral (
select smc_inner.* select smc_inner.*
@@ -202,29 +208,19 @@ as $fn$
select select
s5.site_id, s5.site_id,
s5.day_local, 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( round(
coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000, s5.est_grid_import_wh / 1000.0 * coalesce(s5.buy_p, 0),
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),
4 4
) as grid_import_cashflow_czk, ) as grid_import_cashflow_czk,
round( round(
coalesce(s5.actual_grid_export_wh, abs(least(s5.actual_grid_power_w, 0))::numeric / 4) / 1000.0 s5.est_grid_export_wh / 1000.0 * coalesce(s5.sell_p, 0),
* coalesce(s5.sell_p, 0),
4 4
) as grid_export_revenue_czk, ) as grid_export_revenue_czk,
round( round(
coalesce(s5.actual_grid_import_wh, greatest(s5.actual_grid_power_w, 0)::numeric / 4) / 1000.0 s5.est_grid_import_wh / 1000.0 * coalesce(s5.buy_p, 0)
* coalesce(s5.buy_p, 0) - s5.est_grid_export_wh / 1000.0 * coalesce(s5.sell_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),
4 4
) as dynamic_cost_czk, ) as dynamic_cost_czk,
s5.green_bonus_czk, s5.green_bonus_czk,

View File

@@ -11,8 +11,10 @@ SELECT
site_id, site_id,
(date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague'))::date AS day_local, (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_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(ems.fn_audit_grid_import_wh_for_economics(
ROUND(SUM(COALESCE(actual_grid_export_wh, 0)) / 1000, 3) AS grid_export_kwh, 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_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_batt_discharge_wh, 0)) / 1000, 3) AS batt_discharge_kwh,
ROUND(SUM(COALESCE(actual_load_consumption_wh, 0)) / 1000, 3) AS load_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; (date_trunc('day', interval_start AT TIME ZONE 'Europe/Prague'))::date;
COMMENT ON VIEW ems.vw_energy_flows_daily IS 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.';

View File

@@ -15,25 +15,43 @@ CREATE VIEW ems.vw_economics_interval AS
SELECT SELECT
ai.site_id, ai.site_id,
ai.interval_start, ai.interval_start,
-- Wh-based kWh (per-direction, zachytí bidirectional flow) -- Wh-based kWh (per-direction; u čistého importu/exportu max čítač vs. odhad z P_grid)
ROUND(COALESCE(ai.actual_grid_import_wh, GREATEST(ai.actual_grid_power_w, 0)::NUMERIC / 4) / 1000, 4) 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, 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, AS export_kwh,
-- Směrové cashflow: kolik Kč za import ze sítě / kolik Kč za export do sítě -- Směrové cashflow: kolik Kč za import ze sítě / kolik Kč za export do sítě
ROUND( 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 / 1000.0 * COALESCE(ep.effective_buy_price_czk_kwh, 0), 4
) AS grid_import_cashflow_czk, ) AS grid_import_cashflow_czk,
ROUND( 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 / 1000.0 * COALESCE(ep.effective_sell_price_czk_kwh, 0), 4
) AS grid_export_revenue_czk, ) AS grid_export_revenue_czk,
-- Net cost (zpětná kompatibilita): import_cashflow - export_revenue -- Net cost (zpětná kompatibilita): import_cashflow - export_revenue
ROUND( 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) / 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 / 1000.0 * COALESCE(ep.effective_sell_price_czk_kwh, 0), 4
) AS dynamic_cost_czk, ) AS dynamic_cost_czk,
ai.actual_cost_czk AS stored_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 COMMENT ON VIEW ems.vw_economics_interval IS
'Dynamické ekonomické vyhodnocení per 15min slot. 'Dynamické ekonomické vyhodnocení per 15min slot.
import/export kWh primárně z per-direction Wh sloupců audit_interval (Deye counter / per-minute split), 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).
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 stejného Wh odhadu × efektivní cena.';
grid_import_cashflow_czk / grid_export_revenue_czk = směrové cashflow podle skutečného toku energie.';
CREATE VIEW ems.vw_economics_daily AS CREATE VIEW ems.vw_economics_daily AS
SELECT SELECT

View File

@@ -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í. 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`. 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`. 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. Základní 6 Wh veličin (import/export, PV, baterie, load) zůstává ve Fázi 1; toky jsou nadstavba.