Repository: andig/evcc
Pull request #1299 (2021-08-07 10:09:44)

Update supported chargers
Depends on #1297, #1298


Patch:
diff --git a/README.md b/README.md
index fcbbefa0..3f16ef0a 100644
--- a/README.md
+++ b/README.md
@@ -13,10 +13,12 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
 
 - simple and clean user interface
 - multiple [chargers](#charger):
-  - Wallbe, Phoenix (includes ESL Walli), go-eCharger (includes Heidelberg Energy Control), NRGkick, SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect
+  - Open source: [openWB](https://openwb.de/), [EVSEWifi](https://www.evse-wifi.de) (includes smartWB)
+  - Other commercial: ABL eMH1, go-eCharger, Heidelberg Energy Control, KEBA/BMW, NRGkick, Wallbe, Mobile Charger Connect
+  - Build-your-own: Phoenix (includes ESL Walli), [SimpleEVSE](https://www.evse-wifi.de/produkt-schlagwort/simple-evse-wb/)
   - Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link
 - multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall
-- wide support of vendor-specific [vehicles](#vehicle) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Ford, Hyundai, Kia, Nissan, Niu, Porsche, Renault, Seat, Skoda, Tesla, Volkswagen, Volvo, Tronity
+- wide support of vendor-specific [vehicles](#vehicle) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Ford, Hyundai, Kia, Mini, Nissan, Niu, Porsche, Renault, Seat, Skoda, Tesla, Volkswagen, Volvo, Tronity
 - [plugins](#plugins) for integrating with any charger/ meter/ vehicle: Modbus (meters and grid inverters), HTTP, MQTT, Javascript, WebSockets and shell scripts
 - status notifications using [Telegram](https://telegram.org), [PushOver](https://pushover.net) and [many more](https://containrrr.dev/shoutrrr/)
 - logging using [InfluxDB](https://www.influxdata.com) and [Grafana](https://grafana.com/grafana/)
@@ -50,6 +52,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
 - [API](#api)
   - [REST API](#rest-api)
   - [MQTT API](#mqtt-api)
+- [Sponsorship](#sponsorship)
 - [Background](#background)
 
 
@@ -182,8 +185,11 @@ In general, due to the minimum value of 5% for signalling the EV duty cycle, the
 Charger is responsible for handling EV state and adjusting charge current.
 Available charger implementations are:
 
-- `evsewifi`: chargers with SimpleEVSE controllers using [EVSE-WiFi](https://www.evse-wifi.de/)
-- `go-e`: go-eCharger chargers (both local and cloud API are supported, at least firmware 040.0 required). Includes Heidelberg Energy Control using [github.com/steff393/wbec](https://github.com/steff393/wbec).
+- `abl`: ABL eMH1 (requires Modbus adapter; [sponsors only](#sponsorship))
+- `easee`: Easee Home charger ([sponsors only](#sponsorship))
+- `evsewifi`: chargers with SimpleEVSE controllers using [EVSE-WiFi](https://www.evse-wifi.de/) (includes smartWB)
+- `go-e`: go-eCharger chargers (both local and cloud API are supported, at least firmware 040.0 required)
+- `heidelberg`: Heidelberg Energy Control (requires Modbus adapter; [sponsors only](#sponsorship))
 - `keba`: KEBA KeContact P20/P30 and BMW chargers (see [Preparation](#keba-preparation-))
 - `mcc`: Mobile Charger Connect devices (Audi, Bentley, Porsche)
 - `nrgkick-bluetooth`: NRGkick chargers with Bluetooth connector (Linux only, not supported on Docker)
@@ -195,10 +201,10 @@ Available charger implementations are:
 - `simpleevse`: chargers with SimpleEVSE controllers connected via ModBus (e.g. OpenWB Wallbox, Easy Wallbox B163, ...)
 - `wallbe`: Wallbe Eco chargers (see [Preparation](#wallbe-preparation-)). For older Wallbe boxes (pre 2019) with Phoenix EV-CC-AC1-M3-CBC-RCM-ETH controllers make sure to set `legacy: true` to enable correct current configuration.
 - `warp`: Tinkerforge Warp/ Warp Pro charger
-- `easee`: Easee Home charger (available to Github sponsors only, request sponsor token at https://cloud.evcc.io/)
 - `custom`: default charger implementation using configurable [plugins](#plugins) for integrating any type of charger
 
 Smart-Home outlet charger implementations:
+
 - `fritzdect`: Fritz!DECT 200/210 outlets
 - `shelly`: Shelly outlets
 - `tasmota`: Tasmota outlets
@@ -261,11 +267,12 @@ Available vehicle remote interface implementations are:
 - `audi`: Audi (eTron, Q55)
 - `bmw`: BMW (i3)
 - `carwings`: Nissan (Leaf pre 2019)
-- `citroen`, `opel`, `peugeot`: Follow this [tutorial](https://github.com/flobz/psa_car_controller) to obtain client credentials for PSA.
+- `citroen`, `ds`, `opel`, `peugeot`: All PSA brands
 - `fiat`: Fiat (500e)
 - `ford`: Ford (Kuga, Mustang)
 - `kia`: Kia (Soul and other Bluelink models)
 - `hyundai`: Hyundai (Bluelink vehicles like Kona or Ioniq)
+- `mini`: Mini (Cooper SE)
 - `nissan`: Nissan (Leaf)
 - `niu`: Niu Scooter
 - `tesla`: Tesla (any model)
@@ -278,7 +285,7 @@ Available vehicle remote interface implementations are:
 - `vw`: Volkswagen (eGolf, eUp)
 - `id`: Volkswagen (ID.3, ID.4)
 - `volvo`: Volvo
-- `tronity`: Tronity (available to Github sponsors only, request sponsor token at https://cloud.evcc.io/)
+- `tronity`: Tronity ([sponsors only](#sponsorship))
 - `custom`: default vehicle implementation using configurable [plugins](#plugins) for integrating any type of vehicle
 
 Configuration examples are documented at [andig/evcc-config#vehicles](https://github.com/andig/evcc-config#vehicles)
@@ -597,6 +604,15 @@ The MQTT API follows the REST API's structure, with loadpoint ids starting at `0
 
 Note: to modify writable settings append `/set` to the topic for writing.
 
+## Sponsorship
+
+EVCC believes in open source software. We're committed to provide best in class EV charging experience.
+Maintaining EVCC consumes time and effort. With the vast amount of different devices to support, we depend on community and vendor support to keep EVCC alive.
+
+While EVCC is open source, we would also like to encourage vendors to provide open source hardware devices, public documentation and support open source projects like hours that provide additional value to otherwised closed hardware. Where this is not the case, EVCC requires "sponsor token" to finance ongoing development and support of evcc.
+
+The personal sponsor token requires a [Github Sponsorship](https://github.com/sponsors/andig) and can be requested at [cloud.evcc.io](https://cloud.evcc.io/). A sponsor token is valid for one year and can be renewed any time with active sponsorship.
+
 ## Background
 
 <img src="docs/logo.png" align="right" width="150" />
diff --git a/charger/abl.go b/charger/abl.go
new file mode 100644
index 00000000..8f0b0334
--- /dev/null
+++ b/charger/abl.go
@@ -0,0 +1,178 @@
+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"
+)
+
+// ABLeMH charger implementation
+type ABLeMH struct {
+	log     *util.Logger
+	conn    *modbus.Connection
+	current uint16
+}
+
+const (
+	ablRegFirmware      = 0x01
+	ablRegVehicleStatus = 0x04
+	ablRegAmpsConfig    = 0x14
+	ablRegStatus        = 0x2E
+
+	ablAmpsDisabled uint16 = 0x03E8
+
+	// ablRegMode          = 0x05
+	// ablReset   uint16 = 0x5A5A
+	// ablEnable  uint16 = 0xA1A1
+	// ablDisable uint16 = 0xE0E0
+)
+
+func init() {
+	registry.Add("abl", NewABLeMHFromConfig)
+}
+
+// https://www.goingelectric.de/forum/viewtopic.php?p=1550459#p1550459
+
+// NewABLeMHFromConfig creates a ABLeMH charger from generic config
+func NewABLeMHFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := modbus.Settings{
+		ID: 1,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewABLeMH(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID)
+}
+
+// NewABLeMH creates ABLeMH charger
+func NewABLeMH(uri, device, comset string, baudrate int, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.AsciiFormat, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, errors.New("abl requires evcc sponsorship, register at https://cloud.evcc.io")
+	}
+
+	log := util.NewLogger("abl")
+	conn.Logger(log.TRACE)
+
+	wb := &ABLeMH{
+		log:     log,
+		conn:    conn,
+		current: 0x64, // 6A
+	}
+
+	return wb, nil
+}
+
+// Status implements the api.Charger interface
+func (wb *ABLeMH) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadHoldingRegisters(ablRegVehicleStatus, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	r := rune(b[1]>>4-0x0A) + 'A'
+
+	switch r {
+	case 'A', 'B', 'C':
+		return api.ChargeStatus(r), nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %v", r)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *ABLeMH) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(ablRegAmpsConfig, 1)
+	if err != nil {
+		return false, err
+	}
+
+	enabled := binary.BigEndian.Uint16(b) != ablAmpsDisabled
+
+	return enabled, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *ABLeMH) Enable(enable bool) error {
+	u := ablAmpsDisabled
+	if enable {
+		u = wb.current
+	}
+
+	_, err := wb.conn.WriteSingleRegister(ablRegAmpsConfig, u)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *ABLeMH) MaxCurrent(current int64) error {
+	return wb.MaxCurrentMillis(float64(current))
+}
+
+var _ api.ChargerEx = (*ABLeMH)(nil)
+
+// MaxCurrent implements the api.ChargerEx interface
+func (wb *ABLeMH) MaxCurrentMillis(current float64) error {
+	// calculate duty cycle according to https://www.goingelectric.de/forum/viewtopic.php?p=1575287#p1575287
+	u := uint16(current / 0.06)
+
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, u)
+
+	_, err := wb.conn.WriteSingleRegister(ablRegAmpsConfig, u)
+	if err == nil {
+		wb.current = u
+	}
+
+	return err
+}
+
+var _ api.Meter = (*ABLeMH)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *ABLeMH) CurrentPower() (float64, error) {
+	l1, l2, l3, err := wb.Currents()
+	return 230 * (l1 + l2 + l3), err
+}
+
+var _ api.MeterCurrent = (*ABLeMH)(nil)
+
+// Currents implements the api.MeterCurrent interface
+func (wb *ABLeMH) Currents() (float64, float64, float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(ablRegStatus, 5)
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	var currents []float64
+	for i := 2; i < 5; i++ {
+		u := binary.BigEndian.Uint16(b[2*i:])
+		if u == ablAmpsDisabled {
+			u = 0
+		}
+
+		currents = append(currents, float64(u)/10)
+	}
+
+	return currents[0], currents[1], currents[2], nil
+}
+
+var _ api.Diagnosis = (*ABLeMH)(nil)
+
+// Diagnose implements the api.Diagnosis interface
+func (wb *ABLeMH) Diagnose() {
+	if b, err := wb.conn.ReadHoldingRegisters(ablRegFirmware, 2); err == nil {
+		fmt.Printf("Firmware: %0 x\n", b)
+	}
+}
diff --git a/charger/go-e.go b/charger/go-e.go
index 738ecf69..31f34d5c 100644
--- a/charger/go-e.go
+++ b/charger/go-e.go
@@ -34,6 +34,44 @@ type goeStatusResponse struct {
 	Tmp int    `json:"tmp,string"` // temperature [°C]
 	Dws int    `json:"dws,string"` // energy [Ws]
 	Nrg []int  `json:"nrg"`        // voltage, current, power
+	Uby int    `json:"uby,string"` // unlocked_by
+	Rna string `json:"rna"`        // RFID 1
+	Rnm string `json:"rnm"`        // RFID 2
+	Rne string `json:"rne"`        // RFID 3
+	Rn4 string `json:"rn4"`        // RFID 4
+	Rn5 string `json:"rn5"`        // RFID 5
+	Rn6 string `json:"rn6"`        // RFID 6
+	Rn7 string `json:"rn7"`        // RFID 7
+	Rn8 string `json:"rn8"`        // RFID 8
+	Rn9 string `json:"rn9"`        // RFID 9
+	Rn1 string `json:"rn1"`        // RFID 10
+}
+
+func (g goeStatusResponse) RFIDName(id int) string {
+	switch id {
+	case 1:
+		return g.Rna
+	case 2:
+		return g.Rnm
+	case 3:
+		return g.Rne
+	case 4:
+		return g.Rn4
+	case 5:
+		return g.Rn5
+	case 6:
+		return g.Rn6
+	case 7:
+		return g.Rn7
+	case 8:
+		return g.Rn8
+	case 9:
+		return g.Rn9
+	case 10:
+		return g.Rn1
+	default:
+		return ""
+	}
 }
 
 // GoE charger implementation
@@ -131,6 +169,8 @@ func (c *GoE) apiStatus() (status goeStatusResponse, err error) {
 // goeStatusResponse is only valid for local api. Use Fwv if valid.
 func (c *GoE) apiUpdate(payload string) (goeStatusResponse, error) {
 	if c.token == "" {
+		// let charger settle after update
+		defer time.Sleep(2 * time.Second)
 		return c.localResponse("mqtt", payload)
 	}
 
@@ -143,11 +183,6 @@ func (c *GoE) apiUpdate(payload string) (goeStatusResponse, error) {
 	return status, err
 }
 
-// isValid checks is status response is local
-func isValid(status goeStatusResponse) bool {
-	return status.Fwv != ""
-}
-
 // Status implements the api.Charger interface
 func (c *GoE) Status() (api.ChargeStatus, error) {
 	status, err := c.apiStatus()
@@ -192,8 +227,10 @@ func (c *GoE) Enable(enable bool) error {
 	}
 
 	status, err := c.apiUpdate(fmt.Sprintf("alw=%d", b))
-	if err == nil && isValid(status) && status.Alw != b {
-		return fmt.Errorf("alw update failed: %d", status.Alw)
+	if err == nil {
+		if status, err = c.apiStatus(); err == nil && status.Alw != b {
+			return fmt.Errorf("alw update failed: %d", status.Alw)
+		}
 	}
 
 	return err
@@ -202,8 +239,10 @@ func (c *GoE) Enable(enable bool) error {
 // MaxCurrent implements the api.Charger interface
 func (c *GoE) MaxCurrent(current int64) error {
 	status, err := c.apiUpdate(fmt.Sprintf("amx=%d", current))
-	if err == nil && isValid(status) && int64(status.Amp) != current {
-		return fmt.Errorf("amp update failed: %d", status.Amp)
+	if err == nil {
+		if status, err = c.apiStatus(); err == nil && int64(status.Amp) != current {
+			return fmt.Errorf("amp update failed: %d", status.Amp)
+		}
 	}
 
 	return err
@@ -240,3 +279,14 @@ func (c *GoE) Currents() (float64, float64, float64, error) {
 	}
 	return 0, 0, 0, err
 }
+
+var _ api.Identifier = (*GoE)(nil)
+
+// Identify implements the api.Identifier interface
+func (c *GoE) Identify() (string, error) {
+	status, err := c.apiStatus()
+	if status.Uby == 0 {
+		return "", api.ErrNotAvailable
+	}
+	return status.RFIDName(status.Uby), err
+}
diff --git a/charger/heidelberg-ec.go b/charger/heidelberg-ec.go
new file mode 100644
index 00000000..28ff6870
--- /dev/null
+++ b/charger/heidelberg-ec.go
@@ -0,0 +1,211 @@
+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 := 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, cc.ID)
+}
+
+// NewHeidelbergEC creates HeidelbergEC charger
+func NewHeidelbergEC(uri, device, comset string, baudrate int, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.RtuFormat, 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/charger/phoenix-em-eth.go b/charger/phoenix-em-eth.go
index 7a27bb85..118c45c2 100644
--- a/charger/phoenix-em-eth.go
+++ b/charger/phoenix-em-eth.go
@@ -73,7 +73,7 @@ func NewPhoenixEMEthFromConfig(other map[string]interface{}) (api.Charger, error
 
 // NewPhoenixEMEth creates a Phoenix charger
 func NewPhoenixEMEth(uri string, id uint8) (*PhoenixEMEth, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, false, id)
+	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.TcpFormat, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/phoenix-ev-eth.go b/charger/phoenix-ev-eth.go
index 4f53d73b..4999a311 100644
--- a/charger/phoenix-ev-eth.go
+++ b/charger/phoenix-ev-eth.go
@@ -74,7 +74,7 @@ func NewPhoenixEVEthFromConfig(other map[string]interface{}) (api.Charger, error
 
 // NewPhoenixEVEth creates a Phoenix charger
 func NewPhoenixEVEth(uri string, id uint8) (*PhoenixEVEth, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, false, id)
+	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.TcpFormat, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/phoenix-ev-ser.go b/charger/phoenix-ev-ser.go
index 8f909ed6..578c3086 100644
--- a/charger/phoenix-ev-ser.go
+++ b/charger/phoenix-ev-ser.go
@@ -43,7 +43,7 @@ func NewPhoenixEVSerFromConfig(other map[string]interface{}) (api.Charger, error
 
 // NewPhoenixEVSer creates a Phoenix charger
 func NewPhoenixEVSer(uri, device, comset string, baudrate int, id uint8) (*PhoenixEVSer, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, true, id)
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.RtuFormat, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/simpleevse.go b/charger/simpleevse.go
index af41c136..24ea3d9d 100644
--- a/charger/simpleevse.go
+++ b/charger/simpleevse.go
@@ -45,7 +45,7 @@ func NewSimpleEVSEFromConfig(other map[string]interface{}) (api.Charger, error)
 func NewSimpleEVSE(uri, device, comset string, baudrate int, rtu bool, slaveID uint8) (api.Charger, error) {
 	log := util.NewLogger("evse")
 
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, rtu, slaveID)
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.RtuFormat, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/wallbe.go b/charger/wallbe.go
index c9d56e8b..5dc4af82 100644
--- a/charger/wallbe.go
+++ b/charger/wallbe.go
@@ -104,7 +104,7 @@ func NewWallbeFromConfig(other map[string]interface{}) (api.Charger, error) {
 
 // NewWallbe creates a Wallbe charger
 func NewWallbe(uri string) (*Wallbe, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, false, wbSlaveID)
+	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.TcpFormat, wbSlaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/core/coordinator.go b/core/coordinator.go
new file mode 100644
index 00000000..d2ee716e
--- /dev/null
+++ b/core/coordinator.go
@@ -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
+}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index f7689ef7..ce2c20b4 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -335,7 +335,6 @@ func (lp *LoadPoint) evVehicleConnectHandler() {
 
 	// identify active vehicle
 	lp.startVehicleDetection()
-	lp.findActiveVehicle()
 
 	// immediately allow pv mode activity
 	lp.pvDisableTimer()
@@ -419,7 +418,6 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	lp.publish("maxCurrent", lp.MaxCurrent)
 	lp.publish("phases", lp.Phases)
 	lp.publish("activePhases", lp.Phases)
-	lp.publish("hasVehicle", len(lp.vehicles) > 0)
 
 	lp.Lock()
 	lp.publish("mode", lp.Mode)
@@ -427,10 +425,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 +608,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)
@@ -618,12 +621,14 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
 	if lp.vehicle = vehicle; vehicle != nil {
 		lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)
 
+		lp.publish("hasVehicle", true)
 		lp.publish("socTitle", lp.vehicle.Title())
 		lp.publish("socCapacity", lp.vehicle.Capacity())
 	} else {
 		lp.socEstimator = nil
 
-		lp.publish("socTitle", "unknown")
+		lp.publish("hasVehicle", false)
+		lp.publish("socTitle", "")
 		lp.publish("socCapacity", 0)
 	}
 }
@@ -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
 	}
diff --git a/go.mod b/go.mod
index 82df0a2f..3f3917a5 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,6 @@ require (
 	github.com/benbjohnson/clock v1.1.0
 	github.com/bogosj/tesla v1.0.2
 	github.com/containrrr/shoutrrr v0.4.4
-	github.com/deepmap/oapi-codegen v1.8.2 // indirect
 	github.com/denisbrodbeck/machineid v1.0.1
 	github.com/dustin/go-humanize v1.0.0
 	github.com/dylanmei/iso8601 v0.1.0
@@ -67,10 +66,10 @@ require (
 	github.com/spf13/viper v1.8.1
 	github.com/thoas/go-funk v0.9.0
 	github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
-	github.com/volkszaehler/mbmd v0.0.0-20210706160533-aa268417e0ef
+	github.com/volkszaehler/mbmd v0.0.0-20210808132733-8235dc7d1327
 	gitlab.com/bboehmke/sunny v0.14.2
 	golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
-	golang.org/x/net v0.0.0-20210716203947-853a461950ff
+	golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
 	golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
 	golang.org/x/text v0.3.6
 	google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f // indirect
diff --git a/go.sum b/go.sum
index f57083ff..7c54b402 100644
--- a/go.sum
+++ b/go.sum
@@ -117,6 +117,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -124,7 +125,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/deepmap/oapi-codegen v1.3.6/go.mod h1:aBozjEveG+33xPiP55Iw/XbVkhtZHEGLq3nxlX0+hfU=
 github.com/deepmap/oapi-codegen v1.3.9/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
 github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
-github.com/deepmap/oapi-codegen v1.8.1/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
 github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
 github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
 github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
@@ -319,7 +319,6 @@ github.com/gregdel/pushover v1.1.0 h1:dwHyvrcpZCOS9V1fAnKPaGRRI5OC55cVaKhMybqNsK
 github.com/gregdel/pushover v1.1.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
 github.com/grid-x/modbus v0.0.0-20200108122021-57d05a9f1e1a/go.mod h1:cOS1YDRu+JId5eT7kRulYTlfpACFppj2MpftCvHeiuM=
 github.com/grid-x/modbus v0.0.0-20200704184817-73c05928caa1/go.mod h1:cOS1YDRu+JId5eT7kRulYTlfpACFppj2MpftCvHeiuM=
-github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw=
 github.com/grid-x/modbus v0.0.0-20210714071042-7af2b65ec03b h1:ur6X0rPWCe3R5AC5X7qIySVhP/34pM3A1uQVJ0GEdKw=
 github.com/grid-x/modbus v0.0.0-20210714071042-7af2b65ec03b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw=
 github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08 h1:syBxnRYnSPUDdkdo5U4sy2roxBPQDjNiw4od7xlsABQ=
@@ -609,11 +608,11 @@ github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52
 github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.4.0 h1:WhlbjwB9EGCc8W5Rxdkus+wmH2ASRwwTJk6tgHKwdqQ=
+github.com/spf13/cast v1.4.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
-github.com/spf13/cobra v1.2.0/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
 github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
 github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@@ -662,8 +661,8 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
 github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
 github.com/volkszaehler/mbmd v0.0.0-20200717102329-c4d965bd1eac/go.mod h1:sldLyJCKVO9GQkit55U5WPNr9U4KmUn1SCdqFN9Gjb4=
-github.com/volkszaehler/mbmd v0.0.0-20210706160533-aa268417e0ef h1:OIKfPaGDM8LjDBMyaNfCJ6vgmwbRdj6hsSj2bs2qzJ8=
-github.com/volkszaehler/mbmd v0.0.0-20210706160533-aa268417e0ef/go.mod h1:BDNStSIw76tsTTgAmRvdo6o9ILyCK9BNVXbeFf9jjvI=
+github.com/volkszaehler/mbmd v0.0.0-20210808132733-8235dc7d1327 h1:GhXik8mWOCLDq7sCJjHjSv49fYUvhxOMMhrGYqAFowI=
+github.com/volkszaehler/mbmd v0.0.0-20210808132733-8235dc7d1327/go.mod h1:oh17P6CJFJvVlFfLSgbOh+GdgmtuHpxik7gk7Zr/kxg=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -793,8 +792,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210716203947-853a461950ff h1:j2EK/QoxYNBsXI4R7fQkkRUk8y6wnOBI+6hgPdP/6Ds=
-golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -974,7 +973,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/meter/modbus.go b/meter/modbus.go
index 789d102a..86bd680e 100644
--- a/meter/modbus.go
+++ b/meter/modbus.go
@@ -53,9 +53,12 @@ func NewModbusFromConfig(other map[string]interface{}) (api.Meter, error) {
 		cc.RTU = &b
 	}
 
-	log := util.NewLogger("modbus")
+	format := modbus.TcpFormat
+	if cc.RTU != nil && *cc.RTU {
+		format = modbus.RtuFormat
+	}
 
-	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, *cc.RTU, cc.ID)
+	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, format, cc.ID)
 	if err != nil {
 		return nil, err
 	}
@@ -65,6 +68,7 @@ func NewModbusFromConfig(other map[string]interface{}) (api.Meter, error) {
 		conn.Timeout(cc.Timeout)
 	}
 
+	log := util.NewLogger("modbus")
 	conn.Logger(log.TRACE)
 
 	// prepare device
diff --git a/provider/modbus.go b/provider/modbus.go
index 23278372..e25756f7 100644
--- a/provider/modbus.go
+++ b/provider/modbus.go
@@ -49,7 +49,12 @@ func NewModbusFromConfig(other map[string]interface{}) (IntProvider, error) {
 		cc.RTU = &b
 	}
 
-	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, *cc.RTU, cc.ID)
+	format := modbus.TcpFormat
+	if cc.RTU != nil && *cc.RTU {
+		format = modbus.RtuFormat
+	}
+
+	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, format, cc.ID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index 450e30f0..95826bad 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -16,6 +16,14 @@ import (
 // WriteSingleRegister 16-bit wise write access
 const WriteSingleRegister = 6 // modbus.FuncCodeWriteSingleRegister
 
+type WireFormat int
+
+const (
+	TcpFormat WireFormat = iota
+	RtuFormat
+	AsciiFormat
+)
+
 // Settings contains the ModBus settings
 type Settings struct {
 	ID                  uint8
@@ -140,7 +148,7 @@ func registeredConnection(key string, newConn meters.Connection) meters.Connecti
 }
 
 // NewConnection creates physical modbus device from config
-func NewConnection(uri, device, comset string, baudrate int, rtu bool, slaveID uint8) (*Connection, error) {
+func NewConnection(uri, device, comset string, baudrate int, wire WireFormat, slaveID uint8) (*Connection, error) {
 	var conn meters.Connection
 
 	if device != "" && uri != "" {
@@ -151,15 +159,23 @@ func NewConnection(uri, device, comset string, baudrate int, rtu bool, slaveID u
 		if baudrate == 0 || comset == "" {
 			return nil, errors.New("invalid modbus configuration: need baudrate and comset")
 		}
-		conn = registeredConnection(device, meters.NewRTU(device, baudrate, comset))
+
+		if wire == RtuFormat {
+			conn = registeredConnection(device, meters.NewRTU(device, baudrate, comset))
+		} else {
+			conn = registeredConnection(uri, meters.NewASCII(device, baudrate, comset))
+		}
 	}
 
 	if uri != "" {
 		uri = util.DefaultPort(uri, 502)
 
-		if rtu {
+		switch wire {
+		case RtuFormat:
 			conn = registeredConnection(uri, meters.NewRTUOverTCP(uri))
-		} else {
+		case AsciiFormat:
+			conn = registeredConnection(uri, meters.NewASCIIOverTCP(uri))
+		default:
 			conn = registeredConnection(uri, meters.NewTCP(uri))
 		}
 	}
diff --git a/vehicle/bmw.go b/vehicle/bmw.go
index d83db835..6ef342d0 100644
--- a/vehicle/bmw.go
+++ b/vehicle/bmw.go
@@ -1,47 +1,23 @@
 package vehicle
 
 import (
-	"errors"
-	"fmt"
-	"net/http"
-	"net/url"
-	"strconv"
 	"strings"
 	"time"
 
 	"github.com/andig/evcc/api"
-	"github.com/andig/evcc/provider"
 	"github.com/andig/evcc/util"
-	"github.com/andig/evcc/util/request"
+	"github.com/andig/evcc/vehicle/bmw"
 )
 
-const (
-	bmwAuth = "https://customer.bmwgroup.com/gcdm/oauth/authenticate"
-	bmwAPI  = "https://www.bmw-connecteddrive.com/api"
-)
-
-type bmwDynamicResponse struct {
-	AttributesMap struct {
-		ChargingLevelHv float64 `json:"chargingLevelHv,string"`
-	}
-}
-
-type bmwVehiclesResponse []struct {
-	VIN string `json:"vin"`
-}
-
-// BMW is an api.Vehicle implementation for BMW cars
+// BMW is an api.Vehicle implementation for BMW and Mini cars
 type BMW struct {
 	*embed
-	*request.Helper
-	user, password, vin string
-	token               string
-	tokenValid          time.Time
-	chargeStateG        func() (float64, error)
+	*bmw.Provider // provides the api implementations
 }
 
 func init() {
 	registry.Add("bmw", NewBMWFromConfig)
+	registry.Add("mini", NewBMWFromConfig)
 }
 
 // NewBMWFromConfig creates a new vehicle
@@ -58,125 +34,28 @@ func NewBMWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		return nil, err
 	}
 
+	v := &BMW{
+		embed: &cc.embed,
+	}
+
 	log := util.NewLogger("bmw")
+	identity := bmw.NewIdentity(log)
 
-	v := &BMW{
-		embed:    &cc.embed,
-		Helper:   request.NewHelper(log),
-		user:     cc.User,
-		password: cc.Password,
-		vin:      strings.ToUpper(cc.VIN),
+	if err := identity.Login(cc.User, cc.Password); err != nil {
+		return nil, err
 	}
 
-	v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
+	api := bmw.NewAPI(log, identity)
 
 	var err error
 	if cc.VIN == "" {
-		v.vin, err = findVehicle(v.vehicles())
+		cc.VIN, err = findVehicle(api.Vehicles())
 		if err == nil {
-			log.DEBUG.Printf("found vehicle: %v", v.vin)
+			log.DEBUG.Printf("found vehicle: %v", cc.VIN)
 		}
 	}
 
-	return v, err
-}
-
-func (v *BMW) login(user, password string) error {
-	data := url.Values{
-		"username":      []string{user},
-		"password":      []string{password},
-		"client_id":     []string{"dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"},
-		"redirect_uri":  []string{"https://www.bmw-connecteddrive.com/app/default/static/external-dispatch.html"},
-		"response_type": []string{"token"},
-		"scope":         []string{"authenticate_user fupo"},
-		"state":         []string{"eyJtYXJrZXQiOiJkZSIsImxhbmd1YWdlIjoiZGUiLCJkZXN0aW5hdGlvbiI6ImxhbmRpbmdQYWdlIn0"},
-		"locale":        []string{"DE-de"},
-	}
-
-	req, err := request.New(http.MethodPost, bmwAuth, strings.NewReader(data.Encode()), request.URLEncoding)
-	if err != nil {
-		return err
-	}
-
-	client := &http.Client{
-		Timeout:       v.Helper.Client.Timeout,
-		CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, // don't follow redirects
-	}
-
-	resp, err := client.Do(req)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-
-	query, err := url.ParseQuery(resp.Header.Get("Location"))
-	if err != nil {
-		return err
-	}
-
-	token := query.Get("access_token")
-	expires, err := strconv.Atoi(query.Get("expires_in"))
-	if err != nil || token == "" || expires == 0 {
-		return errors.New("could not obtain token")
-	}
+	v.Provider = bmw.NewProvider(api, strings.ToUpper(cc.VIN), cc.Cache)
 
-	v.token = token
-	v.tokenValid = time.Now().Add(time.Duration(expires) * time.Second)
-
-	return nil
-}
-
-func (v *BMW) request(uri string) (*http.Request, error) {
-	if v.token == "" || time.Since(v.tokenValid) > 0 {
-		if err := v.login(v.user, v.password); err != nil {
-			return nil, err
-		}
-	}
-
-	req, err := http.NewRequest(http.MethodGet, uri, nil)
-	if err == nil {
-		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", v.token))
-	}
-
-	return req, nil
-}
-
-// vehicles implements returns the list of user vehicles
-func (v *BMW) vehicles() ([]string, error) {
-	var resp bmwVehiclesResponse
-	uri := fmt.Sprintf("%s/me/vehicles/v2/", bmwAPI)
-
-	var vehicles []string
-
-	req, err := v.request(uri)
-	if err == nil {
-		err = v.DoJSON(req, &resp)
-	}
-
-	if err == nil {
-		for _, v := range resp {
-			vehicles = append(vehicles, v.VIN)
-		}
-	}
-
-	return vehicles, err
-}
-
-// chargeState implements the api.Vehicle interface
-func (v *BMW) chargeState() (float64, error) {
-	var resp bmwDynamicResponse
-	uri := fmt.Sprintf("%s/vehicle/dynamic/v1/%s", bmwAPI, v.vin)
-
-	req, err := v.request(uri)
-	if err != nil {
-		return 0, err
-	}
-
-	err = v.DoJSON(req, &resp)
-	return resp.AttributesMap.ChargingLevelHv, err
-}
-
-// SoC implements the api.Vehicle interface
-func (v *BMW) SoC() (float64, error) {
-	return v.chargeStateG()
+	return v, err
 }
diff --git a/vehicle/bmw/api.go b/vehicle/bmw/api.go
new file mode 100644
index 00000000..89ce8996
--- /dev/null
+++ b/vehicle/bmw/api.go
@@ -0,0 +1,85 @@
+package bmw
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/andig/evcc/util"
+	"github.com/andig/evcc/util/request"
+	"golang.org/x/oauth2"
+)
+
+// https://github.com/bimmerconnected/bimmer_connected
+
+const ApiURI = "https://b2vapi.bmwgroup.com/webapi/v1"
+
+type StatusResponse struct {
+	VehicleStatus struct {
+		ConnectionStatus       string // CONNECTED
+		ChargingStatus         string // CHARGING, ERROR, FINISHED_FULLY_CHARGED, FINISHED_NOT_FULL, INVALID, NOT_CHARGING, WAITING_FOR_CHARGING
+		ChargingLevelHv        int
+		RemainingRangeElectric int
+		Mileage                int
+		// UpdateTime             time.Time // 2021-08-12T12:00:08+0000
+	}
+}
+
+type VehiclesResponse struct {
+	Vehicles []Vehicle
+}
+
+type Vehicle struct {
+	VIN   string
+	Model string
+}
+
+// API is an api.Vehicle implementation for BMW cars
+type API struct {
+	*request.Helper
+}
+
+// NewAPI creates a new vehicle
+func NewAPI(log *util.Logger, identity oauth2.TokenSource) *API {
+	v := &API{
+		Helper: request.NewHelper(log),
+	}
+
+	// replace client transport with authenticated transport
+	v.Client.Transport = &oauth2.Transport{
+		Source: identity,
+		Base:   v.Client.Transport,
+	}
+
+	return v
+}
+
+// Vehicles implements returns the /user/vehicles api
+func (v *API) Vehicles() ([]string, error) {
+	var resp VehiclesResponse
+	uri := fmt.Sprintf("%s/user/vehicles", ApiURI)
+
+	req, err := http.NewRequest(http.MethodGet, uri, nil)
+	if err == nil {
+		err = v.DoJSON(req, &resp)
+	}
+
+	var vehicles []string
+	for _, v := range resp.Vehicles {
+		vehicles = append(vehicles, v.VIN)
+	}
+
+	return vehicles, err
+}
+
+// Status implements the /user/vehicles/<vin>/status api
+func (v *API) Status(vin string) (StatusResponse, error) {
+	var resp StatusResponse
+	uri := fmt.Sprintf("%s/user/vehicles/%s/status", ApiURI, vin)
+
+	req, err := http.NewRequest(http.MethodGet, uri, nil)
+	if err == nil {
+		err = v.DoJSON(req, &resp)
+	}
+
+	return resp, err
+}
diff --git a/vehicle/bmw/identity.go b/vehicle/bmw/identity.go
new file mode 100644
index 00000000..59f34dad
--- /dev/null
+++ b/vehicle/bmw/identity.go
@@ -0,0 +1,93 @@
+package bmw
+
+import (
+	"errors"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/andig/evcc/util"
+	"github.com/andig/evcc/util/oauth"
+	"github.com/andig/evcc/util/request"
+	"golang.org/x/oauth2"
+)
+
+const AuthURI = "https://customer.bmwgroup.com/gcdm/oauth/authenticate"
+
+type Identity struct {
+	*request.Helper
+	log *util.Logger
+	oauth2.TokenSource
+	user, password string
+}
+
+// NewIdentity creates BMW identity
+func NewIdentity(log *util.Logger) *Identity {
+	v := &Identity{
+		log:    log,
+		Helper: request.NewHelper(log),
+	}
+
+	return v
+}
+
+func (v *Identity) Login(user, password string) error {
+	v.user = user
+	v.password = password
+
+	token, err := v.RefreshToken(nil)
+
+	if err == nil {
+		v.TokenSource = oauth.RefreshTokenSource(token, v)
+	}
+
+	return err
+}
+
+func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
+	data := url.Values{
+		"username":      []string{v.user},
+		"password":      []string{v.password},
+		"client_id":     []string{"31c357a0-7a1d-4590-aa99-33b97244d048"},
+		"redirect_uri":  []string{"com.bmw.connected://oauth"},
+		"response_type": []string{"token"},
+		"scope":         []string{"authenticate_user vehicle_data remote_services"},
+		"nonce":         []string{"login_nonce"},
+	}
+
+	req, err := request.New(http.MethodPost, AuthURI, strings.NewReader(data.Encode()), request.URLEncoding)
+	if err != nil {
+		return nil, err
+	}
+
+	// don't follow redirects
+	v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }
+	defer func() { v.Client.CheckRedirect = nil }()
+
+	resp, err := v.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	uri := resp.Header.Get("Location")
+
+	query, err := url.ParseQuery(strings.TrimPrefix(uri, "com.bmw.connected://oauth#"))
+	if err != nil {
+		return nil, err
+	}
+
+	at := query.Get("access_token")
+	expires, err := strconv.Atoi(query.Get("expires_in"))
+	if err != nil || at == "" || expires == 0 {
+		return nil, errors.New("could not obtain token")
+	}
+
+	token := &oauth2.Token{
+		AccessToken: at,
+		Expiry:      time.Now().Add(time.Duration(expires) * time.Second),
+	}
+
+	return token, nil
+}
diff --git a/vehicle/bmw/provider.go b/vehicle/bmw/provider.go
new file mode 100644
index 00000000..4f222291
--- /dev/null
+++ b/vehicle/bmw/provider.go
@@ -0,0 +1,79 @@
+package bmw
+
+import (
+	"time"
+
+	"github.com/andig/evcc/api"
+	"github.com/andig/evcc/provider"
+)
+
+// Provider implements the evcc vehicle api
+type Provider struct {
+	statusG func() (interface{}, error)
+}
+
+// NewProvider provides the evcc vehicle api provider
+func NewProvider(api *API, vin string, cache time.Duration) *Provider {
+	impl := &Provider{
+		statusG: provider.NewCached(func() (interface{}, error) {
+			return api.Status(vin)
+		}, cache).InterfaceGetter(),
+	}
+	return impl
+}
+
+var _ api.Battery = (*Provider)(nil)
+
+// SoC implements the api.Vehicle interface
+func (v *Provider) SoC() (float64, error) {
+	res, err := v.statusG()
+	if res, ok := res.(StatusResponse); err == nil && ok {
+		return float64(res.VehicleStatus.ChargingLevelHv), nil
+	}
+
+	return 0, err
+}
+
+var _ api.ChargeState = (*Provider)(nil)
+
+// Status implements the api.ChargeState interface
+func (v *Provider) Status() (api.ChargeStatus, error) {
+	status := api.StatusA // disconnected
+
+	res, err := v.statusG()
+	if res, ok := res.(StatusResponse); err == nil && ok {
+		if res.VehicleStatus.ConnectionStatus == "CONNECTED" {
+			status = api.StatusB
+		}
+		if res.VehicleStatus.ChargingStatus == "CHARGING" {
+			status = api.StatusC
+		}
+	}
+
+	return status, err
+}
+
+// var _ api.VehicleFinishTimer = (*Provider)(nil)
+
+// // FinishTime implements the api.VehicleFinishTimer interface
+// func (v *Provider) FinishTime() (time.Time, error) {
+// 	res, err := v.statusG()
+// 	if res, ok := res.(StatusResponse); err == nil && ok {
+// 		ctr := res.VehicleStatus.ChargingTimeRemaining
+// 		return time.Now().Add(time.Duration(ctr) * time.Minute), err
+// 	}
+
+// 	return time.Time{}, err
+// }
+
+var _ api.VehicleRange = (*Provider)(nil)
+
+// Range implements the api.VehicleRange interface
+func (v *Provider) Range() (rng int64, err error) {
+	res, err := v.statusG()
+	if res, ok := res.(StatusResponse); err == nil && ok {
+		rng = int64(res.VehicleStatus.RemainingRangeElectric)
+	}
+
+	return rng, err
+}
diff --git a/vehicle/cloud.go b/vehicle/cloud.go
index 4f2852a5..3bc04eb3 100644
--- a/vehicle/cloud.go
+++ b/vehicle/cloud.go
@@ -14,7 +14,7 @@ import (
 	"github.com/andig/evcc/util/request"
 )
 
-// Cloud is an api.Vehicle implementation for Cloud cars
+// Cloud is an api.Vehicle implementation
 type Cloud struct {
 	*embed
 	token        string
diff --git a/vehicle/nissan.go b/vehicle/nissan.go
index 803fb29f..fe1cbd9d 100644
--- a/vehicle/nissan.go
+++ b/vehicle/nissan.go
@@ -50,14 +50,14 @@ func NewNissanFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	log := util.NewLogger("nissan")
 	identity := nissan.NewIdentity(log)
 
-	err := identity.Login(cc.User, cc.Password)
-	if err != nil {
+	if err := identity.Login(cc.User, cc.Password); err != nil {
 		return v, fmt.Errorf("login failed: %w", err)
 	}
 
 	api := nissan.NewAPI(log, identity, strings.ToUpper(cc.VIN))
 
-	if err == nil && cc.VIN == "" {
+	var err error
+	if cc.VIN == "" {
 		api.VIN, err = findVehicle(api.Vehicles())
 		if err == nil {
 			log.DEBUG.Printf("found vehicle: %v", api.VIN)
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 {
diff --git a/vehicle/ovms.go b/vehicle/ovms.go
index ecb36025..747232cb 100644
--- a/vehicle/ovms.go
+++ b/vehicle/ovms.go
@@ -32,7 +32,6 @@ type Ovms struct {
 	*embed
 	*request.Helper
 	user, password, vehicleId, server string
-	interval                          time.Duration
 	chargeG                           func() (interface{}, error)
 }
 
@@ -63,7 +62,6 @@ func NewOvmsFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		password:  cc.Password,
 		vehicleId: cc.VehicleID,
 		server:    cc.Server,
-		interval:  cc.Cache,
 	}
 
 	v.chargeG = provider.NewCached(v.batteryAPI, cc.Cache).InterfaceGetter()
@@ -87,24 +85,16 @@ func (v *Ovms) loginToServer() (err error) {
 	return err
 }
 
-func (v *Ovms) delete(url string) error {
-	req, err := request.New(http.MethodDelete, url, nil)
-	if err == nil {
-		var resp *http.Response
-		if resp, err = v.Do(req); err == nil {
-			resp.Body.Close()
-		}
-	}
-	return err
-}
-
-func (v *Ovms) authFlow() (bool, error) {
+func (v *Ovms) authFlow() error {
 	var resp ovmsConnectResponse
 	err := v.loginToServer()
 	if err == nil {
 		resp, err = v.connectRequest()
+		if err == nil && resp.NetConnected != 1 {
+			return api.ErrMustRetry
+		}
 	}
-	return resp.NetConnected == 1, err
+	return err
 }
 
 func (v *Ovms) connectRequest() (ovmsConnectResponse, error) {
@@ -121,34 +111,21 @@ func (v *Ovms) chargeRequest() (ovmsChargeResponse, error) {
 	return res, err
 }
 
-func (v *Ovms) disconnect() error {
-	uri := fmt.Sprintf("http://%s:6868/api/vehicle/%s", v.server, v.vehicleId)
-
-	err := v.delete(uri)
-	if err == nil {
-		uri = fmt.Sprintf("http://%s:6868/api/cookie", v.server)
-		return v.delete(uri)
-	}
-
-	return err
-}
-
 // batteryAPI provides battery-status api response
 func (v *Ovms) batteryAPI() (interface{}, error) {
 	var resp ovmsChargeResponse
 
-	ovmsConnected, err := v.authFlow()
-	if err == nil {
-		resp, err = v.chargeRequest()
-
+	resp, err := v.chargeRequest()
+	if err != nil {
+		err = v.authFlow()
 		if err == nil {
-			err = v.disconnect()
+			resp, err = v.chargeRequest()
 		}
+	}
 
-		messageAge := time.Duration(resp.MessageAgeServer) * time.Second
-		if err == nil && messageAge > v.interval+time.Minute && ovmsConnected {
-			err = api.ErrMustRetry
-		}
+	messageAge := time.Duration(resp.MessageAgeServer) * time.Second
+	if err == nil && messageAge > time.Minute {
+		err = api.ErrMustRetry
 	}
 
 	return resp, err
@@ -181,7 +158,7 @@ func (v *Ovms) Status() (api.ChargeStatus, error) {
 		}
 	}
 
-	return status, nil
+	return status, err
 }
 
 var _ api.VehicleRange = (*Ovms)(nil)
@@ -194,7 +171,7 @@ func (v *Ovms) Range() (int64, error) {
 		return strconv.ParseInt(res.EstimatedRange, 0, 64)
 	}
 
-	return 0, nil
+	return 0, err
 }
 
 var _ api.VehicleFinishTimer = (*Ovms)(nil)
@@ -210,5 +187,5 @@ func (v *Ovms) FinishTime() (time.Time, error) {
 		}
 	}
 
-	return time.Time{}, api.ErrNotAvailable
+	return time.Time{}, err
 }
diff --git a/vehicle/psa.go b/vehicle/psa.go
index fe15ab0e..f88ddeb6 100644
--- a/vehicle/psa.go
+++ b/vehicle/psa.go
@@ -11,10 +11,11 @@ import (
 	"github.com/andig/evcc/vehicle/psa"
 )
 
-// https://github.com/flobz/psa_car_controller
+// https://github.com/TA2k/ioBroker.psa
 
 func init() {
 	registry.Add("citroen", NewCitroenFromConfig)
+	registry.Add("ds", NewDSFromConfig)
 	registry.Add("opel", NewOpelFromConfig)
 	registry.Add("peugeot", NewPeugeotFromConfig)
 }
@@ -22,19 +23,37 @@ func init() {
 // NewCitroenFromConfig creates a new vehicle
 func NewCitroenFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	log := util.NewLogger("citroen")
-	return newPSA(log, "citroen.com", "clientsB2CCitroen", other)
+	return newPSA(log,
+		"citroen.com", "clientsB2CCitroen",
+		"5364defc-80e6-447b-bec6-4af8d1542cae", "iE0cD8bB0yJ0dS6rO3nN1hI2wU7uA5xR4gP7lD6vM0oH0nS8dN",
+		other)
+}
+
+// NewDSFromConfig creates a new vehicle
+func NewDSFromConfig(other map[string]interface{}) (api.Vehicle, error) {
+	log := util.NewLogger("ds")
+	return newPSA(log,
+		"driveds.com", "clientsB2CDS",
+		"cbf74ee7-a303-4c3d-aba3-29f5994e2dfa", "X6bE6yQ3tH1cG5oA6aW4fS6hK0cR0aK5yN2wE4hP8vL8oW5gU3",
+		other)
 }
 
 // NewOpelFromConfig creates a new vehicle
 func NewOpelFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	log := util.NewLogger("opel")
-	return newPSA(log, "opel.com", "clientsB2COpel", other)
+	return newPSA(log,
+		"opel.com", "clientsB2COpel",
+		"07364655-93cb-4194-8158-6b035ac2c24c", "F2kK7lC5kF5qN7tM0wT8kE3cW1dP0wC5pI6vC0sQ5iP5cN8cJ8",
+		other)
 }
 
 // NewPeugeotFromConfig creates a new vehicle
 func NewPeugeotFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	log := util.NewLogger("peugeot")
-	return newPSA(log, "peugeot.com", "clientsB2CPeugeot", other)
+	return newPSA(log,
+		"peugeot.com", "clientsB2CPeugeot",
+		"1eebc2d5-5df3-459b-a624-20abfcf82530", "T5tP7iS0cO8sC0lA2iE2aR7gK6uE5rF3lJ8pC3nO1pR7tL8vU1",
+		other)
 }
 
 // PSA is an api.Vehicle implementation for PSA cars
@@ -44,13 +63,17 @@ type PSA struct {
 }
 
 // newPSA creates a new vehicle
-func newPSA(log *util.Logger, brand, realm string, other map[string]interface{}) (api.Vehicle, error) {
+func newPSA(log *util.Logger, brand, realm, id, secret string, other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
-		embed                  `mapstructure:",squash"`
-		ClientID, ClientSecret string
-		User, Password, VIN    string
-		Cache                  time.Duration
+		embed               `mapstructure:",squash"`
+		Credentials         ClientCredentials
+		User, Password, VIN string
+		Cache               time.Duration
 	}{
+		Credentials: ClientCredentials{
+			ID:     id,
+			Secret: secret,
+		},
 		Cache: interval,
 	}
 
@@ -58,20 +81,18 @@ func newPSA(log *util.Logger, brand, realm string, other map[string]interface{})
 		return nil, err
 	}
 
-	if cc.ClientID == "" || cc.ClientSecret == "" {
-		return nil, errors.New("missing client id and/or secret (see https://github.com/flobz/psa_car_controller)")
-	}
-
 	v := &PSA{
 		embed: &cc.embed,
 	}
 
-	api := psa.NewAPI(log, brand, realm, cc.ClientID, cc.ClientSecret)
-	err := api.Login(cc.User, cc.Password)
-	if err != nil {
+	identity := psa.NewIdentity(log, brand, cc.Credentials.ID, cc.Credentials.Secret)
+
+	if err := identity.Login(cc.User, cc.Password); err != nil {
 		return v, fmt.Errorf("login failed: %w", err)
 	}
 
+	api := psa.NewAPI(log, identity, realm, cc.Credentials.ID)
+
 	vehicles, err := api.Vehicles()
 	if err != nil {
 		return nil, err
diff --git a/vehicle/psa/api.go b/vehicle/psa/api.go
index 304f5afa..1fb09c2f 100644
--- a/vehicle/psa/api.go
+++ b/vehicle/psa/api.go
@@ -1,7 +1,6 @@
 package psa
 
 import (
-	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -12,7 +11,6 @@ import (
 	"golang.org/x/oauth2"
 )
 
-// https://github.com/flobz/psa_car_controller
 // https://developer.groupe-psa.io/webapi/b2c/api-reference/specification
 
 // BaseURL is the API base url
@@ -21,49 +19,25 @@ const BaseURL = "https://api.groupe-psa.com/connectedcar/v4"
 // API is an api.Vehicle implementation for PSA cars
 type API struct {
 	*request.Helper
-	brand, realm string
-	id, secret   string // client
+	realm string
+	id    string
 }
 
 // NewAPI creates a new vehicle
-func NewAPI(log *util.Logger, brand, realm, id, secret string) *API {
+func NewAPI(log *util.Logger, identity oauth2.TokenSource, realm, id string) *API {
 	v := &API{
 		Helper: request.NewHelper(log),
-		brand:  brand,
 		realm:  realm,
 		id:     id,
-		secret: secret,
 	}
 
-	return v
-}
-
-// Login performs the login
-func (v *API) Login(user, password string) error {
-	config := oauth2.Config{
-		ClientID:     v.id,
-		ClientSecret: v.secret,
-		Endpoint: oauth2.Endpoint{
-			AuthURL:   "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize",
-			TokenURL:  fmt.Sprintf("https://idpcvs.%s/am/oauth2/access_token", v.brand),
-			AuthStyle: oauth2.AuthStyleInHeader,
-		},
-		Scopes: []string{"openid profile"},
-	}
-
-	ctx := context.WithValue(
-		context.Background(),
-		oauth2.HTTPClient,
-		v.Client,
-	)
-
-	// replace client with authenticated oauth client
-	token, err := config.PasswordCredentialsToken(ctx, user, password)
-	if err == nil {
-		v.Client = config.Client(ctx, token)
+	// replace client transport with authenticated transport
+	v.Client.Transport = &oauth2.Transport{
+		Source: identity,
+		Base:   v.Client.Transport,
 	}
 
-	return err
+	return v
 }
 
 // Vehicle is a single vehicle
diff --git a/vehicle/psa/identity.go b/vehicle/psa/identity.go
new file mode 100644
index 00000000..4b7c965a
--- /dev/null
+++ b/vehicle/psa/identity.go
@@ -0,0 +1,49 @@
+package psa
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/andig/evcc/util"
+	"github.com/andig/evcc/util/request"
+	"golang.org/x/oauth2"
+)
+
+type Identity struct {
+	*request.Helper
+	oc *oauth2.Config
+	oauth2.TokenSource
+}
+
+// NewIdentity creates PSA identity
+func NewIdentity(log *util.Logger, brand, id, secret string) *Identity {
+	return &Identity{
+		Helper: request.NewHelper(log),
+		oc: &oauth2.Config{
+			ClientID:     id,
+			ClientSecret: secret,
+			Endpoint: oauth2.Endpoint{
+				AuthURL:   "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize",
+				TokenURL:  fmt.Sprintf("https://idpcvs.%s/am/oauth2/access_token", brand),
+				AuthStyle: oauth2.AuthStyleInHeader,
+			},
+			Scopes: []string{"openid profile"},
+		},
+	}
+}
+
+func (v *Identity) Login(user, password string) error {
+	ctx := context.WithValue(
+		context.Background(),
+		oauth2.HTTPClient,
+		v.Client,
+	)
+
+	// replace client with authenticated oauth client
+	token, err := v.oc.PasswordCredentialsToken(ctx, user, password)
+	if err == nil {
+		v.TokenSource = v.oc.TokenSource(ctx, token)
+	}
+
+	return err
+}
diff --git a/vehicle/skoda/api.go b/vehicle/skoda/api.go
index 171e5bec..a9f010ed 100644
--- a/vehicle/skoda/api.go
+++ b/vehicle/skoda/api.go
@@ -91,13 +91,6 @@ type ChargerResponse struct {
 	}
 }
 
-// charging/settings
-// {
-//     "autoUnlockPlugWhenCharged": "Permanent",
-//     "maxChargeCurrentAc": "Maximum",
-//     "targetStateOfChargeInPercent": 100
-// }
-
 // Charger implements the /v1/charging/<vin>/status response
 func (v *API) Charger(vin string) (ChargerResponse, error) {
 	var res ChargerResponse
@@ -105,62 +98,3 @@ func (v *API) Charger(vin string) (ChargerResponse, error) {
 	err := v.getJSON(uri, &res)
 	return res, err
 }
-
-// ClimaterResponse is the /bs/climatisation/v1/%s/%s/vehicles/%s/climater api
-// type ClimaterResponse struct {
-// }
-
-// // Climater implements the /climater response
-// func (v *API) Climater(vin string) (ClimaterResponse, error) {
-// 	var res ClimaterResponse
-// 	uri := fmt.Sprintf("%s/bs/climatisation/v1/%s/%s/vehicles/%s/climater", BaseURI, v.brand, v.country, vin)
-// 	err := v.getJSON(uri, &res)
-// 	return res, err
-// }
-
-// const (
-// 	ActionCharge      = "batterycharge"
-// 	ActionChargeStart = "start"
-// 	ActionChargeStop  = "stop"
-// )
-
-// type actionDefinition struct {
-// 	contentType string
-// 	appendix    string
-// }
-
-// var actionDefinitions = map[string]actionDefinition{
-// 	ActionCharge: {
-// 		"application/vnd.vwg.mbb.ChargerAction_v1_0_0+xml",
-// 		"charger/actions",
-// 	},
-// }
-
-// // Action implements vehicle actions
-// func (v *API) Action(vin, action, value string) error {
-// 	def := actionDefinitions[action]
-
-// 	uri := fmt.Sprintf("%s/bs/%s/v1/%s/%s/vehicles/%s/%s", BaseURI, action, v.brand, v.country, vin, def.appendix)
-// 	body := "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><action><type>" + value + "</type></action>"
-
-// 	req, err := request.New(http.MethodPost, uri, strings.NewReader(body), map[string]string{
-// 		"Content-type": def.contentType,
-// 	})
-
-// 	if err == nil {
-// 		var resp *http.Response
-// 		if resp, err = v.Do(req); err == nil {
-// 			resp.Body.Close()
-// 		}
-// 	}
-
-// 	return err
-// }
-
-// // Any implements any api response
-// func (v *API) Any(base, vin string) (interface{}, error) {
-// 	var res interface{}
-// 	uri := fmt.Sprintf("%s/"+strings.TrimLeft(base, "/"), BaseURI, v.brand, v.country, vin)
-// 	err := v.getJSON(uri, &res)
-// 	return res, err
-// }
diff --git a/vehicle/tesla.go b/vehicle/tesla.go
index fe054965..48271299 100644
--- a/vehicle/tesla.go
+++ b/vehicle/tesla.go
@@ -26,7 +26,7 @@ func init() {
 	registry.Add("tesla", NewTeslaFromConfig)
 }
 
-// NewTeslaFromConfig creates a new Tesla vehicle
+// NewTeslaFromConfig creates a new vehicle
 func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
 		embed          `mapstructure:",squash"`
diff --git a/vehicle/tronity.go b/vehicle/tronity.go
index 700ecb2c..24adcf80 100644
--- a/vehicle/tronity.go
+++ b/vehicle/tronity.go
@@ -32,7 +32,7 @@ func init() {
 	registry.Add("tronity", NewTronityFromConfig)
 }
 
-// NewTronityFromConfig creates a new Tronity vehicle
+// NewTronityFromConfig creates a new vehicle
 func NewTronityFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
 		embed       `mapstructure:",squash"`
diff --git a/vehicle/vehicle.go b/vehicle/vehicle.go
index 6b6dc8fc..05ef4bf3 100644
--- a/vehicle/vehicle.go
+++ b/vehicle/vehicle.go
@@ -15,12 +15,12 @@ type embed struct {
 	Identifier_ string `mapstructure:"identifier"`
 }
 
-// Title implements the Vehicle.Title interface
+// Title implements the api.Vehicle interface
 func (v *embed) Title() string {
 	return v.Title_
 }
 
-// Capacity implements the Vehicle.Capacity interface
+// Capacity implements the api.Vehicle interface
 func (v *embed) Capacity() int64 {
 	return v.Capacity_
 }
@@ -61,12 +61,6 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Vehicle, error
 		return nil, err
 	}
 
-	for k, v := range map[string]string{"charge": cc.Charge.PluginType()} {
-		if v == "" {
-			return nil, fmt.Errorf("missing plugin configuration: %s", k)
-		}
-	}
-
 	getter, err := provider.NewFloatGetterFromConfig(cc.Charge)
 	if err != nil {
 		return nil, fmt.Errorf("charge: %w", err)
diff --git a/vehicle/wrapper/wrapper.go b/vehicle/wrapper/wrapper.go
index f612cf90..2470ced9 100644
--- a/vehicle/wrapper/wrapper.go
+++ b/vehicle/wrapper/wrapper.go
@@ -20,12 +20,12 @@ func New(w api.Vehicle, err error) (api.Vehicle, error) {
 	return v, nil
 }
 
-// Title implements the Vehicle.Title interface
+// Title implements the api.Vehicle interface
 func (v *Wrapper) Title() string {
 	return "unavailable"
 }
 
-// Capacity implements the Vehicle.Capacity interface
+// Capacity implements the api.Vehicle interface
 func (v *Wrapper) Capacity() int64 {
 	return 0
 }
@@ -35,7 +35,7 @@ func (v *Wrapper) Identify() (string, error) {
 	return "", v.err
 }
 
-// SoC implements the api.Vehicle interface
+// SoC implements the api.Battery interface
 func (v *Wrapper) SoC() (float64, error) {
 	return 0, v.err
 }


Test patch:
diff --git a/core/coordinator_test.go b/core/coordinator_test.go
new file mode 100644
index 00000000..f424aee6
--- /dev/null
+++ b/core/coordinator_test.go
@@ -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)
+		}
+	}
+
+}
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index afbb24a5..01d8dde8 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -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)
-		}
-	}
-}