Battery boost: resume from hold when limit is lowered (#32999)

This commit is contained in:
github-actions[bot] 2026-08-21 11:05:53 +02:00 • committed by GitHub
parent 769a6a1aa5
commit 13db309c94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 3 deletions

View file

@ -2105,8 +2105,9 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
func (lp *Loadpoint) Update(sitePower, batteryPower float64, consumption, feedin api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64, dim *bool) {
// hold battery boost when SOC drops below the limit: stop draining the battery, but
// keep the vehicle prioritised over recharging it (via sitePower priorityAdjustment)
// until the vehicle disconnects. This holds the battery at the configured level
// instead of the naive on/off which lets the battery recharge and oscillate (#30558).
// until the vehicle disconnects or the limit is relaxed (see SetBatteryBoostLimit).
// This holds the battery at the configured level instead of the naive on/off which
// lets the battery recharge and oscillate (#30558).
if boost := lp.GetBatteryBoost(); boost != boostDisabled && boost != boostHold {
if limit := lp.GetBatteryBoostLimit(); limit < 100 {
if batterySoc := lp.site.GetBatterySoc(); batterySoc < float64(limit) {

View file

@ -680,7 +680,8 @@ func (lp *Loadpoint) GetBatteryBoostLimit() int {
return lp.batteryBoostLimit
}
// SetBatteryBoostLimit sets the battery boost soc limit
// SetBatteryBoostLimit sets the battery boost soc limit. Relaxing the limit
// resumes a boost held by it, 100 disables the feature and ends an active boost.
func (lp *Loadpoint) SetBatteryBoostLimit(limit int) {
lp.Lock()
defer lp.Unlock()
@ -688,9 +689,24 @@ func (lp *Loadpoint) SetBatteryBoostLimit(limit int) {
lp.log.DEBUG.Println("set battery boost limit:", limit)
if lp.batteryBoostLimit != limit {
relaxed := limit < lp.batteryBoostLimit && lp.batteryBoostLimit < 100
lp.batteryBoostLimit = limit
lp.settings.SetInt(keys.BatteryBoostLimit, int64(limit))
lp.publish(keys.BatteryBoostLimit, limit)
// lock is held, hence assign instead of setBatteryBoost
switch {
case limit == 100 && lp.batteryBoost != boostDisabled:
lp.log.DEBUG.Println("battery boost disable: limit removed")
lp.batteryBoost = boostDisabled
lp.publish(keys.BatteryBoost, false)
case relaxed && lp.batteryBoost == boostHold:
lp.log.DEBUG.Println("battery boost resume: limit relaxed")
lp.batteryBoost = boostStart
}
lp.requestUpdate()
}
}

View file

@ -5,6 +5,7 @@ import (
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/settings"
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/util"
"github.com/stretchr/testify/assert"
@ -224,3 +225,41 @@ func TestBoostPowerPhaseSwitchGapBridgingExclusions(t *testing.T) {
})
}
}
// Relaxing the limit resumes a boost it put on hold, tightening it does not.
// Setting 100 disables the feature and ends an active boost.
func TestSetBatteryBoostLimitResume(t *testing.T) {
for _, tc := range []struct {
name string
boost int
from, to int
want int
wantUpdate bool
}{
{"lowering the limit resumes", boostHold, 50, 30, boostStart, true},
{"removing the limit ends held boost", boostHold, 50, 100, boostDisabled, true},
{"removing the limit ends active boost", boostContinue, 50, 100, boostDisabled, true},
{"raising the limit holds", boostHold, 30, 50, boostHold, true},
{"enabling a limit holds", boostHold, 100, 50, boostHold, true},
{"unchanged limit holds", boostHold, 50, 50, boostHold, false},
{"only a held boost resumes", boostContinue, 50, 30, boostContinue, true},
{"disabled boost stays disabled", boostDisabled, 50, 30, boostDisabled, true},
} {
t.Run(tc.name, func(t *testing.T) {
lp := &Loadpoint{
log: util.NewLogger("lp"),
settings: settings.NewDatabaseSettingsAdapter("foo"),
lpChan: make(chan *Loadpoint, 1),
batteryBoost: tc.boost,
batteryBoostLimit: tc.from,
}
lp.SetBatteryBoostLimit(tc.to)
assert.Equal(t, tc.to, lp.batteryBoostLimit)
assert.Equal(t, tc.want, lp.batteryBoost)
// a changed limit must act now instead of on the next update tick
assert.Equal(t, tc.wantUpdate, len(lp.lpChan) == 1)
})
}
}