Repository: andig/evcc
Pull request #1297 (2021-08-07 08:20:13)

Add Heidelberg Energy Control
Fix https://github.com/andig/evcc/issues/591

Zähler fehlt noch- wird der von allen HEC unterstützt oder nur optional?


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/heidelberg-ec.go b/charger/heidelberg-ec.go
new file mode 100644
index 00000000..129f8e05
--- /dev/null
+++ b/charger/heidelberg-ec.go
@@ -0,0 +1,215 @@
+package charger
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+
+	"github.com/andig/evcc/api"
+	"github.com/andig/evcc/util"
+	"github.com/andig/evcc/util/modbus"
+	"github.com/andig/evcc/util/sponsor"
+)
+
+// HeidelbergEC charger implementation
+type HeidelbergEC struct {
+	log     *util.Logger
+	conn    *modbus.Connection
+	current uint16
+}
+
+const (
+	hecRegFirmware      = 1   // Input
+	hecRegVehicleStatus = 5   // Input
+	hecRegTemperature   = 9   // Input
+	hecRegPower         = 14  // Input
+	hecRegEnergy        = 17  // Input
+	hecRegStandby       = 258 // Holding
+	hecRegAmpsConfig    = 261 // Holding
+)
+
+var hecRegCurrents = []uint16{6, 7, 8}
+
+func init() {
+	registry.Add("heidelberg", NewHeidelbergECFromConfig)
+}
+
+// https://wallbox.heidelberg.com/wp-content/uploads/2021/05/EC_ModBus_register_table_20210222.pdf (newer)
+// https://cdn.shopify.com/s/files/1/0101/2409/9669/files/heidelberg-energy-control-modbus.pdf (older)
+
+// NewHeidelbergECFromConfig creates a HeidelbergEC charger from generic config
+func NewHeidelbergECFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := struct {
+		modbus.Settings `mapstructure:",squash"`
+	}{
+		Settings: modbus.Settings{
+			ID: 1,
+		},
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewHeidelbergEC(cc.URI, cc.Device, cc.Comset, cc.Baudrate, true, cc.ID)
+}
+
+// NewHeidelbergEC creates HeidelbergEC charger
+func NewHeidelbergEC(uri, device, comset string, baudrate int, rtu bool, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, rtu, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, errors.New("heidelberg requires evcc sponsorship, register at https://cloud.evcc.io")
+	}
+
+	log := util.NewLogger("hec")
+	conn.Logger(log.TRACE)
+
+	wb := &HeidelbergEC{
+		log:     log,
+		conn:    conn,
+		current: 60, // assume min current
+	}
+
+	return wb, nil
+}
+
+// Status implements the api.Charger interface
+func (wb *HeidelbergEC) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadInputRegisters(hecRegVehicleStatus, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	switch sb := b[1]; sb {
+	case 2, 3:
+		return api.StatusA, nil
+	case 4, 5:
+		return api.StatusB, nil
+	case 6, 7:
+		return api.StatusC, nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %0x", sb)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *HeidelbergEC) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(hecRegAmpsConfig, 1)
+	if err != nil {
+		return false, err
+	}
+
+	cur := binary.BigEndian.Uint16(b)
+
+	enabled := cur != 0
+	if enabled {
+		wb.current = cur
+	}
+
+	return enabled, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *HeidelbergEC) Enable(enable bool) error {
+	var cur uint16
+	if enable {
+		cur = wb.current
+	}
+
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, cur)
+
+	_, err := wb.conn.WriteMultipleRegisters(hecRegAmpsConfig, 1, b)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *HeidelbergEC) MaxCurrent(current int64) error {
+	if current < 6 {
+		return fmt.Errorf("invalid current %d", current)
+	}
+
+	return wb.MaxCurrentMillis(float64(current))
+}
+
+var _ api.ChargerEx = (*HeidelbergEC)(nil)
+
+// MaxCurrentMillis implements the api.ChargerEx interface
+func (wb *HeidelbergEC) MaxCurrentMillis(current float64) error {
+	if current < 6 {
+		return fmt.Errorf("invalid current %.1f", current)
+	}
+
+	cur := uint16(10 * current)
+
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, cur)
+
+	_, err := wb.conn.WriteMultipleRegisters(hecRegAmpsConfig, 1, b)
+	if err == nil {
+		wb.current = cur
+	}
+
+	return err
+}
+
+var _ api.Meter = (*HeidelbergEC)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *HeidelbergEC) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(hecRegPower, 1)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint16(b)), nil
+}
+
+var _ api.MeterEnergy = (*HeidelbergEC)(nil)
+
+// TotalEnergy implements the api.MeterEnergy interface
+func (wb *HeidelbergEC) TotalEnergy() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(hecRegEnergy, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint32(b)) / 1e3, nil
+}
+
+var _ api.MeterCurrent = (*HeidelbergEC)(nil)
+
+// Currents implements the api.MeterCurrent interface
+func (wb *HeidelbergEC) Currents() (float64, float64, float64, error) {
+	var currents []float64
+	for _, regCurrent := range hecRegCurrents {
+		b, err := wb.conn.ReadInputRegisters(regCurrent, 1)
+		if err != nil {
+			return 0, 0, 0, err
+		}
+
+		currents = append(currents, float64(binary.BigEndian.Uint16(b))/10)
+	}
+
+	return currents[0], currents[1], currents[2], nil
+}
+
+var _ api.Diagnosis = (*HeidelbergEC)(nil)
+
+// Diagnose implements the api.Diagnosis interface
+func (wb *HeidelbergEC) Diagnose() {
+	if b, err := wb.conn.ReadInputRegisters(hecRegFirmware, 2); err == nil {
+		fmt.Printf("Firmware:\t%d.%d.%d\n", b[1], b[2], b[3])
+	}
+	if b, err := wb.conn.ReadInputRegisters(hecRegTemperature, 1); err == nil {
+		fmt.Printf("Temperature:\t%.1fC\n", float64(int16(binary.BigEndian.Uint16(b)))/10)
+	}
+	if b, err := wb.conn.ReadHoldingRegisters(hecRegStandby, 1); err == nil {
+		fmt.Printf("Standby:\t%0x\n", b[1])
+	}
+}
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/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 {
diff --git a/vehicle/nissan/api.go b/vehicle/nissan/api.go
index fc05a7c4..f4b13b1c 100644
--- a/vehicle/nissan/api.go
+++ b/vehicle/nissan/api.go
@@ -51,7 +51,7 @@ func (v *API) Vehicles() ([]string, error) {
 
 	var res Vehicles
 	if err == nil {
-		uri := fmt.Sprintf("%s/v2/users/%s/cars", UserBaseURL, user.UserID)
+		uri := fmt.Sprintf("%s/v4/users/%s/cars", UserBaseURL, user.UserID)
 		err = v.GetJSON(uri, &res)
 	}
 
diff --git a/vehicle/nissan/identity.go b/vehicle/nissan/identity.go
index 6682bb0d..0b3b8691 100644
--- a/vehicle/nissan/identity.go
+++ b/vehicle/nissan/identity.go
@@ -1,8 +1,6 @@
 package nissan
 
 import (
-	"bytes"
-	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -11,6 +9,7 @@ import (
 	"github.com/andig/evcc/util"
 	"github.com/andig/evcc/util/oauth"
 	"github.com/andig/evcc/util/request"
+	"github.com/avast/retry-go"
 	"golang.org/x/oauth2"
 )
 
@@ -55,23 +54,23 @@ func (v *Identity) Login(user, password string) error {
 			}
 		}
 
-		var body []byte
-		body, err = json.Marshal(res)
-
-		if err == nil {
-			req, err = request.New(http.MethodPost, uri, bytes.NewReader(body), map[string]string{
-				"Content-type":       "application/json",
-				"Accept":             "application/json",
+		// https://github.com/Tobiaswk/dartnissanconnect/commit/7d28dd5461aaed3e46b5be0c9fd58887e1e0cd0b
+		err = retry.Do(func() error {
+			req, err = request.New(http.MethodPost, uri, request.MarshalJSON(res), map[string]string{
 				"Accept-Api-Version": APIVersion,
 				"X-Username":         "anonymous",
 				"X-Password":         "anonymous",
+				"Content-type":       "application/json",
+				"Accept":             "application/json",
 			})
-		}
 
-		if err == nil {
-			err = v.DoJSON(req, &nToken)
-			realm = strings.Trim(nToken.Realm, "/")
-		}
+			if err == nil {
+				err = v.DoJSON(req, &nToken)
+				realm = strings.Trim(nToken.Realm, "/")
+			}
+
+			return err
+		}, retry.Attempts(10), retry.LastErrorOnly(true))
 	}
 
 	if err == nil {


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