397 lines
10 KiB
Go
397 lines
10 KiB
Go
package core
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
evbus "github.com/asaskevich/EventBus"
|
|
"github.com/benbjohnson/clock"
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/core/coordinator"
|
|
"github.com/evcc-io/evcc/core/settings"
|
|
"github.com/evcc-io/evcc/core/soc"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/stretchr/testify/assert"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
func expectVehiclePublish(vehicle *api.MockVehicle) {
|
|
vehicle.EXPECT().GetTitle().Return("target").AnyTimes()
|
|
vehicle.EXPECT().Capacity().AnyTimes()
|
|
vehicle.EXPECT().Icon().AnyTimes()
|
|
vehicle.EXPECT().Features().AnyTimes()
|
|
vehicle.EXPECT().Phases().AnyTimes()
|
|
vehicle.EXPECT().OnIdentified().AnyTimes()
|
|
}
|
|
|
|
func TestPublishSocAndRange(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
clck := clock.NewMock()
|
|
|
|
charger := api.NewMockCharger(ctrl)
|
|
|
|
vehicle := api.NewMockVehicle(ctrl)
|
|
expectVehiclePublish(vehicle)
|
|
|
|
log := util.NewLogger("foo")
|
|
lp := &Loadpoint{
|
|
log: log,
|
|
bus: evbus.New(),
|
|
clock: clck,
|
|
charger: charger,
|
|
vehicle: vehicle,
|
|
chargeMeter: &Null{}, // silence nil panics
|
|
chargeRater: &Null{}, // silence nil panics
|
|
chargeTimer: &Null{}, // silence nil panics
|
|
socEstimator: soc.NewEstimator(log, charger, vehicle),
|
|
minCurrent: minA,
|
|
maxCurrent: maxA,
|
|
phases: 1,
|
|
mode: api.ModeNow,
|
|
}
|
|
|
|
// populate channels
|
|
x, y, z := createChannels(t)
|
|
attachChannels(lp, x, y, z)
|
|
|
|
assert.Empty(t, lp.socUpdated)
|
|
|
|
tc := []api.ChargeStatus{api.StatusB, api.StatusC}
|
|
|
|
for _, tc := range tc {
|
|
lp.status = tc
|
|
|
|
assert.True(t, lp.vehicleSocPollAllowed())
|
|
vehicle.EXPECT().Soc().Return(0.0, errors.New("foo"))
|
|
lp.publishSocAndRange()
|
|
|
|
clck.Add(time.Second)
|
|
|
|
allowed := tc == api.StatusC
|
|
assert.Equal(t, allowed, lp.vehicleSocPollAllowed())
|
|
if allowed {
|
|
vehicle.EXPECT().Soc().Return(0.0, errors.New("foo"))
|
|
}
|
|
lp.publishSocAndRange()
|
|
}
|
|
}
|
|
|
|
func TestPublishSocAndRangeVehiclesAndChargers(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
clck := clock.NewMock()
|
|
|
|
socVehicle := 70.0
|
|
socCharger := 80.0
|
|
|
|
vehicle := api.NewMockVehicle(ctrl)
|
|
vehicle.EXPECT().Soc().Return(socVehicle, nil).AnyTimes()
|
|
vehicle.EXPECT().Capacity().Return(8.5).AnyTimes() // enable soc-based planning
|
|
vehicle.EXPECT().Features().AnyTimes()
|
|
|
|
offlineVehicle := api.NewMockVehicle(ctrl)
|
|
offlineVehicle.EXPECT().Soc().AnyTimes()
|
|
offlineVehicle.EXPECT().Capacity().Return(8.5).AnyTimes() // enable soc-based planning
|
|
offlineVehicle.EXPECT().Features().Return([]api.Feature{api.Offline}).AnyTimes()
|
|
|
|
charger := api.NewMockCharger(ctrl)
|
|
|
|
chargerSoc := api.NewMockBattery(ctrl)
|
|
chargerSoc.EXPECT().Soc().Return(socCharger, nil).AnyTimes()
|
|
|
|
isoCharger := struct {
|
|
*api.MockCharger
|
|
*api.MockBattery
|
|
}{
|
|
charger, chargerSoc,
|
|
}
|
|
|
|
log := util.NewLogger("foo")
|
|
|
|
tc := []struct {
|
|
name string
|
|
charger api.Charger
|
|
vehicle api.Vehicle
|
|
soc float64
|
|
socBased bool
|
|
}{
|
|
{
|
|
name: "offline vehicle",
|
|
charger: charger,
|
|
vehicle: offlineVehicle,
|
|
soc: 0.0,
|
|
socBased: false,
|
|
},
|
|
{
|
|
name: "regular vehicle",
|
|
charger: charger,
|
|
vehicle: vehicle,
|
|
soc: socVehicle,
|
|
socBased: true,
|
|
},
|
|
{
|
|
name: "offline vehicle with iso charger",
|
|
charger: isoCharger,
|
|
vehicle: offlineVehicle,
|
|
soc: socCharger,
|
|
socBased: true,
|
|
},
|
|
{
|
|
name: "regular vehicle with iso charger",
|
|
charger: isoCharger,
|
|
vehicle: vehicle,
|
|
soc: socCharger,
|
|
socBased: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tc {
|
|
lp := &Loadpoint{
|
|
log: log,
|
|
bus: evbus.New(),
|
|
clock: clck,
|
|
charger: tc.charger,
|
|
vehicle: tc.vehicle,
|
|
chargeMeter: &Null{}, // silence nil panics
|
|
chargeRater: &Null{}, // silence nil panics
|
|
chargeTimer: &Null{}, // silence nil panics
|
|
minCurrent: minA,
|
|
maxCurrent: maxA,
|
|
phases: 1,
|
|
status: api.StatusC,
|
|
mode: api.ModeNow,
|
|
}
|
|
|
|
// populate channels
|
|
x, y, z := createChannels(t)
|
|
attachChannels(lp, x, y, z)
|
|
|
|
test := func(t *testing.T) {
|
|
assert.True(t, lp.vehicleSocPollAllowed())
|
|
lp.publishSocAndRange()
|
|
assert.Equal(t, tc.soc, lp.vehicleSoc)
|
|
|
|
// planner assumptions
|
|
assert.Equal(t, tc.socBased, lp.socBasedPlanning())
|
|
|
|
if tc.soc > 0 {
|
|
d := time.Duration((1 - tc.soc/100) * float64(time.Hour))
|
|
t.Log("d", d)
|
|
assert.True(t, d < lp.GetPlanRequiredDuration(100, 10e3))
|
|
}
|
|
}
|
|
|
|
t.Run(tc.name+" wo/estimator", test)
|
|
|
|
lp.socEstimator = soc.NewEstimator(log, tc.charger, tc.vehicle)
|
|
t.Run(tc.name+" w/estimator", test)
|
|
}
|
|
}
|
|
|
|
func TestVehicleDetectByID(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
|
|
v1 := api.NewMockVehicle(ctrl)
|
|
v2 := api.NewMockVehicle(ctrl)
|
|
|
|
type testcase struct {
|
|
string
|
|
id, i1, i2 string
|
|
res api.Vehicle
|
|
prepare func(testcase)
|
|
}
|
|
tc := []testcase{
|
|
{"1/_/_->0", "1", "", "", nil, func(tc testcase) {
|
|
v1.EXPECT().Identifiers().Return(nil)
|
|
v2.EXPECT().Identifiers().Return(nil)
|
|
v1.EXPECT().Identifiers().Return(nil)
|
|
v2.EXPECT().Identifiers().Return(nil)
|
|
}},
|
|
{"1/1/2->1", "1", "1", "2", v1, func(tc testcase) {
|
|
v1.EXPECT().Identifiers().Return([]string{tc.i1})
|
|
}},
|
|
{"2/1/2->2", "2", "1", "2", v2, func(tc testcase) {
|
|
v1.EXPECT().Identifiers().Return([]string{tc.i1})
|
|
v2.EXPECT().Identifiers().Return([]string{tc.i2})
|
|
}},
|
|
{"11/1*/2->1", "11", "1*", "2", v1, func(tc testcase) {
|
|
v1.EXPECT().Identifiers().Return([]string{tc.i1})
|
|
v2.EXPECT().Identifiers().Return([]string{tc.i2})
|
|
v1.EXPECT().Identifiers().Return([]string{tc.i1})
|
|
// v2.EXPECT().Identifiers().Return([]string{tc.i2})
|
|
}},
|
|
{"22/1*/2*->2", "22", "1*", "2*", v2, func(tc testcase) {
|
|
v1.EXPECT().Identifiers().Return([]string{tc.i1})
|
|
v2.EXPECT().Identifiers().Return([]string{tc.i2})
|
|
v1.EXPECT().Identifiers().Return([]string{tc.i1})
|
|
v2.EXPECT().Identifiers().Return([]string{tc.i2})
|
|
}},
|
|
{"2/_/*->2", "2", "", "*", v2, func(tc testcase) {
|
|
v1.EXPECT().Identifiers().Return(nil)
|
|
v2.EXPECT().Identifiers().Return([]string{tc.i2})
|
|
v1.EXPECT().Identifiers().Return(nil)
|
|
v2.EXPECT().Identifiers().Return([]string{tc.i2})
|
|
}},
|
|
}
|
|
|
|
for _, tc := range tc {
|
|
t.Logf("%+v", tc)
|
|
|
|
lp := &Loadpoint{
|
|
log: util.NewLogger("foo"),
|
|
}
|
|
|
|
lp.coordinator = coordinator.NewAdapter(lp, coordinator.New(util.NewLogger("foo"), []api.Vehicle{v1, v2}))
|
|
|
|
if tc.prepare != nil {
|
|
tc.prepare(tc)
|
|
}
|
|
|
|
if res := lp.selectVehicleByID(tc.id); tc.res != res {
|
|
t.Errorf("expected %v, got %v", tc.res, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultVehicle(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
|
|
mode := api.ModePV
|
|
current := 66.6
|
|
|
|
dflt := api.NewMockVehicle(ctrl)
|
|
dflt.EXPECT().GetTitle().Return("default").AnyTimes()
|
|
dflt.EXPECT().Icon().Return("").AnyTimes()
|
|
dflt.EXPECT().Capacity().AnyTimes()
|
|
dflt.EXPECT().Phases().AnyTimes()
|
|
dflt.EXPECT().OnIdentified().Return(api.ActionConfig{
|
|
Mode: mode,
|
|
MinCurrent: current,
|
|
}).AnyTimes()
|
|
|
|
vehicle := api.NewMockVehicle(ctrl)
|
|
vehicle.EXPECT().GetTitle().Return("target").AnyTimes()
|
|
vehicle.EXPECT().Icon().Return("").AnyTimes()
|
|
vehicle.EXPECT().Capacity().AnyTimes()
|
|
vehicle.EXPECT().Phases().AnyTimes()
|
|
vehicle.EXPECT().OnIdentified().AnyTimes()
|
|
|
|
lp := NewLoadpoint(util.NewLogger("foo"), settings.NewDatabaseSettingsAdapter("foo"))
|
|
lp.DefaultMode = api.ModeOff // ondisconnect
|
|
lp.defaultVehicle = dflt
|
|
|
|
// populate channels
|
|
x, y, z := createChannels(t)
|
|
attachChannels(lp, x, y, z)
|
|
|
|
title := func(v api.Vehicle) string {
|
|
if v == nil {
|
|
return "<nil>"
|
|
}
|
|
return v.GetTitle()
|
|
}
|
|
|
|
// non-default vehicle identified
|
|
lp.setActiveVehicle(vehicle)
|
|
assert.Equal(t, vehicle, lp.vehicle, "expected vehicle "+title(vehicle))
|
|
assert.Equal(t, 6.0, lp.effectiveMinCurrent(), "current")
|
|
|
|
// non-default vehicle disconnected
|
|
lp.evVehicleDisconnectHandler()
|
|
assert.Equal(t, dflt, lp.vehicle, "expected default vehicle")
|
|
assert.Equal(t, mode, lp.GetMode(), "mode")
|
|
assert.Equal(t, current, lp.effectiveMinCurrent(), "current")
|
|
|
|
// default vehicle disconnected and reconnected
|
|
lp.evVehicleDisconnectHandler()
|
|
assert.Equal(t, mode, lp.GetMode(), "mode")
|
|
assert.Equal(t, current, lp.effectiveMinCurrent(), "current")
|
|
|
|
// set non-default vehicle during disconnect - should be default on connect
|
|
lp.tasks.Clear()
|
|
lp.evVehicleConnectHandler()
|
|
assert.Equal(t, dflt, lp.vehicle, "expected default vehicle")
|
|
assert.Equal(t, 1, lp.tasks.Size(), "task queue length")
|
|
|
|
// guest connected
|
|
lp.setActiveVehicle(nil)
|
|
assert.Nil(t, lp.vehicle, "expected no vehicle")
|
|
}
|
|
|
|
func TestReconnectVehicle(t *testing.T) {
|
|
tc := []struct {
|
|
name string
|
|
vehicleId []string
|
|
}{
|
|
{"without vehicle id", nil},
|
|
{"with vehicle id", []string{"foo"}},
|
|
}
|
|
|
|
for _, tc := range tc {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
clck := clock.NewMock()
|
|
|
|
type vehicleT struct {
|
|
*api.MockVehicle
|
|
*api.MockChargeState
|
|
}
|
|
|
|
v := api.NewMockVehicle(ctrl)
|
|
vehicle := &vehicleT{v, api.NewMockChargeState(ctrl)}
|
|
expectVehiclePublish(v)
|
|
vehicle.MockVehicle.EXPECT().Identifiers().AnyTimes().Return(tc.vehicleId)
|
|
vehicle.MockVehicle.EXPECT().Soc().Return(0.0, nil).AnyTimes()
|
|
|
|
charger := api.NewMockCharger(ctrl)
|
|
charger.EXPECT().Status().Return(api.StatusB, nil).AnyTimes()
|
|
|
|
lp := &Loadpoint{
|
|
log: util.NewLogger("foo"),
|
|
bus: evbus.New(),
|
|
clock: clck,
|
|
charger: charger,
|
|
chargeMeter: &Null{}, // silence nil panics
|
|
chargeRater: &Null{}, // silence nil panics
|
|
chargeTimer: &Null{}, // silence nil panics
|
|
wakeUpTimer: NewTimer(),
|
|
minCurrent: minA,
|
|
maxCurrent: maxA,
|
|
phases: 1,
|
|
mode: api.ModeNow,
|
|
}
|
|
|
|
lp.coordinator = coordinator.NewAdapter(lp, coordinator.New(util.NewLogger("foo"), []api.Vehicle{vehicle}))
|
|
|
|
attachListeners(t, lp)
|
|
|
|
// mode now
|
|
charger.EXPECT().MaxCurrent(int64(maxA))
|
|
// sync charger
|
|
charger.EXPECT().Enabled().Return(true, nil)
|
|
|
|
// vehicle not updated yet
|
|
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)
|
|
|
|
lp.Update(0, 0, nil, nil, false, false, 0, nil, nil)
|
|
ctrl.Finish()
|
|
|
|
// detection started
|
|
assert.Equal(t, lp.clock.Now(), lp.vehicleDetect, "vehicle detection not started")
|
|
|
|
// vehicle not detected yet
|
|
assert.Nil(t, lp.vehicle, "vehicle should be <nil>")
|
|
|
|
// sync charger
|
|
charger.EXPECT().Enabled().Return(true, nil)
|
|
// vehicle not updated yet
|
|
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil)
|
|
|
|
lp.Update(0, 0, nil, nil, false, false, 0, nil, nil)
|
|
ctrl.Finish()
|
|
|
|
// vehicle detected
|
|
assert.Equal(t, vehicle, lp.vehicle, "vehicle should be detected")
|
|
})
|
|
}
|
|
}
|