Repository: andig/evcc
Pull request #1254 (2021-07-30 15:55:56)

Retry vehicle detection every 3 minutes for 10 minutes
Fix https://github.com/andig/evcc/issues/1251

xerion3800: Ich habe mal den PR mit meiner Umgebung  getestet. Die Besonderheit an meiner Umgebung ist, das ich keine Hersteller API´s nutze sondern MQTT Nachrichten und zwei LP habe. Wenn ich evcc starte und kein Fahrzeug angeschlossen ist steht bei den LP "unknown". Wenn ich dann ein Fahrzeug anstecke wird dieses nicht erkannt:
[evcc.log](https://github.com/andig/evcc/files/6944380/evcc.log)

Wenn ich evcc bei angestecktem Fahrzeug starte wird es erkannt und auch beim 2. LP als Title angezeigt:
[evcc.log](https://github.com/andig/evcc/files/6944411/evcc.log)

![image](https://user-images.githubusercontent.com/46169403/128485455-b0c46e61-386e-4aa7-95ed-3f1474a35c4f.png)

evcc.yaml Config:
```
vehicles:
- name: hyundai
  type: default
  title: Hyundai Kona
  capacity: 39 # kWh
  cache: 10s
  charge:
      type: mqtt
      topic: sunny5soc/hyundai-kona/soc
      timeout: 20s  
  status:
      type: mqtt
      topic: sunny5soc/hyundai-kona/status
      timeout: 20s 
  range:
      type: mqtt
      topic: sunny5soc/hyundai-kona/range
      timeout: 20s
- name: cupra
  type: default
  title: Cupra Formentor # display name for UI
  capacity: 13 # kWh
  cache: 10s
  charge:
      type: mqtt
      topic: sunny5soc/cupra-formentor/soc
      timeout: 20s  
  status:
      type: mqtt
      topic: sunny5soc/cupra-formentor/status
      timeout: 20s 
  range:
      type: mqtt
      topic: sunny5soc/cupra-formentor/range
      timeout: 20s 
```

evcc vehicle:
[vehicle.log](https://github.com/andig/evcc/files/6944405/vehicle.log)

andig: @xerion3800 das sieht etwas merkwürdig aus. Welchen Commit hast Du getestet? Könntest Du Dich bitte bei Slack melden?
xerion3800: > @xerion3800 das sieht etwas merkwürdig aus. Welchen Commit hast Du getestet? Könntest Du Dich bitte bei Slack melden?
> 
> Im ersten Log ist nichts zu erkennen. Beide Fahrzeuge sagen entweder "abwesend" (also A) oder liefern Fehler:
> 
> ```
> [lp-1  ] DEBUG 2021/08/06 10:41:03 vehicle status: A (Hyundai Kona)
> [lp-1  ] DEBUG 2021/08/06 10:41:03 vehicle status: A (Cupra Formentor)
> 
> [lp-2  ] DEBUG 2021/08/06 10:41:03 vehicle status: A (Hyundai Kona)
> [lp-2  ] DEBUG 2021/08/06 10:41:03 vehicle status: A (Cupra Formentor)
> 
> [lp-1  ] ERROR 2021/08/06 10:41:49 vehicle status: sunny5soc/hyundai-kona/status outdated: 46s
> [lp-1  ] ERROR 2021/08/06 10:43:41 vehicle status: sunny5soc/hyundai-kona/status outdated: 2m38s
> ```
> 
> Damit kann nichts erkannt werden.
> 
> Prinzipiell- da ich hier sunny5soc sehe- nutzt Du evcc oder die Version von Sunny5? Für Letzteres musst Du Dich bitte auch da um Support bemühen!

Nein ich nutze nicht evcc von Sunny das ist nur in Tool für  OBD Daten was wir gemeinsam entwickeln und die Daten per MQTT bereitstellt. Melde mich für den Rest auf Slack.
andig: > und auch beim 2. LP als Title angezeigt:

Behoben. natürlich darf das nur bei Ladepunkten laufen die ein Auto dran haben
andig: Ein Problem bleibt allerdings- bisher gibt es keine Koordination über mehrere Ladepunkte. Die sollte als Teil dieses PRs vmtl auch noch eingebaut werden.
premultiply: Ich votiere auch eher zu jede Minute für 5 Minuten.
Danach kommt doch sowieso nix mehr, oder?
Das Meiste dürfte innerhalb der ersten Minute eintreffen.
xerion3800: > Ein Problem bleibt allerdings- bisher gibt es keine Koordination über mehrere Ladepunkte. Die sollte als Teil dieses PRs vmtl auch noch eingebaut werden.

Ja bitte, denn jetzt ist es mit zwei Fahrzeugen und zwei LP nicht sinnvoll nutzbar. Wenn jetzt an LP 2 Car 2 angeschlossen ist und dann an LP1 Car 1 angesteckt wird, wird einfach das zuletzt angeschlossene Auto auf beiden LP´s angezeigt, getestet  mit 610cd6f4fa511519ce91218258425329dfa246e1
[evcc.log](https://github.com/andig/evcc/files/6945263/evcc.log)

![image](https://user-images.githubusercontent.com/46169403/128504928-70d42724-cb3c-40f2-8588-c46504b82cd2.png)


Patch:
diff --git a/api/api.go b/api/api.go
index df12724f..9b8b25c0 100644
--- a/api/api.go
+++ b/api/api.go
@@ -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
diff --git a/charger/easee.go b/charger/easee.go
index b2604979..2dc569b5 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -12,6 +12,7 @@ import (
 	"github.com/andig/evcc/util"
 	"github.com/andig/evcc/util/request"
 	"github.com/andig/evcc/util/sponsor"
+	"github.com/thoas/go-funk"
 	"golang.org/x/oauth2"
 )
 
@@ -39,6 +40,7 @@ func NewEaseeFromConfig(other map[string]interface{}) (api.Charger, error) {
 		User     string
 		Password string
 		Charger  string
+		Circuit  int
 		Cache    time.Duration
 	}{
 		Cache: 10 * time.Second,
@@ -48,11 +50,11 @@ func NewEaseeFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewEasee(cc.User, cc.Password, cc.Charger, cc.Cache)
+	return NewEasee(cc.User, cc.Password, cc.Charger, cc.Circuit, cc.Cache)
 }
 
 // NewEasee creates Easee charger
-func NewEasee(user, password, charger string, cache time.Duration) (*Easee, error) {
+func NewEasee(user, password, charger string, circuit int, cache time.Duration) (*Easee, error) {
 	log := util.NewLogger("easee")
 
 	if !sponsor.IsAuthorized() {
@@ -62,6 +64,7 @@ func NewEasee(user, password, charger string, cache time.Duration) (*Easee, erro
 	c := &Easee{
 		Helper:  request.NewHelper(log),
 		charger: charger,
+		circuit: circuit,
 		cache:   cache,
 		log:     log,
 	}
@@ -77,6 +80,14 @@ func NewEasee(user, password, charger string, cache time.Duration) (*Easee, erro
 		Base:   c.Client.Transport,
 	}
 
+	// find site
+	site, err := c.chargerDetails()
+	if err != nil {
+		return c, err
+	}
+
+	c.site = site.ID
+
 	// find charger
 	if charger == "" {
 		chargers, err := c.chargers()
@@ -85,47 +96,43 @@ func NewEasee(user, password, charger string, cache time.Duration) (*Easee, erro
 		}
 
 		if len(chargers) != 1 {
-			return c, fmt.Errorf("cannot determine charger id, found: %v", chargers)
+			return c, fmt.Errorf("cannot determine charger id, found: %v", funk.Map(chargers, func(c easee.Charger) string { return c.ID }))
 		}
 
 		c.charger = chargers[0].ID
 	}
 
-	// find site and circuit
-	site, err := c.chargerDetails()
-	if err != nil {
-		return c, err
-	}
+	// find circuit
+	if circuit == 0 {
+		if len(site.Circuits) != 1 {
+			return c, fmt.Errorf("cannot determine circuit id, found: %v", funk.Map(site.Circuits, func(c easee.Circuit) int { return c.ID }))
+		}
 
-	if len(site.Circuits) != 1 {
-		return c, fmt.Errorf("cannot determine circuit id, found: %v", site.Circuits)
+		c.circuit = site.Circuits[0].ID
 	}
 
-	c.site = site.ID
-	c.circuit = site.Circuits[0].ID
-
 	return c, err
 }
 
 func (c *Easee) chargers() (res []easee.Charger, err error) {
 	uri := fmt.Sprintf("%s/chargers", easee.API)
+
 	req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding)
-	if err != nil {
-		return nil, err
+	if err == nil {
+		err = c.DoJSON(req, &res)
 	}
 
-	err = c.DoJSON(req, &res)
 	return res, err
 }
 
 func (c *Easee) chargerDetails() (res easee.Site, err error) {
 	uri := fmt.Sprintf("%s/chargers/%s/site", easee.API, c.charger)
+
 	req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding)
-	if err != nil {
-		return res, err
+	if err == nil {
+		err = c.DoJSON(req, &res)
 	}
 
-	err = c.DoJSON(req, &res)
 	return res, err
 }
 
@@ -145,15 +152,17 @@ func (c *Easee) syncSmartCharging() error {
 			SmartCharging: &newSmartCharging,
 		}
 
-		var req *http.Request
 		uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
-		req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
+		resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data))
 		if err == nil {
-			_, err = c.Do(req)
-			c.updated = time.Time{} // clear cache
-			c.lastChargeMode = c.lp.GetMode()
-			c.lastSmartCharging = newSmartCharging
+			resp.Body.Close()
 		}
+
+		c.updated = time.Time{} // clear cache
+
+		c.lastChargeMode = c.lp.GetMode()
+		c.lastSmartCharging = newSmartCharging
+
 		return err
 	}
 
@@ -227,13 +236,14 @@ func (c *Easee) Enable(enable bool) error {
 			Enabled: &enable,
 		}
 
-		var req *http.Request
 		uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
-		if req, err = request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding); err == nil {
-			_, err = c.Do(req)
-			c.updated = time.Time{} // clear cache
+		resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data))
+		if err == nil {
+			resp.Body.Close()
 		}
 
+		c.updated = time.Time{} // clear cache
+
 		return err
 	}
 
@@ -243,12 +253,9 @@ func (c *Easee) Enable(enable bool) error {
 		action = "resume_charging"
 	}
 
-	var req *http.Request
 	uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action)
-	if req, err = request.New(http.MethodPost, uri, nil, request.JSONEncoding); err == nil {
-		_, err = c.Do(req)
-		c.updated = time.Time{} // clear cache
-	}
+	_, err = c.Post(uri, request.JSONContent, nil)
+	c.updated = time.Time{} // clear cache
 
 	return err
 }
@@ -270,12 +277,13 @@ func (c *Easee) MaxCurrentMillis(current float64) error {
 	}
 
 	uri := fmt.Sprintf("%s/sites/%d/circuits/%d/settings", easee.API, c.site, c.circuit)
-	req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
+	resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data))
 	if err == nil {
-		_, err = c.Do(req)
-		c.updated = time.Time{} // clear cache
+		resp.Body.Close()
 	}
 
