265 lines
7.3 KiB
Go
265 lines
7.3 KiB
Go
package core
|
|
|
|
import (
|
|
"testing"
|
|
"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"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
type mockSite struct {
|
|
site.API
|
|
maxDischargePower *float64
|
|
residualPower float64
|
|
optimized int
|
|
}
|
|
|
|
func (m *mockSite) Optimize() {
|
|
m.optimized++
|
|
}
|
|
|
|
func (m *mockSite) GetBatteryMaxDischargePower() *float64 {
|
|
return m.maxDischargePower
|
|
}
|
|
|
|
func (m *mockSite) GetResidualPower() float64 {
|
|
return m.residualPower
|
|
}
|
|
|
|
func TestBoostPower(t *testing.T) {
|
|
Voltage = 230
|
|
lp := &Loadpoint{
|
|
log: util.NewLogger("lp"),
|
|
status: api.StatusC,
|
|
batteryBoost: boostStart,
|
|
maxCurrent: 16,
|
|
phases: 3,
|
|
}
|
|
s := &mockSite{}
|
|
lp.site = s
|
|
|
|
// No max discharge power limit (nil)
|
|
s.maxDischargePower = nil
|
|
// EffectiveMaxPower will be 230 * 16 * 3 = 11040
|
|
res := lp.boostPower(0)
|
|
assert.Equal(t, 11040.0, res)
|
|
assert.Equal(t, boostContinue, lp.batteryBoost)
|
|
|
|
// Discharge power limit is 0W (battery empty)
|
|
s.maxDischargePower = new(float64)
|
|
lp.batteryBoost = boostStart
|
|
res = lp.boostPower(0)
|
|
assert.Equal(t, 0.0, res)
|
|
|
|
// With max discharge power limit
|
|
limit5000 := 5000.0
|
|
s.maxDischargePower = &limit5000
|
|
lp.batteryBoost = boostStart
|
|
res = lp.boostPower(0)
|
|
assert.Equal(t, 5000.0, res)
|
|
assert.Equal(t, boostContinue, lp.batteryBoost)
|
|
|
|
// boostContinue with limit
|
|
lp.batteryBoost = boostContinue
|
|
s.residualPower = 0
|
|
// delta = math.Max(100, 0) = 100
|
|
// plus EffectiveStepPower = 690
|
|
// delta = 790
|
|
// delta = min(790, max(0, 5000 - 0)) = 790
|
|
// res = 0 + 790 + 0 = 790
|
|
res = lp.boostPower(0)
|
|
assert.Equal(t, 790.0, res)
|
|
|
|
// boostContinue at limit
|
|
// delta = min(790, max(0, 5000 - 5000)) = 0
|
|
// res = 5000 + 0 + 0 = 5000
|
|
res = lp.boostPower(5000)
|
|
assert.Equal(t, 5000.0, res)
|
|
|
|
// boostContinue over limit
|
|
// delta = min(790, max(0, 5000 - 6000)) = 0
|
|
// res = 6000 + 0 + 0 = 6000
|
|
res = lp.boostPower(6000)
|
|
assert.Equal(t, 6000.0, res)
|
|
|
|
// boostStart while battery is charging (negative power)
|
|
// battery charging at 2000W, limit is 5000W
|
|
// max discharge capacity = 5000 - (-2000) = 7000W
|
|
// res = max(0, -2000) + 7000 + 0 = 7000W
|
|
lp.batteryBoost = boostStart
|
|
res = lp.boostPower(-2000)
|
|
assert.Equal(t, 7000.0, res)
|
|
|
|
// boostContinue while battery is charging (negative power)
|
|
// limit is 50W (less than the standard 790W delta)
|
|
// without raw negative power, delta would be restricted to 50W
|
|
// with raw negative power (-2000W), headroom is 2050W, so delta is allowed to be 790W
|
|
limit50 := 50.0
|
|
s.maxDischargePower = &limit50
|
|
s.residualPower = 0 // base delta = 100 + 690 = 790
|
|
lp.batteryBoost = boostContinue
|
|
res = lp.boostPower(-2000)
|
|
// res = max(0, -2000) + 790 + 0 = 790W
|
|
assert.Equal(t, 790.0, res)
|
|
}
|
|
|
|
type plainCharger struct {
|
|
api.Charger
|
|
}
|
|
|
|
type phaseSwitchCharger struct {
|
|
api.Charger
|
|
}
|
|
|
|
func (phaseSwitchCharger) Phases1p3p(int) error { return nil }
|
|
|
|
func TestBoostPowerPhaseSwitchGapBridging(t *testing.T) {
|
|
Voltage = 230
|
|
lp := &Loadpoint{
|
|
log: util.NewLogger("lp"),
|
|
status: api.StatusC,
|
|
charger: phaseSwitchCharger{},
|
|
batteryBoost: boostContinue,
|
|
minCurrent: 6,
|
|
maxCurrent: 16,
|
|
phases: 1,
|
|
phasesConfigured: 3,
|
|
}
|
|
s := &mockSite{}
|
|
lp.site = s
|
|
|
|
// boostContinue on 1p with phase switching: delta must cover the gap
|
|
// between 1p@16A (3680W) and 3p@6A (4140W) = 460W, plus the
|
|
// base delta (100W) and coarse step power (1p: 230W)
|
|
limit := 10000.0
|
|
s.maxDischargePower = &limit
|
|
s.residualPower = 0
|
|
res := lp.boostPower(0)
|
|
// delta = 100 (base) + 230 (step@1p) + 460 (gap) = 790
|
|
// res = 0 + 790 + 0 = 790
|
|
assert.Equal(t, 790.0, res)
|
|
// verify gap alone exceeds the 3p minimum threshold
|
|
// available_power ≈ chargePower(3680) + boostReturn(790) = 4470 > 4140
|
|
assert.Greater(t, Voltage*16+res, Voltage*6*3, "boost must bridge 1p-3p gap")
|
|
|
|
// already on 3p: no phase gap added, only base + step
|
|
lp.phases = 3
|
|
res = lp.boostPower(0)
|
|
// delta = 100 + 690 (step@3p) = 790
|
|
assert.Equal(t, 790.0, res)
|
|
}
|
|
|
|
func TestBoostPowerPhaseSwitchGapBridgingExclusions(t *testing.T) {
|
|
Voltage = 230
|
|
limit10k := 10000.0
|
|
now := time.Now()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
charger api.Charger
|
|
phasesSwitched time.Time
|
|
maxDischargePower *float64
|
|
circuitPower float64
|
|
expected float64
|
|
}{
|
|
{
|
|
name: "no phase switching",
|
|
charger: plainCharger{},
|
|
maxDischargePower: &limit10k,
|
|
circuitPower: 10000,
|
|
expected: 330,
|
|
},
|
|
{
|
|
name: "phase switch not completed",
|
|
charger: phaseSwitchCharger{},
|
|
phasesSwitched: now,
|
|
maxDischargePower: &limit10k,
|
|
circuitPower: 10000,
|
|
expected: 330,
|
|
},
|
|
{
|
|
name: "no max discharge power limit",
|
|
charger: phaseSwitchCharger{},
|
|
maxDischargePower: nil,
|
|
circuitPower: 10000,
|
|
expected: 330,
|
|
},
|
|
{
|
|
name: "circuit not allowing 3p",
|
|
charger: phaseSwitchCharger{},
|
|
maxDischargePower: &limit10k,
|
|
circuitPower: 0,
|
|
expected: 330,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
|
|
circuit := api.NewMockCircuit(ctrl)
|
|
circuit.EXPECT().ValidatePower(gomock.Any(), gomock.Any()).Return(tc.circuitPower).AnyTimes()
|
|
|
|
lp := &Loadpoint{
|
|
log: util.NewLogger("lp"),
|
|
status: api.StatusC,
|
|
charger: tc.charger,
|
|
batteryBoost: boostContinue,
|
|
minCurrent: 6,
|
|
maxCurrent: 16,
|
|
phases: 1,
|
|
phasesConfigured: 3,
|
|
phasesSwitched: tc.phasesSwitched,
|
|
circuit: circuit,
|
|
}
|
|
s := &mockSite{
|
|
maxDischargePower: tc.maxDischargePower,
|
|
}
|
|
lp.site = s
|
|
|
|
res := lp.boostPower(0)
|
|
assert.Equal(t, tc.expected, res)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
}
|