Branch 3: charge-slot-budget v R__063 + odstranit v58 pro BA81/KV1 + fixed evening push
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-06-06-future-neg-buy-evening-export-v64"
|
||||
PLANNER_BUILD_TAG = "2026-06-06-charge-slot-budget-v1"
|
||||
SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||
"strict",
|
||||
"relaxed_expensive_import",
|
||||
@@ -82,8 +82,6 @@ SOLVER_RELAX_STEPS: tuple[str, ...] = (
|
||||
)
|
||||
# Ranní slabá FVE: neaplikovat pv_store ge_pv=0 (jinak curtail při sell < večerní peak).
|
||||
DAWN_LOW_PV_NO_CURTAIL_W = 1500
|
||||
# BA81/KV1: PV→bat jen v těsné blízkosti nejnižšího sell v horizontu (≈ poledne), ne při ~3 Kč ráno.
|
||||
FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH = 0.20
|
||||
# Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu).
|
||||
NIGHT_SELF_CONSUME_IMPORT_SURCHARGE_CZK_KWH = 4.0
|
||||
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
|
||||
@@ -456,6 +454,13 @@ class PlanningSlot:
|
||||
pv_charge_wh_ahead: float | None = None
|
||||
neg_buy_wh_ahead: float | None = None
|
||||
grid_charge_suppressed_reason: str | None = None
|
||||
charge_target_wh: float | None = None
|
||||
pre_window_wh: float | None = None
|
||||
in_window_wh: float | None = None
|
||||
charge_slot_wh: float | None = None
|
||||
charge_cum_wh: float | None = None
|
||||
charge_layer: str | None = None
|
||||
charge_slot_reason: str | None = None
|
||||
#: Pomocny atribut pro green_bonus v planning_interval (Kc/slot); lite default 0.
|
||||
green_bonus_czk_per_slot: float = 0.0
|
||||
|
||||
@@ -1836,12 +1841,13 @@ def _slot_evening_push_profitable(
|
||||
slots: list[PlanningSlot] | None = None,
|
||||
first_neg_sell_idx: int | None = None,
|
||||
kv1_evening_push: bool = False,
|
||||
purchase_fixed: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Push večerní špičky.
|
||||
Spot / obecně: sell > acq+spread (zásoba z levného nabití).
|
||||
KV1 (fixed + block_export, v52): sell ≥ max sell v pásmu 5–11 před 1. sell<0 − spread
|
||||
— neprodávat večer levněji než plánované ranní maximum; bez neg dne v horizontu sell ≥ 1 Kč.
|
||||
Spot: sell > acq+spread (zásoba z levného nabití).
|
||||
Fixní tarif (BA81/KV1): sell > buy+spread (stejně jako R__063 discharge maska).
|
||||
KV1 (fixed + block_export, v52): navíc sell ≥ max sell v pásmu 5–11 před 1. sell<0 − spread.
|
||||
"""
|
||||
sell_t = float(slot.sell_price)
|
||||
if kv1_evening_push:
|
||||
@@ -1852,6 +1858,10 @@ def _slot_evening_push_profitable(
|
||||
if zone_peak is not None:
|
||||
return sell_t >= float(zone_peak) - float(min_spread)
|
||||
return True
|
||||
if purchase_fixed:
|
||||
buy_t = float(slot.buy_price)
|
||||
if buy_t >= 0.0:
|
||||
return sell_t > buy_t + float(min_spread)
|
||||
return sell_t > float(charge_acquisition_czk_kwh) + float(min_spread)
|
||||
|
||||
|
||||
@@ -1864,6 +1874,7 @@ def _evening_push_segment_candidates(
|
||||
discharge_export_ok: set[int] | None = None,
|
||||
first_neg_sell_idx: int | None = None,
|
||||
kv1_evening_push: bool = False,
|
||||
purchase_fixed: bool = False,
|
||||
) -> list[int]:
|
||||
"""Profitable sloty v nočním úseku — výběr pořadí a strop dělá rozpočet Wh (sell desc)."""
|
||||
if not seg:
|
||||
@@ -1881,6 +1892,7 @@ def _evening_push_segment_candidates(
|
||||
slots=slots,
|
||||
first_neg_sell_idx=first_neg_sell_idx,
|
||||
kv1_evening_push=kv1_evening_push,
|
||||
purchase_fixed=purchase_fixed,
|
||||
):
|
||||
continue
|
||||
out.append(t)
|
||||
@@ -2013,6 +2025,7 @@ def _evening_battery_export_push_indices(
|
||||
evening_start_hour: int = 17,
|
||||
first_neg_sell_idx: int | None = None,
|
||||
kv1_evening_push: bool = False,
|
||||
purchase_fixed: bool = False,
|
||||
) -> list[int]:
|
||||
"""
|
||||
Večerní push (≥17h): plný ge_bat v nejdražších slotách (sell desc), rozpočet Wh
|
||||
@@ -2049,6 +2062,7 @@ def _evening_battery_export_push_indices(
|
||||
discharge_export_ok=discharge_export_ok,
|
||||
first_neg_sell_idx=first_neg_sell_idx,
|
||||
kv1_evening_push=kv1_evening_push,
|
||||
purchase_fixed=purchase_fixed,
|
||||
):
|
||||
if t not in seen:
|
||||
seen.add(t)
|
||||
@@ -2078,6 +2092,7 @@ def _evening_push_peak_fallback_indices(
|
||||
discharge_export_ok: set[int] | None,
|
||||
first_neg_sell_idx: int | None,
|
||||
kv1_evening_push: bool,
|
||||
purchase_fixed: bool = False,
|
||||
) -> set[int]:
|
||||
"""Alespoň jeden večerní peak slot (sell desc), když rozpočet Wh nevybral žádný push."""
|
||||
best_t: int | None = None
|
||||
@@ -2094,6 +2109,7 @@ def _evening_push_peak_fallback_indices(
|
||||
slots=slots,
|
||||
first_neg_sell_idx=first_neg_sell_idx,
|
||||
kv1_evening_push=kv1_evening_push,
|
||||
purchase_fixed=purchase_fixed,
|
||||
):
|
||||
continue
|
||||
sell_t = float(s.sell_price)
|
||||
@@ -2944,6 +2960,7 @@ def solve_dispatch(
|
||||
discharge_export_ok=discharge_export_slots,
|
||||
first_neg_sell_idx=first_neg_sell_idx,
|
||||
kv1_evening_push=kv1_evening_push_pre,
|
||||
purchase_fixed=purchase_fixed_pre,
|
||||
)
|
||||
)
|
||||
push_override_raw = _evening_push_override_for_solve(
|
||||
@@ -2977,6 +2994,7 @@ def solve_dispatch(
|
||||
discharge_export_ok=discharge_export_slots,
|
||||
first_neg_sell_idx=first_neg_sell_idx,
|
||||
kv1_evening_push=kv1_evening_push_pre,
|
||||
purchase_fixed=purchase_fixed_pre,
|
||||
)
|
||||
# Tvrdý ge_bat push vypnout jen při neg_sell fallback (ne při prep relax — v64).
|
||||
evening_push_hard_suppressed = bool(neg_sell_phases_fallback)
|
||||
@@ -4216,34 +4234,8 @@ def solve_dispatch(
|
||||
or fixed_pre_neg_pv_export
|
||||
or fixed_block_pv_surplus_export
|
||||
or fixed_mi_low_pv_surplus_export
|
||||
or (
|
||||
purchase_fixed_pre
|
||||
and fixed_horizon_min_sell_pre is not None
|
||||
and sell_t >= 0.0
|
||||
and pv_surplus_w > NIGHT_EXPORT_PV_SUNRISE_SURPLUS_W
|
||||
and sell_t
|
||||
> fixed_horizon_min_sell_pre
|
||||
+ FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH
|
||||
)
|
||||
)
|
||||
fixed_sell_above_horizon_min = (
|
||||
purchase_fixed_pre
|
||||
and fixed_horizon_min_sell_pre is not None
|
||||
and sell_t >= 0.0
|
||||
and sell_t
|
||||
> fixed_horizon_min_sell_pre + FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH
|
||||
)
|
||||
fixed_high_sell_no_pv_charge = (
|
||||
fixed_sell_above_horizon_min
|
||||
and pv_surplus_w > NIGHT_EXPORT_PV_SUNRISE_SURPLUS_W
|
||||
)
|
||||
fixed_grid_charge_unprofitable = (
|
||||
purchase_fixed_pre
|
||||
and buy_t >= 0.0
|
||||
and fixed_sell_above_horizon_min
|
||||
)
|
||||
# Spot: mezi-slotová arbitráž — sell<buy ve slotu je normální (marže). Grid→bat jen
|
||||
# když buy v tomto slotu odpovídá levnému nákupu (≤ charge_acquisition), ne 19:00 za 5,5 při acq 3,25.
|
||||
# Spot: mezi-slotová arbitráž — grid→bat jen když buy ≤ charge_acquisition (v61).
|
||||
spot_grid_charge_not_cheap_buy = (
|
||||
not purchase_fixed_pre
|
||||
and buy_t >= 0.0
|
||||
@@ -4257,10 +4249,8 @@ def solve_dispatch(
|
||||
and not fixed_pre_neg_pv_export
|
||||
and int(s.pv_a_forecast_w) >= DAWN_LOW_PV_NO_CURTAIL_W
|
||||
)
|
||||
if fixed_grid_charge_unprofitable or spot_grid_charge_not_cheap_buy:
|
||||
if spot_grid_charge_not_cheap_buy:
|
||||
prob += bc_gi[t] == 0
|
||||
if fixed_high_sell_no_pv_charge:
|
||||
prob += bc_pv[t] == 0
|
||||
if (
|
||||
purchase_fixed_pre
|
||||
and t in evening_push_ts
|
||||
@@ -4773,6 +4763,25 @@ def solve_dispatch(
|
||||
solver_snapshot: dict[str, Any] = {
|
||||
"version": 1,
|
||||
"planner_build_tag": PLANNER_BUILD_TAG,
|
||||
"charge_slot_budget": {
|
||||
"charge_target_wh": (
|
||||
float(slots[0].charge_target_wh)
|
||||
if slots[0].charge_target_wh is not None
|
||||
else None
|
||||
),
|
||||
"pre_window_wh": (
|
||||
float(slots[0].pre_window_wh)
|
||||
if slots[0].pre_window_wh is not None
|
||||
else None
|
||||
),
|
||||
"in_window_wh": (
|
||||
float(slots[0].in_window_wh)
|
||||
if slots[0].in_window_wh is not None
|
||||
else None
|
||||
),
|
||||
"reliability_factor": 0.85,
|
||||
"planner_build_tag": PLANNER_BUILD_TAG,
|
||||
},
|
||||
"inputs": {
|
||||
"current_soc_wh": float(current_soc_wh),
|
||||
"observed_soc_wh": float(observed_soc_wh),
|
||||
@@ -4897,9 +4906,7 @@ def solve_dispatch(
|
||||
and not push_override_eff
|
||||
),
|
||||
"fixed_horizon_min_sell_czk_kwh": fixed_horizon_min_sell_pre,
|
||||
"fixed_pv_charge_near_min_sell_margin_czk_kwh": (
|
||||
FIXED_PV_CHARGE_ONLY_NEAR_MIN_SELL_CZK_KWH if purchase_fixed_pre else None
|
||||
),
|
||||
"fixed_evening_push_sell_above_buy": bool(purchase_fixed_pre),
|
||||
"charge_commitment_ignored_on_relaxed": bool(
|
||||
commitment_for_solve is None and charge_commitment_prev_w is not None
|
||||
),
|
||||
@@ -5628,7 +5635,9 @@ async def _load_slots(
|
||||
is_daytime_pv_surplus_slot,
|
||||
charge_acquisition_buy_czk_kwh, charge_acquisition_cutoff_at,
|
||||
min_buy_before_cutoff_czk_kwh, pv_charge_wh_ahead, neg_buy_wh_ahead,
|
||||
grid_charge_suppressed_reason
|
||||
grid_charge_suppressed_reason,
|
||||
charge_target_wh, pre_window_wh, in_window_wh,
|
||||
charge_slot_wh, charge_cum_wh, charge_layer, charge_slot_reason
|
||||
from ems.fn_load_planning_slots_full(
|
||||
$1::int, $2::timestamptz, $3::timestamptz, $4::numeric
|
||||
)
|
||||
@@ -5672,6 +5681,13 @@ async def _load_slots(
|
||||
pv_charge_wh_ahead=_slot_float_nullable(d, "pv_charge_wh_ahead"),
|
||||
neg_buy_wh_ahead=_slot_float_nullable(d, "neg_buy_wh_ahead"),
|
||||
grid_charge_suppressed_reason=d.get("grid_charge_suppressed_reason"),
|
||||
charge_target_wh=_slot_float_nullable(d, "charge_target_wh"),
|
||||
pre_window_wh=_slot_float_nullable(d, "pre_window_wh"),
|
||||
in_window_wh=_slot_float_nullable(d, "in_window_wh"),
|
||||
charge_slot_wh=_slot_float_nullable(d, "charge_slot_wh"),
|
||||
charge_cum_wh=_slot_float_nullable(d, "charge_cum_wh"),
|
||||
charge_layer=d.get("charge_layer"),
|
||||
charge_slot_reason=d.get("charge_slot_reason"),
|
||||
)
|
||||
)
|
||||
if not out:
|
||||
|
||||
@@ -337,22 +337,32 @@ def _select_charge_slots(
|
||||
elif purchase_pricing_mode == "fixed" and any(
|
||||
float(s.sell_price) > float(s.buy_price) + degrad for s in slots
|
||||
):
|
||||
min_sell_pos = min(
|
||||
float(s.sell_price) for s in slots if float(s.sell_price) >= 0.0
|
||||
)
|
||||
am_candidates = [
|
||||
(t, getattr(slots[t], "is_predicted_price", False))
|
||||
(
|
||||
t,
|
||||
getattr(slots[t], "is_predicted_price", False),
|
||||
float(slots[t].sell_price),
|
||||
)
|
||||
for t in range(len(slots))
|
||||
if _prague_hour(slots[t]) < 12
|
||||
and float(slots[t].sell_price) >= 0.0
|
||||
and float(slots[t].sell_price) <= min_sell_pos + degrad + 0.05
|
||||
]
|
||||
am_candidates.sort(
|
||||
key=lambda x: (
|
||||
_grid_sort_key(x[0], x[1], 0.0)[0],
|
||||
_grid_sort_key(x[0], x[1], 0.0)[1],
|
||||
_grid_sort_key(x[0], x[1], 0.0)[2],
|
||||
_grid_sort_key(x[0], x[1], x[2])[0],
|
||||
_grid_sort_key(x[0], x[1], x[2])[1],
|
||||
_grid_sort_key(x[0], x[1], x[2])[2],
|
||||
x[2],
|
||||
x[0],
|
||||
)
|
||||
)
|
||||
cum = 0.0
|
||||
grid_am = 0
|
||||
for t, _pred in am_candidates:
|
||||
for t, _pred, _sell in am_candidates:
|
||||
if cum >= chg_am or per_slot_full_wh <= 0 or grid_am >= cap_am:
|
||||
break
|
||||
selected.add(t)
|
||||
@@ -369,21 +379,28 @@ def _select_charge_slots(
|
||||
),
|
||||
)
|
||||
pm_candidates = [
|
||||
(t, getattr(slots[t], "is_predicted_price", False))
|
||||
(
|
||||
t,
|
||||
getattr(slots[t], "is_predicted_price", False),
|
||||
float(slots[t].sell_price),
|
||||
)
|
||||
for t in range(len(slots))
|
||||
if _prague_hour(slots[t]) >= 12
|
||||
and float(slots[t].sell_price) >= 0.0
|
||||
and float(slots[t].sell_price) <= min_sell_pos + degrad + 0.05
|
||||
]
|
||||
pm_candidates.sort(
|
||||
key=lambda x: (
|
||||
_grid_sort_key(x[0], x[1], 0.0)[0],
|
||||
_grid_sort_key(x[0], x[1], 0.0)[1],
|
||||
_grid_sort_key(x[0], x[1], 0.0)[2],
|
||||
_grid_sort_key(x[0], x[1], x[2])[0],
|
||||
_grid_sort_key(x[0], x[1], x[2])[1],
|
||||
_grid_sort_key(x[0], x[1], x[2])[2],
|
||||
x[2],
|
||||
x[0],
|
||||
)
|
||||
)
|
||||
cum = 0.0
|
||||
grid_pm = 0
|
||||
for t, _pred in pm_candidates:
|
||||
for t, _pred, _sell in pm_candidates:
|
||||
if cum >= chg_pm or per_slot_full_wh <= 0 or grid_pm >= cap_pm:
|
||||
break
|
||||
selected.add(t)
|
||||
@@ -398,15 +415,22 @@ def _select_charge_slots(
|
||||
fso = _future_sell(slots, t)
|
||||
if (
|
||||
pv_surplus_w > 0
|
||||
and float(s.sell_price) >= float(s.buy_price) - degrad
|
||||
and (
|
||||
float(s.sell_price) < 0
|
||||
purchase_pricing_mode == "fixed"
|
||||
or float(s.sell_price) >= float(s.buy_price) - degrad
|
||||
)
|
||||
and (
|
||||
purchase_pricing_mode == "fixed"
|
||||
or float(s.sell_price) < 0
|
||||
or float(s.sell_price) >= fso - degrad
|
||||
)
|
||||
):
|
||||
pv_candidates.append((t, _store_score(slots, t), float(pv_surplus_w)))
|
||||
|
||||
pv_candidates.sort(key=lambda x: (-x[1], x[0]))
|
||||
if purchase_pricing_mode == "fixed":
|
||||
pv_candidates.sort(key=lambda x: (float(slots[x[0]].sell_price), x[0]))
|
||||
else:
|
||||
pv_candidates.sort(key=lambda x: (-x[1], x[0]))
|
||||
cum = 0.0
|
||||
for t, _score, pv_surplus_w in pv_candidates:
|
||||
if cum >= pv_layer_cap:
|
||||
|
||||
@@ -1675,7 +1675,9 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_fixed_high_sell_no_pv_charge_near_min_sell(self) -> None:
|
||||
"""v58: ráno sell~3 Kč → export FVE; poledne sell~1,45 Kč → nabíjení z PV."""
|
||||
"""Charge-slot budget: levnější sell (poledne) dostane allow_charge dřív než drahší ráno."""
|
||||
from test_planning_charge_slot_selection import _select_charge_slots
|
||||
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 6, 2, 6, 0, tzinfo=prague)
|
||||
slots: list[PlanningSlot] = []
|
||||
@@ -1693,73 +1695,29 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_charge=True,
|
||||
allow_discharge_export=False,
|
||||
charge_acquisition_buy_czk_kwh=3.088,
|
||||
future_sell_opportunity_czk_kwh=3.2,
|
||||
)
|
||||
)
|
||||
battery = _battery(uc_wh=12_500.0, min_pct=10.0, arb_pct=30.0)
|
||||
battery.max_charge_power_w = 6250
|
||||
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=False,
|
||||
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,
|
||||
),
|
||||
]
|
||||
battery.charge_slot_buffer = 1.3
|
||||
soc0 = 0.35 * battery.usable_capacity_wh
|
||||
results, _, snap = solve_dispatch(
|
||||
charge = _select_charge_slots(
|
||||
slots,
|
||||
battery,
|
||||
hp,
|
||||
grid,
|
||||
[None, None],
|
||||
vehicles,
|
||||
soc0,
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
purchase_pricing_mode="fixed",
|
||||
apply_dynamic_grid_filter=False,
|
||||
)
|
||||
self.assertEqual(
|
||||
snap["inputs"].get("fixed_horizon_min_sell_czk_kwh"),
|
||||
1.45,
|
||||
)
|
||||
r_morning = results[0]
|
||||
r_noon = results[5]
|
||||
self.assertLess(
|
||||
r_morning.grid_setpoint_w,
|
||||
-2000,
|
||||
"vysoký sell: přebytek FVE do sítě",
|
||||
)
|
||||
self.assertLessEqual(
|
||||
r_morning.battery_setpoint_w,
|
||||
200,
|
||||
"vysoký sell: ne nabíjet z PV",
|
||||
)
|
||||
self.assertGreater(
|
||||
r_noon.battery_setpoint_w,
|
||||
800,
|
||||
"min sell: nabíjení z PV",
|
||||
noon_idx = 4
|
||||
cheap_slots = {4, 5, 6, 7}
|
||||
self.assertIn(noon_idx, charge, "min sell slot má allow_charge z PV vrstvy")
|
||||
self.assertTrue(
|
||||
cheap_slots.issubset(charge),
|
||||
"všechny sloty u min sell musí mít allow_charge dřív než dražší ranní",
|
||||
)
|
||||
|
||||
def test_fixed_no_grid_charge_when_sell_above_horizon_min(self) -> None:
|
||||
"""v59: KV1 — grid→bat jen u min sell, ne v noci za 6 Kč při sell 3,5."""
|
||||
"""v59 SQL maska: grid→bat jen u min sell; LP nerespektuje allow_charge bez allow_grid."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
cheap = PlanningSlot(
|
||||
interval_start=datetime(2026, 6, 2, 10, 15, tzinfo=prague).astimezone(
|
||||
@@ -1787,7 +1745,7 @@ class NegativeSellPvChargeTests(unittest.TestCase):
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_charge=True,
|
||||
allow_charge=False,
|
||||
allow_discharge_export=False,
|
||||
charge_acquisition_buy_czk_kwh=6.353,
|
||||
)
|
||||
|
||||
@@ -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í 00–06 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_sell−sell−max(0,buy−sell)); 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:00–06:00 Europe/Prague), safety_soc_target_wh (6–19), '
|
||||
'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.';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Plánování: rozpočet nabíjecích slotů (Wh × ceny × forecast)
|
||||
|
||||
**Stav:** návrh k implementaci (2026-06) — **zatím neimplementováno** v produkčním kódu.
|
||||
**Stav:** **Branch 3 implementováno** (2026-06-06, tag `2026-06-06-charge-slot-budget-v1`) — fixed tarify BA81/KV1; home-01 pre-neg fronta §6 zatím ne.
|
||||
**Účel:** nahradit tvrdé prahy typu `sell > min_sell + 0,20 → bc_pv = 0` (v58) a binární pre-neg „cushion“ (v33) jednotným **energetickým rozpočtem** ve `fn_load_planning_slots_full`, který pokryje fixní tarify (BA81, KV1), spot (home-01) i zkracující se okna `sell < 0` (zima).
|
||||
|
||||
**Související:**
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
- **Tvrdé výkonové limity site/baterie:** `gi ≤ site_grid_connection.max_import_power_w` (breaker); **`bc_pv + bc_gi ≤ asset_battery.max_charge_power_w`**; **`ge ≤ max_export_power_w`** (proměnná `ge`, platí `ge = ge_pv + ge_bat`); **`bd + ge_bat ≤ asset_battery.max_discharge_power_w`** (vybíjení do domu + export z baterie nesmí současně překročit BMS). Dříve LP dovoloval import+nabíjení a dvojnásobné nabíjení; u prodeje hrozilo současné `bd` a `ge_bat` až 2× max discharge — viz `SitePowerCapTests`.
|
||||
- **Hodnota FVE (PV store value):** tvrdé `ge_pv = 0` jen pokud `sell < future_sell_opportunity − degradation` **a** `sell < 0` (spot), nebo u fixního tarifu dle `fixed_pv_b_export_cap`. Při **`sell ≥ 0` (spot home-01, KV1):** `ge_pv` **neblokuje** pv_store — solver volí export vs. `bc_pv` podle `−ge_pv×sell` a degradace; **baterii** na večerní peak drží `ge_bat` (`evening_early` / push), ne curtail FVE. **v31:** při `sell ≥ 0` + PV přebytek **není** plný `ge_bat` push z `pre_neg_buy_discharge` / ranních shortfallů (export cap pro FVE). **Před prvním `sell < 0`:** `allow_pre_neg_pv_export`. Tag `2026-05-28-morning-pv-export-priority-v31`. Testy `Home01PvStoreValueTests`, `PreNegativeSellExportTests`.
|
||||
- **BA81 úsvit + MI (v51):** `fixed_pv_b_export_cap` (`ge_pv ≤ pv_b`) jen pokud **`pv_a_forecast ≥ 1500 W`** (`DAWN_LOW_PV_NO_CURTAIL_W`); při slabším A + přebytku → `fixed_mi_low_pv_surplus_export` (bez pv_store bloku). Exporter: při `forecast < 1500` a bez curtail A → **bez reg 340** (`setpoints.py`). Tag `2026-05-31-ba81-dawn-no-micro-curtail-v51`. Test `test_ba81_dawn_low_pv_no_full_curtail_for_mi_cap`.
|
||||
- **Fixní tarif — PV export vs. nabíjení (v58–v59, dočasné):** v58: při **`sell > min_sell + 0,20`** a PV přebytku → **`bc_pv = 0`**, export FVE; profitable noc **`sell > buy`** mimo `evening_early`. v59: **`bc_gi = 0`** i bez FVE při **`sell < buy`** nebo **`sell > min_sell + 0,20`**; večerní push bez nabíjení; **`R__063`** grid maska podle nejnižšího sell (`sell ASC`), ne `slot_ord`. Tagy `…-v58`, `…-v59`. **Plánovaná náhrada:** energetický rozpočet Wh + řazení výkupů/nákupů — **[`planning-charge-slot-budget.md`](planning-charge-slot-budget.md)** (zrušení v58 v LP, rozšíření `R__063`).
|
||||
- **Fixní tarif — charge-slot budget (v1, 2026-06-06):** **`R__063`** vybírá nabíjecí sloty Wh kumulací; PV vrstva A u fixed = **`sell ASC`**. LP **nezakazuje** `bc_pv`/`bc_gi` prahy v58 (`sell > min+0,20`); respektuje jen `allow_charge` / `allow_grid_charge`. Večerní push: **`sell > buy + spread`** (BA81); KV1 navíc v52 morning-peak pravidlo. Debug: `charge_slot_budget` v `solver_params`, sloupce `charge_layer` / `charge_slot_reason` ve `fn_load_planning_slots_full`. Spec: **[`planning-charge-slot-budget.md`](planning-charge-slot-budget.md)**. Tag **`2026-06-06-charge-slot-budget-v1`**. v59 grid maska (min sell) a večerní push `bc=0` v push slotech zůstávají.
|
||||
- **Drahý nákup → vlastní spotřeba z baterie:** mimo `allow_charge` platí `bd + pv_ld ≥ load_baseline + hp[t]` a `gi ≤ EV + hp[t]` (ne `hp_rated`). **Spot:** drahý slot = `buy > min(buy≥0) + degradace`. **Fixní nákup (DB `purchase_pricing_mode=fixed` nebo heuristika rozptylu buy < 0,25):** navíc `buy > charge_acquisition + degradace`. Na spotu **nesmí** `charge_acquisition` (~0,9 Kč) označit všechny sloty jako drahé → Infeasible (home-01). Při **Infeasible** solver jednou opakuje s `relaxed_expensive_import` (síť smí krmit baseload v drahých slotech; v `solver_params.inputs.relaxed_expensive_import=true`). Testy `AutoPassiveSelfConsumptionTests`, `test_spot_low_acquisition_does_not_mark_all_slots_expensive`, `test_negative_buy_in_horizon_does_not_block_all_grid_import`.
|
||||
- **Záporný výkup (`sell < 0`) bez exportu:** `block_export_on_negative_sell` (KV1) **nebo** `purchase_pricing_mode=fixed` (BA81). **Spot (home-01):** `ge_pv=0` dokud není plná baterie; při plné jen ventil pole B (`ge_pv ≤ pv_b`, `w_pv_b_vent_neg`); výboj baterie při `sell<0` jen **12 slotů** před `buy ≤ planner_extreme_buy_threshold` (default −2), pokud spread do budoucna dává smysl — tag `2026-05-26-neg-sell-bat-dump-extreme-buy-v11`. Večerní discharge maska u spotu: denní peak ≥17:00 (ne `sell > ref_buy` v slotu). **v50:** u **KV1** při `sell≥0` a PV přebytku >500 W i **po** 1. `sell<0` → `ge_pv` (PV_SURPLUS), ne tvrdý `ge_bat` z večerního peak/push.
|
||||
- **Pole B při sell<0 (home-01):** pokud `block_export_on_negative_sell = false`, LP nesmí vynutit `ge_pv = 0` (přebytek neriťitelného PV B). KV1 s `block_export = true` jen curtail A / nabíjení.
|
||||
@@ -153,9 +153,9 @@ flowchart TD
|
||||
|
||||
**Funkce:** … home-01 **v61**; BA81/KV1 fixed **v59** (+ `R__063`).
|
||||
|
||||
### Rozpočet nabíjecích slotů (plánováno, 2026-06)
|
||||
### Rozpočet nabíjecích slotů (charge-slot-budget v1, 2026-06-06)
|
||||
|
||||
Náhrada tvrdých prahů v58 a binárního pre-neg cushion (v33): **deficit Wh**, forecast v okně `sell < 0`, fronta nejlevnějších slotů (buy/sell) s `pv_surplus`. Pokrývá BA81/KV1 (slunečný den nad ~60 % SoC) i home-01 (krátké zimní neg okno → nabíjení před oknem). **Specifikace:** [`planning-charge-slot-budget.md`](planning-charge-slot-budget.md). **Stav:** neimplementováno — viz changelog *Plánováno*.
|
||||
**Branch 3 (BA81/KV1):** `R__063` vrací `charge_target_wh`, `pre_window_wh`, `in_window_wh` a debug sloupce; fixed PV vrstva **`sell ASC`**. LP bez v58 — jen masky SQL. Večerní push fixed: **`sell > buy + spread`**. Tag **`2026-06-06-charge-slot-budget-v1`**. **Zbývá pro home-01:** pre-neg fronta místo v33 cushion, v44 změkčení — [`planning-charge-slot-budget.md`](planning-charge-slot-budget.md) §6.
|
||||
|
||||
### Arbitráž baterie — účtování mezi sloty (povinné čtení)
|
||||
|
||||
|
||||
@@ -42,24 +42,34 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
||||
|
||||
---
|
||||
|
||||
## Plánováno — rozpočet nabíjecích slotů (charge-slot-budget, neimplementováno)
|
||||
## 2026-06-06 — charge-slot budget v1 (Branch 3: BA81/KV1)
|
||||
|
||||
**Stav:** pouze dokumentace (2026-06); implementace později.
|
||||
**Problém:** v58 `sell > min_sell + 0,20 → bc_pv = 0` držel denní SoC ~60 % při slunci (konflikt s `R__063` vrstvou A). Fixní lokality neměly večerní push podle `sell > buy + spread`.
|
||||
|
||||
**Motivace:**
|
||||
**Změna:**
|
||||
|
||||
- **BA81/KV1:** v58 `sell > min_sell + 0,20 → bc_pv = 0` drží denní SoC ~60 % při slunci — konflikt s `R__063` vrstvou A.
|
||||
- **home-01:** v33 binární pre-neg cushion exportuje FVE před `sell < 0` i při středním sell; při kratším zimním okně `sell < 0` / slabší FVE chybí nabíjení **před** oknem.
|
||||
1. **`R__063_fn_load_planning_slots_full.sql`:** nové sloupce `charge_target_wh`, `pre_window_wh`, `in_window_wh`, `charge_slot_wh`, `charge_cum_wh`, `charge_layer`, `charge_slot_reason`; PV vrstva A u **fixed** řazena **`sell ASC`** + Wh kumulace (spot dál `store_score DESC`).
|
||||
2. **`planning_engine.py`:** odstraněn v58 (`fixed_high_sell_no_pv_charge`, `fixed_grid_charge_unprofitable`); LP respektuje jen `allow_charge` / `allow_grid_charge` ze SQL. Večerní push u fixního tarifu: **`sell > buy + spread`** (`fixed_evening_push_sell_above_buy`); KV1 zachovává v52 morning-peak pravidlo.
|
||||
3. **`solver_params.charge_slot_budget`** — audit rozpočtu na aktivním runu.
|
||||
|
||||
**Záměr:**
|
||||
Tag **`2026-06-06-charge-slot-budget-v1`**.
|
||||
|
||||
1. **`fn_load_planning_slots_full`:** `charge_target_wh`, `pre_window_wh` (deficit − forecast v neg okně), fronta slotů řazená **`sell ASC`** (fixed) / **`buy ASC`** (spot), kumulace `pv_surplus` Wh → `allow_charge`.
|
||||
2. **Python:** zrušit v58 (a související fixed `bc_pv`/`bc_gi` prahy); pre-neg export jen v pre-neg slotech **bez** `allow_charge`.
|
||||
3. **Neg den:** změkčit v44 grid před `sell < 0`, pokud `pre_window_wh` > dostupná FVE v okně.
|
||||
**Ověření:**
|
||||
|
||||
**Specifikace:** [`docs/04-modules/planning-charge-slot-budget.md`](04-modules/planning-charge-slot-budget.md).
|
||||
```bash
|
||||
pytest backend/tests/test_planning_charge_slot_selection.py backend/tests/test_planning_dispatch_milp.py \
|
||||
-k "fixed_high_sell or fixed_tariff_evening or fixed_evening or kv1_evening" -q
|
||||
```
|
||||
|
||||
**Plánovaný tag:** `…-charge-slot-budget-v1` (po implementaci).
|
||||
MCP: `planning_run.solver_params->'charge_slot_budget'`; u BA81 večer `evening_push_ts` neprázdné při `sell > buy`.
|
||||
|
||||
**Zbývá (Branch 4–5 spec):** změkčení v44 grid před neg; plná náhrada v33 cushion přes `pre_window_wh` frontu u home-01.
|
||||
|
||||
---
|
||||
|
||||
## Plánováno — charge-slot-budget home-01 (pre-neg fronta, v44)
|
||||
|
||||
**Stav:** Branch 3 hotový pro fixed; spot pre-neg fronta a v44 změkčení — viz [`planning-charge-slot-budget.md`](04-modules/planning-charge-slot-budget.md) §6.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user