Site: don't apply battery priority to loadpoints with battery boost active (#31790)

This commit is contained in:
Alexandre JARDON 2026-07-18 17:58:53 +02:00 • committed by GitHub
parent d3ae621688
commit 5aed453b3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 5 deletions

View file

@ -939,9 +939,12 @@ func optimizerEnabled() bool {
// - the net power exported by the site minus a residual margin
// (negative values mean grid: export, battery: charging
// - if battery buffer can be used for charging
func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, bool, bool, error) {
// - the adjustment applied to sitePower for battery priority below prioritySoc;
// adding it back restores the unadjusted site power for a loadpoint that
// takes priority over the battery (battery boost)
func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, bool, bool, float64, error) {
if err := site.updateMeters(); err != nil {
return 0, false, false, err
return 0, false, false, 0, err
}
// allow using PV as estimate for grid power
@ -950,10 +953,14 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
site.publish(keys.Grid, types.Measurement{Power: site.gridPower})
}
// sitePower adjustment applied for battery priority
var priorityAdjustment float64
// ensure safe default for residual power
residualPower := site.GetResidualPower()
if len(site.batteryMeters) > 0 && site.battery.Soc < site.prioritySoc && residualPower <= 0 {
residualPower = 100 // Wsite.publish(keys.PvPower,
priorityAdjustment += residualPower - 100
residualPower = 100 // W
}
// allow using grid and charge as estimate for pv power
@ -980,6 +987,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
// if battery is charging below prioritySoc give it priority
if site.battery.Soc < site.prioritySoc && batteryPower < 0 {
site.log.DEBUG.Printf("battery has priority at soc %.0f%% (< %.0f%%)", site.battery.Soc, site.prioritySoc)
priorityAdjustment += batteryPower + excessDCPower
batteryPower = 0
excessDCPower = 0
} else {
@ -999,7 +1007,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
site.log.DEBUG.Printf("site power: %.0fW"+flexStr, sitePower)
return sitePower, batteryBuffered, batteryStart, nil
return sitePower, batteryBuffered, batteryStart, priorityAdjustment, nil
}
// updateLoadpoints updates all loadpoints' charge power
@ -1105,7 +1113,7 @@ func (site *Site) update(lp updater) {
flexiblePower = site.prioritizer.GetChargePowerFlexibility(lp)
}
if sitePower, batteryBuffered, batteryStart, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
if sitePower, batteryBuffered, batteryStart, priorityAdjustment, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
// ignore negative pvPower values as that means it is not an energy source but consumption
homePower := site.gridPower + max(0, site.pvPower) + site.battery.Power - totalChargePower
homePower = max(homePower, 0)
@ -1128,6 +1136,12 @@ func (site *Site) update(lp updater) {
// reserve surplus claimed by higher-priority loadpoints that are starting up (#31194)
sitePower += site.reservedPVPower(lp)
// battery boost deliberately drains the battery, hence battery priority
// below prioritySoc does not apply to the boosting loadpoint (#30541)
if lp.GetBatteryBoost() != boostDisabled {
sitePower += priorityAdjustment
}
lp.Update(
sitePower, max(0, site.battery.Power), consumption, feedin, batteryBuffered, batteryStart,
greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints),

View file

@ -5,10 +5,69 @@ import (
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/types"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
// TestSitePowerPriorityAdjustment verifies that sitePower returns the adjustment
// applied for battery priority below prioritySoc, such that adding it back yields
// the unadjusted site power for loadpoints with battery boost active (#30541)
func TestSitePowerPriorityAdjustment(t *testing.T) {
const prioritySoc = 50
for _, tc := range []struct {
name string
soc, power, excessDC float64 // battery
expSitePower, expAdjustment float64
expReconstructed float64 // sitePower + adjustment: the unadjusted site power a boost loadpoint sees
}{
// battery priority does not apply: no adjustment
{"charging above prioritySoc", 80, -2000, 0, -2000, 0, -2000},
// battery charge power hidden and residual power forced to 100W:
// adding the adjustment back restores the unadjusted -2000W
{"charging below prioritySoc", 30, -2000, 0, 100, -2100, -2000},
// battery not charging: only the forced residual power applies
{"discharging below prioritySoc", 30, 500, 0, 600, -100, 500},
// excess DC power can only reach the battery, never the (AC) vehicle, so it
// must stay netted out of the reconstructed surplus: of 2000W charging with
// 500W un-redirectable DC excess, only 1500W is available to a boost loadpoint
{"charging below prioritySoc with excess DC", 30, -2000, 500, 100, -1600, -1500},
} {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
meter := api.NewMockMeter(ctrl)
meter.EXPECT().CurrentPower().Return(tc.power, nil).AnyTimes()
battery := api.NewMockBattery(ctrl)
battery.EXPECT().Soc().Return(tc.soc, nil).AnyTimes()
var bat api.Meter = &struct {
api.Meter
api.Battery
}{
Meter: meter,
Battery: battery,
}
site := &Site{
log: util.NewLogger("foo"),
batteryMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, bat)},
prioritySoc: prioritySoc,
}
site.excessDCPower = tc.excessDC
sitePower, _, _, adjustment, err := site.sitePower(0, 0)
assert.NoError(t, err)
assert.Equal(t, tc.expSitePower, sitePower, "sitePower")
assert.Equal(t, tc.expAdjustment, adjustment, "priority adjustment")
assert.Equal(t, tc.expReconstructed, sitePower+adjustment, "reconstructed (unadjusted) site power")
})
}
}
func TestGreenShare(t *testing.T) {
tc := []struct {
title string