Fix vehicle detection across multiple loadpoints (#1308)

This commit is contained in:
andig 2021-08-10 14:53:10 +02:00 • committed by GitHub
parent 5ee4e16f79
commit 7ac1a9a962
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 146 additions and 84 deletions

75
core/coordinator.go Normal file
View file

@ -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
}

63
core/coordinator_test.go Normal file
View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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)
}
}
}