Skip to content

Commit 4b704c0

Browse files
Stevenclaude
andcommitted
Fix conservative overcurrent mode never reducing charger limit
Conservative mode clamped phase_limit at 0 (max(0, phase_limit + avail)), returning a positive value that PowerAllocator read as surplus, so cuts never fired and the charger was never throttled during overcurrent. Emit the real negative deficit immediately instead, matching the relative-availability contract used by the optimised/default balancers. Conservative stays distinct from optimised by cutting right away rather than waiting for cumulative trip risk to cross the threshold. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 562a392 commit 4b704c0

2 files changed

Lines changed: 27 additions & 6 deletions

File tree

custom_components/evse_load_balancer/balancers/optimised_load_balancer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ def update(self, avail: float, now: int) -> float:
8888
self._cumulative_trip_risk = 0.0
8989
self.phase_limit = avail
9090
else:
91+
# Conservative mode: reduce immediately by emitting the real
92+
# negative deficit so the PowerAllocator distributes cuts.
93+
# This is the safer (more aggressive) mode, cutting right away
94+
# instead of waiting for the cumulative trip risk to build up.
9195
self._cumulative_trip_risk = 0.0
92-
self.phase_limit = max(0, self.phase_limit + avail)
96+
self.phase_limit = avail
9397
else:
9498
risk_decay = self._risk_decay_per_second * elapsed
9599
self._cumulative_trip_risk = max(

tests/balancers/test_optimised_load_balancer.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,19 @@ def test_conservative_mode_immediate_reduction():
8989

9090
for phase in Phase:
9191
assert limits_one[phase] == 20
92-
assert limits_two[phase] == 15
92+
# Conservative mode emits the real negative deficit immediately so the
93+
# PowerAllocator distributes the cut across the chargers.
94+
assert limits_two[phase] == -5
9395

9496

95-
def test_conservative_mode_never_goes_below_zero():
97+
def test_conservative_mode_emits_negative_deficit():
98+
"""Conservative mode must surface a negative availability during overcurrent.
99+
100+
Regression test: previously the overcurrent branch clamped at 0
101+
(``max(0, phase_limit + avail)``) and therefore never returned a negative
102+
value, so PowerAllocator's ``_distribute_cuts`` was never triggered and the
103+
charger limit was never reduced.
104+
"""
96105
lb = OptimisedLoadBalancer(
97106
max_limits=dict.fromkeys(Phase, 25),
98107
overcurrent_mode=OvercurrentMode.CONSERVATIVE,
@@ -105,7 +114,10 @@ def test_conservative_mode_never_goes_below_zero():
105114

106115
for phase in Phase:
107116
assert limits_one[phase] == 5
108-
assert limits_two[phase] == 0
117+
# A sustained overcurrent must produce a negative availability (a cut),
118+
# not a clamped-at-zero value.
119+
assert limits_two[phase] == -10
120+
assert limits_two[phase] < 0
109121

110122

111123
def test_conservative_mode_allows_increases():
@@ -123,7 +135,9 @@ def test_conservative_mode_allows_increases():
123135

124136
for phase in Phase:
125137
assert limits_one[phase] == 10
126-
assert limits_two[phase] == 5
138+
# Overcurrent is emitted as a negative deficit (immediate cut)...
139+
assert limits_two[phase] == -5
140+
# ...and recovery still surfaces the available surplus.
127141
assert limits_three[phase] == 15
128142

129143

@@ -170,5 +184,8 @@ def test_conservative_vs_optimised_mode_behavior():
170184
for phase in Phase:
171185
assert limits_conservative_one[phase] == 20
172186
assert limits_optimised_one[phase] == 20
173-
assert limits_conservative_two[phase] == 17
187+
# Conservative reduces immediately, emitting the negative deficit, while
188+
# optimised tolerates the temporary overcurrent until trip risk builds.
189+
assert limits_conservative_two[phase] == -3
190+
assert limits_conservative_two[phase] < limits_optimised_two[phase]
174191
assert limits_optimised_two[phase] == 20

0 commit comments

Comments
 (0)