Site: don't apply battery priority to loadpoints with battery boost active (#31790)
This commit is contained in:
parent
d3ae621688
commit
5aed453b3e
2 changed files with 78 additions and 5 deletions
24
core/site.go
24
core/site.go
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue