diff --git a/core/loadpoint.go b/core/loadpoint.go index 3223400aa..85ce2e0f3 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -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) { diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index ae60ea6f5..5e9572e95 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -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() } } diff --git a/core/loadpoint_boost_test.go b/core/loadpoint_boost_test.go index c05d917dc..f2223c939 100644 --- a/core/loadpoint_boost_test.go +++ b/core/loadpoint_boost_test.go @@ -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) + }) + } +}