223 lines
6 KiB
Go
223 lines
6 KiB
Go
package core
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"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"
|
|
"github.com/stretchr/testify/require"
|
|
"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,
|
|
}
|
|
state, err := site.updateMeters()
|
|
require.NoError(t, err)
|
|
state.excessDCPower = tc.excessDC
|
|
|
|
res := site.sitePower(state, 0, 0)
|
|
assert.Equal(t, tc.expSitePower, res.power, "sitePower")
|
|
assert.Equal(t, tc.expAdjustment, res.priorityAdjustment, "priority adjustment")
|
|
assert.Equal(t, tc.expReconstructed, res.power+res.priorityAdjustment, "reconstructed (unadjusted) site power")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGreenShare(t *testing.T) {
|
|
tc := []struct {
|
|
title string
|
|
grid, pv, battery, home, lp float64
|
|
greenShareTotal, greenShareHome, greenShareLoadpoints float64
|
|
}{
|
|
{
|
|
"half grid, half pv, green home",
|
|
1000, 1000, 0, 1000, 1000,
|
|
0.5, 1, 0,
|
|
},
|
|
{
|
|
"half grid, half pv, no home",
|
|
1000, 1000, 0, 0, 2000,
|
|
0.5, 1, 0.5,
|
|
},
|
|
{
|
|
"half grid, half pv, no lp",
|
|
2500, 2500, 0, 5000, 0,
|
|
0.5, 0.5, 0,
|
|
},
|
|
{
|
|
"full pv",
|
|
0, 5000, 0, 1000, 4000,
|
|
1, 1, 1,
|
|
},
|
|
{
|
|
"full grid",
|
|
5000, 0, 0, 1000, 4000,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
"half grid, half battery, green home",
|
|
1000, 0, 1000, 1000, 1000,
|
|
0.5, 1, 0,
|
|
},
|
|
{
|
|
"half grid, half battery, no home",
|
|
1000, 0, 1000, 0, 2000,
|
|
0.5, 1, 0.5,
|
|
},
|
|
{
|
|
"half grid, half battery, no lp",
|
|
1000, 0, 1000, 2000, 0,
|
|
0.5, 0.5, 0,
|
|
},
|
|
{
|
|
"full pv, pv export",
|
|
-5000, 10000, 0, 1000, 4000,
|
|
1, 1, 1,
|
|
},
|
|
{
|
|
"full pv, pv export, no lp",
|
|
-5000, 10000, 0, 5000, 0,
|
|
1, 1, 1,
|
|
},
|
|
{
|
|
"full pv, pv export, battery charge",
|
|
-2500, 10000, -2500, 1000, 4000,
|
|
1, 1, 1,
|
|
},
|
|
{
|
|
"full grid, battery charge",
|
|
3000, 0, -1000, 1000, 1000,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
"full grid, battery charge, no lp",
|
|
2000, 0, -1000, 1000, 0,
|
|
0, 0, 0,
|
|
},
|
|
{
|
|
"half grid, half pv, battery charge, no lp",
|
|
1000, 1000, -1000, 1000, 0,
|
|
0.5, 1, 0,
|
|
},
|
|
{
|
|
"half grid, half pv, battery charge, home, lp",
|
|
1000, 1000, -1000, 500, 500,
|
|
0.5, 1, 0,
|
|
},
|
|
{
|
|
"pv ac limited, battery charge & grid import",
|
|
1000, 3000, -1000, 1000, 2000,
|
|
0.75, 1, 0.5,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tc {
|
|
t.Log(tc.title)
|
|
|
|
s := &Site{
|
|
siteState: siteState{
|
|
gridPower: tc.grid,
|
|
pvPower: tc.pv,
|
|
battery: types.BatteryState{
|
|
Power: tc.battery,
|
|
},
|
|
},
|
|
}
|
|
|
|
totalPower := tc.grid + tc.pv + max(0, tc.battery)
|
|
greenShareTotal := s.greenShare(0, totalPower)
|
|
if greenShareTotal != tc.greenShareTotal {
|
|
t.Errorf("greenShareTotal wanted %.3f, got %.3f", tc.greenShareTotal, greenShareTotal)
|
|
}
|
|
greenShareHome := s.greenShare(0, tc.home)
|
|
if greenShareHome != tc.greenShareHome {
|
|
t.Errorf("greenShareHome wanted %.3f, got %.3f", tc.greenShareHome, greenShareHome)
|
|
}
|
|
greenShareLoadpoints := s.greenShare(tc.home+max(0, -tc.battery), totalPower)
|
|
if greenShareLoadpoints != tc.greenShareLoadpoints {
|
|
t.Errorf("greenShareLoadpoints wanted %.3f, got %.3f", tc.greenShareLoadpoints, greenShareLoadpoints)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRequiredBatteryMode(t *testing.T) {
|
|
tc := []struct {
|
|
gridChargeActive bool
|
|
mode, res api.BatteryMode
|
|
}{
|
|
{false, api.BatteryUnknown, api.BatteryUnknown}, // ignore
|
|
{false, api.BatteryNormal, api.BatteryUnknown}, // ignore
|
|
{false, api.BatteryHold, api.BatteryNormal},
|
|
{false, api.BatteryCharge, api.BatteryNormal},
|
|
|
|
{true, api.BatteryUnknown, api.BatteryCharge},
|
|
{true, api.BatteryNormal, api.BatteryCharge},
|
|
{true, api.BatteryHold, api.BatteryCharge},
|
|
{true, api.BatteryCharge, api.BatteryUnknown}, // ignore
|
|
}
|
|
|
|
{
|
|
// no battery
|
|
res := new(Site).requiredBatteryMode(true, api.Rate{})
|
|
assert.Equal(t, api.BatteryUnknown, res, "expected %s, got %s", api.BatteryUnknown, res)
|
|
}
|
|
|
|
for _, tc := range tc {
|
|
t.Logf("%+v", tc)
|
|
|
|
s := &Site{
|
|
batteryMeters: []config.Device[api.Meter]{nil},
|
|
batteryMode: tc.mode,
|
|
}
|
|
|
|
res := s.requiredBatteryMode(tc.gridChargeActive, api.Rate{})
|
|
assert.Equal(t, tc.res, res, "expected %s, got %s", tc.res, res)
|
|
}
|
|
}
|