From 999bed78c5de5da79917d2b98fc1b357a82481a4 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 11 Jul 2026 12:43:38 +0200 Subject: [PATCH] Loadpoint: fix coarse power-limited chargers never enable on fractional current (#31685) --- core/loadpoint_effective.go | 14 +++++++++++++- core/loadpoint_effective_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go index 822dba67f..4069ce734 100644 --- a/core/loadpoint_effective.go +++ b/core/loadpoint_effective.go @@ -1,6 +1,7 @@ package core import ( + "math" "slices" "time" @@ -166,6 +167,11 @@ func (lp *Loadpoint) effectiveMinCurrent() float64 { if c, ok := api.Cap[api.PowerLimiter](lp.charger); ok { if res, _, err := c.GetMinMaxPower(); err == nil && res > 0 { chargerMin = res / (Voltage * float64(lp.minActivePhases())) + // coarse chargers truncate to full amps in setLimit, so round the + // demand up to keep the enable gate reachable (#31549) + if lp.coarseCurrent() { + chargerMin = math.Ceil(chargerMin) + } } } @@ -197,7 +203,13 @@ func (lp *Loadpoint) effectiveMaxCurrent() float64 { if c, ok := api.Cap[api.PowerLimiter](lp.charger); ok { if _, res, err := c.GetMinMaxPower(); err == nil && res > 0 { - maxCurrent = min(maxCurrent, res/(Voltage*float64(lp.maxActivePhases()))) + powerMax := res / (Voltage * float64(lp.maxActivePhases())) + // match effectiveMinCurrent's rounding so a fixed power request + // (min == max) doesn't yield min > max on coarse chargers (#31549) + if lp.coarseCurrent() { + powerMax = math.Ceil(powerMax) + } + maxCurrent = min(maxCurrent, powerMax) } } diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go index 47a3a7ae2..07727e5ee 100644 --- a/core/loadpoint_effective_test.go +++ b/core/loadpoint_effective_test.go @@ -95,6 +95,35 @@ func TestEffectivePowerLimiter(t *testing.T) { assert.Equal(t, 12.0, lp.effectiveMaxCurrent(), "max") } +// coarse power-limited charger with fixed request must not yield min > max (#31549) +func TestEffectivePowerLimiterCoarse(t *testing.T) { + Voltage = 230 + ctrl := gomock.NewController(t) + + lp := NewLoadpoint(util.NewLogger("foo"), nil) + phases := float64(lp.minActivePhases()) + + powerLimiter := api.NewMockPowerLimiter(ctrl) + // fixed 5.5 A/phase request -> fractional, coarse charger truncates to 5 A + power := 230 * phases * 5.5 + powerLimiter.EXPECT().GetMinMaxPower().Return(power, power, nil).AnyTimes() + + // MockCharger does not implement api.ChargerEx -> coarseCurrent() == true + lp.charger = struct { + api.Charger + api.PowerLimiter + }{ + Charger: api.NewMockCharger(ctrl), + PowerLimiter: powerLimiter, + } + + minCurrent := lp.effectiveMinCurrent() + maxCurrent := lp.effectiveMaxCurrent() + assert.Equal(t, 6.0, minCurrent, "min rounded up to full amps") + assert.Equal(t, 6.0, maxCurrent, "max rounded up to full amps") + assert.LessOrEqual(t, minCurrent, maxCurrent, "min must not exceed max") +} + func TestNextPlan(t *testing.T) { clock := clock.NewMock()