Battery Boost: allow scaling to 3p while active (#32776)

Co-authored-by: andig <cpuidle@gmail.com>
This commit is contained in:
mfuchs1984 2026-08-14 12:50:40 +02:00 • committed by GitHub
parent f47dd16e61
commit f7e7ab887e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 0 deletions

View file

@ -1590,6 +1590,16 @@ func (lp *Loadpoint) boostPower(batteryPower float64) float64 {
delta += lp.EffectiveStepPower()
}
// bridge the power gap between 1p max and 3p min so pvScalePhases can trigger a scale-up
if lp.hasPhaseSwitching() && lp.phaseSwitchCompleted() && lp.site.GetBatteryMaxDischargePower() != nil {
if activePhases, maxPhases := lp.ActivePhases(), lp.MaxActivePhases(); activePhases < maxPhases &&
lp.circuitAllowsPhases(maxPhases, lp.effectiveMinCurrent()) {
// max power actually achievable on the active phases
activeMaxPower := min(lp.EffectiveMaxPower(), Voltage*lp.effectiveMaxCurrent()*float64(activePhases))
delta += max(0, lp.EffectiveMinPower()*float64(maxPhases)-activeMaxPower)
}
}
// start boosting by setting maximum power
if boost == boostStart {
delta = lp.EffectiveMaxPower()

View file

@ -2,11 +2,13 @@ package core
import (
"testing"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/util"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
type mockSite struct {
@ -104,3 +106,121 @@ func TestBoostPower(t *testing.T) {
// res = max(0, -2000) + 790 + 0 = 790W
assert.Equal(t, 790.0, res)
}
type plainCharger struct {
api.Charger
}
type phaseSwitchCharger struct {
api.Charger
}
func (phaseSwitchCharger) Phases1p3p(int) error { return nil }
func TestBoostPowerPhaseSwitchGapBridging(t *testing.T) {
Voltage = 230
lp := &Loadpoint{
log: util.NewLogger("lp"),
status: api.StatusC,
charger: phaseSwitchCharger{},
batteryBoost: boostContinue,
minCurrent: 6,
maxCurrent: 16,
phases: 1,
phasesConfigured: 3,
}
s := &mockSite{}
lp.site = s
// boostContinue on 1p with phase switching: delta must cover the gap
// between 1p@16A (3680W) and 3p@6A (4140W) = 460W, plus the
// base delta (100W) and coarse step power (1p: 230W)
limit := 10000.0
s.maxDischargePower = &limit
s.residualPower = 0
res := lp.boostPower(0)
// delta = 100 (base) + 230 (step@1p) + 460 (gap) = 790
// res = 0 + 790 + 0 = 790
assert.Equal(t, 790.0, res)
// verify gap alone exceeds the 3p minimum threshold
// available_power ≈ chargePower(3680) + boostReturn(790) = 4470 > 4140
assert.Greater(t, Voltage*16+res, Voltage*6*3, "boost must bridge 1p-3p gap")
// already on 3p: no phase gap added, only base + step
lp.phases = 3
res = lp.boostPower(0)
// delta = 100 + 690 (step@3p) = 790
assert.Equal(t, 790.0, res)
}
func TestBoostPowerPhaseSwitchGapBridgingExclusions(t *testing.T) {
Voltage = 230
limit10k := 10000.0
now := time.Now()
for _, tc := range []struct {
name string
charger api.Charger
phasesSwitched time.Time
maxDischargePower *float64
circuitPower float64
expected float64
}{
{
name: "no phase switching",
charger: plainCharger{},
maxDischargePower: &limit10k,
circuitPower: 10000,
expected: 330,
},
{
name: "phase switch not completed",
charger: phaseSwitchCharger{},
phasesSwitched: now,
maxDischargePower: &limit10k,
circuitPower: 10000,
expected: 330,
},
{
name: "no max discharge power limit",
charger: phaseSwitchCharger{},
maxDischargePower: nil,
circuitPower: 10000,
expected: 330,
},
{
name: "circuit not allowing 3p",
charger: phaseSwitchCharger{},
maxDischargePower: &limit10k,
circuitPower: 0,
expected: 330,
},
} {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
circuit := api.NewMockCircuit(ctrl)
circuit.EXPECT().ValidatePower(gomock.Any(), gomock.Any()).Return(tc.circuitPower).AnyTimes()
lp := &Loadpoint{
log: util.NewLogger("lp"),
status: api.StatusC,
charger: tc.charger,
batteryBoost: boostContinue,
minCurrent: 6,
maxCurrent: 16,
phases: 1,
phasesConfigured: 3,
phasesSwitched: tc.phasesSwitched,
circuit: circuit,
}
s := &mockSite{
maxDischargePower: tc.maxDischargePower,
}
lp.site = s
res := lp.boostPower(0)
assert.Equal(t, tc.expected, res)
})
}
}