uprava aby rano prodaval do site pred sell < 0 oknem
Some checks failed
CI and deploy / migration-check (push) Failing after 20s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-26 08:29:05 +02:00
parent da79eec077
commit b4e5fc5040
4 changed files with 303 additions and 14 deletions

View File

@@ -21,6 +21,7 @@ from services.planning_engine import (
_neg_sell_phases_enabled,
_pre_neg_buy_soc_ceiling_wh,
_pre_neg_peak_sell_idx,
_pre_neg_pv_export_forecast_cushion_ok,
_prague_hour,
_prewindow_deferral_slots,
_slots_until_buy_le_threshold,
@@ -3845,5 +3846,132 @@ class NegSellSocPhaseTests(unittest.TestCase):
self.assertLessEqual(max(0, -results[-1].grid_setpoint_w), 500)
class PreNegPvExportForecastTests(unittest.TestCase):
"""v33: export FVE před sell<0 jen pokud forecast v sell<0 okně pokryje prep SoC."""
@staticmethod
def _slots_morning_then_neg(n: int = 22, *, neg_pv_scale: float = 1.0) -> list[PlanningSlot]:
base = datetime(2026, 6, 10, 6, 0, tzinfo=timezone.utc)
out: list[PlanningSlot] = []
for i in range(n):
sell = -0.25 if i >= 6 else (2.8 if i < 4 else 1.2)
if i >= 6:
pv_a = (8000 + (i - 6) * 500) * neg_pv_scale
pv_b = 6000.0 * neg_pv_scale
else:
pv_a = 1500 + i * 400
pv_b = 1500.0
future_sell = 6.5 if sell >= 0 else None
out.append(
PlanningSlot(
interval_start=base + timedelta(minutes=15 * i),
buy_price=2.0,
sell_price=sell,
pv_a_forecast_w=pv_a,
pv_b_forecast_w=pv_b,
load_baseline_w=450,
ev1_connected=False,
ev2_connected=False,
allow_charge=True,
allow_discharge_export=False,
future_sell_opportunity_czk_kwh=future_sell,
)
)
return out
def test_cushion_ok_when_neg_window_pv_large(self) -> None:
slots = self._slots_morning_then_neg()
bat = _battery(uc_wh=64_000.0, max_pct=95.0)
bat.planner_neg_sell_prep_soc_percent = 80.0
bat.planner_neg_sell_full_soc_tail_slots = 4
self.assertTrue(
_pre_neg_pv_export_forecast_cushion_ok(
slots,
bat,
0.30 * bat.soc_max_wh,
6,
neg_sell_phases_en=True,
)
)
def test_cushion_fail_when_neg_window_pv_tiny(self) -> None:
slots = self._slots_morning_then_neg(neg_pv_scale=0.05)
bat = _battery(uc_wh=64_000.0, max_pct=95.0)
bat.planner_neg_sell_prep_soc_percent = 80.0
bat.planner_neg_sell_full_soc_tail_slots = 4
self.assertFalse(
_pre_neg_pv_export_forecast_cushion_ok(
slots,
bat,
0.30 * bat.soc_max_wh,
6,
neg_sell_phases_en=True,
)
)
def test_morning_exports_pv_when_cushion_ok(self) -> None:
slots = self._slots_morning_then_neg()
bat = _battery(uc_wh=64_000.0, max_pct=95.0)
bat.planner_neg_sell_prep_soc_percent = 80.0
bat.planner_neg_sell_full_soc_tail_slots = 4
bat.planner_neg_sell_vent_min_sell_czk_kwh = -1.0
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=20_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),
]
results, _, snap = solve_dispatch(
slots,
bat,
hp,
grid,
[None, None],
vehicles,
0.30 * bat.soc_max_wh,
50.0,
operating_mode="AUTO",
)
self.assertTrue(snap["inputs"].get("pre_neg_pv_export_forecast_ok"))
self.assertIn(
slots[2].interval_start.isoformat(),
snap["inputs"].get("pre_neg_pv_export_slots") or [],
)
self.assertLess(results[2].grid_setpoint_w, -500)
def test_morning_charges_when_cushion_fail(self) -> None:
slots = self._slots_morning_then_neg(neg_pv_scale=0.05)
bat = _battery(uc_wh=64_000.0, max_pct=95.0)
bat.planner_neg_sell_prep_soc_percent = 80.0
bat.planner_neg_sell_full_soc_tail_slots = 4
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=20_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),
]
results, _, snap = solve_dispatch(
slots,
bat,
hp,
grid,
[None, None],
vehicles,
0.30 * bat.soc_max_wh,
50.0,
operating_mode="AUTO",
)
self.assertFalse(snap["inputs"].get("pre_neg_pv_export_forecast_ok"))
self.assertGreater(results[2].battery_setpoint_w, 2000)
if __name__ == "__main__":
unittest.main()