+	c.updated = time.Time{} // clear cache
+
 	return err
 }
 
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 984624f4..f7689ef7 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -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 == vehicle {
+		return
+	}
+
+	from := "unknown"
 	if lp.vehicle != nil {
-		lp.log.INFO.Printf("vehicle updated: %s -> %s", lp.vehicle.Title(), vehicle.Title())
+		from = lp.vehicle.Title()
+	}
+	to := "unknown"
+	if vehicle != nil {
+		to = vehicle.Title()
 	}
+	lp.log.INFO.Printf("vehicle updated: %s -> %s", from, to)
 
-	// update successful
-	lp.vehicleIdError = nil
+	if lp.vehicle = vehicle; vehicle != nil {
+		lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)
 
-	lp.vehicle = vehicle
-	lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)
+		lp.publish("socTitle", lp.vehicle.Title())
+		lp.publish("socCapacity", lp.vehicle.Capacity())
+	} else {
+		lp.socEstimator = nil
+
+		lp.publish("socTitle", "unknown")
+		lp.publish("socCapacity", 0)
+	}
+}
 
-	lp.publish("socTitle", lp.vehicle.Title())
-	lp.publish("socCapacity", lp.vehicle.Capacity())
+// 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 returns true if active vehicle has not yet been identified
+// 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
-	}
-
-	// 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
-		}
-	}
+	return nil
+}
 
