Repository: evcc-io/evcc
Pull request #19264 (2025-02-28 16:00:59)

RCT: rewrite as async 
Fix https://github.com/evcc-io/evcc/issues/18991. Depends on https://github.com/mlnoga/rct/pull/22.

/cc @Maschga @TobiasHuber1980 wollt ihr es mal probieren?


Patch:
diff --git a/assets/js/components/Config/TariffsModal.vue b/assets/js/components/Config/TariffsModal.vue
index 017512a6..8896ab46 100644
--- a/assets/js/components/Config/TariffsModal.vue
+++ b/assets/js/components/Config/TariffsModal.vue
@@ -3,7 +3,7 @@
 		id="tariffsModal"
 		:title="$t('config.tariffs.title')"
 		:description="$t('config.tariffs.description')"
-		docs="/docs/reference/configuration/tariffs"
+		docs="/docs/tariffs"
 		:defaultYaml="defaultYaml"
 		removeKey="tariffs"
 		endpoint="/config/tariffs"
diff --git a/charger/eebus.go b/charger/eebus.go
index f4589165..7cd12647 100644
--- a/charger/eebus.go
+++ b/charger/eebus.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"slices"
@@ -49,11 +50,11 @@ type EEBus struct {
 }
 
 func init() {
-	registry.Add("eebus", NewEEBusFromConfig)
+	registry.AddCtx("eebus", NewEEBusFromConfig)
 }
 
 // NewEEBusFromConfig creates an EEBus charger from generic config
-func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewEEBusFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		Ski           string
 		Ip            string
@@ -68,13 +69,13 @@ func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewEEBus(cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy, cc.VasVW)
+	return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy, cc.VasVW)
 }
 
 //go:generate go tool decorate -f decorateEEBus -b *EEBus -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.ChargeRater,ChargedEnergy,func() (float64, error)"
 
 // NewEEBus creates EEBus charger
