dalsi
This commit is contained in:
@@ -15,7 +15,9 @@ from services.planning_engine import (
|
||||
_dispatch_result_comparison,
|
||||
_evening_battery_export_push_indices,
|
||||
_evening_peak_export_indices,
|
||||
_evening_push_calendar_segments,
|
||||
_evening_push_discharge_budget_wh,
|
||||
_in_evening_push_hour_window,
|
||||
_in_night_battery_export_window,
|
||||
_neg_sell_day_phases,
|
||||
_neg_sell_phases_enabled,
|
||||
@@ -242,9 +244,85 @@ class EveningPushBudgetTests(unittest.TestCase):
|
||||
soc_max_wh=bat.soc_max_wh,
|
||||
per_slot_discharge_wh=per_slot,
|
||||
discharge_slot_buffer=1.5,
|
||||
discharge_export_ok={0, 1, 2},
|
||||
)
|
||||
self.assertIn(2, push, "00:00 max sell musí být v push")
|
||||
self.assertEqual(max(float(slots[t].sell_price) for t in push), 3.586)
|
||||
self.assertNotIn(2, push, "v43: push jen ≥17h, ne půlnoc")
|
||||
self.assertIn(1, push, "23:30 večerní push")
|
||||
self.assertEqual(max(float(slots[t].sell_price) for t in push), 3.323)
|
||||
|
||||
def test_no_predawn_push_before_17h(self) -> None:
|
||||
"""v43: žádný tvrdý push ve 02–06h (sell < buy v dead of night)."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 5, 26, 2, 0, tzinfo=prague)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
interval_start=base + timedelta(minutes=15 * i),
|
||||
buy_price=4.8,
|
||||
sell_price=2.9 - 0.01 * i,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=500,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.5,
|
||||
)
|
||||
for i in range(8)
|
||||
]
|
||||
bat = _battery(uc_wh=64_000.0, min_pct=10.0, max_pct=95.0)
|
||||
per_slot = 13_500 * 0.95 * 0.25
|
||||
push = _evening_battery_export_push_indices(
|
||||
slots,
|
||||
charge_acquisition_czk_kwh=0.5,
|
||||
degrad_czk_kwh=0.15,
|
||||
current_soc_wh=0.85 * bat.soc_max_wh,
|
||||
min_soc_wh=bat.min_soc_wh,
|
||||
soc_max_wh=bat.soc_max_wh,
|
||||
per_slot_discharge_wh=per_slot,
|
||||
discharge_slot_buffer=1.5,
|
||||
discharge_export_ok=set(range(8)),
|
||||
)
|
||||
self.assertEqual(push, [])
|
||||
|
||||
def test_per_calendar_evening_push_budget_split(self) -> None:
|
||||
"""Dva večery v horizontu → každý dostane část Wh rozpočtu (druhý den ne prázdný)."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
slots: list[PlanningSlot] = []
|
||||
for day in (25, 26):
|
||||
for h, m in ((18, 0), (18, 15), (18, 30)):
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=datetime(2026, 5, day, h, m, tzinfo=prague),
|
||||
buy_price=5.0,
|
||||
sell_price=4.0 + 0.1 * (h - 18),
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=800,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.5,
|
||||
)
|
||||
)
|
||||
segs = _evening_push_calendar_segments(slots, discharge_export_ok=set(range(len(slots))))
|
||||
self.assertEqual(len(segs), 2)
|
||||
bat = _battery(uc_wh=64_000.0, min_pct=10.0, max_pct=95.0)
|
||||
per_slot = 13_500 * 0.95 * 0.25
|
||||
push = _evening_battery_export_push_indices(
|
||||
slots,
|
||||
charge_acquisition_czk_kwh=0.5,
|
||||
degrad_czk_kwh=0.15,
|
||||
current_soc_wh=0.9 * bat.soc_max_wh,
|
||||
min_soc_wh=bat.min_soc_wh,
|
||||
soc_max_wh=bat.soc_max_wh,
|
||||
per_slot_discharge_wh=per_slot,
|
||||
discharge_slot_buffer=1.5,
|
||||
discharge_export_ok=set(range(len(slots))),
|
||||
)
|
||||
day25 = {t for t in push if slots[t].interval_start.day == 25}
|
||||
day26 = {t for t in push if slots[t].interval_start.day == 26}
|
||||
self.assertGreaterEqual(len(day25), 1)
|
||||
self.assertGreaterEqual(len(day26), 1)
|
||||
|
||||
def test_evening_push_budget_matches_r063_formula(self) -> None:
|
||||
bat = _battery(uc_wh=64_000.0, min_pct=10.0, max_pct=95.0)
|
||||
@@ -2394,7 +2472,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_midnight_higher_sell_gets_battery_export(self) -> None:
|
||||
"""home-01 archetyp: export v 00:00 (vyšší sell), ne jen 23:30."""
|
||||
"""v43: push jen ≥17h — export v 23:30, ne predawn půlnoc."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
@@ -2457,9 +2535,10 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertEqual(snap["planner_build_tag"], PLANNER_BUILD_TAG)
|
||||
r_midnight = results[2]
|
||||
self.assertEqual(r_midnight.export_mode, "BATTERY_SELL")
|
||||
self.assertGreaterEqual(abs(r_midnight.grid_setpoint_w), 12_500)
|
||||
r_evening = results[0]
|
||||
self.assertEqual(r_evening.export_mode, "BATTERY_SELL")
|
||||
self.assertGreaterEqual(abs(r_evening.grid_setpoint_w), 12_500)
|
||||
self.assertNotEqual(results[2].export_mode, "BATTERY_SELL")
|
||||
|
||||
def test_evening_push_export_near_site_cap_home01(self) -> None:
|
||||
"""home-01 večer: export ≈ min(13.5 kW, 18 kW − load), ne (max−load)/2."""
|
||||
@@ -2716,6 +2795,60 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
self.assertLessEqual(len(push), 4)
|
||||
self.assertEqual(push, [0, 1, 2, 3][: len(push)])
|
||||
|
||||
def test_night_self_consume_prefers_battery_over_grid(self) -> None:
|
||||
"""v43: mezi push sloty baterie krmí dům místo importu za ~5 Kč."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
sells = [3.9, 3.8, 3.1, 3.0]
|
||||
base = datetime(2026, 5, 29, 20, 0, tzinfo=prague)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
interval_start=base + timedelta(minutes=15 * i),
|
||||
buy_price=5.0,
|
||||
sell_price=sells[i],
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=2000,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.7,
|
||||
)
|
||||
for i in range(4)
|
||||
]
|
||||
battery = _battery(uc_wh=64_000.0, terminal_soc_value_factor=0.0)
|
||||
battery.max_discharge_power_w = 18_000
|
||||
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)
|
||||
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, _ms, snap = solve_dispatch(
|
||||
slots,
|
||||
battery,
|
||||
hp,
|
||||
grid,
|
||||
[None, None],
|
||||
vehicles,
|
||||
0.75 * battery.soc_max_wh,
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
push_iso = set(snap["inputs"].get("evening_push_ts") or [])
|
||||
for i in (2, 3):
|
||||
iso = slots[i].interval_start.isoformat()
|
||||
if iso not in push_iso:
|
||||
self.assertLessEqual(
|
||||
results[i].battery_setpoint_w,
|
||||
-1500,
|
||||
msg=f"slot {i} mimo push: vlastní spotřeba z baterie",
|
||||
)
|
||||
self.assertLessEqual(
|
||||
results[i].grid_setpoint_w,
|
||||
500,
|
||||
msg=f"slot {i} mimo push: ne import pro dům",
|
||||
)
|
||||
|
||||
def test_no_pv_export_at_low_sell_when_evening_peak_much_higher(self) -> None:
|
||||
"""Odpolední sell ~1,4 a večer ~5,5 — PV do baterie, ne FVE→síť za haléř."""
|
||||
base = datetime(2026, 5, 21, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
Reference in New Issue
Block a user