From 56c1d56626245bdf27abff1677037d37d33f68e1 Mon Sep 17 00:00:00 2001 From: mfuchs1984 <57141790+mfuchs1984@users.noreply.github.com> Date: Sat, 7 Mar 2026 09:46:46 +0100 Subject: [PATCH] Loadpoint: fix fast charging phase scaling (#27972) --- core/loadpoint.go | 10 +++-- core/loadpoint_phases_test.go | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 8d85af6fe..45401bb02 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1282,12 +1282,14 @@ func (lp *Loadpoint) scalePhases(phases int) error { func (lp *Loadpoint) fastCharging() error { if lp.hasPhaseSwitching() { phases := 3 - maxPower1p := Voltage * lp.effectiveMaxCurrent() // load management limit active - if circuitMaxPower := circuitMaxPower(lp.circuit); circuitMaxPower > 0 && circuitMaxPower < 1.1*maxPower1p { - phases = 1 - lp.log.DEBUG.Printf("fast charging: scaled to 1p to match %.0fW max circuit power", circuitMaxPower) + if lp.circuit != nil { + minPower3p := currentToPower(lp.effectiveMinCurrent(), 3) + if powerLimit := lp.circuit.ValidatePower(lp.chargePower, minPower3p); powerLimit < minPower3p { + phases = 1 + lp.log.DEBUG.Printf("fast charging: scaled to 1p to match %.0fW available circuit power", powerLimit) + } } if err := lp.scalePhasesIfAvailable(phases); err != nil { diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go index eefb6f6d4..d7e720cba 100644 --- a/core/loadpoint_phases_test.go +++ b/core/loadpoint_phases_test.go @@ -479,3 +479,74 @@ func TestScalePhasesIfAvailable(t *testing.T) { ctrl.Finish() } } + +func TestFastChargingCircuitBasedPhaseScaling(t *testing.T) { + Voltage = 230 + + tc := []struct { + desc string + phases int + chargePower float64 + availableCircuitPower float64 // ValidatePower return for 3p request + expectedPhases int + noCircuit bool + }{ + {desc: "no circuit", phases: 3, chargePower: 0, expectedPhases: 3, noCircuit: true}, + {desc: "low limit, no surplus", phases: 3, chargePower: 0, availableCircuitPower: 3680, expectedPhases: 1}, + {desc: "low limit, with surplus", phases: 1, chargePower: 0, availableCircuitPower: 11040, expectedPhases: 3}, + {desc: "already charging, low limit", phases: 3, chargePower: 3680, availableCircuitPower: 3680, expectedPhases: 1}, + {desc: "already charging, high limit", phases: 1, chargePower: 3680, availableCircuitPower: 11040, expectedPhases: 3}, + {desc: "edge case: just below 3p minimum", phases: 3, chargePower: 0, availableCircuitPower: 4140 - 1, expectedPhases: 1}, + {desc: "edge case: just at 3p minimum", phases: 1, chargePower: 0, availableCircuitPower: 4140, expectedPhases: 3}, + } + + for _, tc := range tc { + t.Run(tc.desc, func(t *testing.T) { + ctrl := gomock.NewController(t) + + lp := NewLoadpoint(util.NewLogger("foo"), nil) + lp.minCurrent = 6 + lp.maxCurrent = 16 + lp.phases = tc.phases + lp.chargePower = tc.chargePower + lp.offeredCurrent = 0 // ensure MaxCurrent is called + lp.wakeUpTimer = NewTimer() + + plainCharger := api.NewMockCharger(ctrl) + phaseCharger := api.NewMockPhaseSwitcher(ctrl) + lp.charger = struct { + *api.MockCharger + *api.MockPhaseSwitcher + }{plainCharger, phaseCharger} + + if !tc.noCircuit { + circuit := api.NewMockCircuit(ctrl) + lp.circuit = circuit + + minPower3p := Voltage * 6 * 3 + + // fastCharging call to ValidatePower + circuit.EXPECT().ValidatePower(tc.chargePower, minPower3p).Return(tc.availableCircuitPower) + + // setLimit calls + circuit.EXPECT().ValidateCurrent(gomock.Any(), lp.maxCurrent).Return(lp.maxCurrent) + circuit.EXPECT().ValidatePower(tc.chargePower, float64(tc.expectedPhases)*Voltage*lp.maxCurrent).Return(float64(tc.expectedPhases) * Voltage * lp.maxCurrent) + } + + plainCharger.EXPECT().Enabled().Return(true, nil).AnyTimes() + plainCharger.EXPECT().Enable(gomock.Any()).Return(nil).AnyTimes() + + if tc.phases != tc.expectedPhases { + phaseCharger.EXPECT().Phases1p3p(tc.expectedPhases).Return(nil) + } + + plainCharger.EXPECT().MaxCurrent(int64(lp.maxCurrent)).Return(nil) + + err := lp.fastCharging() + require.NoError(t, err) + require.Equal(t, tc.expectedPhases, lp.phases, tc.desc) + + ctrl.Finish() + }) + } +}