-func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, error) {
+func NewEEBus(ctx context.Context, ski, ip string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, error) {
 	if eebus.Instance == nil {
 		return nil, errors.New("eebus not configured")
 	}
@@ -93,7 +94,7 @@ func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charg
 		return nil, err
 	}
 
-	if err := c.Wait(90 * time.Second); err != nil {
+	if err := c.Wait(ctx); err != nil {
 		return c, err
 	}
 
diff --git a/charger/ocpp.go b/charger/ocpp.go
index 18907f46..15e25f5e 100644
--- a/charger/ocpp.go
+++ b/charger/ocpp.go
@@ -19,6 +19,7 @@ package charger
 
 import (
 	"cmp"
+	"context"
 	"fmt"
 	"math"
 	"slices"
@@ -50,11 +51,11 @@ type OCPP struct {
 const defaultIdTag = "evcc" // RemoteStartTransaction only
 
 func init() {
-	registry.Add("ocpp", NewOCPPFromConfig)
+	registry.AddCtx("ocpp", NewOCPPFromConfig)
 }
 
 // NewOCPPFromConfig creates a OCPP charger from generic config
-func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewOCPPFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		StationId      string
 		IdTag          string
@@ -84,7 +85,8 @@ func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) {
 
 	stackLevelZero := cc.StackLevelZero != nil && *cc.StackLevelZero
 
-	c, err := NewOCPP(cc.StationId, cc.Connector, cc.IdTag,
+	c, err := NewOCPP(ctx,
+		cc.StationId, cc.Connector, cc.IdTag,
 		cc.MeterValues, cc.MeterInterval,
 		stackLevelZero, cc.RemoteStart,
 		cc.ConnectTimeout)
@@ -137,7 +139,8 @@ func NewOCPPFromConfig(other map[string]interface{}) (api.Charger, error) {
 //go:generate go tool decorate -f decorateOCPP -b *OCPP -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.CurrentGetter,GetMaxCurrent,func() (float64, error)" -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.Battery,Soc,func() (float64, error)"
 
 // NewOCPP creates OCPP charger
-func NewOCPP(id string, connector int, idTag string,
+func NewOCPP(ctx context.Context,
+	id string, connector int, idTag string,
 	meterValues string, meterInterval time.Duration,
 	stackLevelZero, remoteStart bool,
 	connectTimeout time.Duration,
@@ -154,12 +157,14 @@ func NewOCPP(id string, connector int, idTag string,
 			log.DEBUG.Printf("waiting for chargepoint: %v", connectTimeout)
 
 			select {
+			case <-ctx.Done():
+				return ctx.Err()
 			case <-time.After(connectTimeout):
 				return api.ErrTimeout
 			case <-cp.HasConnected():
 			}
 
-			return cp.Setup(meterValues, meterInterval)
+			return cp.Setup(ctx, meterValues, meterInterval)
 		},
 	)
 	if err != nil {
@@ -188,7 +193,9 @@ func NewOCPP(id string, connector int, idTag string,
 		stackLevelZero: stackLevelZero,
 	}
 
-	go conn.WatchDog(10 * time.Second)
+	if cp.HasRemoteTriggerFeature {
+		go conn.WatchDog(ctx, 10*time.Second)
+	}
 
 	return c, conn.Initialized()
 }
diff --git a/charger/ocpp/connector.go b/charger/ocpp/connector.go
index 43cbf7a8..258e5202 100644
--- a/charger/ocpp/connector.go
+++ b/charger/ocpp/connector.go
@@ -1,6 +1,7 @@
 package ocpp
 
 import (
+	"context"
 	"fmt"
 	"strconv"
 	"strings"
@@ -100,16 +101,22 @@ func (conn *Connector) GetScheduleLimit(duration int) (float64, error) {
 
 // WatchDog triggers meter values messages if older than timeout.
 // Must be wrapped in a goroutine.
-func (conn *Connector) WatchDog(timeout time.Duration) {
+func (conn *Connector) WatchDog(ctx context.Context, timeout time.Duration) {
 	tick := time.NewTicker(2 * time.Second)
-	for ; true; <-tick.C {
+	for {
 		conn.mu.Lock()
 		update := conn.clock.Since(conn.meterUpdated) > timeout
 		conn.mu.Unlock()
 
-		if update && conn.cp.HasRemoteTriggerFeature {
+		if update {
 			conn.TriggerMessageRequest(core.MeterValuesFeatureName)
 		}
+
+		select {
+		case <-ctx.Done():
+			return
+		case <-tick.C:
+		}
 	}
 }
 
diff --git a/charger/ocpp/cp_setup.go b/charger/ocpp/cp_setup.go
index 1b999994..5f9d7d49 100644
--- a/charger/ocpp/cp_setup.go
+++ b/charger/ocpp/cp_setup.go
@@ -1,6 +1,7 @@
 package ocpp
 
 import (
+	"context"
 	"strconv"
 	"strings"
 	"time"
@@ -12,7 +13,7 @@ import (
 	"github.com/samber/lo"
 )
 
-func (cp *CP) Setup(meterValues string, meterInterval time.Duration) error {
+func (cp *CP) Setup(ctx context.Context, meterValues string, meterInterval time.Duration) error {
 	if err := cp.ChangeAvailabilityRequest(0, core.AvailabilityTypeOperative); err != nil {
 		cp.log.DEBUG.Printf("failed configuring availability: %v", err)
 	}
@@ -136,6 +137,8 @@ func (cp *CP) Setup(meterValues string, meterInterval time.Duration) error {
 		if err := cp.TriggerMessageRequest(0, core.MeterValuesFeatureName); err == nil {
 			// wait for meter values
 			select {
+			case <-ctx.Done():
+				return ctx.Err()
 			case <-time.After(Timeout):
 				cp.log.WARN.Println("meter timeout")
 			case <-cp.meterC:
diff --git a/go.mod b/go.mod
index 580751f5..c96ebb90 100644
--- a/go.mod
+++ b/go.mod
@@ -117,6 +117,7 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bitly/go-simplejson v0.5.0 // indirect
 	github.com/breml/rootcerts v0.2.10 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
 	github.com/cronokirby/saferith v0.33.0 // indirect
@@ -220,3 +221,5 @@ replace gopkg.in/yaml.v3 => github.com/andig/yaml v0.0.0-20240531135838-1ff5761a
 replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20241027151224-722a7a5ae529
 
 replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20241230132027-815870498cc3
+
+replace github.com/mlnoga/rct => github.com/andig/rct v0.1.2-0.20250301173047-991884936f23
diff --git a/go.sum b/go.sum
index ce1df833..34612c04 100644
--- a/go.sum
+++ b/go.sum
@@ -39,6 +39,8 @@ github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b h1:81UMfM949I7StrR
 github.com/andig/gosunspec v0.0.0-20240918203654-860ce51d602b/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k=
 github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e h1:m/NTP3JWpR7M0ljLxiQU4fzR25jjhe1LDtxLMNcoNJQ=
 github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM=
+github.com/andig/rct v0.1.2-0.20250301173047-991884936f23 h1:2kEZKOc1EzKCgzSXuG/iEkEf9yc/U4ArGGGmfFcqU3M=
+github.com/andig/rct v0.1.2-0.20250301173047-991884936f23/go.mod h1:KRjBDV8kuvGd2kJfN8/F0kjH8JjkFVpBpn7MhW+ACL8=
 github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467 h1:JqIoHxsQSV39xaemvVRu3HOkSf5AbhQ1YEENJa2cL3Q=
 github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@@ -82,6 +84,8 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -458,8 +462,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/mlnoga/rct v0.1.2-0.20250202175314-91abcbf32e53 h1:wav6CUbSFXojyUwSRmMhgzIQHUuVCoS3V7BZ3nRi4vE=
-github.com/mlnoga/rct v0.1.2-0.20250202175314-91abcbf32e53/go.mod h1:0lfd2mmBnBzIvuzYtdhG+2371u+cUfIxsYErm4P9KRI=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
diff --git a/hems/config.go b/hems/config.go
index c0843cff..d388a065 100644
--- a/hems/config.go
+++ b/hems/config.go
@@ -23,7 +23,7 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}
 	case "sma", "shm", "semp":
 		return semp.New(other, site, httpd)
 	case "eebus":
-		return eebus.New(other, site)
+		return eebus.New(ctx, other, site)
 	case "relay":
 		return relay.New(ctx, other, site)
 	default:
diff --git a/hems/eebus/eebus.go b/hems/eebus/eebus.go
index 12f75515..8acb8032 100644
--- a/hems/eebus/eebus.go
+++ b/hems/eebus/eebus.go
@@ -1,6 +1,7 @@
 package eebus
 
 import (
+	"context"
 	"errors"
 	"sync"
 	"time"
@@ -40,7 +41,7 @@ type Limits struct {
 }
 
 // New creates an EEBus HEMS from generic config
-func New(other map[string]interface{}, site site.API) (*EEBus, error) {
+func New(ctx context.Context, other map[string]interface{}, site site.API) (*EEBus, error) {
 	cc := struct {
 		Ski    string
 		Limits `mapstructure:",squash"`
@@ -75,11 +76,11 @@ func New(other map[string]interface{}, site site.API) (*EEBus, error) {
 	}
 	site.SetCircuit(lpc)
 
-	return NewEEBus(cc.Ski, cc.Limits, lpc)
+	return NewEEBus(ctx, cc.Ski, cc.Limits, lpc)
 }
 
 // NewEEBus creates EEBus charger
-func NewEEBus(ski string, limits Limits, root api.Circuit) (*EEBus, error) {
+func NewEEBus(ctx context.Context, ski string, limits Limits, root api.Circuit) (*EEBus, error) {
 	if eebus.Instance == nil {
 		return nil, errors.New("eebus not configured")
 	}
@@ -104,7 +105,7 @@ func NewEEBus(ski string, limits Limits, root api.Circuit) (*EEBus, error) {
 		return nil, err
 	}
 
-	if err := c.Wait(90 * time.Second); err != nil {
+	if err := c.Wait(ctx); err != nil {
 		return c, err
 	}
 
diff --git a/meter/battery_power.go b/meter/battery_power.go
index 100cc5f5..c270b01d 100644
--- a/meter/battery_power.go
+++ b/meter/battery_power.go
@@ -4,6 +4,8 @@ type maxpower struct {
 	MaxACPower float64
 }
 
+// var _ api.MaxACPowerGetter = (*maxpower)(nil)
+
 // MaxACPowerDecorator returns the max AC power decorator
 func (m *maxpower) Decorator() func() float64 {
 	if m.MaxACPower == 0 {
diff --git a/meter/eebus.go b/meter/eebus.go
index 909130ce..f670caa7 100644
--- a/meter/eebus.go
+++ b/meter/eebus.go
@@ -1,6 +1,7 @@
 package meter
 
 import (
+	"context"
 	"errors"
 	"time"
 
@@ -23,11 +24,11 @@ type EEBus struct {
 }
 
 func init() {
-	registry.Add("eebus", NewEEBusFromConfig)
+	registry.AddCtx("eebus", NewEEBusFromConfig)
 }
 
 // New creates an EEBus HEMS from generic config
-func NewEEBusFromConfig(other map[string]interface{}) (api.Meter, error) {
+func NewEEBusFromConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) {
 	cc := struct {
 		Ski     string
 		Ip      string
@@ -40,11 +41,11 @@ func NewEEBusFromConfig(other map[string]interface{}) (api.Meter, error) {
 		return nil, err
 	}
 
-	return NewEEBus(cc.Ski, cc.Ip, cc.Timeout)
+	return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Timeout)
 }
 
 // NewEEBus creates EEBus charger
-func NewEEBus(ski, ip string, timeout time.Duration) (*EEBus, error) {
+func NewEEBus(ctx context.Context, ski, ip string, timeout time.Duration) (*EEBus, error) {
 	if eebus.Instance == nil {
 		return nil, errors.New("eebus not configured")
 	}
@@ -63,7 +64,7 @@ func NewEEBus(ski, ip string, timeout time.Duration) (*EEBus, error) {
 		return nil, err
 	}
 
-	if err := c.Wait(90 * time.Second); err != nil {
+	if err := c.Wait(ctx); err != nil {
 		return c, err
 	}
 
diff --git a/meter/rct.go b/meter/rct.go
index a42d5234..10beeaf1 100644
--- a/meter/rct.go
+++ b/meter/rct.go
@@ -1,63 +1,33 @@
 package meter
 
 import (
+	"context"
 	"encoding/binary"
 	"errors"
 	"fmt"
 	"math"
 	"strings"
-	"sync"
 	"time"
 
-	"github.com/cenkalti/backoff/v4"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/mlnoga/rct"
 )
 
-/*
-This meter supports devices implementing the RCT communication protocol, e.g. the RCT PS 6.0 with / without battery.
-
-** Usages **
-The following usages are supported:
-- grid    ... for reading the power imported or exported to the grid
-- pv      ... for reading the power produced by the pv
-- battery ... for reading the power imported or exported to the battery
-
-** Example configuration **
-meters:
-- name: GridMeter
-  type: rct
-  uri: 192.168.1.23
-  cache: 2s
-  usage: grid
-- name: PvMeter
-  type: rct
-  uri: 192.168.1.23
-  cache: 2s
-  usage: pv
-- name: BatteryMeter
-  type: rct
-  uri: 192.168.1.23
-  cache: 2s
-  usage: battery
-*/
-
 // RCT implements the api.Meter interface
 type RCT struct {
-	bo    *backoff.ExponentialBackOff
 	conn  *rct.Connection // connection with the RCT device
 	usage string          // grid, pv, battery
 }
 
 func init() {
-	registry.Add("rct", NewRCTFromConfig)
+	registry.AddCtx("rct", NewRCTFromConfig)
 }
 
 //go:generate go tool decorate -f decorateRCT -b *RCT -r api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryController,SetBatteryMode,func(api.BatteryMode) error" -t "api.BatteryCapacity,Capacity,func() float64"
 
 // NewRCTFromConfig creates an RCT from generic config
-func NewRCTFromConfig(other map[string]interface{}) (api.Meter, error) {
+func NewRCTFromConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) {
 	cc := struct {
 		capacity       `mapstructure:",squash"`
 		Uri, Usage     string
@@ -75,29 +45,25 @@ func NewRCTFromConfig(other map[string]interface{}) (api.Meter, error) {
 		return nil, errors.New("missing usage")
 	}
 
-	return NewRCT(cc.Uri, cc.Usage, cc.MinSoc, cc.MaxSoc, cc.Cache, cc.capacity.Decorator())
+	return NewRCT(ctx, cc.Uri, cc.Usage, cc.MinSoc, cc.MaxSoc, cc.Cache, cc.capacity.Decorator())
 }
 
-var rctMu sync.Mutex
-
 // NewRCT creates an RCT meter
-func NewRCT(uri, usage string, minSoc, maxSoc int, cache time.Duration, capacity func() float64) (api.Meter, error) {
-	rctMu.Lock()
-	defer rctMu.Unlock()
+func NewRCT(ctx context.Context, uri, usage string, minSoc, maxSoc int, cache time.Duration, capacity func() float64) (api.Meter, error) {
+	log := util.NewLogger("rct")
 
-	conn, err := rct.NewConnection(uri, cache)
+	conn, err := rct.NewConnection(ctx, uri, rct.WithErrorCallback(func(err error) {
+		if err != nil {
+			log.ERROR.Println(err)
+		}
+	}), rct.WithTimeout(cache))
 	if err != nil {
 		return nil, err
 	}
 
-	bo := backoff.NewExponentialBackOff(
-		backoff.WithInitialInterval(10*time.Millisecond),
-		backoff.WithMaxElapsedTime(time.Second))
-
 	m := &RCT{
 		usage: strings.ToLower(usage),
 		conn:  conn,
-		bo:    bo,
 	}
 
 	// decorate api.MeterEnergy
@@ -232,32 +198,11 @@ func (m *RCT) batterySoc() (float64, error) {
 
 // queryFloat adds retry logic of recoverable errors to QueryFloat32
 func (m *RCT) queryFloat(id rct.Identifier) (float64, error) {
-	m.bo.Reset()
-
-	res, err := backoff.RetryWithData(func() (float32, error) {
-		res, err := m.conn.QueryFloat32(id)
-		if err != nil && !errors.As(err, new(rct.RecoverableError)) {
-			err = backoff.Permanent(err)
-		}
-
-		return res, err
-	}, m.bo)
-
+	res, err := m.conn.QueryFloat32(id)
 	return float64(res), err
 }
 
 // queryInt32 adds retry logic of recoverable errors to QueryInt32
 func (m *RCT) queryInt32(id rct.Identifier) (int32, error) {
-	m.bo.Reset()
-
-	res, err := backoff.RetryWithData(func() (int32, error) {
-		res, err := m.conn.QueryInt32(id)
-		if err != nil && !errors.As(err, new(rct.RecoverableError)) {
-			err = backoff.Permanent(err)
-		}
-
-		return res, err
-	}, m.bo)
-
-	return res, err
+	return m.conn.QueryInt32(id)
 }
diff --git a/server/eebus/connector.go b/server/eebus/connector.go
index e9f7dfe6..9ebf8c0b 100644
--- a/server/eebus/connector.go
+++ b/server/eebus/connector.go
@@ -1,12 +1,15 @@
 package eebus
 
 import (
+	"context"
 	"sync"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
 )
 
+const registerTimeout = 90 * time.Second
+
 type Connector struct {
 	once     sync.Once
 	connectC chan struct{}
@@ -16,9 +19,11 @@ func NewConnector() *Connector {
 	return &Connector{connectC: make(chan struct{})}
 }
 
-func (c *Connector) Wait(timeout time.Duration) error {
+func (c *Connector) Wait(ctx context.Context) error {
 	select {
-	case <-time.After(timeout):
+	case <-ctx.Done():
+		return ctx.Err()
+	case <-time.After(registerTimeout):
 		return api.ErrTimeout
 	case <-c.connectC:
 		return nil
diff --git a/templates/definition/vehicle/seat-cupra.yaml b/templates/definition/vehicle/seat-cupra.yaml
index d78848e2..f82d23f5 100644
--- a/templates/definition/vehicle/seat-cupra.yaml
+++ b/templates/definition/vehicle/seat-cupra.yaml
@@ -2,7 +2,7 @@ template: cupra
 products:
   - brand: Seat
     description:
-      generic: CupraConnect Gen4 (Born, Tavascan)
+      generic: CupraConnect Gen4 (Born, Formentor, Tavascan)
 params:
   - preset: vehicle-base
   - preset: vehicle-features


Test patch:
diff --git a/charger/ocpp_test.go b/charger/ocpp_test.go
index 2339c388..4dbc72bd 100644
--- a/charger/ocpp_test.go
+++ b/charger/ocpp_test.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"errors"
 	"testing"
 	"time"
@@ -112,7 +113,7 @@ func (suite *ocppTestSuite) TestConnect() {
 	suite.Require().True(cp1.IsConnected())
 
 	// 1st charge point- local
-	c1, err := NewOCPP("test-1", 1, "", "", 0, false, true, ocppTestConnectTimeout)
+	c1, err := NewOCPP(context.TODO(), "test-1", 1, "", "", 0, false, true, ocppTestConnectTimeout)
 	suite.Require().NoError(err)
 
 	// status and meter values
@@ -161,7 +162,7 @@ func (suite *ocppTestSuite) TestConnect() {
 	suite.Require().True(cp2.IsConnected())
 
 	// 2nd charge point - local
-	c2, err := NewOCPP("test-2", 1, "", "", 0, false, true, ocppTestConnectTimeout)
+	c2, err := NewOCPP(context.TODO(), "test-2", 1, "", "", 0, false, true, ocppTestConnectTimeout)
 	suite.Require().NoError(err)
 
 	{
@@ -203,7 +204,7 @@ func (suite *ocppTestSuite) TestAutoStart() {
 	suite.Require().True(cp1.IsConnected())
 
 	// 1st charge point- local
-	c1, err := NewOCPP("test-3", 1, "", "", 0, false, false, ocppTestConnectTimeout)
+	c1, err := NewOCPP(context.TODO(), "test-3", 1, "", "", 0, false, false, ocppTestConnectTimeout)
 	suite.Require().NoError(err)
 
 	// status and meter values
@@ -250,7 +251,7 @@ func (suite *ocppTestSuite) TestTimeout() {
 	})
 
 	// 1st charge point- local
-	_, err := NewOCPP("test-4", 1, "", "", 0, false, false, ocppTestConnectTimeout)
+	_, err := NewOCPP(context.TODO(), "test-4", 1, "", "", 0, false, false, ocppTestConnectTimeout)
 
 	suite.Require().NoError(err)
 }
diff --git a/meter/meter_test.go b/meter/meter_test.go
new file mode 100644
index 00000000..9316d3c8
--- /dev/null
+++ b/meter/meter_test.go
@@ -0,0 +1,30 @@
+package meter
+
+import (
+	"context"
+	"testing"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestACPower(t *testing.T) {
+	m, err := NewConfigurableFromConfig(context.TODO(), map[string]any{
+		"capacity": 23,
+		"soc": map[string]any{
+			"source": "const",
+			"value":  47,
+		},
+		"maxacpower": 1000,
+		"power": map[string]any{
+			"source": "const",
+			"value":  1000,
+		},
+	})
+	require.NoError(t, err)
+	_, ok := m.(api.BatteryCapacity)
+	assert.True(t, ok, "api.BatteryCapacity")
+	_, ok = m.(api.MaxACPowerGetter)
+	assert.True(t, ok, "api.MaxACPowerGetter")
+}