dalsi
This commit is contained in:
@@ -4260,10 +4260,19 @@ class NegSellSocPhaseTests(unittest.TestCase):
|
||||
_ph, _tg, _w, meta = _neg_sell_day_phases(slots, bat)
|
||||
self.assertIsNotNone(meta.get("t_detach_idx"))
|
||||
self.assertGreaterEqual(int(meta["t_detach_idx"]), 0)
|
||||
self.assertLess(int(meta["t_detach_idx"]), 8)
|
||||
self.assertLessEqual(int(meta["t_detach_idx"]), 8)
|
||||
self.assertGreater(float(meta.get("e_surplus_after_t_wh") or 0), 0.0)
|
||||
self.assertIn("post_detach_prep_ts", meta)
|
||||
|
||||
def test_prep_leaves_headroom_when_pv_a_b_forecast_high(self) -> None:
|
||||
"""v44: zpětná soc_need z A+B FVE, ne jen B — 1. sell<0 cíl pod soc_max."""
|
||||
slots = self._neg_sell_slots(12, pv_a=8000, pv_b=6000)
|
||||
bat = self._phase_battery(tail_slots=4)
|
||||
_ph, targets, _w, meta = _neg_sell_day_phases(slots, bat)
|
||||
first_neg = int(meta["days"][0]["first_neg_idx"])
|
||||
tgt_first = float(targets[first_neg] or 0)
|
||||
self.assertLess(tgt_first, bat.soc_max_wh * 0.95)
|
||||
|
||||
def test_t_detach_not_first_neg_on_long_sunny_day(self) -> None:
|
||||
"""Bod T až po nabití rampy (~85 % soc_max), ne na prvním sell<0 slotu."""
|
||||
slots = self._neg_sell_slots(24, pv_b=7000, pv_a=5000)
|
||||
@@ -4703,6 +4712,82 @@ class NegSellPrepWindowV36Tests(unittest.TestCase):
|
||||
self.assertIsNotNone(snap["inputs"].get("neg_evening_export_budget_wh"))
|
||||
|
||||
|
||||
class NegDayPvHeadroomV44Tests(unittest.TestCase):
|
||||
"""v44: neg den — žádný grid před sell<0; headroom pro FVE + levný buy v okně."""
|
||||
|
||||
def test_no_grid_charge_before_first_negative_sell(self) -> None:
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 5, 30, 5, 45, tzinfo=prague)
|
||||
slots: list[PlanningSlot] = []
|
||||
first_neg_idx: int | None = None
|
||||
for i in range(24):
|
||||
local = base + timedelta(minutes=15 * i)
|
||||
sell = (
|
||||
-0.18
|
||||
if local.hour > 7 or (local.hour == 7 and local.minute >= 45)
|
||||
else 3.0
|
||||
)
|
||||
if first_neg_idx is None and sell < 0:
|
||||
first_neg_idx = i
|
||||
buy = 3.2 if local.hour < 8 else 0.48
|
||||
allow_chg = sell < 0
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=local.astimezone(timezone.utc),
|
||||
buy_price=buy,
|
||||
sell_price=sell,
|
||||
pv_a_forecast_w=4000 if local.hour >= 8 else 500,
|
||||
pv_b_forecast_w=3000 if local.hour >= 8 else 500,
|
||||
load_baseline_w=2000,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_charge=allow_chg,
|
||||
allow_discharge_export=True,
|
||||
)
|
||||
)
|
||||
self.assertIsNotNone(first_neg_idx)
|
||||
bat = NegSellSocPhaseTests._phase_battery()
|
||||
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=13_500,
|
||||
block_export_on_negative_sell=False,
|
||||
)
|
||||
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,
|
||||
bat,
|
||||
hp,
|
||||
grid,
|
||||
[None, None],
|
||||
vehicles,
|
||||
0.50 * bat.soc_max_wh,
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
assert first_neg_idx is not None
|
||||
for t in range(first_neg_idx):
|
||||
self.assertLessEqual(
|
||||
res[t].battery_setpoint_w,
|
||||
200,
|
||||
msg=f"grid/PV bat charge before neg at slot {t}",
|
||||
)
|
||||
self.assertLess(
|
||||
res[first_neg_idx].battery_soc_target,
|
||||
92.0,
|
||||
"baterie nesmí být plná těsně před sell<0 oknem",
|
||||
)
|
||||
|
||||
|
||||
class ObservedSocNegPrepTests(unittest.TestCase):
|
||||
"""v40: neg-prep a večerní výboj z pozorovaného SoC (telemetrie), ne z LP trajektorie."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user