uprava aby rano prodaval do site pred sell < 0 oknem
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user