From 7ac1a9a962372ed2b18632cb4360cdcc16cd6cd1 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 10 Aug 2021 14:53:10 +0200 Subject: [PATCH] Fix vehicle detection across multiple loadpoints (#1308) --- core/coordinator.go | 75 ++++++++++++++++++++++++++++++++++++++++ core/coordinator_test.go | 63 +++++++++++++++++++++++++++++++++ core/loadpoint.go | 34 +++++------------- core/loadpoint_test.go | 58 ------------------------------- 4 files changed, 146 insertions(+), 84 deletions(-) create mode 100644 core/coordinator.go create mode 100644 core/coordinator_test.go diff --git a/core/coordinator.go b/core/coordinator.go new file mode 100644 index 000000000..d2ee716eb --- /dev/null +++ b/core/coordinator.go @@ -0,0 +1,75 @@ +package core + +import ( + "github.com/andig/evcc/api" + "github.com/andig/evcc/util" +) + +type vehicleCoordinator struct { + tracked map[api.Vehicle]interface{} +} + +var coordinator *vehicleCoordinator + +func init() { + coordinator = &vehicleCoordinator{ + tracked: make(map[api.Vehicle]interface{}), + } +} + +func (lp *vehicleCoordinator) aquire(owner interface{}, vehicle api.Vehicle) { + lp.tracked[vehicle] = owner +} + +func (lp *vehicleCoordinator) release(vehicle api.Vehicle) { + delete(lp.tracked, vehicle) +} + +func (lp *vehicleCoordinator) availableVehicles(owner interface{}, vehicles []api.Vehicle) []api.Vehicle { + var res []api.Vehicle + + for _, vv := range vehicles { + if _, ok := vv.(api.ChargeState); ok { + if o, ok := lp.tracked[vv]; o == owner || !ok { + res = append(res, vv) + } + } + } + + return res +} + +// find active vehicle by charge state +func (lp *vehicleCoordinator) findActiveVehicleByStatus(log *util.Logger, owner interface{}, vehicles []api.Vehicle) api.Vehicle { + var res api.Vehicle + + available := lp.availableVehicles(owner, vehicles) + // log.DEBUG.Printf("!!available vehicles: %v", funk.Map(available, func(v api.Vehicle) string { + // return v.Title() + // })) + + for _, vehicle := range available { + if vs, ok := vehicle.(api.ChargeState); ok { + status, err := vs.Status() + + if err != nil { + log.ERROR.Println("vehicle status:", err) + continue + } + + log.DEBUG.Printf("vehicle status: %s (%s)", status, vehicle.Title()) + + // vehicle is plugged or charging, so it should be the right one + if status == api.StatusB || status == api.StatusC { + if res != nil { + log.DEBUG.Printf("vehicle status: >1 matches, giving up") + return nil + } + + res = vehicle + } + } + } + + return res +} diff --git a/core/coordinator_test.go b/core/coordinator_test.go new file mode 100644 index 000000000..f424aee63 --- /dev/null +++ b/core/coordinator_test.go @@ -0,0 +1,63 @@ +package core + +import ( + "testing" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/mock" + "github.com/andig/evcc/util" + "github.com/golang/mock/gomock" +) + +func TestVehicleDetectByStatus(t *testing.T) { + ctrl := gomock.NewController(t) + + type vehicle struct { + *mock.MockVehicle + *mock.MockChargeState + } + + v1 := &vehicle{mock.NewMockVehicle(ctrl), mock.NewMockChargeState(ctrl)} + v2 := &vehicle{mock.NewMockVehicle(ctrl), mock.NewMockChargeState(ctrl)} + + type testcase struct { + string + v1, v2 api.ChargeStatus + res api.Vehicle + } + tc := []testcase{ + {"A/A->0", api.StatusA, api.StatusA, nil}, + {"B/A->1", api.StatusB, api.StatusA, v1}, + {"B/A->1", api.StatusB, api.StatusA, v1}, + {"A/B->2", api.StatusA, api.StatusB, v2}, + {"A/B->2", api.StatusA, api.StatusB, v2}, + {"B/B->1", api.StatusB, api.StatusB, nil}, + } + + log := util.NewLogger("foo") + vehicles := []api.Vehicle{v1, v2} + + lp := &LoadPoint{} + c := &vehicleCoordinator{make(map[api.Vehicle]interface{})} + + for _, tc := range tc { + t.Logf("%+v", tc) + + v1.MockChargeState.EXPECT().Status().Return(tc.v1, nil) + v2.MockChargeState.EXPECT().Status().Return(tc.v2, nil) + v1.MockVehicle.EXPECT().Title().Return("v1") + v2.MockVehicle.EXPECT().Title().Return("v2") + + res := c.findActiveVehicleByStatus(log, lp, vehicles) + if tc.res != res { + t.Errorf("expected %v, got %v", tc.res, res) + } + + if res != nil { + c.aquire(lp, res) + } else { + c.release(res) + } + } + +} diff --git a/core/loadpoint.go b/core/loadpoint.go index f7689ef78..1ab21219b 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -427,10 +427,13 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even lp.publish("minSoC", lp.SoC.Min) lp.Unlock() - // use first vehicle for estimator // run during prepare() to ensure cache has been attached if len(lp.vehicles) > 0 { - lp.setActiveVehicle(lp.vehicles[0]) + // associate first vehicle if it cannot be auto-detected + if _, ok := lp.vehicles[0].(api.ChargeState); !ok { + lp.setActiveVehicle(lp.vehicles[0]) + } + lp.startVehicleDetection() } @@ -607,10 +610,12 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) { from := "unknown" if lp.vehicle != nil { + coordinator.release(lp.vehicle) from = lp.vehicle.Title() } to := "unknown" if vehicle != nil { + coordinator.aquire(lp, vehicle) to = vehicle.Title() } lp.log.INFO.Printf("vehicle updated: %s -> %s", from, to) @@ -691,29 +696,6 @@ func (lp *LoadPoint) findActiveVehicleByID() api.Vehicle { return nil } -// find active vehicle by charge state -func (lp *LoadPoint) findActiveVehicleByStatus() api.Vehicle { - for _, vehicle := range lp.vehicles { - if vs, ok := vehicle.(api.ChargeState); ok { - status, err := vs.Status() - - if err != nil { - lp.log.ERROR.Println("vehicle status:", err) - return nil - } - - lp.log.DEBUG.Printf("vehicle status: %s (%s)", status, vehicle.Title()) - - // vehicle is plugged or charging, so it should be the right one - if status == api.StatusB || status == api.StatusC { - return vehicle - } - } - } - - return nil -} - // findActiveVehicle validates if the active vehicle is still connected to the loadpoint func (lp *LoadPoint) findActiveVehicle() { if len(lp.vehicles) <= 1 { @@ -725,7 +707,7 @@ func (lp *LoadPoint) findActiveVehicle() { return } - if vehicle := lp.findActiveVehicleByStatus(); vehicle != nil { + if vehicle := coordinator.findActiveVehicleByStatus(lp.log, lp, lp.vehicles); vehicle != nil { lp.setActiveVehicle(vehicle) return } diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index afbb24a51..01d8dde8e 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -828,61 +828,3 @@ func TestVehicleDetectByID(t *testing.T) { } } } - -func TestVehicleDetectByStatus(t *testing.T) { - ctrl := gomock.NewController(t) - - type vehicle struct { - *mock.MockVehicle - *mock.MockChargeState - } - - v1 := &vehicle{mock.NewMockVehicle(ctrl), mock.NewMockChargeState(ctrl)} - v2 := &vehicle{mock.NewMockVehicle(ctrl), mock.NewMockChargeState(ctrl)} - - type testcase struct { - string - v1, v2 api.ChargeStatus - res api.Vehicle - prepare func(testcase) - } - tc := []testcase{ - {"A/A->0", api.StatusA, api.StatusA, nil, func(t testcase) { - v1.MockChargeState.EXPECT().Status().Return(t.v1, nil) - v1.MockVehicle.EXPECT().Title().Return("v1") - v2.MockChargeState.EXPECT().Status().Return(t.v2, nil) - v2.MockVehicle.EXPECT().Title().Return("v2") - }}, - {"B/A->1", api.StatusB, api.StatusA, v1, func(t testcase) { - v1.MockChargeState.EXPECT().Status().Return(t.v1, nil) - v1.MockVehicle.EXPECT().Title().Return("v1") - }}, - {"A/B->2", api.StatusA, api.StatusB, v2, func(t testcase) { - v1.MockChargeState.EXPECT().Status().Return(t.v1, nil) - v1.MockVehicle.EXPECT().Title().Return("v1") - v2.MockChargeState.EXPECT().Status().Return(t.v2, nil) - v2.MockVehicle.EXPECT().Title().Return("v2") - }}, - {"B/B->1", api.StatusB, api.StatusB, v1, func(t testcase) { - v1.MockChargeState.EXPECT().Status().Return(t.v1, nil) - v1.MockVehicle.EXPECT().Title().Return("v1") - }}, - } - - for _, tc := range tc { - t.Logf("%+v", tc) - - lp := &LoadPoint{ - log: util.NewLogger("foo"), - vehicles: []api.Vehicle{v1, v2}, - } - - if tc.prepare != nil { - tc.prepare(tc) - } - - if res := lp.findActiveVehicleByStatus(); tc.res != res { - t.Errorf("expected %v, got %v", tc.res, res) - } - } -}