-	// 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
diff --git a/mock/mock_api.go b/mock/mock_api.go
index dd1455ef..39b9cffe 100644
--- a/mock/mock_api.go
+++ b/mock/mock_api.go
@@ -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
diff --git a/util/request/functions.go b/util/request/functions.go
index 2ac67501..84cc1f1a 100644
--- a/util/request/functions.go
+++ b/util/request/functions.go
@@ -7,18 +7,21 @@ import (
 )
 
 var (
+	FormContent = "application/x-www-form-urlencoded"
+	JSONContent = "application/json"
+
 	// URLEncoding specifies application/x-www-form-urlencoded
-	URLEncoding = map[string]string{"Content-Type": "application/x-www-form-urlencoded"}
+	URLEncoding = map[string]string{"Content-Type": FormContent}
 
 	// JSONEncoding specifies application/json
 	JSONEncoding = map[string]string{
-		"Content-Type": "application/json",
-		"Accept":       "application/json",
+		"Content-Type": JSONContent,
+		"Accept":       JSONContent,
 	}
 
 	// AcceptJSON accepting application/json
 	AcceptJSON = map[string]string{
-		"Accept": "application/json",
+		"Accept": JSONContent,
 	}
 )
 
diff --git a/vehicle/fiat/identity.go b/vehicle/fiat/identity.go
index dfc17b1f..50b4b19a 100644
--- a/vehicle/fiat/identity.go
+++ b/vehicle/fiat/identity.go
@@ -57,7 +57,11 @@ func (v *Identity) Login() error {
 	req, err := request.New(http.MethodGet, uri, nil, headers)
 	if err == nil {
 		req.URL.RawQuery = data.Encode()
-		_, err = v.Do(req)
+
+		var resp *http.Response
+		if resp, err = v.Do(req); err == nil {
+			resp.Body.Close()
+		}
 	}
 
 	var res struct {


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 855bfc80..afbb24a5 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -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)
+		}
+	}
+}
diff --git a/provider/cache_test.go b/provider/cache_test.go
index c0683eb8..d12fb79e 100644
--- a/provider/cache_test.go
+++ b/provider/cache_test.go
@@ -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)
+}