diff --git a/core/site.go b/core/site.go index 27f4f025e..9e6b276a5 100644 --- a/core/site.go +++ b/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), diff --git a/core/site_test.go b/core/site_test.go index a1ad32903..c53316de3 100644 --- a/core/site_test.go +++ b/core/site_test.go @@ -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