Loadpoint: scale down to 1p for feed-in priority in min+pv mode (#30327)

This commit is contained in:
andig 2026-05-31 11:45:04 +02:00 • committed by GitHub
parent fd94f8a376
commit 545d239495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 74 additions and 3 deletions

View file

@ -1324,6 +1324,19 @@ func (lp *Loadpoint) fastCharging() error {
return lp.setLimit(lp.effectiveMaxCurrent())
}
// minCharging scales to 1p if available and sets minimum current
func (lp *Loadpoint) minCharging() error {
if lp.hasPhaseSwitching() {
// ignore api.ErrNotAvailable: the phase switch could not be performed
// right now, continue with the current phase configuration
if err := lp.scalePhasesIfAvailable(1); err != nil && !errors.Is(err, api.ErrNotAvailable) {
return err
}
}
return lp.setLimit(lp.effectiveMinCurrent())
}
// pvScalePhases switches phases if necessary and returns number of phases switched to
func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) int {
phases := lp.GetPhases()
@ -2096,11 +2109,13 @@ func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, consumption, f
rate, _ := feedin.At(time.Now())
lp.log.DEBUG.Printf("smart feed-in active: %.2f", rate.Value)
var targetCurrent float64
if mode == api.ModeMinPV {
targetCurrent = lp.GetMinCurrent()
// minimize self-consumption to maximize feed-in by scaling down
// to the absolute minimum of 1 phase at min current
err = lp.minCharging()
} else {
err = lp.setLimit(0)
}
err = lp.setLimit(targetCurrent)
lp.resetPhaseTimer()
lp.elapsePVTimer() // let PV mode disable immediately afterwards

View file

@ -515,6 +515,62 @@ func TestScalePhasesNotAvailable(t *testing.T) {
require.Equal(t, 1, lp.GetPhases())
}
// TestMinChargingPhaseScaling verifies that minCharging scales down to 1 phase
// (the absolute minimum) when phase switching is available, so feed-in priority
// in min+pv mode drops to 1p min current instead of staying on 3p (issue #30298).
func TestMinChargingPhaseScaling(t *testing.T) {
Voltage = 230
tc := []struct {
desc string
phasesConfigured int
phases int
expectedPhases int
}{
{desc: "auto, 3p active scales down to 1p", phasesConfigured: 0, phases: 3, expectedPhases: 1},
{desc: "auto, already 1p", phasesConfigured: 0, phases: 1, expectedPhases: 1},
{desc: "fixed 3p cannot scale down", phasesConfigured: 3, phases: 3, expectedPhases: 3},
{desc: "fixed 1p stays 1p", phasesConfigured: 1, phases: 1, expectedPhases: 1},
}
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.phasesConfigured = tc.phasesConfigured
lp.phases = tc.phases
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}
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)
}
// minimum current is offered at the scaled phase count
plainCharger.EXPECT().MaxCurrent(int64(lp.minCurrent)).Return(nil)
err := lp.minCharging()
require.NoError(t, err)
require.Equal(t, tc.expectedPhases, lp.phases, tc.desc)
ctrl.Finish()
})
}
}
func TestFastChargingCircuitBasedPhaseScaling(t *testing.T) {
Voltage = 230