11"""Regression-style tests based on a real overnight grid-charge scenario."""
22import datetime
33
4- import numpy as np
54import pytest
65from pytest import approx
76
8- from batcontrol .logic .common import CommonLogic
97from batcontrol .logic .default import DefaultLogic
10- from batcontrol .logic .logic_interface import CalculationInput , CalculationParameters
118from batcontrol .logic .next import NextLogic
129
13-
14- CAPACITY_WH = 10240
15- MIN_SOC = 0.10
16- MAX_CHARGING_FROM_GRID_LIMIT = 0.89
17- MIN_GRID_CHARGE_SOC = 0.55
18- CHEAP_PRICE = 0.4635
19- EXPENSIVE_PRICE = 0.7018
20-
21-
22- @pytest .fixture (autouse = True )
23- def reset_common_logic ():
24- """Keep the CommonLogic singleton from leaking settings between cases."""
25- CommonLogic ._instance = None
26- yield
27- CommonLogic ._instance = None
10+ from .helpers import (
11+ CAPACITY_WH ,
12+ CHEAP_PRICE ,
13+ EXPENSIVE_PRICE ,
14+ MIN_GRID_CHARGE_SOC ,
15+ make_calc_input ,
16+ make_logic ,
17+ target_usable_energy ,
18+ )
2819
2920
3021@pytest .mark .parametrize ("logic_cls" , [DefaultLogic , NextLogic ])
3122def test_min_grid_charge_soc_preserves_battery_at_start_of_cheap_window (logic_cls ):
3223 """A fixed target preserves battery instead of discharging through a cheap plateau."""
33- logic = _make_logic (logic_cls )
34- calc_input = _make_input (
24+ logic = make_logic (logic_cls )
25+ calc_input = make_calc_input (
3526 # Forecast snapshot around 2026-04-28 22:00.
3627 production = [0 , 0 , 0 , 0 , 0 , 0 , 0 , 129 , 570 , 1361 , 2281 , 2579 , 2406 ],
3728 consumption = [854 , 765 , 635 , 691 , 571 , 708 , 912 , 1208 , 1221 , 1469 , 1237 , 1106 , 983 ],
@@ -45,14 +36,14 @@ def test_min_grid_charge_soc_preserves_battery_at_start_of_cheap_window(logic_cl
4536 calc_output = logic .get_calculation_output ()
4637
4738 assert result .allow_discharge is False
48- assert calc_output .reserved_energy == approx (_target_usable_energy ())
39+ assert calc_output .reserved_energy == approx (target_usable_energy ())
4940
5041
5142@pytest .mark .parametrize ("logic_cls" , [DefaultLogic , NextLogic ])
5243def test_min_grid_charge_soc_charges_at_last_cheap_hour_before_expensive_window (logic_cls ):
5344 """At the last cheap hour, a fixed target causes grid charging before high prices."""
54- logic = _make_logic (logic_cls )
55- calc_input = _make_input (
45+ logic = make_logic (logic_cls )
46+ calc_input = make_calc_input (
5647 # Forecast snapshot around 2026-04-29 05:00.
5748 production = [128 , 541 , 1196 , 2022 , 2615 , 2728 ],
5849 consumption = [1208 , 1221 , 1469 , 1237 , 1106 , 983 ],
@@ -75,8 +66,8 @@ def test_min_grid_charge_soc_charges_at_last_cheap_hour_before_expensive_window(
7566@pytest .mark .parametrize ("logic_cls" , [DefaultLogic , NextLogic ])
7667def test_min_grid_charge_soc_stops_grid_charging_when_gap_is_below_threshold (logic_cls ):
7768 """When the fixed target is nearly reached, avoid a tiny final grid-charge burst."""
78- logic = _make_logic (logic_cls )
79- calc_input = _make_input (
69+ logic = make_logic (logic_cls )
70+ calc_input = make_calc_input (
8071 # Similar to 2026-04-29 14:54: target gap was below min_recharge_amount.
8172 production = [0 , 0 ],
8273 consumption = [1000 , 1000 ],
@@ -97,8 +88,8 @@ def test_min_grid_charge_soc_stops_grid_charging_when_gap_is_below_threshold(log
9788@pytest .mark .parametrize ("logic_cls" , [DefaultLogic , NextLogic ])
9889def test_min_grid_charge_soc_allows_discharge_after_target_when_price_is_high (logic_cls ):
9990 """After reaching the target, high-price periods can use the preserved battery."""
100- logic = _make_logic (logic_cls )
101- calc_input = _make_input (
91+ logic = make_logic (logic_cls )
92+ calc_input = make_calc_input (
10293 # Similar to 2026-04-29 15:00 after the battery reached the 55% target.
10394 production = [2491 , 2118 , 1946 , 1687 , 1234 , 664 , 179 , 0 , 0 , 0 ],
10495 consumption = [746 , 986 , 989 , 1026 , 1361 , 1316 , 1047 , 790 , 564 , 665 ],
@@ -114,40 +105,3 @@ def test_min_grid_charge_soc_allows_discharge_after_target_when_price_is_high(lo
114105 assert result .allow_discharge is True
115106 assert result .charge_from_grid is False
116107 assert calc_output .reserved_energy == 0.0
117-
118-
119- def _make_logic (logic_cls ):
120- CommonLogic .get_instance (
121- charge_rate_multiplier = 1.1 ,
122- always_allow_discharge_limit = 0.90 ,
123- max_capacity = CAPACITY_WH ,
124- min_charge_energy = 100 ,
125- )
126- logic = logic_cls (timezone = datetime .timezone .utc , interval_minutes = 60 )
127- logic .set_calculation_parameters (CalculationParameters (
128- max_charging_from_grid_limit = MAX_CHARGING_FROM_GRID_LIMIT ,
129- min_price_difference = 0.05 ,
130- min_price_difference_rel = 0.10 ,
131- max_capacity = CAPACITY_WH ,
132- min_grid_charge_soc = MIN_GRID_CHARGE_SOC ,
133- preserve_min_grid_charge_soc = True ,
134- peak_shaving_enabled = False ,
135- ))
136- return logic
137-
138-
139- def _make_input (production , consumption , prices , soc ):
140- stored_energy = CAPACITY_WH * soc / 100
141- min_soc_energy = CAPACITY_WH * MIN_SOC
142- return CalculationInput (
143- production = np .array (production , dtype = float ),
144- consumption = np .array (consumption , dtype = float ),
145- prices = {slot : price for slot , price in enumerate (prices )},
146- stored_energy = stored_energy ,
147- stored_usable_energy = max (0.0 , stored_energy - min_soc_energy ),
148- free_capacity = CAPACITY_WH - stored_energy ,
149- )
150-
151-
152- def _target_usable_energy ():
153- return CAPACITY_WH * (MIN_GRID_CHARGE_SOC - MIN_SOC )
0 commit comments