oprava KV 1
This commit is contained in:
@@ -71,7 +71,7 @@ NEG_BUY_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 100.0
|
||||
PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0
|
||||
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
|
||||
PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0
|
||||
PLANNER_BUILD_TAG = "2026-05-28-neg-prep-window-v36d"
|
||||
PLANNER_BUILD_TAG = "2026-05-28-neg-prep-window-v36e"
|
||||
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
|
||||
NEG_SELL_POST_DETACH_BCPV_DISCOURAGE_CZK_KWH = 250.0
|
||||
# Večer před neg dnem: výboj do sítě (měkký shortfall na ge_bat).
|
||||
@@ -2047,6 +2047,20 @@ def solve_dispatch(
|
||||
slots,
|
||||
degrad_czk_kwh=float(degradation_cost_effective),
|
||||
)
|
||||
purchase_fixed_pre = _purchase_pricing_fixed(grid)
|
||||
block_export_neg_sell_pre = bool(
|
||||
getattr(grid, "block_export_on_negative_sell", False)
|
||||
)
|
||||
if purchase_fixed_pre and block_export_neg_sell_pre:
|
||||
evening_peak_export_ts = sorted(
|
||||
set(evening_peak_export_ts)
|
||||
| {
|
||||
t
|
||||
for t, st in enumerate(slots)
|
||||
if _in_night_battery_export_window(st)
|
||||
and float(st.sell_price) > 0.0
|
||||
}
|
||||
)
|
||||
non_negative_buys_pre = [
|
||||
float(s.buy_price) for s in slots if float(s.buy_price) >= 0.0
|
||||
]
|
||||
@@ -2056,7 +2070,6 @@ def solve_dispatch(
|
||||
else min(float(s.buy_price) for s in slots)
|
||||
)
|
||||
min_spread_pre = float(degradation_cost_effective)
|
||||
purchase_fixed_pre = _purchase_pricing_fixed(grid)
|
||||
fixed_tariff_like_pre = purchase_fixed_pre or _horizon_fixed_tariff_like(slots)
|
||||
neg_sell_phases_en = (
|
||||
om == "AUTO"
|
||||
@@ -2153,6 +2166,14 @@ def solve_dispatch(
|
||||
fixed_tariff=fixed_tariff_like_pre,
|
||||
):
|
||||
profitable_export_ts_pre.add(_t)
|
||||
elif (
|
||||
purchase_fixed_pre
|
||||
and block_export_neg_sell_pre
|
||||
and _t in evening_peak_export_ts
|
||||
and float(slots[_t].sell_price) > 0.0
|
||||
):
|
||||
# KV1: večerní sell může být < fixní buy; peak sloty stejně vývoz bat.
|
||||
profitable_export_ts_pre.add(_t)
|
||||
evening_push_ts: set[int] = set()
|
||||
evening_early_export_penalty_ts: set[int] = set()
|
||||
if om == "AUTO":
|
||||
@@ -3287,11 +3308,18 @@ def solve_dispatch(
|
||||
or t < first_neg_buy_idx
|
||||
)
|
||||
) or (
|
||||
# Spot (home-01, KV1): při sell>=0 neblokovat ge_pv — solver export vs bc_pv;
|
||||
# šetření na večerní peak = ge_bat, ne curtail FVE (pv_store jen sell<0 / fixed).
|
||||
# Spot: při sell>=0 neblokovat ge_pv (export vs bc_pv; večerní peak = ge_bat).
|
||||
not purchase_fixed_pre
|
||||
and sell_t >= 0
|
||||
and pv_surplus_w > 500
|
||||
) or (
|
||||
# KV1 (fixed + block_export, jen PV A): bez pole B neplatí fixed_pv_b_export_cap;
|
||||
# jinak ge_pv==0 → plný curtail při plné baterii místo prodeje do site.
|
||||
purchase_fixed_pre
|
||||
and bool(getattr(grid, "block_export_on_negative_sell", False))
|
||||
and float(s.pv_b_forecast_w) <= 0.0
|
||||
and sell_t >= 0.0
|
||||
and pv_surplus_w > 500
|
||||
)
|
||||
# BA81: export pole B jen při kladném sell (po sell<0 jinak ge==0 výše).
|
||||
fixed_pv_b_export_cap = (
|
||||
|
||||
@@ -3190,20 +3190,73 @@ class PreNegativeSellExportTests(unittest.TestCase):
|
||||
max_import_power_w=17_000,
|
||||
max_export_power_w=8000,
|
||||
block_export_on_negative_sell=True,
|
||||
purchase_pricing_mode="fixed",
|
||||
)
|
||||
vehicles = [
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
]
|
||||
soc0 = 0.85 * battery.soc_max_wh
|
||||
results, _, _ = solve_dispatch(
|
||||
results, _, snap = solve_dispatch(
|
||||
slots, battery, hp, grid, [None, None], vehicles, soc0, 50.0, operating_mode="AUTO"
|
||||
)
|
||||
self.assertLess(results[0].grid_setpoint_w, -500, "ráno: přebytek FVE do sítě před sell<0")
|
||||
self.assertLess(results[0].pv_a_curtailed_w, 500, "fixed KV1: ne plný curtail při kladném sell")
|
||||
neg = results[8]
|
||||
self.assertGreater(neg.battery_setpoint_w, 500, "záporný sell: PV do baterie")
|
||||
self.assertEqual(neg.export_mode, "NONE")
|
||||
|
||||
def test_kv1_evening_battery_push_when_sell_below_fixed_buy(self) -> None:
|
||||
"""KV1: večerní sell < fixní buy — přesto vývoz bat (ne jen jeden peak slot)."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 5, 26, 17, 0, tzinfo=prague)
|
||||
sells = [1.9, 3.0, 3.7, 2.0, 2.8, 3.3, 4.0, 2.9, 3.5, 4.4, 6.57, 5.4, 5.5, 5.1, 5.2, 4.3]
|
||||
slots: list[PlanningSlot] = []
|
||||
for i, sell in enumerate(sells):
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=(base + timedelta(minutes=15 * i)).astimezone(timezone.utc),
|
||||
buy_price=6.35,
|
||||
sell_price=sell,
|
||||
pv_a_forecast_w=800 if sell < 4 else 200,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_charge=False,
|
||||
allow_discharge_export=sell > 0,
|
||||
charge_acquisition_buy_czk_kwh=6.35,
|
||||
future_sell_opportunity_czk_kwh=6.57,
|
||||
)
|
||||
)
|
||||
battery = _battery(uc_wh=12_500.0, min_pct=10.0, arb_pct=30.0)
|
||||
battery.max_discharge_power_w = 6250
|
||||
battery.discharge_slot_buffer = 1.5
|
||||
hp = SimpleNamespace(rated_heating_power_w=0, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0)
|
||||
grid = SimpleNamespace(
|
||||
max_import_power_w=17_000,
|
||||
max_export_power_w=8000,
|
||||
block_export_on_negative_sell=True,
|
||||
purchase_pricing_mode="fixed",
|
||||
)
|
||||
vehicles = [
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
]
|
||||
res, _, _ = solve_dispatch(
|
||||
slots,
|
||||
battery,
|
||||
hp,
|
||||
grid,
|
||||
[None, None],
|
||||
vehicles,
|
||||
0.95 * battery.soc_max_wh,
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertLess(res[10].grid_setpoint_w, -500, "20:15 sell<buy: vývoz bat, ne jen peak 19:45")
|
||||
self.assertLess(res[0].pv_a_curtailed_w, 500, "17:15: FVE do sítě, ne curtail")
|
||||
|
||||
|
||||
class Home01PvStoreValueTests(unittest.TestCase):
|
||||
"""FVE: spot sell<0 → nabít/vent B; sell>=0 → LP volí export vs bc (ne tvrdý curtail)."""
|
||||
@@ -4195,7 +4248,7 @@ class NegSellPrepWindowV36Tests(unittest.TestCase):
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-neg-prep-window-v36d")
|
||||
self.assertEqual(snap.get("planner_build_tag"), "2026-05-28-neg-prep-window-v36e")
|
||||
anchors = snap["inputs"].get("neg_evening_reserve_soc_anchors") or []
|
||||
self.assertGreaterEqual(len(anchors), 1)
|
||||
anchor_iso = anchors[-1]["slot"]
|
||||
|
||||
@@ -71,6 +71,7 @@ declare
|
||||
v_ref_buy_am_czk_kwh numeric;
|
||||
v_ref_buy_pm_czk_kwh numeric;
|
||||
v_purchase_pricing_mode text;
|
||||
v_block_export_neg_sell boolean;
|
||||
v_lookahead_slots int := 4;
|
||||
v_grid_charge_cap_am int;
|
||||
v_grid_charge_cap_pm int;
|
||||
@@ -281,6 +282,13 @@ begin
|
||||
|
||||
v_purchase_pricing_mode := coalesce(v_purchase_pricing_mode, 'spot');
|
||||
|
||||
select coalesce(sgc.block_export_on_negative_sell, false)
|
||||
into v_block_export_neg_sell
|
||||
from ems.site_grid_connection sgc
|
||||
where sgc.site_id = p_site_id;
|
||||
|
||||
v_block_export_neg_sell := coalesce(v_block_export_neg_sell, false);
|
||||
|
||||
v_per_slot_charge_wh := v_max_charge_w * v_charge_eff * 0.25;
|
||||
v_per_slot_discharge_wh := v_max_discharge_w * v_discharge_eff * 0.25;
|
||||
v_energy_to_fill := v_soc_max_wh - p_current_soc_wh;
|
||||
@@ -853,15 +861,14 @@ begin
|
||||
where (wk.interval_start at time zone 'Europe/Prague')::date = r_slot.plan_date
|
||||
and extract(hour from wk.interval_start at time zone 'Europe/Prague')
|
||||
>= v_evening_peak_start_hour
|
||||
and wk.sell_price >= r_slot.evening_peak_sell - v_degrad_czk_kwh
|
||||
and (
|
||||
case
|
||||
when v_purchase_pricing_mode = 'fixed' then
|
||||
-- Večerní peak: vyvést i když sell < fixní buy (KV1), pokud je to denní maximum výkupu.
|
||||
true
|
||||
when v_purchase_pricing_mode = 'fixed'
|
||||
and v_block_export_neg_sell then
|
||||
-- KV1: fixní buy ~6,3; večerní sell často < buy — vývoz ve všech kladných sell slotech ≥17h.
|
||||
wk.sell_price > 0
|
||||
else
|
||||
-- Spot (home-01): denní večerní maximum výkupu; sell často < buy v tomže slotu.
|
||||
true
|
||||
wk.sell_price >= r_slot.evening_peak_sell - v_degrad_czk_kwh
|
||||
end
|
||||
);
|
||||
end if;
|
||||
|
||||
@@ -11,6 +11,16 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
|
||||
**Rozhodnutí home-01** (souhrn v [`docs/06-open-questions.md`](06-open-questions.md)): rampa/**T** odvozené z PV B (bez fixních 80 % v LP); TČ ne v pre-neg exportu; bazén min 4 h/den + Shelly; spirála Loxone; **workshop UI flex zátěží před v37** (§ 9.1 strategie).
|
||||
|
||||
## 2026-05-28 — KV1 fixed + block_export (v36e)
|
||||
|
||||
**Kód:** `planning_engine.py` tag `2026-05-28-neg-prep-window-v36e`; `R__063_fn_load_planning_slots_full.sql`.
|
||||
|
||||
**Problém:** KV1 (fixní buy ~6,35, jen PV A, `block_export_on_negative_sell`) — od v34/v36 logiky pro spot/home-01: ráno **curtail** místo exportu do site; večer jen **jeden** discharge slot (sell peak 6,57 vs buy 6,35). BA81 má pole B (`fixed_pv_b_export_cap`) a nižší buy → chová se správně.
|
||||
|
||||
**Změna:** `skip_pv_store_block` pro fixed+block_export bez PV B při `sell≥0`; večerní `evening_peak_export_ts` + profitable export pro všechny kladné sell sloty v nočním okně; SQL maska `allow_discharge_export` stejně pro KV1 večer.
|
||||
|
||||
**Ověření:** `PreNegativeSellExportTests` (s `purchase_pricing_mode=fixed`); po deployi KV1 plán: odpoledne `PV_SURPLUS` / export, večer více `BATTERY_SELL` slotů.
|
||||
|
||||
## 2026-05-28 — Přípravné okno neg dne (v36 / v36b / v36d)
|
||||
|
||||
**Kód:** `backend/services/planning_engine.py` — tag `2026-05-28-neg-prep-window-v36d`.
|
||||
|
||||
Reference in New Issue
Block a user