Fix vehicle detection across multiple loadpoints (#1308)
This commit is contained in:
parent
5ee4e16f79
commit
7ac1a9a962
4 changed files with 146 additions and 84 deletions
75
core/coordinator.go
Normal file
75
core/coordinator.go
Normal 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
63
core/coordinator_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue