dalsi
Some checks failed
CI and deploy / migration-check (push) Failing after 23s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-29 23:34:16 +02:00
parent 308c24f029
commit d3e9caf0fb
5 changed files with 154 additions and 11 deletions

View File

@@ -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."""