Branch 3: charge-slot-budget v R__063 + odstranit v58 pro BA81/KV1 + fixed evening push
Some checks failed
CI and deploy / migration-check (push) Failing after 25s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-06-06 22:32:48 +02:00
parent 09bca0a903
commit a7879f1141
7 changed files with 252 additions and 162 deletions

View File

@@ -36,7 +36,14 @@ returns table (
min_buy_before_cutoff_czk_kwh numeric,
pv_charge_wh_ahead numeric,
neg_buy_wh_ahead numeric,
grid_charge_suppressed_reason text
grid_charge_suppressed_reason text,
charge_target_wh numeric,
pre_window_wh numeric,
in_window_wh numeric,
charge_slot_wh numeric,
charge_cum_wh numeric,
charge_layer text,
charge_slot_reason text
)
language plpgsql
volatile
@@ -104,6 +111,10 @@ declare
v_cum_allowed numeric;
v_pv_ahead_total numeric;
v_target_deficit numeric;
v_charge_target_wh numeric;
v_pre_window_wh numeric := 0;
v_in_window_wh numeric := 0;
v_charge_reliability_factor numeric := 0.85;
r_unlock record;
begin
v_plan_day_prague := (p_from at time zone 'Europe/Prague')::date;
@@ -308,6 +319,7 @@ begin
);
end if;
v_discharge_target_wh := v_exportable * v_discharge_buf;
v_charge_target_wh := greatest(v_grid_target_wh, 0);
-- Referenční nákup: globální min (export brána) + per AM/PM pás (grid nabíjení).
select coalesce(min(wk.buy_price), 0)
@@ -334,7 +346,11 @@ begin
add column if not exists min_buy_before_cutoff numeric,
add column if not exists pv_charge_wh_ahead numeric,
add column if not exists neg_buy_wh_ahead numeric,
add column if not exists grid_charge_suppressed_reason text;
add column if not exists grid_charge_suppressed_reason text,
add column if not exists charge_slot_wh numeric default 0,
add column if not exists charge_cum_wh numeric,
add column if not exists charge_layer text,
add column if not exists charge_slot_reason text;
-- První výkupní okno **per kalendářní den** (Prague). Globální min přes dny by
-- zablokoval NT grid nabíjení (včerejší večerní peak → dnešní 0006 už „po okně“).
@@ -477,7 +493,12 @@ begin
exit when v_per_slot_charge_wh <= 0;
exit when v_grid_slots_am >= v_grid_charge_cap_am;
update _ems_plan_slot_wk wk
set allow_charge = true, allow_grid_charge = true
set allow_charge = true,
allow_grid_charge = true,
charge_layer = 'grid_am',
charge_slot_reason = 'grid_layer_b',
charge_slot_wh = v_per_slot_charge_wh,
charge_cum_wh = v_cum + v_per_slot_charge_wh
where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + v_per_slot_charge_wh;
v_grid_slots_am := v_grid_slots_am + 1;
@@ -525,7 +546,12 @@ begin
exit when v_per_slot_charge_wh <= 0;
exit when v_grid_slots_pm >= v_grid_charge_cap_pm;
update _ems_plan_slot_wk wk
set allow_charge = true, allow_grid_charge = true
set allow_charge = true,
allow_grid_charge = true,
charge_layer = 'grid_pm',
charge_slot_reason = 'grid_layer_b',
charge_slot_wh = v_per_slot_charge_wh,
charge_cum_wh = v_cum + v_per_slot_charge_wh
where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + v_per_slot_charge_wh;
v_grid_slots_pm := v_grid_slots_pm + 1;
@@ -534,7 +560,11 @@ begin
-- Spot: záporný buy → grid nabíjení ve všech slotech (maximální arbitráž), mimo AM/PM rozpočet.
update _ems_plan_slot_wk wk
set allow_charge = true, allow_grid_charge = true
set allow_charge = true,
allow_grid_charge = true,
charge_layer = coalesce(wk.charge_layer, 'buy_negative'),
charge_slot_reason = coalesce(wk.charge_slot_reason, 'buy_negative'),
charge_slot_wh = greatest(wk.charge_slot_wh, v_per_slot_charge_wh)
where wk.buy_price < 0;
-- Self-konzistentni filtr vrstvy B (spot): vyloucit drahe grid sloty, pokud PV / buy<0
@@ -697,7 +727,12 @@ begin
exit when v_per_slot_charge_wh <= 0;
exit when v_grid_slots_am >= v_grid_charge_cap_am;
update _ems_plan_slot_wk wk
set allow_charge = true, allow_grid_charge = true
set allow_charge = true,
allow_grid_charge = true,
charge_layer = 'grid_am',
charge_slot_reason = 'grid_layer_b',
charge_slot_wh = v_per_slot_charge_wh,
charge_cum_wh = v_cum + v_per_slot_charge_wh
where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + v_per_slot_charge_wh;
v_grid_slots_am := v_grid_slots_am + 1;
@@ -749,7 +784,12 @@ begin
exit when v_per_slot_charge_wh <= 0;
exit when v_grid_slots_pm >= v_grid_charge_cap_pm;
update _ems_plan_slot_wk wk
set allow_charge = true, allow_grid_charge = true
set allow_charge = true,
allow_grid_charge = true,
charge_layer = 'grid_pm',
charge_slot_reason = 'grid_layer_b',
charge_slot_wh = v_per_slot_charge_wh,
charge_cum_wh = v_cum + v_per_slot_charge_wh
where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + v_per_slot_charge_wh;
v_grid_slots_pm := v_grid_slots_pm + 1;
@@ -760,40 +800,53 @@ begin
-- A) PV-surplus: jen zbytek kapacity po grid vrstvě B
v_pv_layer_cap_wh := greatest(v_energy_to_fill - v_grid_filled_wh, 0);
-- Rezervace SoC pro sell<0 okno: pokud v zápor. výkup. slotech máme
-- očekávaný PV přebytek X Wh (po efektivitě), snížíme PV vrstvu A o X.
-- Důsledek: do okna nedorazíme „plní" (98 % SoC), zbude prostor přijmout PV
-- z neg-sell slotů místo exportu do mínusu / curtail pole A.
-- Sample neg-sell PV sloty (sell<0 a buy<0, kde sell<buy) jsou vyloučené
-- z hlavního A-loopu (filtr sell >= buy degrad), takže redukce je čistá.
declare
v_neg_window_pv_surplus_wh numeric := 0;
begin
select coalesce(sum(least(wk.pv_surplus_w::numeric, v_max_charge_w) * v_charge_eff * 0.25), 0)
into v_neg_window_pv_surplus_wh
from _ems_plan_slot_wk wk
where wk.sell_price < 0
and wk.pv_surplus_w > 0;
if v_neg_window_pv_surplus_wh > 0 then
v_pv_layer_cap_wh := greatest(v_pv_layer_cap_wh - v_neg_window_pv_surplus_wh, 0);
end if;
end;
-- Dodávka z forecastu v sell<0 okně snižuje potřebu nabíjení před oknem.
select coalesce(sum(least(wk.pv_surplus_w::numeric, v_max_charge_w) * v_charge_eff * 0.25), 0)
into v_in_window_wh
from _ems_plan_slot_wk wk
where wk.sell_price < 0
and wk.pv_surplus_w > 0;
if v_in_window_wh > 0 then
v_pv_layer_cap_wh := greatest(v_pv_layer_cap_wh - v_in_window_wh, 0);
v_pre_window_wh := greatest(
0,
v_charge_target_wh - v_in_window_wh * v_charge_reliability_factor
);
end if;
v_cum := 0;
for r_slot in
select wk.slot_ord, wk.pv_surplus_w
from _ems_plan_slot_wk wk
where wk.pv_surplus_w > 0
and wk.sell_price >= wk.buy_price - v_degrad_czk_kwh
-- Držet PV na večerní peak jen při kladném výkupu; při sell<0 (záporný výkup) vždy nabíjet z FVE.
and (
wk.sell_price < 0
v_purchase_pricing_mode = 'fixed'
or wk.sell_price >= wk.buy_price - v_degrad_czk_kwh
)
-- Spot: neukládat do bat při výrazně lepším sell později; fixed: řazení sell ASC (§ charge-slot-budget).
and (
v_purchase_pricing_mode = 'fixed'
or wk.sell_price < 0
or wk.sell_price >= wk.future_sell_lookahead - v_degrad_czk_kwh
)
order by wk.store_score desc nulls last, wk.slot_ord
order by
case when v_purchase_pricing_mode = 'fixed' then wk.sell_price end asc nulls last,
wk.store_score desc nulls last,
wk.slot_ord
loop
exit when v_cum >= v_pv_layer_cap_wh;
update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
update _ems_plan_slot_wk wk
set allow_charge = true,
charge_layer = coalesce(wk.charge_layer, 'pv_a'),
charge_slot_reason = coalesce(wk.charge_slot_reason, 'pv_layer_a'),
charge_slot_wh = greatest(
wk.charge_slot_wh,
least(r_slot.pv_surplus_w, v_max_charge_w) * v_charge_eff * 0.25
),
charge_cum_wh = v_cum
+ least(r_slot.pv_surplus_w, v_max_charge_w) * v_charge_eff * 0.25
where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + least(r_slot.pv_surplus_w, v_max_charge_w) * v_charge_eff * 0.25;
end loop;
end if;
@@ -1007,12 +1060,22 @@ begin
-- Záporný buy: vždy grid nabíjení (mimo rozpočet 6 slotů / PV vrstvu A).
update _ems_plan_slot_wk wk
set allow_charge = true, allow_grid_charge = true
set allow_charge = true,
allow_grid_charge = true,
charge_layer = coalesce(wk.charge_layer, 'buy_negative'),
charge_slot_reason = coalesce(wk.charge_slot_reason, 'buy_negative'),
charge_slot_wh = greatest(wk.charge_slot_wh, v_per_slot_charge_wh)
where wk.buy_price < 0;
-- Záporný výkup + PV přebytek: nabíjení z FVE (KV1/BA81 block_export), bez filtru future_sell.
update _ems_plan_slot_wk wk
set allow_charge = true
set allow_charge = true,
charge_layer = coalesce(wk.charge_layer, 'neg_window'),
charge_slot_reason = coalesce(wk.charge_slot_reason, 'neg_window_pv'),
charge_slot_wh = greatest(
wk.charge_slot_wh,
least(wk.pv_surplus_w::numeric, v_max_charge_w) * v_charge_eff * 0.25
)
where wk.sell_price < 0
and wk.pv_surplus_w > 0;
@@ -1021,6 +1084,9 @@ begin
update _ems_plan_slot_wk wk
set allow_charge = true,
allow_grid_charge = true,
charge_layer = coalesce(wk.charge_layer, 'neg_window'),
charge_slot_reason = coalesce(wk.charge_slot_reason, 'neg_window_grid_charge'),
charge_slot_wh = greatest(wk.charge_slot_wh, v_per_slot_charge_wh),
grid_charge_suppressed_reason = coalesce(
wk.grid_charge_suppressed_reason,
'neg_window_grid_charge'
@@ -1176,7 +1242,14 @@ begin
w.min_buy_before_cutoff as min_buy_before_cutoff_czk_kwh,
coalesce(w.pv_charge_wh_ahead, 0) as pv_charge_wh_ahead,
coalesce(w.neg_buy_wh_ahead, 0) as neg_buy_wh_ahead,
w.grid_charge_suppressed_reason
w.grid_charge_suppressed_reason,
v_charge_target_wh as charge_target_wh,
v_pre_window_wh as pre_window_wh,
v_in_window_wh as in_window_wh,
coalesce(w.charge_slot_wh, 0) as charge_slot_wh,
w.charge_cum_wh,
w.charge_layer,
w.charge_slot_reason
from _ems_plan_slot_wk w
cross join night_tot nt
)
@@ -1204,7 +1277,14 @@ begin
e.min_buy_before_cutoff_czk_kwh,
e.pv_charge_wh_ahead,
e.neg_buy_wh_ahead,
e.grid_charge_suppressed_reason
e.grid_charge_suppressed_reason,
e.charge_target_wh,
e.pre_window_wh,
e.in_window_wh,
e.charge_slot_wh,
e.charge_cum_wh,
e.charge_layer,
e.charge_slot_reason
from enriched e
order by e.slot_ord;
end;
@@ -1212,11 +1292,13 @@ $fn$;
comment on function ems.fn_load_planning_slots_full is
'15min sloty s cenami, forecastem, baseline a maskami proti mikro-cyklu (charge/discharge-export). '
'Charge mask A: PV-surplus dle store_score DESC (future_sellsellmax(0,buysell)); zbytek → PV export. '
'Charge mask B: spot, nejlevnější buy v AM/PM do Wh rozpočtu (priorita den plánu, před exportním oknem). '
'Charge-slot budget: charge_target_wh, pre_window_wh (deficit forecast v sell<0), in_window_wh; '
'debug charge_layer / charge_slot_reason / charge_cum_wh. '
'Charge mask A: spot = store_score DESC; fixed = sell ASC + Wh kumulace pv_surplus. '
'Charge mask B: spot nejlevnější buy v AM/PM; fixed nejnižší sell v AM/PM do Wh rozpočtu. '
'ref_buy = min(buy) horizontu. Discharge-export: nejdražší sell kde sell>ref_buy+degrad (spot). '
'Strop SoC pro výpočet energie k dobití: coalesce(planner_max_soc_percent, max_soc_percent). '
'Denní safety vstupy: night_baseload_* (20:0006:00 Europe/Prague), safety_soc_target_wh (619), '
'lookahead max buy/sell pro měkké LP penalizace. '
'charge_acquisition_buy_czk_kwh: vážený buy v allow_charge slotech před charge_acquisition_cutoff_at. '
'charge_acquisition_buy_czk_kwh: vážený buy v allow_grid_charge slotech před charge_acquisition_cutoff_at. '
'Grid maska B běží před PV vrstvou A; AM/PM rozpočet Wh 50/50; cap slotů z rozpočtu / per_slot_charge_wh.';