Retry vehicle detection every 3 minutes for 10 minutes (#1254)

This commit is contained in:
andig 2021-08-07 11:50:47 +02:00 • committed by GitHub
parent 2123ae4c61
commit f127eca315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 349 additions and 74 deletions

View file

@ -2,7 +2,7 @@ package api
import "time"
//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,Meter,MeterEnergy,Vehicle,ChargeRater
//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater
// ChargeMode are charge modes modeled after OpenWB
type ChargeMode string

View file

@ -28,7 +28,9 @@ const (
evVehicleConnect = "connect" // vehicle connected
evVehicleDisconnect = "disconnect" // vehicle disconnected
minActiveCurrent = 1.0 // minimum current at which a phase is treated as active
minActiveCurrent = 1.0 // minimum current at which a phase is treated as active
vehicleDetectInterval = 3 * time.Minute
vehicleDetectDuration = 10 * time.Minute
)
// PollConfig defines the vehicle polling mode and interval
@ -95,21 +97,22 @@ type LoadPoint struct {
MaxCurrent float64 // Max allowed current. Physically ensured by the charger
GuardDuration time.Duration // charger enable/disable minimum holding time
enabled bool // Charger enabled state
chargeCurrent float64 // Charger current limit
guardUpdated time.Time // Charger enabled/disabled timestamp
socUpdated time.Time // SoC updated timestamp (poll: connected)
enabled bool // Charger enabled state
chargeCurrent float64 // Charger current limit
guardUpdated time.Time // Charger enabled/disabled timestamp
socUpdated time.Time // SoC updated timestamp (poll: connected)
vehicleConnected time.Time // Vehicle connected timestamp
vehicleConnectedTicker *clock.Ticker
charger api.Charger
chargeTimer api.ChargeTimer
chargeRater api.ChargeRater
chargeMeter api.Meter // Charger usage meter
vehicle api.Vehicle // Currently active vehicle
vehicles []api.Vehicle // Assigned vehicles
socEstimator *soc.Estimator
socTimer *soc.Timer
vehicleIdError error // state of last vehicle identification
chargeMeter api.Meter // Charger usage meter
vehicle api.Vehicle // Currently active vehicle
vehicles []api.Vehicle // Assigned vehicles
socEstimator *soc.Estimator
socTimer *soc.Timer
// cached state
status api.ChargeStatus // Charger status
@ -327,9 +330,11 @@ func (lp *LoadPoint) evVehicleConnectHandler() {
}
// flush all vehicles before updating state
lp.log.DEBUG.Println("vehicle api refresh")
provider.ResetCached()
// identify active vehicle
lp.startVehicleDetection()
lp.findActiveVehicle()
// immediately allow pv mode activity
@ -348,6 +353,11 @@ func (lp *LoadPoint) evVehicleDisconnectHandler() {
lp.pushEvent(evVehicleDisconnect)
// remove active vehicle
if len(lp.vehicles) > 1 {
lp.setActiveVehicle(nil)
}
// set default mode on disconnect
if lp.OnDisconnect.Mode != "" && lp.GetMode() != api.ModeOff {
lp.SetMode(lp.OnDisconnect.Mode)
@ -421,7 +431,7 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
// run during prepare() to ensure cache has been attached
if len(lp.vehicles) > 0 {
lp.setActiveVehicle(lp.vehicles[0])
lp.findActiveVehicle()
lp.startVehicleDetection()
}
// read initial charger state to prevent immediately disabling charger
@ -591,35 +601,64 @@ func (lp *LoadPoint) remoteControlled(demand RemoteDemand) bool {
// setActiveVehicle assigns currently active vehicle and configures soc estimator
func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
if lp.vehicle != nil {
lp.log.INFO.Printf("vehicle updated: %s -> %s", lp.vehicle.Title(), vehicle.Title())
if lp.vehicle == vehicle {
return
}
// update successful
lp.vehicleIdError = nil
from := "unknown"
if lp.vehicle != nil {
from = lp.vehicle.Title()
}
to := "unknown"
if vehicle != nil {
to = vehicle.Title()
}
lp.log.INFO.Printf("vehicle updated: %s -> %s", from, to)
lp.vehicle = vehicle
lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)
if lp.vehicle = vehicle; vehicle != nil {
lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)
lp.publish("socTitle", lp.vehicle.Title())
lp.publish("socCapacity", lp.vehicle.Capacity())
lp.publish("socTitle", lp.vehicle.Title())
lp.publish("socCapacity", lp.vehicle.Capacity())
} else {
lp.socEstimator = nil
lp.publish("socTitle", "unknown")
lp.publish("socCapacity", 0)
}
}
// vehicleIdentificationAllowed returns true if active vehicle has not yet been identified
// startVehicleDetection resets connection timer and starts api refresh timer
func (lp *LoadPoint) startVehicleDetection() {
lp.vehicleConnected = lp.clock.Now()
lp.vehicleConnectedTicker = lp.clock.Ticker(vehicleDetectInterval)
}
// vehicleIdentificationAllowed checks if loadpoint has multiple vehicles associated and starts discovery period
func (lp *LoadPoint) vehicleIdentificationAllowed() bool {
return errors.Is(lp.vehicleIdError, api.ErrMustRetry)
res := len(lp.vehicles) > 1 && lp.connected() && lp.clock.Since(lp.vehicleConnected) < vehicleDetectDuration
// request vehicle api refresh while waiting to identify
if res {
select {
case <-lp.vehicleConnectedTicker.C:
lp.log.DEBUG.Println("vehicle api refresh")
provider.ResetCached()
default:
}
}
return res
}
// findActiveVehicle validates if the active vehicle is still connected to the loadpoint
func (lp *LoadPoint) findActiveVehicle() {
// find vehicles by id
// find active vehicle by id
func (lp *LoadPoint) findActiveVehicleByID() api.Vehicle {
if identifier, ok := lp.charger.(api.Identifier); ok {
id, err := identifier.Identify()
if err != nil {
lp.vehicleIdError = err
lp.log.ERROR.Println("charger vehicle id:", err)
return
return nil
}
if id != "" {
@ -628,14 +667,13 @@ func (lp *LoadPoint) findActiveVehicle() {
// find exact match
for _, vehicle := range lp.vehicles {
if vid, err := vehicle.Identify(); err == nil && vid == id {
lp.setActiveVehicle(vehicle)
return
return vehicle
}
}
// find placeholder match
for _, vehicle := range lp.vehicles {
if vid, err := vehicle.Identify(); err == nil {
if vid, err := vehicle.Identify(); err == nil && vid != "" {
re, err := regexp.Compile(strings.ReplaceAll(vid, "*", ".*?"))
if err != nil {
lp.log.ERROR.Printf("vehicle identity: %v", err)
@ -643,64 +681,59 @@ func (lp *LoadPoint) findActiveVehicle() {
}
if re.MatchString(id) {
lp.setActiveVehicle(vehicle)
return
return vehicle
}
}
}
// TODO implement removing vehicle
// lp.setActiveVehicle(nil)
}
}
if len(lp.vehicles) <= 1 {
return
}
return nil
}
// find vehicles by charge state - current vehicle
if vs, ok := lp.vehicle.(api.ChargeState); ok {
status, err := vs.Status()
if err != nil {
lp.vehicleIdError = err
lp.log.ERROR.Println("vehicle charge state:", err)
return
}
lp.log.DEBUG.Printf("vehicle status: %s (%s)", status, lp.vehicle.Title())
// vehicle is plugged or charging, so it should be the right one
if status == api.StatusB || status == api.StatusC {
lp.vehicleIdError = nil
return
}
}
// find vehicles by charge state
// find active vehicle by charge state
func (lp *LoadPoint) findActiveVehicleByStatus() api.Vehicle {
for _, vehicle := range lp.vehicles {
if vehicle == lp.vehicle {
continue
}
if vs, ok := vehicle.(api.ChargeState); ok {
status, err := vs.Status()
if err != nil {
lp.vehicleIdError = err
lp.log.ERROR.Println("vehicle charge state:", err)
return
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 {
lp.setActiveVehicle(vehicle)
return
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 {
return
}
if vehicle := lp.findActiveVehicleByID(); vehicle != nil {
lp.setActiveVehicle(vehicle)
return
}
if vehicle := lp.findActiveVehicleByStatus(); vehicle != nil {
lp.setActiveVehicle(vehicle)
return
}
// remove previous vehicle if status was not confirmed
if _, ok := lp.vehicle.(api.ChargeState); ok {
lp.setActiveVehicle(nil)
}
}
// updateChargerStatus updates charger status and detects car connected/disconnected events
@ -762,7 +795,7 @@ func (lp *LoadPoint) effectiveCurrent() float64 {
// pvDisableTimer puts the pv enable/disable timer into elapsed state
func (lp *LoadPoint) pvDisableTimer() {
lp.pvTimer = time.Now().Add(-lp.Disable.Delay)
lp.pvTimer = lp.clock.Now().Add(-lp.Disable.Delay)
}
// pvMaxCurrent calculates the maximum target current for PV mode

View file

@ -747,3 +747,142 @@ func TestMinSoC(t *testing.T) {
}
}
}
func TestVehicleDetectByID(t *testing.T) {
ctrl := gomock.NewController(t)
type charger struct {
*mock.MockCharger
*mock.MockIdentifier
}
c := &charger{mock.NewMockCharger(ctrl), mock.NewMockIdentifier(ctrl)}
v1 := mock.NewMockVehicle(ctrl)
v2 := mock.NewMockVehicle(ctrl)
type testcase struct {
string
id, i1, i2 string
res api.Vehicle
prepare func(testcase)
}
tc := []testcase{
{"_/_/_->0", "", "", "", nil, func(tc testcase) {
c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil)
}},
{"1/_/_->0", "1", "", "", nil, func(tc testcase) {
c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
}},
{"1/1/2->1", "1", "1", "2", v1, func(tc testcase) {
c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
}},
{"2/1/2->2", "2", "1", "2", v2, func(tc testcase) {
c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
}},
{"11/1*/2->1", "11", "1*", "2", v1, func(tc testcase) {
c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
// v2.EXPECT().Identify().Return(tc.i2, nil)
}},
{"22/1*/2*->2", "22", "1*", "2*", v2, func(tc testcase) {
c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
}},
{"2/_/*->2", "2", "", "*", v2, func(tc testcase) {
c.MockIdentifier.EXPECT().Identify().Return(tc.id, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
v1.EXPECT().Identify().Return(tc.i1, nil)
v2.EXPECT().Identify().Return(tc.i2, nil)
}},
}
for _, tc := range tc {
t.Logf("%+v", tc)
lp := &LoadPoint{
log: util.NewLogger("foo"),
charger: c,
vehicles: []api.Vehicle{v1, v2},
}
if tc.prepare != nil {
tc.prepare(tc)
}
if res := lp.findActiveVehicleByID(); tc.res != res {
t.Errorf("expected %v, got %v", tc.res, res)
}
}
}
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)
}
}
}

View file

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/andig/evcc/api (interfaces: Charger,Meter,MeterEnergy,Vehicle,ChargeRater)
// Source: github.com/andig/evcc/api (interfaces: Charger,ChargeState,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater)
// Package mock is a generated GoMock package.
package mock
@ -92,6 +92,82 @@ func (mr *MockChargerMockRecorder) Status() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockCharger)(nil).Status))
}
// MockChargeState is a mock of ChargeState interface.
type MockChargeState struct {
ctrl *gomock.Controller
recorder *MockChargeStateMockRecorder
}
// MockChargeStateMockRecorder is the mock recorder for MockChargeState.
type MockChargeStateMockRecorder struct {
mock *MockChargeState
}
// NewMockChargeState creates a new mock instance.
func NewMockChargeState(ctrl *gomock.Controller) *MockChargeState {
mock := &MockChargeState{ctrl: ctrl}
mock.recorder = &MockChargeStateMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockChargeState) EXPECT() *MockChargeStateMockRecorder {
return m.recorder
}
// Status mocks base method.
func (m *MockChargeState) Status() (api.ChargeStatus, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Status")
ret0, _ := ret[0].(api.ChargeStatus)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Status indicates an expected call of Status.
func (mr *MockChargeStateMockRecorder) Status() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockChargeState)(nil).Status))
}
// MockIdentifier is a mock of Identifier interface.
type MockIdentifier struct {
ctrl *gomock.Controller
recorder *MockIdentifierMockRecorder
}
// MockIdentifierMockRecorder is the mock recorder for MockIdentifier.
type MockIdentifierMockRecorder struct {
mock *MockIdentifier
}
// NewMockIdentifier creates a new mock instance.
func NewMockIdentifier(ctrl *gomock.Controller) *MockIdentifier {
mock := &MockIdentifier{ctrl: ctrl}
mock.recorder = &MockIdentifierMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIdentifier) EXPECT() *MockIdentifierMockRecorder {
return m.recorder
}
// Identify mocks base method.
func (m *MockIdentifier) Identify() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Identify")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Identify indicates an expected call of Identify.
func (mr *MockIdentifierMockRecorder) Identify() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Identify", reflect.TypeOf((*MockIdentifier)(nil).Identify))
}
// MockMeter is a mock of Meter interface.
type MockMeter struct {
ctrl *gomock.Controller

View file

@ -28,8 +28,8 @@ func TestCachedGetter(t *testing.T) {
duration := time.Second
c := NewCached(g, duration)
clck := clock.NewMock()
c.clock = clck
clock := clock.NewMock()
c.clock = clock
getter := c.FloatGetter()
expect := func(s struct {
@ -45,9 +45,36 @@ func TestCachedGetter(t *testing.T) {
expect(cases[0])
expect(cases[0])
clck.Add(2 * duration)
clock.Add(2 * duration)
expect(cases[1])
clck.Add(2 * duration)
clock.Add(2 * duration)
expect(cases[2])
}
func TestCacheReset(t *testing.T) {
var i int64
g := func() (int64, error) {
i++
return i, nil
}
c := NewCached(g, 10*time.Minute)
clock := clock.NewMock()
c.clock = clock
test := func(exp int64) {
v, _ := c.IntGetter()()
if exp != v {
t.Errorf("expected %d, got %d", exp, v)
}
}
test(1)
test(1)
ResetCached()
test(2)
test(2)
clock.Add(10*time.Minute + 1)
test(3)
}