Repository: evcc-io/evcc
Pull request #10845 (2023-11-20 10:50:04)

VW: remove legacy WeConnect
Refs https://github.com/evcc-io/evcc/issues/10832

StefanSchoof: Weiter ref https://github.com/evcc-io/evcc/discussions/10891

Patch:
diff --git a/charger/easee.go b/charger/easee.go
index a3d0e037..5c4f0d5a 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -840,6 +840,8 @@ func (c *Easee) updateSmartCharging() {
 	}
 }
 
+var _ loadpoint.Controller = (*Easee)(nil)
+
 // LoadpointControl implements loadpoint.Controller
 func (c *Easee) LoadpointControl(lp loadpoint.API) {
 	c.lp = lp
diff --git a/charger/ocpp.go b/charger/ocpp.go
index 9d9854fe..94bad37f 100644
--- a/charger/ocpp.go
+++ b/charger/ocpp.go
@@ -493,6 +493,8 @@ func (c *OCPP) phases1p3p(phases int) error {
 // 	return "", errors.New("not implemented")
 // }
 
+var _ loadpoint.Controller = (*OCPP)(nil)
+
 // LoadpointControl implements loadpoint.Controller
 func (c *OCPP) LoadpointControl(lp loadpoint.API) {
 	c.lp = lp
diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go
index 35c8a30d..8e96d6df 100644
--- a/charger/openwb-2.0.go
+++ b/charger/openwb-2.0.go
@@ -228,5 +228,5 @@ func (wb *OpenWB20) identify() (string, error) {
 	if err != nil {
 		return "", err
 	}
-	return utf16BytesToString(b, binary.BigEndian), nil
+	return bytesAsString(b), nil
 }
diff --git a/charger/openwb.go b/charger/openwb.go
index 62d1db99..baed6950 100644
--- a/charger/openwb.go
+++ b/charger/openwb.go
@@ -74,7 +74,7 @@ func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, p1p
 	}
 
 	// check if loadpoint configured
-	configured, err := to.BoolGetter(mq(openwb.ConfiguredTopic))
+	configured, err := provider.NewMqtt(log, client, fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.ConfiguredTopic), timeout).BoolGetter()
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/shelly.go b/charger/shelly.go
index 67f0cba1..e992bb42 100644
--- a/charger/shelly.go
+++ b/charger/shelly.go
@@ -59,8 +59,7 @@ func (c *Shelly) Enabled() (bool, error) {
 
 // Enable implements the api.Charger interface
 func (c *Shelly) Enable(enable bool) error {
-	err := c.conn.Enable(enable)
-	if err != nil {
+	if err := c.conn.Enable(enable); err != nil {
 		return err
 	}
 
diff --git a/charger/switchsocket.go b/charger/switchsocket.go
index 7e4f5fe6..99ef1333 100644
--- a/charger/switchsocket.go
+++ b/charger/switchsocket.go
@@ -1,6 +1,9 @@
 package charger
 
-import "github.com/evcc-io/evcc/api"
+import (
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/core/loadpoint"
+)
 
 // switchSocket implements the api.Charger Status and CurrentPower methods
 // using basic generic switch socket functions
@@ -9,6 +12,7 @@ type switchSocket struct {
 	enabled      func() (bool, error)
 	currentPower func() (float64, error)
 	standbypower float64
+	lp           loadpoint.API
 }
 
 func NewSwitchSocket(
@@ -27,6 +31,10 @@ func NewSwitchSocket(
 
 // Status calculates a generic switches status
 func (c *switchSocket) Status() (api.ChargeStatus, error) {
+	if c.lp != nil && c.lp.GetMode() == api.ModeOff {
+		return api.StatusA, nil
+	}
+
 	res := api.StatusB
 
 	// static mode
@@ -83,3 +91,10 @@ func (c *switchSocket) CurrentPower() (float64, error) {
 
 	return power, err
 }
+
+var _ loadpoint.Controller = (*switchSocket)(nil)
+
+// LoadpointControl implements loadpoint.Controller
+func (c *switchSocket) LoadpointControl(lp loadpoint.API) {
+	c.lp = lp
+}
diff --git a/charger/warp2.go b/charger/warp2.go
index cd405eb0..dd15be57 100644
--- a/charger/warp2.go
+++ b/charger/warp2.go
@@ -60,19 +60,19 @@ func NewWarp2FromConfig(other map[string]interface{}) (api.Charger, error) {
 	}
 
 	var currentPower, totalEnergy func() (float64, error)
-	if wb.hasFeature(cc.Topic, warp.FeatureMeter) {
+	if wb.hasFeature(cc.Topic, warp.FeatureMeter, cc.Timeout) {
 		currentPower = wb.currentPower
 		totalEnergy = wb.totalEnergy
 	}
 
 	var currents, voltages func() (float64, float64, float64, error)
-	if wb.hasFeature(cc.Topic, warp.FeatureMeterPhases) {
+	if wb.hasFeature(cc.Topic, warp.FeatureMeterPhases, cc.Timeout) {
 		currents = wb.currents
 		voltages = wb.voltages
 	}
 
 	var identity func() (string, error)
-	if wb.hasFeature(cc.Topic, warp.FeatureNfc) {
+	if wb.hasFeature(cc.Topic, warp.FeatureNfc, cc.Timeout) {
 		identity = wb.identify
 	}
 
@@ -160,14 +160,14 @@ func NewWarp2(mqttconf mqtt.Config, topic, emTopic string, timeout time.Duration
 	return wb, nil
 }
 
-func (wb *Warp2) hasFeature(root, feature string) bool {
+func (wb *Warp2) hasFeature(root, feature string, timeout time.Duration) bool {
 	if wb.features != nil {
 		return slices.Contains(wb.features, feature)
 	}
 
 	topic := fmt.Sprintf("%s/info/features", root)
 
-	if dataG, err := provider.NewMqtt(wb.log, wb.client, topic, 0).StringGetter(); err == nil {
+	if dataG, err := provider.NewMqtt(wb.log, wb.client, topic, timeout).StringGetter(); err == nil {
 		if data, err := dataG(); err == nil {
 			if err := json.Unmarshal([]byte(data), &wb.features); err == nil {
 				return slices.Contains(wb.features, feature)
diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go
index 43fe8704..b1b832d6 100644
--- a/cmd/configure/configure.go
+++ b/cmd/configure/configure.go
@@ -17,15 +17,14 @@ type device struct {
 }
 
 type loadpoint struct {
-	Title             string // TODO Perspektivisch können wir was aus core wiederverwenden, für später
-	Charger           string
-	ChargeMeter       string
-	Vehicle           string
-	Mode              string
-	MinCurrent        int
-	MaxCurrent        int
-	Phases            int
-	ResetOnDisconnect string
+	Title       string // TODO Perspektivisch können wir was aus core wiederverwenden, für später
+	Charger     string
+	ChargeMeter string
+	Vehicle     string
+	Mode        string
+	MinCurrent  int
+	MaxCurrent  int
+	Phases      int
 }
 
 type globalConfig struct {
diff --git a/cmd/configure/configure.tpl b/cmd/configure/configure.tpl
index 1bc1e6d7..07c85359 100644
--- a/cmd/configure/configure.tpl
+++ b/cmd/configure/configure.tpl
@@ -57,7 +57,6 @@ loadpoints:
   phases: {{ .Phases }}
   mincurrent: {{ .MinCurrent }}
   maxcurrent: {{ .MaxCurrent }}
-  resetOnDisconnect: {{ .ResetOnDisconnect }}
 {{- end }}
 {{- end }}
 
diff --git a/cmd/configure/main.go b/cmd/configure/main.go
index 283584b9..041142df 100644
--- a/cmd/configure/main.go
+++ b/cmd/configure/main.go
@@ -426,12 +426,6 @@ func (c *CmdConfigure) configureLoadpoints() {
 
 		fmt.Println()
 		loadpoint.Mode = c.askValue(question{valueType: templates.TypeChargeModes, excludeNone: true})
-
-		fmt.Println()
-		loadpoint.ResetOnDisconnect = c.askValue(question{
-			label:     c.localizedString("Loadpoint_ResetOnDisconnect"),
-			valueType: templates.TypeBool,
-		})
 		c.configuration.AddLoadpoint(loadpoint)
 
 		fmt.Println()
diff --git a/cmd/setup.go b/cmd/setup.go
index 01775efe..a76112a8 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"net"
 	"net/http"
+	"regexp"
 	"slices"
 	"strconv"
 	"strings"
@@ -68,6 +69,8 @@ var conf = globalConfig{
 	},
 }
 
+var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`)
+
 type globalConfig struct {
 	Network      networkConfig
 	Log          string
@@ -271,6 +274,10 @@ func configureChargers(static []config.Named, names ...string) error {
 }
 
 func vehicleInstance(cc config.Named) (api.Vehicle, error) {
+	if !nameRE.MatchString(cc.Name) {
+		return nil, fmt.Errorf("vehicle name must not contain special characters or spaces: %s", cc.Name)
+	}
+
 	instance, err := vehicle.NewFromConfig(cc.Type, cc.Other)
 	if err != nil {
 		var ce *util.ConfigError
diff --git a/core/loadpoint.go b/core/loadpoint.go
index d4c2e415..fbe24e6d 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -384,8 +384,20 @@ func (lp *Loadpoint) pushEvent(event string) {
 
 // publish sends values to UI and databases
 func (lp *Loadpoint) publish(key string, val interface{}) {
-	if lp.uiChan != nil {
-		lp.uiChan <- util.Param{Key: key, Val: val}
+	// test helper
+	if lp.uiChan == nil {
+		return
+	}
+
+	p := util.Param{Key: key, Val: val}
+
+	// https://github.com/evcc-io/evcc/issues/11191 prevent deadlock
+	select {
+	case lp.uiChan <- p:
+	default:
+		go func() {
+			lp.uiChan <- p
+		}()
 	}
 }
 
diff --git a/core/site.go b/core/site.go
index 3d97340a..1213948e 100644
--- a/core/site.go
+++ b/core/site.go
@@ -360,9 +360,15 @@ func (site *Site) publish(key string, val interface{}) {
 		val = s.String()
 	}
 
-	site.uiChan <- util.Param{
-		Key: key,
-		Val: val,
+	p := util.Param{Key: key, Val: val}
+
+	// https://github.com/evcc-io/evcc/issues/11191 prevent deadlock
+	select {
+	case site.uiChan <- p:
+	default:
+		go func() {
+			site.uiChan <- p
+		}()
 	}
 }
 
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 338cb8f8..65a78e45 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -95,8 +95,6 @@ vehicles:
     vin: WREN...
     onIdentify: # set defaults when vehicle is identified
       mode: pv # enable PV-charging when vehicle is identified
-      minSoc: 20 # immediately charge to 20% regardless of mode unless "off" (disabled)
-      targetSoc: 90 # limit charge to 90%
 
 # site describes the EVU connection, PV and home battery
 site:
diff --git a/i18n/da.toml b/i18n/da.toml
index 39862337..39ac4ef4 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -209,6 +209,7 @@ currents = "Ladestrøm"
 default = "standard"
 disclaimerHint = "Bemærk:"
 disclaimerText = "Ændringer vil gå tabt, efter at serveren er genstartet."
+onlyForSocBasedCharging = "Disse indstillinger er kun tilgængelig for køretøjer med kendt opladningsniveau"
 title = "Indstillinger {0}"
 vehicle = "Køretøj"
 
@@ -248,12 +249,13 @@ logout = "Log ud"
 activate = "Tænd"
 co2Limit = "CO₂ grænse på {co2}"
 costLimitIgnore = "Den konfigurerede {limit} vil blive ignoreret i denne periode."
-currentPlan = "Opladningsplan"
+currentPlan = "Aktiv plan"
 descriptionEnergy = "Hvornår skal {targetEnergy} være sendt til køretøjet?"
 descriptionSoc = "Hvornår skal køretøjet være opladt til {targetSoc} %?"
 inactiveLabel = "Planlagt tid"
 modalTitle = "Sæt måltid"
 noActivePlan = "Ingen aktiv plan"
+notReachableInTime = "Mål kan ikke opnås i tide. Forventet færdig: {endTime}."
 onlyInPvMode = "Opladningsplan virker kun i solar mode"
 planDescription = "Angiv et afgangstidpunkt og evcc vil oplade køretøjet så billigt og miljøvenligt som muligt"
 planDuration = "Opladningstid"
diff --git a/i18n/es.toml b/i18n/es.toml
index 6f2114e3..77eaedf2 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -179,6 +179,7 @@ departureTab = "Salidas"
 goal = "Objetivo de la carga"
 modalTitle = "Plan de carga"
 none = "ninguno"
+remove = "Quitar"
 time = "Tiempo"
 title = "Plan"
 titleMinSoc = "Carga mínima"
@@ -217,6 +218,7 @@ currents = "Corriente de carga"
 default = "defecto"
 disclaimerHint = "Nota:"
 disclaimerText = "Actualmente, los cambios aún no son persistentes y se restablecen al iniciar el servicio."
+onlyForSocBasedCharging = "Estas opciones sólo están disponibles para vehículos con nivel de carga conocido"
 title = "Configuraciones {0}"
 vehicle = "Vehículo"
 
@@ -256,12 +258,13 @@ logout = "cerrar sesión"
 activate = "Activar"
 co2Limit = "Límite de CO₂ del {co2}"
 costLimitIgnore = "El {limit} configurado será ignorado durante este periodo."
-currentPlan = "Horarios programados"
+currentPlan = "Plan activo"
 descriptionEnergy = "¿Hasta cuándo deben haberse cargado {targetEnergy} al vehículo?"
 descriptionSoc = "¿Hasta cuándo debe haberse cargarse el vehículo al {targetSoc}%?"
 inactiveLabel = "Tiempo objetivo"
 modalTitle = "Establecer el tiempo objetivo"
 noActivePlan = "Ningún plan activo"
+notReachableInTime = "Objetivo no alcanzable en el tiempo. Fin estimado: {endTime}"
 onlyInPvMode = "El plan de carga sólo funciona en modo solar"
 planDescription = "Introduzca una hora de salida y evcc cargará el vehículo de la forma más económica o ecológica posible"
 planDuration = "Tiempo de carga"
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 051b15c0..89242b9e 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -179,6 +179,7 @@ departureTab = "Išvykimas"
 goal = "Įkrovimo tikslas"
 modalTitle = "Įkrovimo planas"
 none = "nėra"
+remove = "Pašalinti"
 time = "Laikas"
 title = "Planas"
 titleMinSoc = "Minimali įkrova"
@@ -217,6 +218,7 @@ currents = "Įkrovimo srovė"
 default = "standartiškai"
 disclaimerHint = "Pastaba:"
 disclaimerText = "Šie pakeitimai neišlieka po evcc serverio restarto."
+onlyForSocBasedCharging = "Šie pasirinkimai galimi tik automobiliams su žinomu įkrovos lygiu."
 title = "Nustatymai {0}"
 vehicle = "Automobilis"
 
@@ -256,12 +258,13 @@ logout = "atsijungti"
 activate = "Aktyvuoti"
 co2Limit = "CO₂ riba iš {co2}"
 costLimitIgnore = "Šiuo laikotarpiu sukonfigūruotas {limit} bus ignoruojamas."
-currentPlan = "Įkrovimo grafikas"
+currentPlan = "Planas aktyvus"
 descriptionEnergy = "Iki kada {targetEnergy} turėtų būti įkrauta į automobilį?"
 descriptionSoc = "Kada automobilis turėtų būti įkrautas iki {targetSoc}%?"
 inactiveLabel = "Suplanuotas laikas"
 modalTitle = "Nustatyti įkrovimo pabaigos laiką"
 noActivePlan = "Nėra Aktyvaus Plano"
+notReachableInTime = "Tikslo pasiekti nespėsime. Numatoma pabaiga: {endTime}."
 onlyInPvMode = "Įkrovimo planas veikia tik nustatyme Saulė."
 planDescription = "Įveskite išvykimo laiką ir evcc įkraus automobilį kiek įmanoma ekonomiškiau arba ekologiškiau."
 planDuration = "Įkrovimo laikas"
diff --git a/i18n/pt.toml b/i18n/pt.toml
index b9d7cef7..766f4858 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -172,6 +172,7 @@ modalTitle = "Tens a certeza que queres reiniciar"
 vehicles = "Estacionamento"
 
 [main.chargingPlan]
+active = "Ativo"
 arrivalTab = "Chegada"
 day = "Dia"
 departureTab = "Partida"
@@ -182,6 +183,7 @@ time = "Tempo"
 title = "Plano"
 titleMinSoc = "Carga mínima"
 titleTargetCharge = "Partida"
+update = "Aplicar"
 
 [main.energyflow]
 battery = "Bateria"
@@ -215,6 +217,7 @@ currents = "Corrente de carga"
 default = "predefinado"
 disclaimerHint = "Aviso:"
 disclaimerText = "As alterações serão perdidas depois que o servidor for reiniciado."
+onlyForSocBasedCharging = "Estas opções só estão disponíveis para veículos com nível de carregamento conhecido."
 title = "Configurações {0}"
 vehicle = "Veículo"
 
@@ -254,10 +257,12 @@ logout = "encerrar sessão"
 activate = "Ativar"
 co2Limit = "Limite de CO₂ de {co2}"
 costLimitIgnore = "O {limit} configurado será ignorado durante este período."
+currentPlan = "Plano ativo"
 descriptionEnergy = "Até quando {targetEnergy} deve ser carregado no veículo?"
 descriptionSoc = "Quando o veículo deve ser carregado até {targetSoc}%?"
 inactiveLabel = "Tempo alvo"
 modalTitle = "Definir hora-alvo"
+notReachableInTime = "O objetivo não é alcançável no tempo. Final estimado: {endTime}."
 onlyInPvMode = "O plano de carregamento só funciona no modo solar."
 planDescription = "Entrar um tempo de partida, e evcc irá carregar o veículo o mais rentável ou ambientalmente amigável possível."
 planDuration = "Tempo de carga"
diff --git a/i18n/sv.toml b/i18n/sv.toml
index 4e05252b..87d1e3a9 100644
--- a/i18n/sv.toml
+++ b/i18n/sv.toml
@@ -254,6 +254,7 @@ descriptionSoc = "När ska fordonet vara laddat till {targetSoc}%?"
 inactiveLabel = "Mål-tid"
 modalTitle = "Ange måltid"
 noActivePlan = "Ingen aktiv prenumeration"
+notReachableInTime = "Laddmål kan inte nås i tid. Beräknad sluttid: {endTime}"
 onlyInPvMode = "Laddplan fungerar enbart i sol-läge."
 planDescription = "Ange din avgångstid och evcc kommer att ladda så kostnadseffektivt och miljövänligt som möjligt."
 planDuration = "Laddtid"
diff --git a/meter/openwb.go b/meter/openwb.go
index 3f623477..11242cb2 100644
--- a/meter/openwb.go
+++ b/meter/openwb.go
@@ -26,8 +26,8 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) {
 		Usage       string
 		capacity    `mapstructure:",squash"`
 	}{
-		Topic:   "openWB",
-		Timeout: 15 * time.Second,
+		Topic:   openwb.RootTopic,
+		Timeout: openwb.Timeout,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
@@ -76,7 +76,8 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) {
 		currents = collectPhaseProviders(curr)
 
 	case "pv":
-		configuredG, err := to.BoolGetter(mq("%s/pv/1/%s", cc.Topic, openwb.PvConfigured)) // first pv
+		// first pv
+		configuredG, err := provider.NewMqtt(log, client, fmt.Sprintf("%s/pv/1/%s", cc.Topic, openwb.PvConfigured), cc.Timeout).BoolGetter()
 		if err != nil {
 			return nil, err
 		}
@@ -99,7 +100,7 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) {
 		}
 
 	case "battery":
-		configuredG, err := to.BoolGetter(mq("%s/housebattery/%s", cc.Topic, openwb.BatteryConfigured))
+		configuredG, err := provider.NewMqtt(log, client, fmt.Sprintf("%s/housebattery/%s", cc.Topic, openwb.BatteryConfigured), cc.Timeout).BoolGetter()
 		if err != nil {
 			return nil, err
 		}
diff --git a/provider/config.go b/provider/config.go
index da01fca1..38a540e7 100644
--- a/provider/config.go
+++ b/provider/config.go
@@ -1,6 +1,7 @@
 package provider
 
 import (
+	"errors"
 	"fmt"
 )
 
@@ -43,6 +44,9 @@ func (r providerRegistry) Add(name string, factory func(map[string]interface{})
 }
 
 func (r providerRegistry) Get(name string) (func(map[string]interface{}) (Provider, error), error) {
+	if name == "" {
+		return nil, errors.New("missing configuration")
+	}
 	factory, exists := r[name]
 	if !exists {
 		return nil, fmt.Errorf("invalid plugin source: %s", name)
diff --git a/provider/mqtt/client.go b/provider/mqtt/client.go
index 98b4ef6f..e5932464 100644
--- a/provider/mqtt/client.go
+++ b/provider/mqtt/client.go
@@ -129,7 +129,7 @@ func (m *Client) Publish(topic string, retained bool, payload interface{}) error
 	return nil
 }
 
-// Listen validates uniqueness and registers and attaches listener
+// Listen attaches listener to slice of listeners for given topic
 func (m *Client) Listen(topic string, callback func(string)) error {
 	m.mux.Lock()
 	m.listener[topic] = append(m.listener[topic], callback)
diff --git a/server/mqtt.go b/server/mqtt.go
index 3ab37436..f52c4144 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -287,7 +287,8 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.
 	}
 	if err == nil {
 		err = m.Handler.ListenSetter(topic+"/vehicle", func(payload string) error {
-			if payload == "" {
+			// https://github.com/evcc-io/evcc/issues/11184 empty payload is swallowed by listener
+			if payload == "-" {
 				lp.SetVehicle(nil)
 				return nil
 			}
diff --git a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
index 69df646a..47c24ae3 100644
--- a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
+++ b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
@@ -23,7 +23,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -34,7 +33,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37121 # Active energy import from the grid
@@ -44,7 +42,6 @@ render: |
   currents:
   - source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37107 # Huawei phase A grid current
@@ -53,7 +50,6 @@ render: |
     scale: -0.01
   - source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37109 # Huawei phase B grid current
@@ -62,7 +58,6 @@ render: |
     scale: -0.01
   - source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37111 # Huawei phase C grid current
@@ -74,7 +69,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -84,7 +78,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 32106 # Accumulated energy yield
@@ -96,7 +89,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -112,7 +104,6 @@ render: |
   soc:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       {{- if eq .storageunit "1" }}
@@ -127,7 +118,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       {{- if eq .storageunit "1" }}
diff --git a/templates/definition/meter/huawei-sun2000-dongle.yaml b/templates/definition/meter/huawei-sun2000-dongle.yaml
index f4fe7b12..f6987b43 100644
--- a/templates/definition/meter/huawei-sun2000-dongle.yaml
+++ b/templates/definition/meter/huawei-sun2000-dongle.yaml
@@ -15,7 +15,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -25,7 +24,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 32106 # Accumulated energy yield
diff --git a/templates/definition/meter/keba-kecontact.yaml b/templates/definition/meter/keba-kecontact.yaml
new file mode 100644
index 00000000..deca220e
--- /dev/null
+++ b/templates/definition/meter/keba-kecontact.yaml
@@ -0,0 +1,26 @@
+template: keba-kecontact
+# clone of kostal-ksem with different slave id
+products:
+  - brand: KEBA
+    description:
+      generic: KeContact E10
+params:
+  - name: usage
+    choice: ["grid"]
+  - name: modbus
+    choice: ["tcpip"]
+    id: 1
+render: |
+  type: modbus
+  {{- include "modbus" . }}
+  model: sunspec
+  power: Power
+  energy: Import
+  currents:
+    - CurrentL1
+    - CurrentL2
+    - CurrentL3
+  powers:
+    - PowerL1
+    - PowerL2
+    - PowerL3
diff --git a/templates/definition/meter/kostal-plenticore.yaml b/templates/definition/meter/kostal-plenticore.yaml
index ae4e8641..9d8a73ed 100644
--- a/templates/definition/meter/kostal-plenticore.yaml
+++ b/templates/definition/meter/kostal-plenticore.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Kostal
     description:
       generic: Plenticore Hybrid
+capabilities: ["battery-control"]
 linked:
   - template: kostal-ksem-inverter
     usage: grid
diff --git a/templates/definition/meter/sofarsolar-g3.yaml b/templates/definition/meter/sofarsolar-g3.yaml
index cb232641..0b8c6bd0 100644
--- a/templates/definition/meter/sofarsolar-g3.yaml
+++ b/templates/definition/meter/sofarsolar-g3.yaml
@@ -12,17 +12,21 @@ products:
   - brand: SofarSolar
     description:
       generic: SOFAR 5…24KTL-G3
+capabilities: ["battery-control"]
+requirements:
+  description:
+    de: Es wird dringend empfohlen die Verbindung über einen LSE-3 Logger Stick herzustellen. Andere Verbindungswege (z.B. LSW-3 oder via RS485) sind u.U. instabil und benötigen Verzögerungen (delay) > 0s.
+    en: It is strongly recommended to connect via an LSE-3 logger stick. Other connection paths (e.g. LSW-3 or via RS485) may be unstable and require delays > 0s.
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
   - name: modbus
-    choice: ["rs485", "tcpip"]
-    baudrate: 9600
-    comset: 8N1
-    port: 502
+    choice: ["tcpip", "rs485"]
+    port: 8899
     id: 1
   - name: delay
-    default: 1s
+    default: 0s
+    advanced: true
   - name: capacity
     advanced: true
   # battery control
diff --git a/templates/definition/meter/sungrow-hybrid.yaml b/templates/definition/meter/sungrow-hybrid.yaml
index b87769df..697f7811 100644
--- a/templates/definition/meter/sungrow-hybrid.yaml
+++ b/templates/definition/meter/sungrow-hybrid.yaml
@@ -4,6 +4,7 @@ products:
   - brand: Sungrow
     description:
       generic: SH Series Hybrid Inverter
+capabilities: ["battery-control"]
 requirements:
   description:
     de: Die Verbindung ist seriell (RS485) oder über die native (hintere) LAN-Buchse am Wechselrichter möglich. Verbindungen über das WiNet-S-Dongle (WiFi oder LAN) funktionieren NICHT korrekt.
diff --git a/templates/definition/meter/tesla-powerwall.yaml b/templates/definition/meter/tesla-powerwall.yaml
index 8ef478fb..6c33b672 100644
--- a/templates/definition/meter/tesla-powerwall.yaml
+++ b/templates/definition/meter/tesla-powerwall.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Tesla
     description:
       generic: Powerwall
+capabilities: ["battery-control"]
 requirements:
   description:
     de: |
diff --git a/templates/definition/meter/victron-energy.yaml b/templates/definition/meter/victron-energy.yaml
index 4d66baad..9f003520 100644
--- a/templates/definition/meter/victron-energy.yaml
+++ b/templates/definition/meter/victron-energy.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Victron
     description:
       generic: Energy
+capabilities: ["battery-control"]
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
diff --git a/templates/definition/vehicle/vw-id.yaml b/templates/definition/vehicle/vw-id.yaml
deleted file mode 100644
index d617c217..00000000
--- a/templates/definition/vehicle/vw-id.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-template: id
-products:
-  - brand: Volkswagen
-    description:
-      generic: We Connect ID
-requirements:
-  description:
-    de: ID.3, ID.4 (funktioniert auch für e-Golf, e-Up)
-    en: ID.3, ID.4 (also works for e-Golf, e-Up)
-params:
-  - preset: vehicle-base
-  - preset: vehicle-identify
-  - name: vin
-    example: WVWZZZ...
-  - name: timeout
-    default: 10s
-render: |
-  type: id
-  {{ include "vehicle-base" . }}
-  {{ include "vehicle-identify" . }}
-  timeout: {{ .timeout }}
diff --git a/templates/definition/vehicle/vw.yaml b/templates/definition/vehicle/vw.yaml
index f0259b74..382cb8e1 100644
--- a/templates/definition/vehicle/vw.yaml
+++ b/templates/definition/vehicle/vw.yaml
@@ -1,12 +1,13 @@
 template: vw
+covers: ["id"]
 products:
   - brand: Volkswagen
     description:
-      generic: We Connect (legacy)
+      generic: We Connect ID
 requirements:
   description:
-    de: ältere Modelle wie e-Up, e-Golf, etc.
-    en: older Models like e-Up, e-Golf, etc.
+    de: e-Golf, e-Up, ID Familie
+    en: e-Golf, e-Up, ID family
 params:
   - preset: vehicle-base
   - preset: vehicle-identify
diff --git a/templates/evcc.io/.gitignore b/templates/evcc.io/.gitignore
index b5990282..f59ec20a 100644
--- a/templates/evcc.io/.gitignore
+++ b/templates/evcc.io/.gitignore
@@ -1,1 +1,1 @@
-!*.json
\ No newline at end of file
+*
\ No newline at end of file
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index 3b22463d..06ab4e69 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -107,6 +107,7 @@
   "Huawei",
   "inepro",
   "Janitza",
+  "KEBA",
   "Kostal",
   "LG",
   "Loxone",
diff --git a/util/monitor.go b/util/monitor.go
index 7f056a26..98693a7c 100644
--- a/util/monitor.go
+++ b/util/monitor.go
@@ -7,6 +7,7 @@ import (
 	"github.com/evcc-io/evcc/api"
 )
 
+// Monitor monitors values for regular updates
 type Monitor[T any] struct {
 	val     T
 	mu      sync.RWMutex
@@ -16,6 +17,7 @@ type Monitor[T any] struct {
 	timeout time.Duration
 }
 
+// NewMonitor created a new monitor with given timeout
 func NewMonitor[T any](timeout time.Duration) *Monitor[T] {
 	return &Monitor[T]{
 		done:    make(chan struct{}),
@@ -62,9 +64,32 @@ func (m *Monitor[T]) GetFunc(get func(T)) error {
 		default:
 			return api.ErrOutdated
 		}
-	} else if time.Since(m.updated) > m.timeout {
+	}
+
+	if time.Since(m.updated) > m.timeout {
+		err := api.ErrOutdated
+
+		// wait once on very first call
+		if m.updated.IsZero() {
+			m.mu.RUnlock()
+
+			// mark as waited once
+			m.mu.Lock()
+			m.updated.Add(time.Nanosecond)
+			m.mu.Unlock()
+
+			select {
+			case <-m.done:
+				// got value and updated timestamp
+				err = nil
+			case <-time.After(m.timeout):
+			}
+
+			m.mu.RLock()
+		}
+
 		get(m.val)
-		return api.ErrOutdated
+		return err
 	}
 
 	get(m.val)
diff --git a/util/tee.go b/util/tee.go
index 7bbae57d..6fb4e873 100644
--- a/util/tee.go
+++ b/util/tee.go
@@ -20,7 +20,7 @@ type Tee struct {
 func (t *Tee) Attach() <-chan Param {
 	// TODO find better approach to prevent deadlocks
 	// this will buffer the receiver channel to prevent deadlocks when consumers use mutex-protected loadpoint api
-	out := make(chan Param, 16)
+	out := make(chan Param, 128)
 	t.add(out)
 	return out
 }
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index d6a581fd..0f76b31c 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -303,10 +303,6 @@ presets:
     params:
       - name: mode
         advanced: true
-      - name: minSoc
-        advanced: true
-      - name: targetSoc
-        advanced: true
       - name: minCurrent
         advanced: true
       - name: maxCurrent
diff --git a/util/templates/generate/main.go b/util/templates/generate/main.go
index 15934bc4..8e7a9c4b 100644
--- a/util/templates/generate/main.go
+++ b/util/templates/generate/main.go
@@ -20,7 +20,9 @@ const (
 
 func main() {
 	for _, lang := range []string{"de", "en"} {
-		generateDocs(lang)
+		if err := generateDocs(lang); err != nil {
+			panic(err)
+		}
 	}
 
 	if err := generateBrandJSON(); err != nil {
@@ -28,13 +30,13 @@ func main() {
 	}
 }
 
-func generateDocs(lang string) {
+func generateDocs(lang string) error {
 	for _, class := range templates.ClassValues() {
 		path := fmt.Sprintf("%s/%s/%s", docsPath, lang, strings.ToLower(class.String()))
 		_, err := os.Stat(path)
 		if os.IsNotExist(err) {
 			if err := os.MkdirAll(path, 0o755); err != nil {
-				panic(err)
+				return err
 			}
 		}
 		if err := clearDir(path); err != nil {
@@ -42,9 +44,11 @@ func generateDocs(lang string) {
 		}
 
 		if err := generateClass(class, lang); err != nil {
-			panic(err)
+			return err
 		}
 	}
+
+	return nil
 }
 
 func generateClass(class templates.Class, lang string) error {
diff --git a/util/templates/includes/vehicle-identify.tpl b/util/templates/includes/vehicle-identify.tpl
index 9ef7ed7d..41a7bfab 100644
--- a/util/templates/includes/vehicle-identify.tpl
+++ b/util/templates/includes/vehicle-identify.tpl
@@ -1,5 +1,5 @@
 {{ define "vehicle-identify" }}
-{{- if or .mode .minSoc .targetSoc .minCurrent .maxCurrent .priority }}
+{{- if or .mode .minCurrent .maxCurrent .priority }}
 onIdentify:
 {{- if .mode }}
   mode: {{ .mode }}
diff --git a/util/templates/types.go b/util/templates/types.go
index 3542a1a2..192f6f7b 100644
--- a/util/templates/types.go
+++ b/util/templates/types.go
@@ -54,14 +54,15 @@ const (
 var ValidDependencies = []string{DependencyCheckEmpty, DependencyCheckNotEmpty, DependencyCheckEqual}
 
 const (
-	CapabilityISO151182 = "iso151182" // ISO 15118-2 support
-	CapabilityMilliAmps = "mA"        // Granular current control support
-	CapabilityRFID      = "rfid"      // RFID support
-	Capability1p3p      = "1p3p"      // 1P/3P phase switching support
-	CapabilitySMAHems   = "smahems"   // SMA HEMS Support
+	CapabilityISO151182      = "iso151182"       // ISO 15118-2 support
+	CapabilityMilliAmps      = "mA"              // Granular current control support
+	CapabilityRFID           = "rfid"            // RFID support
+	Capability1p3p           = "1p3p"            // 1P/3P phase switching support
+	CapabilitySMAHems        = "smahems"         // SMA HEMS support
+	CapabilityBatteryControl = "battery-control" // Battery control support
 )
 
-var ValidCapabilities = []string{CapabilityISO151182, CapabilityMilliAmps, CapabilityRFID, Capability1p3p, CapabilitySMAHems}
+var ValidCapabilities = []string{CapabilityISO151182, CapabilityMilliAmps, CapabilityRFID, Capability1p3p, CapabilitySMAHems, CapabilityBatteryControl}
 
 const (
 	RequirementEEBUS       = "eebus"       // EEBUS Setup is required
diff --git a/vehicle/bmw/api.go b/vehicle/bmw/api.go
index ea7b0ca0..7df5de7c 100644
--- a/vehicle/bmw/api.go
+++ b/vehicle/bmw/api.go
@@ -55,11 +55,13 @@ func (v *API) Status(vin string) (VehicleStatus, error) {
 	var res VehicleStatus
 	uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles/state?apptimezone=120&appDateTime=%d", regions[v.region].CocoApiURI, time.Now().UnixMilli())
 
-	req, err := request.New(http.MethodGet, uri, nil, map[string]string{
+	req, _ := request.New(http.MethodGet, uri, nil, map[string]string{
 		"bmw-vin": vin,
 	})
-	if err == nil {
-		err = v.DoJSON(req, &res)
+
+	err := v.DoJSON(req, &res)
+	if err != nil && res.StatusCode != 0 {
+		err = fmt.Errorf("%d: %s (%w)", res.StatusCode, res.Message, err)
 	}
 
 	return res, err
diff --git a/vehicle/bmw/types.go b/vehicle/bmw/types.go
index ddd54fd0..6d2f7aa7 100644
--- a/vehicle/bmw/types.go
+++ b/vehicle/bmw/types.go
@@ -7,7 +7,9 @@ type Vehicle struct {
 }
 
 type VehicleStatus struct {
-	State struct {
+	StatusCode int
+	Message    string
+	State      struct {
 		CurrentMileage        int64
 		Range                 int64
 		ElectricChargingState struct {
diff --git a/vehicle/cloud.go b/vehicle/cloud.go
index 0adaf754..f5ed9dda 100644
--- a/vehicle/cloud.go
+++ b/vehicle/cloud.go
@@ -101,7 +101,6 @@ func (v *Cloud) chargeState() (float64, error) {
 	defer cancel()
 
 	res, err := v.client.SoC(ctx, req)
-
 	if err != nil && strings.Contains(err.Error(), api.ErrMustRetry.Error()) {
 		return 0, api.ErrMustRetry
 	}
diff --git a/vehicle/smart/hello/api.go b/vehicle/smart/hello/api.go
index a76e1681..1a1d2a76 100644
--- a/vehicle/smart/hello/api.go
+++ b/vehicle/smart/hello/api.go
@@ -156,8 +156,7 @@ func (v *API) UpdateSession(vin string) error {
 }
 
 func (v *API) Status(vin string) (VehicleStatus, error) {
-	if _, err := v.identity.UpdateSession(vin); err != nil {
-		// if err := v.UpdateSession(vin); err != nil {
+	if err := v.UpdateSession(vin); err != nil {
 		return VehicleStatus{}, fmt.Errorf("update session failed: %w", err)
 	}
 
diff --git a/vehicle/smart/hello/helper.go b/vehicle/smart/hello/helper.go
index c9ee01b1..cf82541f 100644
--- a/vehicle/smart/hello/helper.go
+++ b/vehicle/smart/hello/helper.go
@@ -5,8 +5,8 @@ import (
 	"crypto/md5"
 	"crypto/sha1"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
+	"io"
 	"net/url"
 	"strconv"
 	"time"
@@ -14,13 +14,13 @@ import (
 	"github.com/samber/lo"
 )
 
-func createSignature(method, path string, params url.Values, post any) (string, string, string, error) {
+func createSignature(method, path string, params url.Values, body io.Reader) (string, string, string, error) {
 	nonce := lo.RandomString(16, lo.AlphanumericCharset)
 	ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
 
 	md5Hash := "1B2M2Y8AsgTpgAmY7PhCfg=="
-	if post != nil {
-		bytes, err := json.Marshal(post)
+	if body != nil {
+		bytes, err := io.ReadAll(body)
 		if err != nil {
 			return "", "", "", err
 		}
diff --git a/vehicle/smart/hello/identity.go b/vehicle/smart/hello/identity.go
index 854c0ede..80616a44 100644
--- a/vehicle/smart/hello/identity.go
+++ b/vehicle/smart/hello/identity.go
@@ -173,7 +173,7 @@ func (v *Identity) appToken(token *oauth2.Token) (*oauth2.Token, string, error)
 	}
 
 	path := "/auth/account/session/secure"
-	nonce, ts, sign, err := createSignature(http.MethodPost, path, params, data)
+	nonce, ts, sign, err := createSignature(http.MethodPost, path, params, request.MarshalJSON(data))
 	if err != nil {
 		return nil, "", err
 	}
@@ -211,54 +211,3 @@ func (v *Identity) appToken(token *oauth2.Token) (*oauth2.Token, string, error)
 
 	return &tok, res.Data.UserId, nil
 }
-
-func (v *Identity) UpdateSession(vin string) (string, error) {
-	token, err := v.Token()
-	if err != nil {
-		return "", err
-	}
-
-	params := url.Values{
-		"identity_type": []string{"smart"},
-	}
-
-	data := map[string]string{
-		"vin":          vin,
-		"sessionToken": token.AccessToken,
-		"language":     "",
-	}
-
-	path := "/device-platform/user/session/update"
-	nonce, ts, sign, err := createSignature(http.MethodPost, path, params, data)
-	if err != nil {
-		return "", err
-	}
-
-	uri := fmt.Sprintf("%s/%s?%s", ApiURI, strings.TrimPrefix(path, "/"), params.Encode())
-	req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{
-		"Accept":                  "application/json;responseformat=3",
-		"Content-Type":            "application/json; charset=utf-8",
-		"X-Api-Signature-Version": "1.0",
-		"X-Api-Signature-Nonce":   nonce,
-		"Authorization":           token.AccessToken,
-		"X-App-Id":                appID,
-		"X-Device-Identifier":     v.deviceID,
-		"X-Operator-Code":         operatorCode,
-		"X-Signature":             sign,
-		"X-Timestamp":             ts,
-	})
-
-	var res struct {
-		Code    ResponseCode
-		Message string
-		Data    AppToken
-	}
-
-	if err := v.DoJSON(req, &res); err != nil {
-		return "", err
-	} else if res.Code != ResponseOK {
-		return "", fmt.Errorf("%d: %s", res.Code, res.Message)
-	}
-
-	return "", nil
-}
diff --git a/vehicle/vw-id.go b/vehicle/vw-id.go
deleted file mode 100644
index ac833a4d..00000000
--- a/vehicle/vw-id.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package vehicle
-
-import (
-	"time"
-
-	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/util"
-	"github.com/evcc-io/evcc/util/request"
-	"github.com/evcc-io/evcc/vehicle/vag/loginapps"
-	"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
-	"github.com/evcc-io/evcc/vehicle/vw/id"
-)
-
-// https://github.com/TA2k/ioBroker.vw-connect
-
-// ID is an api.Vehicle implementation for ID cars
-type ID struct {
-	*embed
-	*id.Provider // provides the api implementations
-}
-
-func init() {
-	registry.Add("id", NewIDFromConfig)
-}
-
-// NewIDFromConfig creates a new vehicle
-func NewIDFromConfig(other map[string]interface{}) (api.Vehicle, error) {
-	cc := struct {
-		embed               `mapstructure:",squash"`
-		User, Password, VIN string
-		Cache               time.Duration
-		Timeout             time.Duration
-	}{
-		Cache:   interval,
-		Timeout: request.Timeout,
-	}
-
-	if err := util.DecodeOther(other, &cc); err != nil {
-		return nil, err
-	}
-
-	if cc.User == "" || cc.Password == "" {
-		return nil, api.ErrMissingCredentials
-	}
-
-	v := &ID{
-		embed: &cc.embed,
-	}
-
-	log := util.NewLogger("id").Redact(cc.User, cc.Password, cc.VIN)
-
-	q, err := vwidentity.LoginWithAuthURL(log, id.LoginURL, id.AuthParams, cc.User, cc.Password)
-	if err != nil {
-		return nil, err
-	}
-
-	apps := loginapps.New(log)
-	token, err := apps.Exchange(q)
-	if err != nil {
-		return nil, err
-	}
-
-	api := id.NewAPI(log, apps.TokenSource(token))
-	api.Client.Timeout = cc.Timeout
-
-	vehicle, err := ensureVehicleEx(
-		cc.VIN, api.Vehicles,
-		func(v id.Vehicle) string {
-			return v.VIN
-		},
-	)
-
-	if v.Title_ == "" {
-		v.Title_ = vehicle.Nickname
-	}
-
-	if err == nil {
-		v.Provider = id.NewProvider(api, vehicle.VIN, cc.Cache)
-	}
-
-	return v, err
-}
diff --git a/vehicle/vw.go b/vehicle/vw.go
index f2db0016..54126dfb 100644
--- a/vehicle/vw.go
+++ b/vehicle/vw.go
@@ -6,25 +6,26 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"github.com/evcc-io/evcc/vehicle/vag/service"
-	"github.com/evcc-io/evcc/vehicle/vw"
+	"github.com/evcc-io/evcc/vehicle/vag/loginapps"
+	"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
+	"github.com/evcc-io/evcc/vehicle/vw/id"
 )
 
-// https://github.com/trocotronic/weconnect
 // https://github.com/TA2k/ioBroker.vw-connect
 
-// VW is an api.Vehicle implementation for VW cars
-type VW struct {
+// ID is an api.Vehicle implementation for ID cars
+type ID struct {
 	*embed
-	*vw.Provider // provides the api implementations
+	*id.Provider // provides the api implementations
 }
 
 func init() {
-	registry.Add("vw", NewVWFromConfig)
+	registry.Add("vw", NewIDFromConfig)
+	registry.Add("id", NewIDFromConfig)
 }
 
-// NewVWFromConfig creates a new vehicle
-func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
+// NewIDFromConfig creates a new vehicle
+func NewIDFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
 		embed               `mapstructure:",squash"`
 		User, Password, VIN string
@@ -43,27 +44,39 @@ func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		return nil, api.ErrMissingCredentials
 	}
 
-	v := &VW{
+	v := &ID{
 		embed: &cc.embed,
 	}
 
-	log := util.NewLogger("vw").Redact(cc.User, cc.Password, cc.VIN)
+	log := util.NewLogger("id").Redact(cc.User, cc.Password, cc.VIN)
 
-	trs, err := service.TokenRefreshServiceTokenSource(log, vw.TRSParams, vw.AuthParams, cc.User, cc.Password)
+	q, err := vwidentity.LoginWithAuthURL(log, id.LoginURL, id.AuthParams, cc.User, cc.Password)
 	if err != nil {
 		return nil, err
 	}
 
-	ts := service.MbbTokenSource(log, trs, vw.AuthClientID)
-	api := vw.NewAPI(log, ts, vw.Brand, vw.Country)
+	apps := loginapps.New(log)
+	token, err := apps.Exchange(q)
+	if err != nil {
+		return nil, err
+	}
+
+	api := id.NewAPI(log, apps.TokenSource(token))
 	api.Client.Timeout = cc.Timeout
 
-	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
+	vehicle, err := ensureVehicleEx(
+		cc.VIN, api.Vehicles,
+		func(v id.Vehicle) string {
+			return v.VIN
+		},
+	)
+
+	if v.Title_ == "" {
+		v.Title_ = vehicle.Nickname
+	}
 
 	if err == nil {
-		if err = api.HomeRegion(cc.VIN); err == nil {
-			v.Provider = vw.NewProvider(api, cc.VIN, cc.Cache)
-		}
+		v.Provider = id.NewProvider(api, vehicle.VIN, cc.Cache)
 	}
 
 	return v, err


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 9411da11..89330116 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -526,6 +526,7 @@ func cacheExpecter(t *testing.T, lp *Loadpoint) (*util.Cache, func(key string, v
 	go cache.Run(paramC)
 
 	expect := func(key string, val interface{}) {
+		time.Sleep(100 * time.Millisecond) // wait for cache to catch up
 		p := cache.Get(key)
 		t.Logf("%s: %.f", key, p.Val) // REMOVE
 		if p.Val != val {