Skip to content

Commit 16c07dd

Browse files
CopilotMaStr
andcommitted
feat: gate early overhang charging by price proximity
Co-authored-by: MaStr <1036501+MaStr@users.noreply.github.com>
1 parent aa320b0 commit 16c07dd

2 files changed

Lines changed: 59 additions & 3 deletions

File tree

src/batcontrol/logic/default.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,13 +423,18 @@ def __get_required_recharge_energy(self, calc_input: CalculationInput ,
423423
if self.enable_precharge_overhang:
424424
recharge_energy = self.__get_recharge_overhang_energy(
425425
recharge_energy,
426-
turning_point_hour
426+
turning_point_hour,
427+
current_price,
428+
prices,
429+
min_price_difference
427430
)
428431

429432
self.calculation_output.required_recharge_energy = recharge_energy
430433
return recharge_energy
431434

432-
def __get_recharge_overhang_energy(self, recharge_energy: float, turning_point_hour: Optional[int]) -> float:
435+
def __get_recharge_overhang_energy(self, recharge_energy: float, turning_point_hour: Optional[int],
436+
current_price: float, prices: dict,
437+
min_price_difference: float) -> float:
433438
""" Return recharge overhang if more than one charging slot is needed """
434439
if turning_point_hour is None or turning_point_hour < 1:
435440
return recharge_energy
@@ -447,6 +452,17 @@ def __get_recharge_overhang_energy(self, recharge_energy: float, turning_point_h
447452
if required_slots <= 1:
448453
return recharge_energy
449454

455+
next_lowest_price = min(prices[h] for h in range(1, len(prices)))
456+
allowed_price_distance = min_price_difference / 2
457+
if abs(current_price - next_lowest_price) > allowed_price_distance:
458+
logger.debug(
459+
"[Rule] Skip recharge overhang before turning point. Current price %.4f is not within %.4f of next lowest price %.4f.",
460+
current_price,
461+
allowed_price_distance,
462+
next_lowest_price
463+
)
464+
return recharge_energy
465+
450466
overhang_energy = recharge_energy - usable_charge_per_slot
451467
logger.debug(
452468
"[Rule] Recharge overhang detected (%0.1f Wh, %d slots). Charging overhang before turning point in hour %d.",

tests/batcontrol/logic/test_default.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def test_recharge_overhang_is_charged_before_turning_point(self):
349349
calc_input = CalculationInput(
350350
consumption=np.array([100, 3500, 100]),
351351
production=np.array([0, 0, 0]),
352-
prices={0: 0.20, 1: 0.40, 2: 0.10},
352+
prices={0: 0.12, 1: 0.40, 2: 0.10},
353353
stored_energy=stored_energy,
354354
stored_usable_energy=stored_usable_energy,
355355
free_capacity=free_capacity,
@@ -366,6 +366,46 @@ def test_recharge_overhang_is_charged_before_turning_point(self):
366366
msg="Expected to charge only the overhang before the turning point"
367367
)
368368

369+
def test_recharge_overhang_is_skipped_when_current_price_too_high(self):
370+
"""Test that overhang precharge is skipped if current price is too far from next low."""
371+
self.logic.enable_precharge_overhang = True
372+
self.logic.max_charge_loss_factor = 0.1
373+
self.logic.set_calculation_parameters(
374+
CalculationParameters(
375+
max_charging_from_grid_limit=0.79,
376+
min_price_difference=0.05,
377+
min_price_difference_rel=0.2,
378+
max_capacity=self.max_capacity,
379+
max_grid_charge_rate=2000
380+
)
381+
)
382+
383+
stored_energy = 1000
384+
stored_usable_energy, free_capacity = self._calculate_battery_values(
385+
stored_energy,
386+
self.max_capacity
387+
)
388+
389+
calc_input = CalculationInput(
390+
consumption=np.array([100, 3500, 100]),
391+
production=np.array([0, 0, 0]),
392+
prices={0: 0.20, 1: 0.40, 2: 0.10},
393+
stored_energy=stored_energy,
394+
stored_usable_energy=stored_usable_energy,
395+
free_capacity=free_capacity,
396+
)
397+
398+
calc_timestamp = datetime.datetime(2025, 6, 20, 12, 0, 0, tzinfo=datetime.timezone.utc)
399+
self.assertTrue(self.logic.calculate(calc_input, calc_timestamp))
400+
calc_output = self.logic.get_calculation_output()
401+
402+
self.assertAlmostEqual(
403+
calc_output.required_recharge_energy,
404+
3100.0,
405+
delta=0.1,
406+
msg="Expected full recharge amount when current price is still too expensive"
407+
)
408+
369409
def test_recharge_overhang_feature_flag_disables_behavior(self):
370410
"""Test that disabling the feature flag keeps full recharge amount."""
371411
self.logic.enable_precharge_overhang = False

0 commit comments

Comments
 (0)