Repository: evcc-io/evcc
Pull request #19267 (2025-02-28 17:00:11)

Config UI: improve device error handling
fixes https://github.com/evcc-io/evcc/issues/19229

- 📍 show real error on fail (instead of creating failed)
- ⏱️ abort test-request after 15s

TODO:

- [x] make create request terminate @andig 


Patch:
diff --git a/api/globalconfig/types.go b/api/globalconfig/types.go
index 0c4e2fb9..03365666 100644
--- a/api/globalconfig/types.go
+++ b/api/globalconfig/types.go
@@ -54,8 +54,8 @@ type Go struct {
 
 type ModbusProxy struct {
 	Port            int
-	ReadOnly        string
-	modbus.Settings `mapstructure:",squash"`
+	ReadOnly        string `yaml:",omitempty" json:",omitempty"`
+	modbus.Settings `mapstructure:",squash" yaml:",inline,omitempty" json:",omitempty"`
 }
 
 var _ api.Redactor = (*Hems)(nil)
diff --git a/assets/js/components/Config/ChargerModal.vue b/assets/js/components/Config/ChargerModal.vue
index 903212fb..7dcd008c 100644
--- a/assets/js/components/Config/ChargerModal.vue
+++ b/assets/js/components/Config/ChargerModal.vue
@@ -355,8 +355,7 @@ export default {
 				this.$emit("updated");
 				this.close();
 			} catch (e) {
-				console.error(e);
-				alert("create failed");
+				this.handleCreateError(e);
 			}
 			this.saving = false;
 		},
@@ -368,7 +367,7 @@ export default {
 			if (!this.isNew) {
 				url += `/merge/${this.id}`;
 			}
-			return await api.post(url, this.apiData);
+			return await api.post(url, this.apiData, { timeout: this.testTimeout });
 		},
 		async update() {
 			if (this.testUnknown) {
@@ -382,8 +381,7 @@ export default {
 				this.$emit("updated");
 				this.close();
 			} catch (e) {
-				console.error(e);
-				alert("update failed");
+				this.handleUpdateError(e);
 			}
 			this.saving = false;
 		},
@@ -394,8 +392,7 @@ export default {
 				this.$emit("updated");
 				this.close();
 			} catch (e) {
-				console.error(e);
-				alert("delete failed");
+				this.handleRemoveError(e);
 			}
 		},
 		open() {
diff --git a/assets/js/components/Config/MeterModal.vue b/assets/js/components/Config/MeterModal.vue
index cf129fc3..6d81158d 100644
--- a/assets/js/components/Config/MeterModal.vue
+++ b/assets/js/components/Config/MeterModal.vue
@@ -367,7 +367,7 @@ export default {
 			if (!this.isNew) {
 				url += `/merge/${this.id}`;
 			}
-			return await api.post(url, this.apiData);
+			return await api.post(url, this.apiData, { timeout: this.testTimeout });
 		},
 		async update() {
 			if (this.testUnknown) {
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/assets/js/components/Config/VehicleModal.vue b/assets/js/components/Config/VehicleModal.vue
index 2b3d4415..a55bb031 100644
--- a/assets/js/components/Config/VehicleModal.vue
+++ b/assets/js/components/Config/VehicleModal.vue
@@ -436,7 +436,7 @@ export default {
 			if (!this.isNew) {
 				url += `/merge/${this.id}`;
 			}
-			return await api.post(url, this.apiData);
+			return await api.post(url, this.apiData, { timeout: this.testTimeout });
 		},
 		async update() {
 			if (this.testUnknown) {
diff --git a/charger/abb.go b/charger/abb.go
index 3fb47ce7..526207db 100644
--- a/charger/abb.go
+++ b/charger/abb.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"time"
@@ -52,11 +53,11 @@ const (
 )
 
 func init() {
-	registry.Add("abb", NewABBFromConfig)
+	registry.AddCtx("abb", NewABBFromConfig)
 }
 
 // NewABBFromConfig creates a ABB charger from generic config
-func NewABBFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewABBFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		ID: 1,
 	}
@@ -65,12 +66,12 @@ func NewABBFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewABB(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
+	return NewABB(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewABB creates ABB charger
-func NewABB(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+func NewABB(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/abl-em4.go b/charger/abl-em4.go
index ed282176..441d4612 100644
--- a/charger/abl-em4.go
+++ b/charger/abl-em4.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -48,11 +49,11 @@ type AblEm4 struct {
 }
 
 func init() {
-	registry.Add("abl-em4", NewAblEm4FromConfig)
+	registry.AddCtx("abl-em4", NewAblEm4FromConfig)
 }
 
 // NewAblEm4FromConfig creates an ABL eM4 charger from generic config
-func NewAblEm4FromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewAblEm4FromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		modbus.TcpSettings `mapstructure:",squash"`
 		Connector          uint16
@@ -67,12 +68,12 @@ func NewAblEm4FromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewAblEm4(cc.URI, cc.ID, cc.Connector)
+	return NewAblEm4(ctx, cc.URI, cc.ID, cc.Connector)
 }
 
 // NewAblEm4 creates an ABL eM4 charger
-func NewAblEm4(uri string, id uint8, connector uint16) (*AblEm4, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewAblEm4(ctx context.Context, uri string, id uint8, connector uint16) (*AblEm4, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/abl.go b/charger/abl.go
index 40263284..3656d9f0 100644
--- a/charger/abl.go
+++ b/charger/abl.go
@@ -19,6 +19,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"time"
@@ -73,13 +74,13 @@ var ablStatus = map[byte]string{
 }
 
 func init() {
-	registry.Add("abl", NewABLeMHFromConfig)
+	registry.AddCtx("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) {
+func NewABLeMHFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		modbus.Settings `mapstructure:",squash"`
 		Timeout         time.Duration
@@ -93,14 +94,14 @@ func NewABLeMHFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewABLeMH(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID, cc.Timeout)
+	return NewABLeMH(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID, cc.Timeout)
 }
 
 //go:generate go tool decorate -f decorateABLeMH -b *ABLeMH -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)"
 
 // NewABLeMH creates ABLeMH charger
-func NewABLeMH(uri, device, comset string, baudrate int, slaveID uint8, timeout time.Duration) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.Ascii, slaveID)
+func NewABLeMH(ctx context.Context, uri, device, comset string, baudrate int, slaveID uint8, timeout time.Duration) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, modbus.Ascii, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/alfen.go b/charger/alfen.go
index b1a2965d..d0f2c289 100644
--- a/charger/alfen.go
+++ b/charger/alfen.go
@@ -73,7 +73,7 @@ func NewAlfenFromConfig(ctx context.Context, other map[string]interface{}) (api.
 
 // NewAlfen creates Alfen charger
 func NewAlfen(ctx context.Context, uri string, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/alphatec.go b/charger/alphatec.go
index 7e76df63..25b826e5 100644
--- a/charger/alphatec.go
+++ b/charger/alphatec.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"time"
@@ -42,11 +43,11 @@ const (
 )
 
 func init() {
-	registry.Add("alphatec", NewAlphatecFromConfig)
+	registry.AddCtx("alphatec", NewAlphatecFromConfig)
 }
 
 // NewAlphatecFromConfig creates a Alphatec charger from generic config
-func NewAlphatecFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewAlphatecFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		ID: 1,
 	}
@@ -55,12 +56,12 @@ func NewAlphatecFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewAlphatec(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
+	return NewAlphatec(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewAlphatec creates Alphatec charger
-func NewAlphatec(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+func NewAlphatec(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/amperfied.go b/charger/amperfied.go
index ba0d9010..80115ece 100644
--- a/charger/amperfied.go
+++ b/charger/amperfied.go
@@ -80,7 +80,7 @@ func NewAmperfiedFromConfig(ctx context.Context, other map[string]interface{}) (
 
 // NewAmperfied creates Amperfied charger
 func NewAmperfied(ctx context.Context, uri string, slaveID uint8, phases bool) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/bender.go b/charger/bender.go
index 917a509d..bf959698 100644
--- a/charger/bender.go
+++ b/charger/bender.go
@@ -24,6 +24,7 @@ package charger
 // * Set 'Allow UID Disclose' to On
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"math"
@@ -68,11 +69,11 @@ const (
 )
 
 func init() {
-	registry.Add("bender", NewBenderCCFromConfig)
+	registry.AddCtx("bender", NewBenderCCFromConfig)
 }
 
 // NewBenderCCFromConfig creates a BenderCC charger from generic config
-func NewBenderCCFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewBenderCCFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 255,
 	}
@@ -81,14 +82,14 @@ func NewBenderCCFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewBenderCC(cc.URI, cc.ID)
+	return NewBenderCC(ctx, cc.URI, cc.ID)
 }
 
 //go:generate go tool decorate -f decorateBenderCC -b *BenderCC -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.Identifier,Identify,func() (string, error)"
 
 // NewBenderCC creates BenderCC charger
-func NewBenderCC(uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewBenderCC(ctx context.Context, uri string, id uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/cfos.go b/charger/cfos.go
index e8100b07..62199b65 100644
--- a/charger/cfos.go
+++ b/charger/cfos.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -34,13 +35,13 @@ type CfosPowerBrain struct {
 }
 
 func init() {
-	registry.Add("cfos", NewCfosPowerBrainFromConfig)
+	registry.AddCtx("cfos", NewCfosPowerBrainFromConfig)
 }
 
 //go:generate go tool decorate -f decorateCfos -b *CfosPowerBrain -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.PhaseSwitcher,Phases1p3p,func(int) error"
 
 // NewCfosPowerBrainFromConfig creates a cFos charger from generic config
-func NewCfosPowerBrainFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewCfosPowerBrainFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 1,
 	}
@@ -49,14 +50,14 @@ func NewCfosPowerBrainFromConfig(other map[string]interface{}) (api.Charger, err
 		return nil, err
 	}
 
-	return NewCfosPowerBrain(cc.URI, cc.ID)
+	return NewCfosPowerBrain(ctx, cc.URI, cc.ID)
 }
 
 // NewCfosPowerBrain creates a cFos charger
-func NewCfosPowerBrain(uri string, id uint8) (api.Charger, error) {
+func NewCfosPowerBrain(ctx context.Context, uri string, id uint8) (api.Charger, error) {
 	uri = util.DefaultPort(uri, 4701)
 
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/dadapower.go b/charger/dadapower.go
index 71abe6b0..3cb8bc90 100644
--- a/charger/dadapower.go
+++ b/charger/dadapower.go
@@ -53,7 +53,7 @@ func NewDadapowerFromConfig(ctx context.Context, other map[string]interface{}) (
 
 // NewDadapower creates a Dadapower charger
 func NewDadapower(ctx context.Context, uri string, id uint8) (*Dadapower, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/daheimladen-mb.go b/charger/daheimladen-mb.go
index 15e50937..e70d70cb 100644
--- a/charger/daheimladen-mb.go
+++ b/charger/daheimladen-mb.go
@@ -75,7 +75,7 @@ func NewDaheimLadenMBFromConfig(ctx context.Context, other map[string]interface{
 
 // NewDaheimLadenMB creates DaheimLadenMB charger
 func NewDaheimLadenMB(ctx context.Context, uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/delta.go b/charger/delta.go
index e1cd3807..2ebfcc15 100644
--- a/charger/delta.go
+++ b/charger/delta.go
@@ -84,7 +84,7 @@ func NewDeltaFromConfig(ctx context.Context, other map[string]interface{}) (api.
 
 // NewDelta creates Delta charger
 func NewDelta(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, connector uint16) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/eebus.go b/charger/eebus.go
index f4589165..5618d3f6 100644
--- a/charger/eebus.go
+++ b/charger/eebus.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"slices"
@@ -49,32 +50,33 @@ 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) {
-	cc := struct {
+func NewEEBusFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
+	var cc struct {
 		Ski           string
 		Ip            string
 		Meter         bool
-		ChargedEnergy bool
+		ChargedEnergy *bool
 		VasVW         bool
-	}{
-		ChargedEnergy: true,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
 		return nil, err
 	}
 
-	return NewEEBus(cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy, cc.VasVW)
+	// default true
+	hasChargedEnergy := cc.ChargedEnergy != nil && *cc.ChargedEnergy
+
+	return NewEEBus(ctx, cc.Ski, cc.Ip, cc.Meter, hasChargedEnergy, 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 +95,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/em2go.go b/charger/em2go.go
index 49abf176..21bcabe9 100644
--- a/charger/em2go.go
+++ b/charger/em2go.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"time"
@@ -62,14 +63,14 @@ const (
 )
 
 func init() {
-	registry.Add("em2go", NewEm2GoFromConfig)
-	registry.Add("em2go-home", NewEm2GoFromConfig)
+	registry.AddCtx("em2go", NewEm2GoFromConfig)
+	registry.AddCtx("em2go-home", NewEm2GoFromConfig)
 }
 
 //go:generate go tool decorate -f decorateEm2Go -b *Em2Go -r api.Charger -t "api.ChargerEx,MaxCurrentMillis,func(float64) error" -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)"
 
 // NewEm2GoFromConfig creates a Em2Go charger from generic config
-func NewEm2GoFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewEm2GoFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 255,
 	}
@@ -78,14 +79,14 @@ func NewEm2GoFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewEm2Go(cc.URI, cc.ID)
+	return NewEm2Go(ctx, cc.URI, cc.ID)
 }
 
 // NewEm2Go creates Em2Go charger
-func NewEm2Go(uri string, slaveID uint8) (api.Charger, error) {
+func NewEm2Go(ctx context.Context, uri string, slaveID uint8) (api.Charger, error) {
 	uri = util.DefaultPort(uri, 502)
 
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/etrel.go b/charger/etrel.go
index 2fa84cc4..ffd39af3 100644
--- a/charger/etrel.go
+++ b/charger/etrel.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"math"
@@ -59,11 +60,11 @@ type Etrel struct {
 }
 
 func init() {
-	registry.Add("etrel", NewEtrelFromConfig)
+	registry.AddCtx("etrel", NewEtrelFromConfig)
 }
 
 // NewEtrelFromConfig creates a Etrel charger from generic config
-func NewEtrelFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewEtrelFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		Connector          int
 		modbus.TcpSettings `mapstructure:",squash"`
@@ -78,12 +79,12 @@ func NewEtrelFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewEtrel(cc.Connector, cc.URI, cc.ID)
+	return NewEtrel(ctx, cc.URI, cc.ID, cc.Connector)
 }
 
 // NewEtrel creates a Etrel charger
-func NewEtrel(connector int, uri string, id uint8) (*Etrel, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewEtrel(ctx context.Context, uri string, id uint8, connector int) (*Etrel, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/evsedin.go b/charger/evsedin.go
index fb7820d4..c891ce66 100644
--- a/charger/evsedin.go
+++ b/charger/evsedin.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"fmt"
 	"time"
 
@@ -21,14 +22,14 @@ const (
 )
 
 func init() {
-	registry.Add("simpleevse", NewEvseDINFromConfig) // deprecated
-	registry.Add("evsedin", NewEvseDINFromConfig)
+	registry.AddCtx("simpleevse", NewEvseDINFromConfig) // deprecated
+	registry.AddCtx("evsedin", NewEvseDINFromConfig)
 }
 
 // https://files.ev-power.eu/inc/_doc/attach/StoItem/4418/evse-wb-din_Manual.pdf
 
 // NewEvseDINFromConfig creates an EVSE DIN charger from generic config
-func NewEvseDINFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewEvseDINFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		Baudrate: 9600,
 		Comset:   "8N1",
@@ -39,14 +40,14 @@ func NewEvseDINFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewEvseDIN(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
+	return NewEvseDIN(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewEvseDIN creates EVSE DIN charger
-func NewEvseDIN(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
+func NewEvseDIN(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
 	log := util.NewLogger("evse")
 
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/heidelberg-ec.go b/charger/heidelberg-ec.go
index b5675897..530bdf31 100644
--- a/charger/heidelberg-ec.go
+++ b/charger/heidelberg-ec.go
@@ -76,7 +76,7 @@ func NewHeidelbergECFromConfig(ctx context.Context, other map[string]interface{}
 
 // NewHeidelbergEC creates HeidelbergEC charger
 func NewHeidelbergEC(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/hesotec.go b/charger/hesotec.go
index 9a6b913e..d8c5d00b 100644
--- a/charger/hesotec.go
+++ b/charger/hesotec.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"time"
@@ -55,11 +56,11 @@ const (
 )
 
 func init() {
-	registry.Add("hesotec", NewHesotecFromConfig)
+	registry.AddCtx("hesotec", NewHesotecFromConfig)
 }
 
 // NewHesotecFromConfig creates a Hesotec charger from generic config
-func NewHesotecFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewHesotecFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 1,
 	}
@@ -68,12 +69,12 @@ func NewHesotecFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewHesotec(cc.URI, cc.ID)
+	return NewHesotec(ctx, cc.URI, cc.ID)
 }
 
 // NewHesotec creates Hesotec charger
-func NewHesotec(uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewHesotec(ctx context.Context, uri string, id uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/innogy.go b/charger/innogy.go
index 1f66c95d..fd3f0a18 100644
--- a/charger/innogy.go
+++ b/charger/innogy.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"math"
@@ -51,11 +52,11 @@ type Innogy struct {
 }
 
 func init() {
-	registry.Add("innogy", NewInnogyFromConfig)
+	registry.AddCtx("innogy", NewInnogyFromConfig)
 }
 
 // NewInnogyFromConfig creates a Innogy charger from generic config
-func NewInnogyFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewInnogyFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 1,
 	}
@@ -64,7 +65,7 @@ func NewInnogyFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	wb, err := NewInnogy(cc.URI, cc.ID)
+	wb, err := NewInnogy(ctx, cc.URI, cc.ID)
 	if err != nil {
 		return nil, err
 	}
@@ -85,8 +86,8 @@ func NewInnogyFromConfig(other map[string]interface{}) (api.Charger, error) {
 //go:generate go tool decorate -f decorateInnogy -b *Innogy -r api.Charger -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)"
 
 // NewInnogy creates a Innogy charger
-func NewInnogy(uri string, id uint8) (*Innogy, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewInnogy(ctx context.Context, uri string, id uint8) (*Innogy, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/keba-modbus.go b/charger/keba-modbus.go
index a7d33fc0..6ad7d20c 100644
--- a/charger/keba-modbus.go
+++ b/charger/keba-modbus.go
@@ -139,7 +139,7 @@ func NewKebaFromConfig(ctx context.Context, other map[string]interface{}) (api.C
 
 // NewKeba creates a new charger
 func NewKeba(ctx context.Context, embed embed, uri string, slaveID uint8) (*Keba, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/kse.go b/charger/kse.go
index b44c1ecc..afcd8885 100644
--- a/charger/kse.go
+++ b/charger/kse.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -52,11 +53,11 @@ const (
 )
 
 func init() {
-	registry.Add("kse", NewKSEFromConfig)
+	registry.AddCtx("kse", NewKSEFromConfig)
 }
 
 // NewKSEFromConfig creates a KSE charger from generic config
-func NewKSEFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewKSEFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		ID:       100,
 		Baudrate: 9600,
@@ -67,14 +68,14 @@ func NewKSEFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewKSE(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID)
+	return NewKSE(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID)
 }
 
 //go:generate go tool decorate -f decorateKSE -b *KSE -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)" -t "api.Identifier,Identify,func() (string, error)"
 
 // NewKSE creates KSE charger
-func NewKSE(uri, device, comset string, baudrate int, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.Rtu, slaveID)
+func NewKSE(ctx context.Context, uri, device, comset string, baudrate int, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, modbus.Rtu, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/mennekes-compact.go b/charger/mennekes-compact.go
index 8a697a8a..39495f41 100644
--- a/charger/mennekes-compact.go
+++ b/charger/mennekes-compact.go
@@ -91,7 +91,7 @@ func NewMennekesCompactFromConfig(ctx context.Context, other map[string]interfac
 
 // NewMennekesCompact creates Mennekes charger
 func NewMennekesCompact(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, timeout time.Duration) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/mennekes-hcc3.go b/charger/mennekes-hcc3.go
index 2107fe67..a1ab5f9c 100644
--- a/charger/mennekes-hcc3.go
+++ b/charger/mennekes-hcc3.go
@@ -19,6 +19,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -49,11 +50,11 @@ const (
 )
 
 func init() {
-	registry.Add("mennekes-hcc3", NewMennekesHcc3FromConfig)
+	registry.AddCtx("mennekes-hcc3", NewMennekesHcc3FromConfig)
 }
 
 // NewMennekesHcc3FromConfig creates a Mennekes mennekesHcc3 charger from generic config
-func NewMennekesHcc3FromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewMennekesHcc3FromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 255,
 	}
@@ -62,14 +63,14 @@ func NewMennekesHcc3FromConfig(other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	return NewMennekesHcc3(cc.URI, cc.ID)
+	return NewMennekesHcc3(ctx, cc.URI, cc.ID)
 }
 
 // NewMennekesHcc3 creates Mennekes HCC3 charger
-func NewMennekesHcc3(uri string, slaveID uint8) (api.Charger, error) {
+func NewMennekesHcc3(ctx context.Context, uri string, slaveID uint8) (api.Charger, error) {
 	uri = util.DefaultPort(uri, 502)
 
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/mypv.go b/charger/mypv.go
index b0137007..152542db 100644
--- a/charger/mypv.go
+++ b/charger/mypv.go
@@ -84,7 +84,7 @@ func newMyPvFromConfig(ctx context.Context, name string, other map[string]interf
 
 // NewMyPv creates myPV AC Elwa 2 or Thor charger
 func NewMyPv(ctx context.Context, name, uri string, slaveID uint8, tempSource int, statusC uint16) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/nrggen2.go b/charger/nrggen2.go
index 2b4e5003..1254d68a 100644
--- a/charger/nrggen2.go
+++ b/charger/nrggen2.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"math"
@@ -45,13 +46,13 @@ const (
 )
 
 func init() {
-	registry.Add("nrggen2", NewNRGKickGen2FromConfig)
+	registry.AddCtx("nrggen2", NewNRGKickGen2FromConfig)
 }
 
 //go:generate go tool decorate -f decorateNRGKickGen2 -b *NRGKickGen2 -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error"
 
 // NewNRGKickGen2FromConfig creates a NRGKickGen2 charger from generic config
-func NewNRGKickGen2FromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewNRGKickGen2FromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		modbus.TcpSettings `mapstructure:",squash"`
 		Phases1p3p         bool
@@ -66,7 +67,7 @@ func NewNRGKickGen2FromConfig(other map[string]interface{}) (api.Charger, error)
 		return nil, err
 	}
 
-	nrg, err := NewNRGKickGen2(cc.URI, cc.ID)
+	nrg, err := NewNRGKickGen2(ctx, cc.URI, cc.ID)
 	if err != nil {
 		return nil, err
 	}
@@ -85,8 +86,8 @@ func NewNRGKickGen2FromConfig(other map[string]interface{}) (api.Charger, error)
 }
 
 // NewNRGKickGen2 creates NRGKickGen2 charger
-func NewNRGKickGen2(uri string, slaveID uint8) (*NRGKickGen2, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+func NewNRGKickGen2(ctx context.Context, uri string, slaveID uint8) (*NRGKickGen2, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/obo.go b/charger/obo.go
index 3dc85067..826ffa32 100644
--- a/charger/obo.go
+++ b/charger/obo.go
@@ -45,7 +45,7 @@ func NewOboFromConfig(ctx context.Context, other map[string]interface{}) (api.Ch
 
 // NewObo creates OBO Bettermann charger
 func NewObo(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/ocpp.go b/charger/ocpp.go
index 18907f46..4160c0c5 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 {
@@ -176,7 +181,7 @@ func NewOCPP(id string, connector int, idTag string,
 		idTag = lo.CoalesceOrEmpty(idTag, cp.IdTag, defaultIdTag)
 	}
 
-	conn, err := ocpp.NewConnector(log, connector, cp, idTag)
+	conn, err := ocpp.NewConnector(log, connector, cp, idTag, meterInterval)
 	if err != nil {
 		return nil, err
 	}
@@ -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..d2acea3d 100644
--- a/charger/ocpp/connector.go
+++ b/charger/ocpp/connector.go
@@ -1,6 +1,7 @@
 package ocpp
 
 import (
+	"context"
 	"fmt"
 	"strconv"
 	"strings"
@@ -31,9 +32,11 @@ type Connector struct {
 	idTag string
 
 	remoteIdTag string
+
+	meterInterval time.Duration
 }
 
-func NewConnector(log *util.Logger, id int, cp *CP, idTag string) (*Connector, error) {
+func NewConnector(log *util.Logger, id int, cp *CP, idTag string, meterInterval time.Duration) (*Connector, error) {
 	conn := &Connector{
 		log:          log,
 		cp:           cp,
@@ -42,7 +45,8 @@ func NewConnector(log *util.Logger, id int, cp *CP, idTag string) (*Connector, e
 		statusC:      make(chan struct{}, 1),
 		measurements: make(map[types.Measurand]types.SampledValue),
 
-		remoteIdTag: idTag,
+		remoteIdTag:   idTag,
+		meterInterval: meterInterval,
 	}
 
 	if err := cp.registerConnector(id, conn); err != nil {
@@ -100,16 +104,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:
+		}
 	}
 }
 
@@ -184,7 +194,7 @@ func (conn *Connector) isWaitingForAuth() bool {
 // isMeterTimeout checks if meter values are outdated.
 // Must only be called while holding lock.
 func (conn *Connector) isMeterTimeout() bool {
-	return conn.clock.Since(conn.meterUpdated) > Timeout
+	return conn.clock.Since(conn.meterUpdated) > max(conn.meterInterval+10*time.Second, Timeout)
 }
 
 var _ api.CurrentGetter = (*Connector)(nil)
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/charger/openwb-2.0.go b/charger/openwb-2.0.go
index 48540b72..39eb9c80 100644
--- a/charger/openwb-2.0.go
+++ b/charger/openwb-2.0.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -35,7 +36,7 @@ const (
 )
 
 func init() {
-	registry.Add("openwb-2.0", NewOpenWB20FromConfig)
+	registry.AddCtx("openwb-2.0", NewOpenWB20FromConfig)
 }
 
 // https://openwb.de/main/wp-content/uploads/2023/10/ModbusTCP-openWB-series2-Pro-1.pdf
@@ -43,7 +44,7 @@ func init() {
 //go:generate go tool decorate -f decorateOpenWB20 -b *OpenWB20 -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.Identifier,Identify,func() (string, error)"
 
 // NewOpenWB20FromConfig creates a OpenWB20 charger from generic config
-func NewOpenWB20FromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewOpenWB20FromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		Connector          uint16
 		Phases1p3p         bool
@@ -59,7 +60,7 @@ func NewOpenWB20FromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	wb, err := NewOpenWB20(cc.URI, cc.ID, cc.Connector)
+	wb, err := NewOpenWB20(ctx, cc.URI, cc.ID, cc.Connector)
 	if err != nil {
 		return nil, err
 	}
@@ -78,10 +79,10 @@ func NewOpenWB20FromConfig(other map[string]interface{}) (api.Charger, error) {
 }
 
 // NewOpenWB20 creates OpenWB20 charger
-func NewOpenWB20(uri string, slaveID uint8, connector uint16) (*OpenWB20, error) {
+func NewOpenWB20(ctx context.Context, uri string, slaveID uint8, connector uint16) (*OpenWB20, error) {
 	uri = util.DefaultPort(uri, 1502)
 
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/peblar.go b/charger/peblar.go
index 153ee428..4e21bf72 100644
--- a/charger/peblar.go
+++ b/charger/peblar.go
@@ -20,6 +20,7 @@ package charger
 // Details on the Peblar modbus server obtained from: https://developer.peblar.com/modbus-api
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -67,13 +68,13 @@ const (
 )
 
 func init() {
-	registry.Add("peblar", NewPeblarFromConfig)
+	registry.AddCtx("peblar", NewPeblarFromConfig)
 }
 
 //go:generate go tool decorate -f decoratePeblar -b *Peblar -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)"
 
 // NewPeblarFromConfig creates a Peblar charger from generic config
-func NewPeblarFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewPeblarFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 255,
 	}
@@ -82,12 +83,12 @@ func NewPeblarFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewPeblar(cc.URI, cc.ID)
+	return NewPeblar(ctx, cc.URI, cc.ID)
 }
 
 // NewPeblar creates Peblar charger
-func NewPeblar(uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewPeblar(ctx context.Context, uri string, id uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/phoenix-charx.go b/charger/phoenix-charx.go
index 24be5e11..62a22d37 100644
--- a/charger/phoenix-charx.go
+++ b/charger/phoenix-charx.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"time"
@@ -42,13 +43,13 @@ type PhoenixCharx struct {
 }
 
 func init() {
-	registry.Add("phoenix-charx", NewPhoenixCharxFromConfig)
+	registry.AddCtx("phoenix-charx", NewPhoenixCharxFromConfig)
 }
 
 //go:generate go tool decorate -f decoratePhoenixCharx -b *PhoenixCharx -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)"
 
 // NewPhoenixCharxFromConfig creates a Phoenix charger from generic config
-func NewPhoenixCharxFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewPhoenixCharxFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		modbus.TcpSettings `mapstructure:",squash"`
 		Connector          uint16
@@ -63,7 +64,7 @@ func NewPhoenixCharxFromConfig(other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	wb, err := NewPhoenixCharx(cc.URI, cc.ID, cc.Connector)
+	wb, err := NewPhoenixCharx(ctx, cc.URI, cc.ID, cc.Connector)
 	if err != nil {
 		return nil, err
 	}
@@ -81,8 +82,8 @@ func NewPhoenixCharxFromConfig(other map[string]interface{}) (api.Charger, error
 }
 
 // NewPhoenixCharx creates a Phoenix charger
-func NewPhoenixCharx(uri string, id uint8, connector uint16) (*PhoenixCharx, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewPhoenixCharx(ctx context.Context, uri string, id uint8, connector uint16) (*PhoenixCharx, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/phoenix-em-eth.go b/charger/phoenix-em-eth.go
index b52babcb..1d5e1aea 100644
--- a/charger/phoenix-em-eth.go
+++ b/charger/phoenix-em-eth.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"fmt"
 	"time"
 
@@ -28,13 +29,13 @@ type PhoenixEMEth struct {
 }
 
 func init() {
-	registry.Add("phoenix-em-eth", NewPhoenixEMEthFromConfig)
+	registry.AddCtx("phoenix-em-eth", NewPhoenixEMEthFromConfig)
 }
 
 //go:generate go tool decorate -f decoratePhoenixEMEth -b *PhoenixEMEth -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)"
 
 // NewPhoenixEMEthFromConfig creates a Phoenix charger from generic config
-func NewPhoenixEMEthFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewPhoenixEMEthFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 180,
 	}
@@ -43,7 +44,7 @@ func NewPhoenixEMEthFromConfig(other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	wb, err := NewPhoenixEMEth(cc.URI, cc.ID)
+	wb, err := NewPhoenixEMEth(ctx, cc.URI, cc.ID)
 	if err != nil {
 		return nil, err
 	}
@@ -67,8 +68,8 @@ func NewPhoenixEMEthFromConfig(other map[string]interface{}) (api.Charger, error
 }
 
 // NewPhoenixEMEth creates a Phoenix charger
-func NewPhoenixEMEth(uri string, slaveID uint8) (*PhoenixEMEth, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+func NewPhoenixEMEth(ctx context.Context, uri string, slaveID uint8) (*PhoenixEMEth, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/phoenix-ev-eth.go b/charger/phoenix-ev-eth.go
index 0f2d7e72..2772322c 100644
--- a/charger/phoenix-ev-eth.go
+++ b/charger/phoenix-ev-eth.go
@@ -24,6 +24,7 @@ package charger
 // * Set DIP switch 10 to ON
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/evcc-io/evcc/api"
@@ -58,13 +59,13 @@ const (
 )
 
 func init() {
-	registry.Add("phoenix-ev-eth", NewPhoenixEVEthFromConfig)
+	registry.AddCtx("phoenix-ev-eth", NewPhoenixEVEthFromConfig)
 }
 
 //go:generate go tool decorate -f decoratePhoenixEVEth -b *PhoenixEVEth -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.ChargerEx,MaxCurrentMillis,func(float64) error" -t "api.Identifier,Identify,func() (string, error)"
 
 // NewPhoenixEVEthFromConfig creates a PhoenixEVEth charger from generic config
-func NewPhoenixEVEthFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewPhoenixEVEthFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 255,
 	}
@@ -73,12 +74,12 @@ func NewPhoenixEVEthFromConfig(other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	return NewPhoenixEVEth(cc.URI, cc.ID)
+	return NewPhoenixEVEth(ctx, cc.URI, cc.ID)
 }
 
 // NewPhoenixEVEth creates a PhoenixEVEth charger
-func NewPhoenixEVEth(uri string, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+func NewPhoenixEVEth(ctx context.Context, uri string, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/phoenix-ev-ser.go b/charger/phoenix-ev-ser.go
index 3e5366d1..6e679907 100644
--- a/charger/phoenix-ev-ser.go
+++ b/charger/phoenix-ev-ser.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/evcc-io/evcc/api"
@@ -21,11 +22,11 @@ type PhoenixEVSer struct {
 }
 
 func init() {
-	registry.Add("phoenix-ev-ser", NewPhoenixEVSerFromConfig)
+	registry.AddCtx("phoenix-ev-ser", NewPhoenixEVSerFromConfig)
 }
 
 // NewPhoenixEVSerFromConfig creates a Phoenix charger from generic config
-func NewPhoenixEVSerFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewPhoenixEVSerFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		ID: 1,
 	}
@@ -34,12 +35,12 @@ func NewPhoenixEVSerFromConfig(other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	return NewPhoenixEVSer(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
+	return NewPhoenixEVSer(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewPhoenixEVSer creates a Phoenix charger
-func NewPhoenixEVSer(uri, device, comset string, baudrate int, proto modbus.Protocol, id uint8) (*PhoenixEVSer, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, id)
+func NewPhoenixEVSer(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, id uint8) (*PhoenixEVSer, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/prachtalpha.go b/charger/prachtalpha.go
index 6bdeb75e..9a3c5046 100644
--- a/charger/prachtalpha.go
+++ b/charger/prachtalpha.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"time"
@@ -46,11 +47,11 @@ const (
 )
 
 func init() {
-	registry.Add("pracht-alpha", NewPrachtAlphaFromConfig)
+	registry.AddCtx("pracht-alpha", NewPrachtAlphaFromConfig)
 }
 
 // NewPrachtAlphaFromConfig creates a PrachtAlpha charger from generic config
-func NewPrachtAlphaFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewPrachtAlphaFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		Connector       uint16
 		modbus.Settings `mapstructure:",squash"`
@@ -66,12 +67,12 @@ func NewPrachtAlphaFromConfig(other map[string]interface{}) (api.Charger, error)
 		return nil, err
 	}
 
-	return NewPrachtAlpha(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID, cc.Timeout, cc.Connector)
+	return NewPrachtAlpha(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID, cc.Timeout, cc.Connector)
 }
 
 // NewPrachtAlpha creates PrachtAlpha charger
-func NewPrachtAlpha(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, timeout time.Duration, vehicle uint16) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+func NewPrachtAlpha(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, timeout time.Duration, vehicle uint16) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/pulsares.go b/charger/pulsares.go
index 34a0ac77..7277ae51 100644
--- a/charger/pulsares.go
+++ b/charger/pulsares.go
@@ -78,7 +78,7 @@ func NewPulsaresFromConfig(ctx context.Context, other map[string]interface{}) (a
 
 // NewPulsares creates Pulsares charger
 func NewPulsares(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (*Pulsares, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/schneider-v3.go b/charger/schneider-v3.go
index 66c63c3d..b00b269e 100644
--- a/charger/schneider-v3.go
+++ b/charger/schneider-v3.go
@@ -76,7 +76,7 @@ func NewSchneiderV3FromConfig(ctx context.Context, other map[string]interface{})
 
 // NewSchneiderV3 creates Schneider charger
 func NewSchneiderV3(ctx context.Context, uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/smartevse.go b/charger/smartevse.go
index 7d1c480b..00079417 100644
--- a/charger/smartevse.go
+++ b/charger/smartevse.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 	"strconv"
@@ -64,11 +65,11 @@ const (
 )
 
 func init() {
-	registry.Add("smartevse", NewsmartEVSEFromConfig)
+	registry.AddCtx("smartevse", NewsmartEVSEFromConfig)
 }
 
 // NewsmartEVSEFromConfig creates a new smartEVSE ModbusTCP charger
-func NewsmartEVSEFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewsmartEVSEFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		ID: 1,
 	}
@@ -77,12 +78,12 @@ func NewsmartEVSEFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewsmartEVSE(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
+	return NewsmartEVSE(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewsmartEVSE creates a new charger
-func NewsmartEVSE(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (*smartEVSE, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+func NewsmartEVSE(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (*smartEVSE, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/solax.go b/charger/solax.go
index 09dc7237..813fe371 100644
--- a/charger/solax.go
+++ b/charger/solax.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -57,11 +58,11 @@ const (
 )
 
 func init() {
-	registry.Add("solax", NewSolaxFromConfig)
+	registry.AddCtx("solax", NewSolaxFromConfig)
 }
 
 // NewSolaxFromConfig creates a Solax charger from generic config
-func NewSolaxFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewSolaxFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		ID: 1,
 	}
@@ -70,12 +71,12 @@ func NewSolaxFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewSolax(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
+	return NewSolax(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewSolax creates Solax charger
-func NewSolax(uri, device, comset string, baudrate int, proto modbus.Protocol, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, id)
+func NewSolax(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, id uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/sungrow.go b/charger/sungrow.go
index 50fadcb2..be824f5a 100644
--- a/charger/sungrow.go
+++ b/charger/sungrow.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -62,11 +63,11 @@ var (
 )
 
 func init() {
-	registry.Add("sungrow", NewSungrowFromConfig)
+	registry.AddCtx("sungrow", NewSungrowFromConfig)
 }
 
 // NewSungrowFromConfig creates a Sungrow charger from generic config
-func NewSungrowFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewSungrowFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.Settings{
 		ID: 248,
 	}
@@ -75,12 +76,12 @@ func NewSungrowFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewSungrow(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
+	return NewSungrow(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewSungrow creates Sungrow charger
-func NewSungrow(uri, device, comset string, baudrate int, proto modbus.Protocol, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, id)
+func NewSungrow(ctx context.Context, uri, device, comset string, baudrate int, proto modbus.Protocol, id uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, device, comset, baudrate, proto, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/versicharge.go b/charger/versicharge.go
index 72bb5b96..fc907691 100644
--- a/charger/versicharge.go
+++ b/charger/versicharge.go
@@ -19,6 +19,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -55,11 +56,11 @@ type Versicharge struct {
 }
 
 func init() {
-	registry.Add("versicharge", NewVersichargeFromConfig)
+	registry.AddCtx("versicharge", NewVersichargeFromConfig)
 }
 
 // NewVersichargeFromConfig creates a Versicharge charger from generic config
-func NewVersichargeFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewVersichargeFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 2,
 	}
@@ -68,12 +69,12 @@ func NewVersichargeFromConfig(other map[string]interface{}) (api.Charger, error)
 		return nil, err
 	}
 
-	return NewVersicharge(cc.URI, cc.ID)
+	return NewVersicharge(ctx, cc.URI, cc.ID)
 }
 
 // NewVersicharge creates a Versicharge charger
-func NewVersicharge(uri string, id uint8) (*Versicharge, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewVersicharge(ctx context.Context, uri string, id uint8) (*Versicharge, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/vestel.go b/charger/vestel.go
index 1066d0cb..b9773300 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -83,7 +83,7 @@ func NewVestelFromConfig(ctx context.Context, other map[string]interface{}) (api
 
 // NewVestel creates a Vestel charger
 func NewVestel(ctx context.Context, uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
@@ -105,7 +105,7 @@ func NewVestel(ctx context.Context, uri string, id uint8) (api.Charger, error) {
 		phasesS func(int) error
 		phasesG func() (int, error)
 	)
-	if b, err := wb.conn.ReadHoldingRegisters(vestelRegNumberPhases, 1); err == nil && binary.BigEndian.Uint16(b) == 1 {
+	if b, err := wb.conn.ReadInputRegisters(vestelRegNumberPhases, 1); err == nil && binary.BigEndian.Uint16(b) == 1 {
 		phasesS = wb.phases1p3p
 		phasesG = wb.getPhases
 	}
@@ -314,6 +314,12 @@ func (wb *Vestel) Diagnose() {
 		fmt.Printf("Firmware:\t%s\n", b)
 	}
 	if b, err := wb.conn.ReadHoldingRegisters(vestelRegFailsafeTimeout, 1); err == nil {
-		fmt.Printf("Failsafe timeout (plain):\t%d\n", binary.BigEndian.Uint16(b))
+		fmt.Printf("Failsafe timeout:\t%#x\n", binary.BigEndian.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(vestelRegNumberPhases, 1); err == nil {
+		fmt.Printf("Number of phases:\t%#x\n", binary.BigEndian.Uint16(b))
+	}
+	if b, err := wb.conn.ReadHoldingRegisters(vestelRegPhasesSwitch, 1); err == nil {
+		fmt.Printf("Phase switch:\t%#x\n", binary.BigEndian.Uint16(b))
 	}
 }
diff --git a/charger/victron.go b/charger/victron.go
index 2c4ca75d..a35a2668 100644
--- a/charger/victron.go
+++ b/charger/victron.go
@@ -18,6 +18,7 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"errors"
 	"fmt"
@@ -67,22 +68,22 @@ var (
 )
 
 func init() {
-	registry.Add("victron", NewVictronGXFromConfig)
-	registry.Add("victron-evcs", NewVictronEVCSFromConfig)
+	registry.AddCtx("victron", NewVictronGXFromConfig)
+	registry.AddCtx("victron-evcs", NewVictronEVCSFromConfig)
 }
 
 // NewVictronGXFromConfig creates a ABB charger from generic config
-func NewVictronGXFromConfig(other map[string]interface{}) (api.Charger, error) {
-	return NewVictronFromConfig(other, victronGX)
+func NewVictronGXFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
+	return NewVictronFromConfig(ctx, other, victronGX)
 }
 
 // NewVictronEVCSFromConfig creates a ABB charger from generic config
-func NewVictronEVCSFromConfig(other map[string]interface{}) (api.Charger, error) {
-	return NewVictronFromConfig(other, victronEVCS)
+func NewVictronEVCSFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
+	return NewVictronFromConfig(ctx, other, victronEVCS)
 }
 
 // NewVictronFromConfig creates a ABB charger from generic config
-func NewVictronFromConfig(other map[string]interface{}, regs victronRegs) (api.Charger, error) {
+func NewVictronFromConfig(ctx context.Context, other map[string]interface{}, regs victronRegs) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: cast.ToUint8(regs.isGX) * 100,
 	}
@@ -91,12 +92,12 @@ func NewVictronFromConfig(other map[string]interface{}, regs victronRegs) (api.C
 		return nil, err
 	}
 
-	return NewVictron(cc.URI, cc.ID, regs)
+	return NewVictron(ctx, cc.URI, cc.ID, regs)
 }
 
 // NewVictron creates Victron charger
-func NewVictron(uri string, slaveID uint8, regs victronRegs) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+func NewVictron(ctx context.Context, uri string, slaveID uint8, regs victronRegs) (api.Charger, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/wallbe.go b/charger/wallbe.go
index c4030e29..4c02349d 100644
--- a/charger/wallbe.go
+++ b/charger/wallbe.go
@@ -1,6 +1,7 @@
 package charger
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
 
@@ -37,13 +38,13 @@ type Wallbe struct {
 }
 
 func init() {
-	registry.Add("wallbe", NewWallbeFromConfig)
+	registry.AddCtx("wallbe", NewWallbeFromConfig)
 }
 
 //go:generate go tool decorate -f decorateWallbe -b *Wallbe -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.ChargerEx,MaxCurrentMillis,func(float64) error"
 
 // NewWallbeFromConfig creates a Wallbe charger from generic config
-func NewWallbeFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewWallbeFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
 		URI    string
 		Legacy bool
@@ -58,7 +59,7 @@ func NewWallbeFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	wb, err := NewWallbe(cc.URI)
+	wb, err := NewWallbe(ctx, cc.URI)
 	if err != nil {
 		return nil, err
 	}
@@ -91,8 +92,8 @@ 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, modbus.Tcp, wbSlaveID)
+func NewWallbe(ctx context.Context, uri string) (*Wallbe, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, wbSlaveID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/webasto-next.go b/charger/webasto-next.go
index deb7c088..41e03be6 100644
--- a/charger/webasto-next.go
+++ b/charger/webasto-next.go
@@ -70,7 +70,7 @@ func NewWebastoNextFromConfig(ctx context.Context, other map[string]interface{})
 
 // NewWebastoNext creates WebastoNext charger
 func NewWebastoNext(ctx context.Context, uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git "a/charger/weidm\303\274ller.go" "b/charger/weidm\303\274ller.go"
index 9afbae91..82345013 100644
--- "a/charger/weidm\303\274ller.go"
+++ "b/charger/weidm\303\274ller.go"
@@ -74,7 +74,7 @@ func NewWeidmüllerFromConfig(ctx context.Context, other map[string]interface{})
 
 // NewWeidmüller creates Weidmüller charger
 func NewWeidmüller(ctx context.Context, uri string, id uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
diff --git a/cmd/setup.go b/cmd/setup.go
index 69295e0f..d44db535 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -171,7 +171,7 @@ NEXT:
 		}
 
 		log := util.NewLogger("circuit-" + cc.Name)
-		instance, err := circuit.NewFromConfig(log, cc.Other)
+		instance, err := circuit.NewFromConfig(context.TODO(), log, cc.Other)
 		if err != nil {
 			return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err)
 		}
diff --git a/core/circuit/circuit.go b/core/circuit/circuit.go
index 4741982d..76fd821f 100644
--- a/core/circuit/circuit.go
+++ b/core/circuit/circuit.go
@@ -39,7 +39,7 @@ type Circuit struct {
 }
 
 // NewFromConfig creates a new Circuit
-func NewFromConfig(log *util.Logger, other map[string]interface{}) (api.Circuit, error) {
+func NewFromConfig(ctx context.Context, log *util.Logger, other map[string]interface{}) (api.Circuit, error) {
 	cc := struct {
 		Title         string         // title
 		ParentRef     string         `mapstructure:"parent"` // parent circuit reference
@@ -71,12 +71,12 @@ func NewFromConfig(log *util.Logger, other map[string]interface{}) (api.Circuit,
 		return nil, err
 	}
 
-	circuit.getMaxPower, err = cc.GetMaxPower.FloatGetter(context.TODO())
+	circuit.getMaxPower, err = cc.GetMaxPower.FloatGetter(ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	circuit.getMaxCurrent, err = cc.GetMaxCurrent.FloatGetter(context.TODO())
+	circuit.getMaxCurrent, err = cc.GetMaxCurrent.FloatGetter(ctx)
 	if err != nil {
 		return nil, err
 	}
diff --git a/go.mod b/go.mod
index 580751f5..87772285 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.20250302110530-e1b03e640f26
diff --git a/go.sum b/go.sum
index ce1df833..3a48bba7 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.20250302110530-e1b03e640f26 h1:Kqq17bxhK6TsBs54gf2Bk4pghBAAeJw1WN/RU0pgV1M=
+github.com/andig/rct v0.1.2-0.20250302110530-e1b03e640f26/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/i18n/cs.toml b/i18n/cs.toml
index b2166c51..2e3e63fa 100644
--- a/i18n/cs.toml
+++ b/i18n/cs.toml
@@ -5,6 +5,7 @@ control = "Ovládání baterie"
 discharge = "Zabránit vybití v rychlém režimu a plánovaném nabíjení."
 disclaimerHint = "Poznámka:"
 disclaimerText = "Tato nastavení ovlivňují pouze solární režim. Chování dobíjení je příslušně upraveno."
+gridChargeTab = "Nabíjení ze sítě"
 legendBottomName = "priorita domu"
 legendBottomSubline = "nepoužito pro dobíjení"
 legendMiddleName = "nejdříve vozidlo"
@@ -14,6 +15,7 @@ legendTopAutostart = "startuje automaticky"
 legendTopName = "dobíjení podporované baterií"
 legendTopSubline = "bez přerušování"
 modalTitle = "Nastavení baterie"
+usageTab = "Využití baterie"
 
 [batterySettings.bufferStart]
 above = "když je nad {soc}"
@@ -28,6 +30,19 @@ never = "pouze když je dost přebytků"
 titleAdd = "Přidat měřič baterie"
 titleEdit = "Upravit měřič baterie"
 
+[config.charge]
+titleAdd = "Přidat měřič nabití"
+titleEdit = "Upravit měřič nabití"
+
+[config.charger]
+chargers = "EV nabíječky"
+ocppHelp = "Zkopíruj tuto adresu do nastavení nabíječky"
+ocppLabel = "OCPP-Server URL"
+switchsockets = "Přepínatelné zásuvky"
+template = "Výrobce"
+titleAdd = "Přidat nabíječku"
+titleEdit = "Upravit nabíječku"
+
 [config.circuits]
 
 [config.control]
diff --git a/i18n/da.toml b/i18n/da.toml
index 4cb50be1..448ac033 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -447,6 +447,31 @@ modalUpdateNow = "Installer nu"
 modalUpdateStarted = "Starter den nye version af evcc..."
 modalUpdateStatusStart = "Installation startede:"
 
+[forecast]
+modalTitle = "Prognose"
+
+[forecast.co2]
+average = "Gennemsnit"
+lowestHour = "Reneste time"
+range = "spændvidde"
+
+[forecast.price]
+average = "Gennemsnit"
+lowestHour = "Billigste time"
+range = "Spændvidde"
+
+[forecast.solar]
+dayAfterTomorrow = "I overmorgen"
+partly = "delvis"
+remaining = "resterende"
+today = "I dag"
+tomorrow = "I morgen"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Pris"
+solar = "Sol"
+
 [header]
 about = "Om"
 blog = "Blog"
@@ -538,6 +563,7 @@ batteryGridChargeActive = "Oplader fra elnettet"
 batteryGridChargeLimit = "Oplader fra elnettet når"
 batteryHold = "Batteri (låst)"
 batteryTooltip = "{energi} af {total} ({soc})"
+forecastTooltip = "prognose: resterende solenergi produktion i dag"
 gridImport = "Forbrug fra elnet"
 homePower = "Forbrug"
 loadpoints = "Oplader| Oplader | {count} opladere"
diff --git a/i18n/el.toml b/i18n/el.toml
index ed2fb239..b94b4643 100644
--- a/i18n/el.toml
+++ b/i18n/el.toml
@@ -28,6 +28,15 @@ never = "μόνο με αρκετό πλεόνασμα."
 titleAdd = "Πρόσθεσε Μπαταρία"
 titleEdit = "Eπεξεργασία Mπαταρίας"
 
+[config.charger]
+chargers = "Φορτιστές οχημάτων"
+generic = "Γενικές ενσωματώσεις"
+ocppHelp = "Αντιγράψτε αυτή τη διεύθυνση στη διαμόρφωση των φορτιστών σας."
+ocppLabel = "OCPP-Server URL"
+template = "Κατασκευαστής"
+titleAdd = "Προσθήκη Φορτιστή"
+titleEdit = "Επεξεργασία Φορτιστή"
+
 [config.circuits]
 description = "Διασφαλίζει ότι το άθροισμα όλων των σημείων φόρτισης που είναι συνδεδεμένα σε ένα κύκλωμα δεν υπερβαίνει τα οριζόμενα όρια ισχύος και έντασης ρεύματος. Τα κυκλώματα μπορούν να είναι σε ένθετες δομές για τη δημιουργία μιας ιεραρχίας."
 title = "Διαχείριση Φορτίου"
@@ -63,22 +72,31 @@ energy = "Ενέργεια"
 feedinPrice = "Ταρίφα Feed-in"
 gridPrice = "Τιμή δικτύου"
 hemsType = "Σύστημα"
+identifier = "Αναγνωριστικό RFID"
 no = "όχι"
 odometer = "Οδόμετρο"
 org = "Οργανισμός"
 phaseCurrents = "Ένταση L1, L2, L3"
 phasePowers = "Ισχύς L1, L2, L3"
 phaseVoltages = "Τάση L1, L2, L3"
+phases1p3p = "Eναλλαγή φάσεων"
 power = "Ισχύς"
 powerRange = "Ισχύς"
 range = "Αυτονομία"
+singlePhase = "Μονοφασικό"
 soc = "Κατάσταση φόρτισης"
 socLimit = "Όριο"
 temp = "Θερμοκρασία"
 topic = "Topic"
 url = "URL"
+vehicleLimitSoc = "Όριο οχήματος"
 yes = "ναι"
 
+[config.deviceValueChargeStatus]
+A = "A (δεν είναι συνδεδεμένο)"
+B = "B (συνδεδεμένο)"
+C = "C (φορτίζει)"
+
 [config.eebus]
 description = "Διαμόρφωση που επιτρέπει στο evcc να επικοινωνεί με άλλες συσκευές EEBus."
 title = "EEBus"
@@ -130,9 +148,71 @@ title = "InfluxDB"
 v1Support = "Με υποστήριξη για την InfluxDB 1.x;"
 v2Support = "Επιστροφή σε InfluxDB 2.x"
 
+[config.loadpoint]
+addCharger = "Προσθήκη φορτιστή"
+addMeter = "Προσθήκη αποκλειστικού μετρητή για το φορτιστή"
+cancel = "Άκυρο"
+chargerLabel = "Φορτιστής"
+chargerPower11kw = "11 kW"
+chargerPower11kwHelp = "Θα χρησιμοποιήσει εύρος έντασης ρεύματος από 6 έως 16 Α."
+chargerPower22kw = "22 kW"
+chargerPower22kwHelp = "Θα χρησιμοποιήσει εύρος έντασης ρεύματος από 6 έως 32 A."
+chargerPowerCustom = "άλλο"
+chargerPowerCustomHelp = "Ορίστε ένα προσαρμοσμένο εύρος έντασης ρεύματος."
+chargerTypeLabel = "Τύπος φορτιστή"
+chargingTitle = "Φορτίζει"
+circuitLabel = "Κύκλωμα"
+circuitUnassigned = "μη εκχωρημένο"
+defaultModeHelp = "Λειτουργία φόρτισης όταν συνδεθεί το όχημα."
+defaultModeHelpKeep = "Διατηρεί τον τελευταίο επιλεγμένο τρόπο φόρτισης."
+defaultModeLabel = "Προεπιλεγμένη λειτουργία"
+delete = "Διαγραφή"
+electricalSubtitle = "Όταν έχετε αμφιβολίες, ρωτήστε τον ηλεκτρολόγο σας."
+electricalTitle = "Ηλεκτρολογικό"
+energyMeterHelp = "Πρόσθετος μετρητής εάν ο φορτιστής δεν έχει ενσωματωμένο."
+energyMeterLabel = "Μετρητής ενέργειας"
+estimateLabel = "Παρεμβολή επιπέδου φόρτισης μεταξύ ενημερώσεων API"
+maxCurrentHelp = "Πρέπει να είναι μεγαλύτερο από την ελάχιστη ένταση."
+maxCurrentLabel = "Μέγιστη ένταση"
+minCurrentHelp = "Πηγαίνετε κάτω από 6A μόνο εάν ξέρετε τι κάνετε."
+minCurrentLabel = "Ελάχιστη ένταση"
+noVehicles = "Δεν έχουν προστεθεί οχήματα."
+phases1p = "μονοφασικό"
+phases3p = "τριφασικό"
+phasesAutomatic = "Αυτόματα οι φάσεις"
+phasesAutomaticHelp = "Ο φορτιστής σας υποστηρίζει αυτόματη εναλλαγή μεταξύ φόρτισης 1 και 3 φάσεων. Στην οθόνη του μπορείτε να προσαρμόσετε αυτή τη λειτουργία επιλογής φάσεων κατά τη φόρτιση."
+phasesHelp = "Αριθμός φάσεων που είναι συνδεδεμένες στον φορτιστή."
+phasesLabel = "Φάσεις"
+pollIntervalHelp = "Χρόνος μεταξύ των ενημερώσεων API του οχήματος. Τα μικρά διαστήματα ενδέχεται να ταλαιπωρήσουν τη μπαταρία του οχήματος."
+pollIntervalLabel = "Διάστημα ενημέρωσης"
+pollModeAlways = "πάντοτε"
+pollModeAlwaysHelp = "Να ζητά πάντα ενημερώσεις κατάστασης σε τακτά χρονικά διαστήματα."
+pollModeCharging = "φορτίζει"
+pollModeChargingHelp = "Να ζητά ενημερώσεις κατάστασης οχήματος μόνο κατά τη φόρτιση."
+pollModeConnected = "συνδεδεμένο"
+pollModeConnectedHelp = "Ενημέρωση της κατάστασης του οχήματος σε τακτά χρονικά διαστήματα όταν είναι συνδεδεμένο."
+pollModeLabel = "Συμπεριφορά ενημέρωσης"
+priorityHelp = "Τα σημεία φόρτισης μεγαλύτερης προτεραιότητας έχουν προτιμότερη πρόσβαση σε ηλιακό πλεόνασμα."
+priorityLabel = "Προτεραιότητα"
+save = "Αποθήκευση"
+showAllSettings = "Εμφάνιση όλων των ρυθμίσεων"
+solarBehaviorCustomHelp = "Ορίστε το δικό σας εύρος ενεργοποίησης και απενεργοποίησης κατώτατων ορίων και καθυστερήσεων."
+solarModeCustom = "προσαρμοσμένo"
+titleAdd = "Προσθήκη Σημείου Φόρτισης"
+titleEdit = "Επεξεργασία Σημείου Φόρτισης"
+titleExample = "Γκαράζ, Υπόστεγο, κλπ."
+titleLabel = "Τίτλος"
+vehicleAutoDetection = "αυτόματος εντοπισμός"
+vehicleHelpAutoDetection = "Επιλέγει αυτόματα το πιο πιθανό όχημα. Είναι δυνατή και η χειροκίνητη επιλογή."
+vehicleHelpDefault = "Υποθέτει ότι πάντα θα φορτίζει εδώ αυτό το όχημα. Η αυτόματη ανίχνευση είναι απενεργοποιημένη. Είναι δυνατή η χειροκίνητη επιλογή."
+vehicleLabel = "Προεπιλεγμένο όχημα"
+vehiclesTitle = "Οχήματα"
+
 [config.main]
+addGrid = "Προσθήκη Μετρητή Δικτύου"
 addLoadpoint = "Προσθήκη σημείου φόρτισης"
 addPvBattery = "Προσθήκη Φ/Β ενέργειας ή μπαταρίας"
+addTariffs = "Προσθήκη τιμολογίων"
 addVehicle = "Προσθήκη οχήματος"
 configured = "ρυθμίστηκε"
 edit = "επεξεργασία"
@@ -199,6 +279,11 @@ little = "little-endian"
 http = "HTTP (χωρίς κρυπτογράφηση)"
 https = "HTTPS (με κρυπτογράφηση)"
 
+[config.options.status]
+A = "A (δεν είναι συνδεδεμένο)"
+B = "B (συνδεδεμένο)"
+C = "C (φορτίζει)"
+
 [config.pv]
 titleAdd = "Προσθήκη Μετρητή Φ/Β"
 titleEdit = "Επεξεργασία Μετρητή Φ/Β"
@@ -332,6 +417,31 @@ modalUpdateNow = "Εγκατάσταση τώρα"
 modalUpdateStarted = "Εκκίνηση της νέας έκδοσης του evcc…"
 modalUpdateStatusStart = "Ξεκίνησε η εγκατάσταση:"
 
+[forecast]
+modalTitle = "Πρόγνωση"
+
+[forecast.co2]
+average = "Μέσος όρος"
+lowestHour = "«Πιο καθαρή ώρα»"
+range = "'Εύρος"
+
+[forecast.price]
+average = "Μέσος όρος"
+lowestHour = "Πιο φθηνή ώρα"
+range = "'Εύρος"
+
+[forecast.solar]
+dayAfterTomorrow = "Μεθαύριο"
+partly = "μερικώς"
+remaining = "Απομένει"
+today = "Σήμερα"
+tomorrow = "Αύριο"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Τιμή"
+solar = "Ηλιακή"
+
 [header]
 about = "Σχετικά"
 blog = "Blog"
@@ -388,7 +498,9 @@ reset = "Επαναφορά κωδικού πρόσβασης;"
 title = "Πιστοποίηση"
 
 [main]
+startConfiguration = "Ας ξεκινήσουμε τη διαμόρφωση"
 vehicles = "Στάθμευση"
+welcome = "Γεια σας!"
 
 [main.chargingPlan]
 active = "Ενεργό"
@@ -420,6 +532,7 @@ batteryGridChargeActive = "ενεργή φόρτιση από το δίκτυο"
 batteryGridChargeLimit = "φόρτιση από το δίκτυο όταν"
 batteryHold = "Μπαταρία (κλειδωμένο)"
 batteryTooltip = "{energy} από {total} ({soc})"
+forecastTooltip = "πρόβλεψη: εναπομένουσα ηλιακή παραγωγή σήμερα"
 gridImport = "Χρήση δικτύου"
 homePower = "Κατανάλωση"
 loadpoints = "Φορτιστής| Φορτιστής | {count} φορτιστές"
diff --git a/i18n/fi.toml b/i18n/fi.toml
index aace5bc5..2c3339de 100644
--- a/i18n/fi.toml
+++ b/i18n/fi.toml
@@ -30,6 +30,20 @@ never = "ainoastaan kun riittävästi ylijäämää."
 titleAdd = "Lisää akku"
 titleEdit = "Muokkaa akkua"
 
+[config.charge]
+titleAdd = "Lisää latausmittari"
+titleEdit = "Muokkaa latausmittaria"
+
+[config.charger]
+chargers = "Sähköauton latausasemat"
+generic = "Yleiset integraatiot"
+ocppHelp = "Kopioi tämä osoite laturien määrityksiin."
+ocppLabel = "OCPP-palvelimen URL"
+switchsockets = "Kytkettävät pistorasiat"
+template = "Valmistaja"
+titleAdd = "Lisää laturi"
+titleEdit = "Muokkaa laturia"
+
 [config.circuits]
 description = "Varmistaa, että kaikkien piiriin kytkettyjen kuormituspisteiden summa ei ylitä määritettyjä teho- ja virtarajoja. Piirejä voi rakentaa sisäkäin hierarkiaan."
 title = "Kuormanhallinta"
@@ -65,32 +79,43 @@ energy = "Energia"
 feedinPrice = "Sähköverkkoon myynninhinta"
 gridPrice = "Verkosta ostonhinta"
 hemsType = "Järjestelmä"
+identifier = "RFID-tunniste"
 no = "ei"
 odometer = "Matkamittari"
 org = "Organisaatio"
 phaseCurrents = "Virta L1, L2, L3"
 phasePowers = "Teho L1, L2, L3"
 phaseVoltages = "Jännite L1, L2, L3"
+phases1p3p = "Vaihekytkin"
 power = "Teho"
 powerRange = "Teho"
 range = "Toimintasäde"
-soc = "SoC"
+singlePhase = "Yksivaiheinen"
+soc = "Lataus"
 socLimit = "Raja"
 temp = "Lämpötila"
 topic = "Aihe"
 url = "URL"
+vehicleLimitSoc = "Ajoneuvonrajoitus"
 yes = "kyllä"
 
+[config.deviceValueChargeStatus]
+A = "A (ei yhdistetty)"
+B = "B (yhdistetty)"
+C = "C (lataa)"
+
 [config.eebus]
 description = "Konfiguraatio, jonka avulla evcc voi kommunikoida muiden EEBus-laitteiden kanssa."
 title = "EEBus"
 
 [config.form]
+deprecated = "vanhentunut"
 example = "Esimerkiksi"
 optional = "valinnainen"
 
 [config.general]
 cancel = "Peruuta"
+delete = "Poista"
 docsLink = "Katso dokumentaatio"
 experimental = "Kokeellinen"
 hideAdvancedSettings = "Piilota edistyneet asetukset"
@@ -103,6 +128,7 @@ save = "Tallenna"
 showAdvancedSettings = "Näytä edistyneet asetukset"
 telemetry = "Telemetria"
 title = "Otsikko"
+validateSave = "Vahvista ja tallenna"
 
 [config.grid]
 title = "Sähköverkonmittari"
@@ -129,16 +155,92 @@ title = "InfluxDB"
 v1Support = "Tarvitsetko tukea InfluxDB 1.x:lle?"
 v2Support = "Takaisin InfluxDB 2.x:ään"
 
+[config.loadpoint]
+addCharger = "Lisää laturi"
+addMeter = "Lisää erillinen latausmittari"
+cancel = "Peruuta"
+chargerLabel = "Laturi"
+chargerPower11kw = "11 kW"
+chargerPower11kwHelp = "Käyttää 6-16 A:n virta-aluetta."
+chargerPower22kw = "22 kW"
+chargerPower22kwHelp = "Käyttää 6-32 A:n virta-aluetta."
+chargerPowerCustom = "muu"
+chargerPowerCustomHelp = "Määritä mukautettu virta-alue."
+chargerTypeLabel = "Laturin tyyppi"
+chargingTitle = "Lataa"
+circuitLabel = "Piiri"
+circuitUnassigned = "määräämätön"
+defaultModeHelp = "Lataustila ajoneuvoa kytkettäessä."
+defaultModeHelpKeep = "Säilyttää viimeksi valitun lataustilan."
+defaultModeLabel = "Oletustila"
+delete = "Poista"
+electricalSubtitle = "Jos olet epävarma, kysy sähkömieheltäsi."
+electricalTitle = "Sähköinen"
+energyMeterHelp = "Lisämittari, jos laturissa ei ole integroitua energiamittaria."
+energyMeterLabel = "Energiamittari"
+estimateLabel = "Arvioi lataustaso API-päivitysten välillä"
+maxCurrentHelp = "On oltava suurempi kuin minimivirta."
+maxCurrentLabel = "Maksimivirta"
+minCurrentHelp = "Valitse alle 6 A vain, jos tiedät mitä olet tekemässä."
+minCurrentLabel = "Minimivirta"
+noVehicles = "Ajoneuvoja ei ole määritetty."
+phases1p = "1-vaihe"
+phases3p = "3-vaihe"
+phasesAutomatic = "Automaattinen"
+phasesAutomaticHelp = "Laturisi tukee automaattista vaihtoa 1- ja 3-vaiheisen latauksen välillä. Päänäytössä voit säätää vaihekäyttäytymistä latauksen aikana."
+phasesHelp = "Laturiin kytkettyjen vaiheiden lukumäärä."
+phasesLabel = "Vaiheet"
+pollIntervalHelp = "Ajoneuvon API-päivitysten välinen aika. Lyhyet välit voivat tyhjentää ajoneuvon akun."
+pollIntervalLabel = "Päivitysväli"
+pollModeAlways = "aina"
+pollModeAlwaysHelp = "Pyydä aina tilapäivityksiä säännöllisin väliajoin."
+pollModeCharging = "lataa"
+pollModeChargingHelp = "Pyydä ajoneuvon tilapäivityksiä vain latauksen aikana."
+pollModeConnected = "yhdistetty"
+pollModeConnectedHelp = "Päivitä ajoneuvon tila säännöllisin väliajoin, kun yhteys on muodostettu."
+pollModeLabel = "Päivitys käyttäytyminen"
+priorityHelp = "Korkeamman prioriteetin latauspisteet saavat ensisijaisen pääsyn aurinkoenergian ylijäämään."
+priorityLabel = "Prioriteetti"
+save = "Tallenna"
+showAllSettings = "Näytä kaikki asetukset"
+solarBehaviorCustomHelp = "Määritä omat päälle- ja poiskytkentä sekä viiveet."
+solarBehaviorDefaultHelp = "Lataa vain aurinkoenergian ylijäämällä. Aloita ylijäämän {enableDelay} jälkeen. Lopeta, kun ylijäämää ei ole tarpeeksi {disableDelay}:lle."
+solarBehaviorLabel = "PV käyttäytyminen"
+solarModeCustom = "muokattu"
+solarModeMaximum = "maksimi PV"
+thresholdDisableDelayLabel = "Poista viive käytöstä"
+thresholdDisableHelpInvalid = "Käytä positiivista arvoa."
+thresholdDisableHelpPositive = "Lopeta lataaminen, kun verkosta käytetään enemmän kuin {power}, {delay} ajaksi."
+thresholdDisableHelpZero = "Pysäytä, kun vähimmäislataustehoa ei voida täyttää {delay} ajaksi."
+thresholdDisableLabel = "Poista verkkoenergia käytöstä"
+thresholdEnableDelayLabel = "Ota viive käyttöön"
+thresholdEnableHelpInvalid = "Käytä negatiivista arvoa."
+thresholdEnableHelpNegative = "Aloita lataaminen, kun {surplus} ylijäämää on käytettävissä {delay}."
+thresholdEnableHelpZero = "Aloita, kun vähimmäislatausteho on käytettävissä {delay}."
+thresholdEnableLabel = "Ota verkkoenergia käyttöön"
+titleAdd = "Lisää latauspiste"
+titleEdit = "Muokkaa latauspistettä"
+titleExample = "Autotalli, autokatos jne."
+titleLabel = "Otsikko"
+vehicleAutoDetection = "automaattinen tunnistus"
+vehicleHelpAutoDetection = "Valitsee automaattisesti todennäköisimmän ajoneuvon. Manuaalinen ohitus on mahdollista."
+vehicleHelpDefault = "Olettaa aina, että tämä ajoneuvo latautuu täällä. Automaattinen tunnistus poistettu käytöstä. Manuaalinen ohitus on mahdollista."
+vehicleLabel = "Oletusajoneuvo"
+vehiclesTitle = "Ajoneuvot"
+
 [config.main]
+addGrid = "Lisää verkkomittari"
 addLoadpoint = "Lisää latauspiste"
 addPvBattery = "Lisää aurinkovoimala tai akusto"
+addTariffs = "Lisää tariffit"
 addVehicle = "Lisää ajoneuvo"
 configured = "asetettu"
 edit = "muokkaa"
+name = "Nimi"
 title = "Määritykset"
 unconfigured = "ei määritetty"
 vehicles = "Minun ajoneuvot"
-yaml = "Määritetty tiedostossa evcc.yaml. Ei muokattavissa käyttöliittymästä."
+yaml = "Laitteita jotka ovat määritetty evcc.yaml, ei voi muokata."
 
 [config.messaging]
 description = "Vastaanota viestejä koskien lataustapahtumiasi."
@@ -197,6 +299,11 @@ little = "little-endian"
 http = "HTTP (salaamaton)"
 https = "HTTPS (salattu)"
 
+[config.options.status]
+A = "A (ei yhdistetty)"
+B = "B (yhdistetty)"
+C = "C (lataa)"
+
 [config.pv]
 titleAdd = "Lisää aurinkovoimalan mittari"
 titleEdit = "Muokkaa aurinkovoimalan mittaria"
@@ -211,13 +318,16 @@ system = "Järjestelmä"
 vehicles = "Ajoneuvot"
 
 [config.sponsor]
-addToken = "Anna sponsoritunnus"
-changeToken = "Vaihda sponsoritunnus"
+addToken = "Anna tunnus"
+changeToken = "Vaihda tunnus"
 description = "Sponsorointi auttaa meitä ylläpitämään projektia ja rakentamaan kestävästi uusia ja jännittäviä ominaisuuksia. Sponsorina saat käyttöösi kaikki latureiden toteutukset."
 descriptionToken = "Saat tunnuksen osoitteesta {url}. Tarjoamme myös kokeilutunnuksen testausta varten."
 error = "Sponsoritunnus ei kelpaa."
 labelToken = "Sponsoritunnus"
 title = "Sponsorointi"
+tokenRequired = "Sinun on määritettävä sponsoritunnus ennen kuin voit luoda tämän laitteen."
+tokenRequiredLearnMore = "Lisätietoja."
+trialToken = "Kokeilutunnus"
 
 [config.system]
 logs = "Logi"
@@ -333,6 +443,31 @@ modalUpdateNow = "Asenna nyt"
 modalUpdateStarted = "Käynnistetään evcc:n uusin versio..."
 modalUpdateStatusStart = "Asennus aloitettu:"
 
+[forecast]
+modalTitle = "Ennuste"
+
+[forecast.co2]
+average = "Keskimäärin"
+lowestHour = "Puhtain tunti"
+range = "Toimintamatka"
+
+[forecast.price]
+average = "Keskimääräinen"
+lowestHour = "Edullisin tunti"
+range = "Toimintamatka"
+
+[forecast.solar]
+dayAfterTomorrow = "Ylihuomenna"
+partly = "osittain"
+remaining = "jäljellä"
+today = "Tänään"
+tomorrow = "Huomenna"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Hinta"
+solar = "Aurinko"
+
 [header]
 about = "Tietoa"
 blog = "Blogi"
@@ -390,7 +525,9 @@ reset = "Määritä salasana uudelleen?"
 title = "Vahvistus"
 
 [main]
+startConfiguration = "Aloita määritys"
 vehicles = "Pysäköinti"
+welcome = "Hei kyytiin!"
 
 [main.chargingPlan]
 active = "Aktiivinen"
@@ -418,8 +555,11 @@ weekdays = "Päivät"
 battery = "Akku"
 batteryCharge = "Akunlataus"
 batteryDischarge = "Akku purkautuu"
+batteryGridChargeActive = "lataaminen verkkosta aktiivinen"
+batteryGridChargeLimit = "milloin ladataan verkosta"
 batteryHold = "Akku (lukittu)"
 batteryTooltip = "{energy} / {total} ({soc})"
+forecastTooltip = "ennuste: jäljellä oleva aurinkotuotanto tänään"
 gridImport = "Kulutus sähköverkosta"
 homePower = "Kodin sähkönkulutus"
 loadpoints = "Latauslaite| Latauslaite | {count} latauslaitetta"
@@ -584,6 +724,7 @@ scale3p = "Siirrytään pian 3-vaihe lataukseen"
 targetChargeActive = "Lataussuunnitelma aktiivinen. Arvioitu valmistuminen {duration} kuluttua."
 targetChargePlanned = "Lataussuunnitelma alkaa {duration} kuluttua."
 targetChargeWaitForVehicle = "Lataussuunnitelma valmis. Odotetaan ajoneuvoa…"
+unknown = ""
 vehicleLimit = "Ajoneuvon latausraja"
 vehicleLimitReached = "Ajoneuvon raja saavutettu."
 waitForVehicle = "Valmiina. Odotetaan ajoneuvoa…"
diff --git a/i18n/fr.toml b/i18n/fr.toml
index db682172..363dc8cf 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -447,6 +447,31 @@ modalUpdateNow = "Installer maintenant"
 modalUpdateStarted = "Démarrage de la nouvelle version de evcc..."
 modalUpdateStatusStart = "L'installation a commencé:"
 
+[forecast]
+modalTitle = "Prévisions"
+
+[forecast.co2]
+average = "Moyenne"
+lowestHour = "Heure la plus propre"
+range = "Amplitude"
+
+[forecast.price]
+average = "Moyenne"
+lowestHour = "Heure la moins chère"
+range = "Amplitude"
+
+[forecast.solar]
+dayAfterTomorrow = "Après demain"
+partly = "Partiellement"
+remaining = "Restant"
+today = "Aujourd'hui"
+tomorrow = "Demain"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Prix"
+solar = "Solaire"
+
 [header]
 about = "À propos"
 blog = "Blog"
@@ -543,6 +568,7 @@ batteryGridChargeActive = "charge réseau active"
 batteryGridChargeLimit = "charge réseau quand"
 batteryHold = "Batterie (verrouillée)"
 batteryTooltip = "{energy} sur {total} ({soc})"
+forecastTooltip = "prévision : production solaire restante aujourd’hui"
 gridImport = "Utilisation du réseau"
 homePower = "Consommation"
 loadpoints = "Recharge| Recharge | {count} chargeurs"
@@ -672,7 +698,7 @@ moreActions = "Plus d'actions"
 none = "Pas de véhicule"
 notReachable = "Impossible d’atteindre le véhicule. Essayez de redémarrer evcc."
 targetSoc = "Limite"
-temp = "Temp."
+temp = "Température"
 tempLimit = "Temp. limite"
 unknown = "Véhicule invité"
 vehicleSoc = "Charge"
diff --git a/i18n/hr.toml b/i18n/hr.toml
index 8bbedb54..51e18948 100644
--- a/i18n/hr.toml
+++ b/i18n/hr.toml
@@ -30,6 +30,20 @@ never = "samo s dovoljno viška."
 titleAdd = "Dodaj bateriju"
 titleEdit = "Uredi bateriju"
 
+[config.charge]
+titleAdd = "Dodaj strujomjer"
+titleEdit = "Uredi strujomjer"
+
+[config.charger]
+chargers = "EV punjači"
+generic = "Standardne integracije"
+ocppHelp = "Kopirajte ovu adresu u postavke punjača."
+ocppLabel = "URL OCPP poslužitelja"
+switchsockets = "Promijenjive utičnice"
+template = "Proizvođač"
+titleAdd = "Dodati punjač"
+titleEdit = "Urediti punjač"
+
 [config.circuits]
 description = "Osigurava da suma svih točaka punjenja povezanih na mrežu ne prelazi postavljene granice snage i struje. Krugovi mogu biti složeni hijerarhijski."
 title = "Upravljanje opterećenjem"
@@ -65,32 +79,43 @@ energy = "Energija"
 feedinPrice = "Cijena za nabavu"
 gridPrice = "Cijena mreže"
 hemsType = "Sustav"
+identifier = "RFID-identifikator"
 no = "ne"
 odometer = "Odometar"
 org = "Organizacija"
 phaseCurrents = "Struja L1, L2, L3"
 phasePowers = "Snaga L1, L2, L3"
 phaseVoltages = "Napon L1, L2, L3"
+phases1p3p = "Promjena faza"
 power = "Snaga"
 powerRange = "Ukupna snaga"
 range = "Doseg"
+singlePhase = "Jedna faza"
 soc = "Napunjenost"
 socLimit = "Ograničenje"
 temp = "Temperatura"
 topic = "Naslov"
 url = "URL"
+vehicleLimitSoc = "Ograničenje vozila"
 yes = "da"
 
+[config.deviceValueChargeStatus]
+A = "A (nije povezano)"
+B = "B (povezano)"
+C = "C (punjenje)"
+
 [config.eebus]
 description = "Postavke koje omogućuju evcc-u komunikaciju s drugim EEBus uređajima."
 title = "EEBus"
 
 [config.form]
+deprecated = "zastarjelo"
 example = "Primjer"
 optional = "opcionalno"
 
 [config.general]
 cancel = "Odustani"
+delete = "Obrisati"
 docsLink = "Vidjeti dokumentaciju."
 experimental = "Eksperimentalno"
 hideAdvancedSettings = "Sakrij napredne postavke"
@@ -103,6 +128,7 @@ save = "Spremiti"
 showAdvancedSettings = "Prikaži napredne postavke"
 telemetry = "Telemetrija"
 title = "Ime"
+validateSave = "Provjeriti i spremiti"
 
 [config.grid]
 title = "Mrežno brojilo"
@@ -129,6 +155,55 @@ title = "InfluxDB"
 v1Support = "Potrebna podrška za InfluxDB 1.x?"
 v2Support = "Natrag na InfluxDB 2.x"
 
+[config.loadpoint]
+addCharger = "Dodati punjač"
+addMeter = "Dodati zasebni strujomjer"
+cancel = "Odustati"
+chargerLabel = "Punjač"
+chargerPower11kw = "11 kW"
+chargerPower11kwHelp = "Koristiti će raspon struje od 6 i 16A."
+chargerPower22kw = "22 kW"
+chargerPower22kwHelp = "Koristiti će raspon struje od 6 do 32A."
+chargerPowerCustom = "drugo"
+chargerPowerCustomHelp = "Postaviti zaseban raspon struje."
+chargerTypeLabel = "Tip punjača"
+chargingTitle = "Punjenje"
+circuitLabel = "Strujni krug"
+circuitUnassigned = "nepostavljen"
+defaultModeHelp = "Način punjenja kada se vozilo poveže."
+defaultModeHelpKeep = "Zadržava posljednje korišteni način punjenja."
+defaultModeLabel = "Uobičajeni način"
+delete = "Obrisati"
+electricalSubtitle = "Ako niste sigurni, pitajte električara."
+electricalTitle = "Elektrooprema"
+energyMeterHelp = "Dodatni strujomjer ako punjač nema integrirani."
+energyMeterLabel = "Strujomjer"
+estimateLabel = "Intepolirati razine punjenja između API očitavanja"
+maxCurrentHelp = "Mora biti veće od minimalne struje."
+maxCurrentLabel = "Maksimalna struja"
+minCurrentHelp = "Postavite ispod 6A samo ako znate što radite."
+minCurrentLabel = "Minimalna struja"
+noVehicles = "Niti jedno vozilo nije postavljeno."
+phases1p = "1 faza"
+phases3p = "3 faze"
+phasesAutomatic = "Automatske faze"
+phasesAutomaticHelp = "Vaš punjač podržava prebacivanje između 1 i 3 faze. Na glavom zaslonu možete postaviti željeno ponašanje prilikom punjenja."
+phasesHelp = "Broj faza povezanih s punjačem."
+phasesLabel = "Faze"
+pollIntervalHelp = "Vrijeme između API očitavanja. Kratki intervali mogu isprazniti bateriju vozila."
+pollIntervalLabel = "Interval očitavanja"
+pollModeAlways = "uvijek"
+pollModeAlwaysHelp = "Uvijek ažurirati status pri standardnim intervalima."
+pollModeCharging = "punjenje"
+pollModeChargingHelp = "Ažurirati status vozila samo prilikom punjenja."
+pollModeConnected = "povezano"
+pollModeConnectedHelp = "Ažurirati status vozila u standardnim intervalima samo kad je vozilo povezano."
+pollModeLabel = "Ponašanje ažuriranja"
+priorityHelp = "Punjači višeg prioriteta imaju prednost pri korištenju viškova iz solarne elektrane."
+priorityLabel = "Prioritet"
+save = "Spremiti"
+showAllSettings = "Prikazati sve postavke"
+
 [config.main]
 addLoadpoint = "Dodati punjač"
 addPvBattery = "Dodati PV ili bateriju"
@@ -138,7 +213,7 @@ edit = "uredi"
 title = "Postavke"
 unconfigured = "nije postavljeno"
 vehicles = "Moja vozila"
-yaml = "Postavljeno u evcc.yaml. Nije moguće urediti u korisničkom sučelju."
+yaml = "Uređaji od evcc.yaml se ne mogu urediti."
 
 [config.messaging]
 description = "Primite poruke o vašim postupcima punjenja."
@@ -211,8 +286,8 @@ system = "Sustav"
 vehicles = "Vozila"
 
 [config.sponsor]
-addToken = "Unesite sponzorski token"
-changeToken = "Promijeniti sponzorski token"
+addToken = "Unesite token"
+changeToken = "Promijenite token"
 description = "Sponzorski model nam omogućava održavanje projekta i razvoj novih mogućnosti. Sponzori imaju pristup svim implementacijama za punjače."
 descriptionToken = "Token možete dobiti na {url}. Moguće je zatražiti i probni token za testiranje."
 error = "Sponzorski token nije ispravan."
diff --git a/i18n/hu.toml b/i18n/hu.toml
index aa6f59b5..8d7e3b8e 100644
--- a/i18n/hu.toml
+++ b/i18n/hu.toml
@@ -435,6 +435,31 @@ modalUpdateNow = "Telepítés most"
 modalUpdateStarted = "Az új evcc verzió indítása…"
 modalUpdateStatusStart = "A telepítés elkezdődött:"
 
+[forecast]
+modalTitle = "Előrejelzés"
+
+[forecast.co2]
+average = "Átlag"
+lowestHour = "Legtisztább óra"
+range = "Tartomány"
+
+[forecast.price]
+average = "Átlag"
+lowestHour = "Legolcsóbb óra"
+range = "Tartomány"
+
+[forecast.solar]
+dayAfterTomorrow = "Holnap után"
+partly = "részlegesen"
+remaining = "hátralévő"
+today = "Ma"
+tomorrow = "Holnap"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Ár"
+solar = "Szolár"
+
 [header]
 about = "Névjegy"
 blog = "Blog"
@@ -525,6 +550,7 @@ batteryGridChargeActive = "hálózatból töltés aktív"
 batteryGridChargeLimit = "hálózatból töltés ha"
 batteryHold = "Energiatároló (lezárva)"
 batteryTooltip = "{energy} / {total} ({soc})"
+forecastTooltip = "előrejelzés: ma fennmaradó napenergia-termelés"
 gridImport = "Hálózatból import"
 homePower = "Fogyasztás"
 loadpoints = "Töltő| Töltő | {count} töltő"
diff --git a/i18n/it.toml b/i18n/it.toml
index 6ba1334a..ea1c000a 100644
--- a/i18n/it.toml
+++ b/i18n/it.toml
@@ -2,24 +2,24 @@
 batteryLevel = "Livello batteria"
 capacity = "{energy} di {total}"
 control = "Controllo batteria"
-discharge = "Prevenire la scarica della batteria in modalità veloce oppure quando è attiva una pianificazione."
+discharge = "Previeni la scarica della batteria in modalità veloce oppure quando è attiva una pianificazione."
 disclaimerHint = "Nota:"
 disclaimerText = "Queste impostazioni riguardano solamente la modalità solare. Il comportamento di ricarica è adattato di conseguenza."
 gridChargeTab = "Carica in corso dalla rete"
 legendBottomName = "Priorità alla carica della batteria di casa"
-legendBottomSubline = "Fino al raggiungimento del {soc}."
+legendBottomSubline = "fino al {soc}."
 legendMiddleName = "Priorità alla carica del veicolo"
 legendMiddleSubline = "quando la batteria supera il {soc}."
 legendTitle = "“Come dovrebbe essere utilizzata l’energia solare?”"
-legendTopAutostart = "Parte automaticamente"
+legendTopAutostart = "Parti automaticamente"
 legendTopName = "Ricarica del veicolo assistita dalla batteria"
 legendTopSubline = "quando la batteria supera il {soc}."
 modalTitle = "Batteria di casa"
 usageTab = "Uso della batteria"
 
 [batterySettings.bufferStart]
-above = "Quando supera il {soc}."
-full = "Quando è al {soc}."
+above = "quando supera il {soc}."
+full = "quando è al {soc}."
 never = "solo con abbastanza surplus."
 
 [config]
@@ -28,17 +28,31 @@ never = "solo con abbastanza surplus."
 titleAdd = "Aggiungi Batteria"
 titleEdit = "Modifica Batteria"
 
+[config.charge]
+titleAdd = "“Aggiungi un contatore di carica\""
+titleEdit = "Modifica contatore di carica"
+
+[config.charger]
+chargers = "Caricatori EV"
+generic = "Integrazioni generiche"
+ocppHelp = "Copia questo indirizzo nella configurazione dei tuoi caricatori"
+ocppLabel = "URL Server OCPP"
+switchsockets = "Prese commutabili"
+template = "Produttore"
+titleAdd = "Aggiungi caricabatterie"
+titleEdit = "Modifica caricabatterie"
+
 [config.circuits]
 description = "Garantisce che la somma dei carichi connessi ad un circuito non superi i limiti di corrente configurati. I circuiti possono essere annidati in una struttura gerarchica."
-title = "Gestione del Carico"
+title = "Gestione del carico"
 
 [config.control]
-description = "Normalmente le impostazioni di default vanno bene. Modificale solo se sai cosa stai facendo."
+description = "Normalmente le impostazioni di default sono adeguate. Modificale solo se sai cosa stai facendo."
 descriptionInterval = "Intervallo ciclo di aggiornamento in secondi. Definisce quanto spesso evcc rileva i dati, modifica la potenza di carica e aggiorna l'interfaccia utente. Intervalli ridotti (<30s) possono causare oscillazioni e comportamenti indesiderati."
-descriptionResidualPower = "Modifica la soglia di corrente per il ciclo di controllo. Se hai una batteria è consigliabile impostare un valore minimo di 100W, in questo modo la batteria riceverà una priorità rispetto all'uso della rete elettrica"
+descriptionResidualPower = "Modifica la soglia di corrente per il ciclo di controllo. Se hai una batteria è consigliabile impostare un valore minimo di 100W: in questo modo la batteria riceverà una priorità rispetto all'uso della rete elettrica"
 labelInterval = "Intervallo di aggiornamento"
 labelResidualPower = "Potenza residua"
-title = "Controllo del comportamento"
+title = "Comportamento di controllo"
 
 [config.deviceValue]
 amount = "Quantità"
@@ -63,37 +77,48 @@ energy = "Energia"
 feedinPrice = "Prezzo di vendita"
 gridPrice = "Prezzo d'acquisto"
 hemsType = "Sistema"
+identifier = "Identificatore RFID"
 no = "no"
-odometer = "Odometro"
+odometer = "Chilometraggio"
 org = "Organizzazione"
 phaseCurrents = "Corrente L1, L2, L3"
 phasePowers = "Potenza L1, L2, L3"
 phaseVoltages = "Voltaggio L1, L2, L3"
+phases1p3p = "Commutazione di fase"
 power = "Potenza"
 powerRange = "Potenza"
 range = "Intervallo"
-soc = "Stato di Carica"
+singlePhase = "Singola fase"
+soc = "Carica"
 socLimit = "Limite"
 temp = "Temperatura"
 topic = "Argomento"
 url = "URL"
+vehicleLimitSoc = "Limite del veicolo"
 yes = "sì"
 
+[config.deviceValueChargeStatus]
+A = "A (non collegato)"
+B = "B (collegato)"
+C = "C (in carica)"
+
 [config.eebus]
 description = "Configurazione che permette a evcc di comunicare con altri dispositivi EEBus"
 title = "EEBus"
 
 [config.form]
+deprecated = "deprecato"
 example = "Esempio"
 optional = "facoltativo"
 
 [config.general]
-cancel = "Cancella"
+cancel = "Annulla"
+delete = "Cancella"
 docsLink = "Vedi la documentazione."
 experimental = "Sperimentale"
 hideAdvancedSettings = "Nascondi impostazioni avanzate"
-off = "spento"
-on = "acceso"
+off = "off"
+on = "on"
 password = "Password"
 readFromFile = "Importa da file"
 remove = "Rimuovi"
@@ -101,11 +126,12 @@ save = "Salva"
 showAdvancedSettings = "Mostra impostazioni avanzate"
 telemetry = "Telemetria"
 title = "Titolo"
+validateSave = "Conferma e salva"
 
 [config.grid]
-title = "Contatore"
-titleAdd = "Aggiungi Contatore"
-titleEdit = "Modifica Contatore"
+title = "Contatore di rete"
+titleAdd = "Aggiungi contatore di rete"
+titleEdit = "Modifica contatore di rete"
 
 [config.hems]
 description = "Connetti evcc ad un altro sistema di gestione della corrente di casa."
@@ -127,28 +153,104 @@ title = "InfluxDB"
 v1Support = "Serve supporto per InfluxDB 1.x?"
 v2Support = "Torna a InfluxDB 2.x"
 
+[config.loadpoint]
+addCharger = "Aggiungi caricabatterie"
+addMeter = "Aggiungi un contatore dedicato per il caricabatterie"
+cancel = "Annulla"
+chargerLabel = "Caricabatterie"
+chargerPower11kw = "11 kW"
+chargerPower11kwHelp = "Utilizzerà un intervallo di corrente tra 6 e 16A"
+chargerPower22kw = "22 kW"
+chargerPower22kwHelp = "Utilizzerà un intervallo di corrente tra 6 e 32A"
+chargerPowerCustom = "altro"
+chargerPowerCustomHelp = "Definire un intervallo di corrente personalizzato"
+chargerTypeLabel = "Tipo di caricabatterie"
+chargingTitle = "In carica"
+circuitLabel = "Circuito"
+circuitUnassigned = "non assegnato"
+defaultModeHelp = "Modalità di ricarica quando si collega il veicolo."
+defaultModeHelpKeep = "Mantiene l'ultima modalità di ricarica selezionata."
+defaultModeLabel = "Modalità predefinita"
+delete = "Cancella"
+electricalSubtitle = "Se non sei sicuro, chiedi al tuo elettricista."
+electricalTitle = "Elettricità"
+energyMeterHelp = "Contatore supplementare se il caricabatterie non ne possiede uno integrato."
+energyMeterLabel = "Misuratore di energia"
+estimateLabel = "Stima il livello di carica tra gli aggiornamenti dell'API"
+maxCurrentHelp = "Deve essere maggiore della corrente minima."
+maxCurrentLabel = "Corrente massima"
+minCurrentHelp = "Scendere al di sotto dei 6A solo se si sa quello che si sta facendo."
+minCurrentLabel = "Corrente minima"
+noVehicles = "Nessun veicolo configurato."
+phases1p = "Monofase"
+phases3p = "Trifase"
+phasesAutomatic = "Fasi automatiche"
+phasesAutomaticHelp = "Il tuo caricabatterie supporta il passaggio automatico tra la carica monofase e quella trifase. Nella schermata principale è possibile modificare la gestione della fase durante la ricarica."
+phasesHelp = "Numero delle fasi collegate al caricabatterie."
+phasesLabel = "Fasi"
+pollIntervalHelp = "Intervallo tra gli aggiornamenti tramite API del veicolo. Intervalli più brevi possono scaricare la batteria."
+pollIntervalLabel = "Intervallo di aggiornamento"
+pollModeAlways = "sempre"
+pollModeAlwaysHelp = "Aggiorna sempre lo stato del veicolo ad intervalli regolari."
+pollModeCharging = "in carica"
+pollModeChargingHelp = "Aggiorna lo stato del veicolo soltanto durante la carica."
+pollModeConnected = "collegato"
+pollModeConnectedHelp = "Aggiorna lo stato del veicolo ad intervalli regolari quando collegato."
+pollModeLabel = "Modalità di aggiornamento"
+priorityHelp = "I punti di ricarica a maggiore priorità hanno un accesso preferenziale al surplus di produzione solare."
+priorityLabel = "Priorità"
+save = "Salva"
+showAllSettings = "Mostra tutte le impostazioni"
+solarBehaviorCustomHelp = "Definisci soglie personalizzate di attivazione e disattivazione della ricarica."
+solarBehaviorDefaultHelp = "Carica soltanto con surplus solare. Inizia se il surplus è mantenuto per {enableDelay}. Interrompi quando non c'è sufficiente surplus per {disableDelay}."
+solarBehaviorLabel = "Comportamento modalità solare"
+solarModeCustom = "personalizzato"
+solarModeMaximum = "massimo solare"
+thresholdDisableDelayLabel = "Ritardo di disattivazione"
+thresholdDisableHelpInvalid = "Inserisci un valore positivo."
+thresholdDisableHelpPositive = "Interrompi la ricarica in caso di prelievo dalla rete di più di {power} per più di {delay}."
+thresholdDisableHelpZero = "Interrompi quando la potenza minima di ricarica non può essere sostenuta per {delay}."
+thresholdDisableLabel = "Soglia di disattivazione (W)"
+thresholdEnableDelayLabel = "Ritardo di attivazione"
+thresholdEnableHelpInvalid = "Inserisci un valore negativo."
+thresholdEnableHelpNegative = "Inizia la ricarica quando un surplus di {surplus} è disponibile per {delay}."
+thresholdEnableHelpZero = "Inizia la ricarica quando la potenza di ricarica minima è disponibile per {delay}."
+thresholdEnableLabel = "Soglia di attivazione (W)"
+titleAdd = "Aggiungi punto di ricarica"
+titleEdit = "Modifica punto di ricarica"
+titleExample = "Garage, posto auto, ecc."
+titleLabel = "Titolo"
+vehicleAutoDetection = "identificazione automatica"
+vehicleHelpAutoDetection = "Seleziona automaticamente il veicolo più plausibile. E' comunque possibile la modifica manuale."
+vehicleHelpDefault = "Seleziona sempre questo veicolo in questo punto di ricarica. Identificazione automatica disabilitata. E' comunque possibile la modifica manuale."
+vehicleLabel = "Veicolo predefinito"
+vehiclesTitle = "Veicoli"
+
 [config.main]
-addLoadpoint = "Aggiungi stazione di ricarica"
+addGrid = "Aggiungi contatore di rete"
+addLoadpoint = "Aggiungi punto di ricarica"
 addPvBattery = "Aggiungi fotovoltaico o batteria"
-addVehicle = "Aggiungi automobile"
+addTariffs = "Aggiungi tariffe"
+addVehicle = "Aggiungi veicolo"
 configured = "configurato"
 edit = "modifica"
+name = "Nome"
 title = "Configurazione"
 unconfigured = "non configurato"
 vehicles = "I miei veicoli"
-yaml = "Configurato in evcc.yaml. Non modificabile tramite UI."
+yaml = "I dispositivi configurati in evcc.yaml non sono modificabili tramite UI."
 
 [config.messaging]
 description = "Ricevi messaggi sulle sessioni di ricarica."
 title = "Notifiche"
 
 [config.meter]
-cancel = "Cancella"
+cancel = "Annulla"
 delete = "Elimina"
 save = "Salva"
 template = "Produttore"
-titleChoice = "Cosa Vuoi Aggiungere?"
-validateSave = "Valida e salva"
+titleChoice = "Cosa vuoi aggiungere?"
+validateSave = "Conferma e salva"
 
 [config.modbusproxy]
 description = "Permetti a più client di accedere ad un singolo dispositivo Modbus"
@@ -195,6 +297,11 @@ little = "little-endian"
 http = "HTTP (non crittografato)"
 https = "HTTPS (crittografato)"
 
+[config.options.status]
+A = "A (non collegato)"
+B = "B (collegato)"
+C = "C (in carica)"
+
 [config.pv]
 titleAdd = "Aggiungi Contatore Fotovoltaico"
 titleEdit = "Modifica Contatore Fotovoltaico"
@@ -209,13 +316,16 @@ system = "Sistema"
 vehicles = "Veicoli"
 
 [config.sponsor]
-addToken = "Inserisci lo sponsor token"
-changeToken = "Modifica lo sponsor token"
+addToken = "Inserisci il token"
+changeToken = "Modifica il token"
 description = "Il modello di sponsorizzazione ci aiuta a mantenere il progetto e a introdurre in modo sostenibile nuove ed entusiasmanti funzionalità. Come sponsor hai accesso a tutte le implementazioni del caricabatterie."
 descriptionToken = "Ricevi il token da {url}. Offriamo anche un token di prova per i test."
 error = "Il token sponsor non è valido."
 labelToken = "Token sponsor"
 title = "Sponsorizzazione"
+tokenRequired = "Devi configurare un token sponsor prima di poter creare questo dispositivo."
+tokenRequiredLearnMore = "Maggiori informazioni"
+trialToken = "Token di prova"
 
 [config.system]
 logs = "Logs"
@@ -267,7 +377,7 @@ greenShareSub2 = "Solare e accumulo di batterie"
 power = "Potenza di carica"
 powerSub1 = "{activeClients} di {totalClients} partecipanti"
 powerSub2 = "in carica..."
-tabTitle = "Live community"
+tabTitle = "Comunità online"
 
 [footer.savings]
 co2Saved = "{value} di risparmio"
@@ -331,6 +441,31 @@ modalUpdateNow = "Installa ora"
 modalUpdateStarted = "Evcc ripartirà dopo l'aggiornamento..."
 modalUpdateStatusStart = "Installazione avviata:"
 
+[forecast]
+modalTitle = "Previsioni"
+
+[forecast.co2]
+average = "Media"
+lowestHour = "Ora più ecologica"
+range = "Intervallo"
+
+[forecast.price]
+average = "Media"
+lowestHour = "Ora più economica"
+range = "Intervallo"
+
+[forecast.solar]
+dayAfterTomorrow = "Dopodomani"
+partly = "parzialmente"
+remaining = "rimanenti"
+today = "Oggi"
+tomorrow = "Domani"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Prezzo"
+solar = "Solare"
+
 [header]
 about = "Riguardo"
 blog = "Blog"
@@ -393,7 +528,9 @@ reset = "Reimpostare la password?"
 title = "Autenticazione"
 
 [main]
+startConfiguration = "Iniziamo la configurazione"
 vehicles = "Parcheggio"
+welcome = "Benvenuto!"
 
 [main.chargingPlan]
 active = "Attivo"
@@ -425,6 +562,7 @@ batteryGridChargeActive = "Ricarica dalla rete attiva"
 batteryGridChargeLimit = "Ricarica dalla rete quando"
 batteryHold = "Batteria (locked)"
 batteryTooltip = "{energy} di {total} ({soc})"
+forecastTooltip = "previsioni: produzione solare rimanente per oggi"
 gridImport = "Uso della rete"
 homePower = "Consumo"
 loadpoints = "Caricabatterie| Caricabatterie | {count} caricabatterie"
@@ -445,7 +583,7 @@ duration = "Durata"
 fallbackName = "Punto di ricarica"
 power = "Potenza"
 price = "Costo"
-remaining = "Rimanente"
+remaining = "Rimanenti"
 remoteDisabledHard = "{source}: Disabilitato"
 remoteDisabledSoft = "{source}: Ricarica FV adattiva disabilitata"
 solar = "Solare"
@@ -626,8 +764,8 @@ date = "Periodo"
 delete = "Elimina"
 finished = "Finito"
 meter = "Contatore"
-meterstart = "Meter start"
-meterstop = "Meter stop"
+meterstart = "Avvio contatore"
+meterstop = "Stop contatore"
 odometer = "Contachilometri"
 price = "Prezzo"
 started = "Iniziato"
diff --git a/i18n/lb.toml b/i18n/lb.toml
index 4fa9c553..b5bc2d4f 100644
--- a/i18n/lb.toml
+++ b/i18n/lb.toml
@@ -440,6 +440,31 @@ modalUpdateNow = "Elo installéieren"
 modalUpdateStarted = "Déi nei Versioun vun evcc starten ..."
 modalUpdateStatusStart = "Installatioun ugefaang:"
 
+[forecast]
+modalTitle = "Prognose"
+
+[forecast.co2]
+average = "Moyenne"
+lowestHour = "Déi properst Stonn"
+range = "Beräich"
+
+[forecast.price]
+average = "Duerchschnitt"
+lowestHour = "Bëllegst Stonn"
+range = "Beräich"
+
+[forecast.solar]
+dayAfterTomorrow = "Iwwermuer"
+partly = "deelweis"
+remaining = "verbleiwend"
+today = "Haut"
+tomorrow = "Muer"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Präis"
+solar = "Solar"
+
 [header]
 about = "Iwwer"
 blog = "Blog"
@@ -536,6 +561,7 @@ batteryGridChargeActive = "Netzlueden aktiv"
 batteryGridChargeLimit = "Netzluede wann"
 batteryHold = "Batterie (gespäert)"
 batteryTooltip = "{energy} vun {total} ({soc})"
+forecastTooltip = "Prognose: Rescht Solarproduktioun fir haut"
 gridImport = "Verbrauch vum Stroumnetz"
 homePower = "Verbrauch"
 loadpoints = "Wallbox | Wallbox | {count} Wallboxen"
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 3115e097..1a6c047b 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -447,6 +447,31 @@ modalUpdateNow = "Instaliuoti dabar"
 modalUpdateStarted = "Startuoja nauja evcc versija..."
 modalUpdateStatusStart = "Instaliavimas pradėtas:"
 
+[forecast]
+modalTitle = "Prognozė"
+
+[forecast.co2]
+average = "Vidurkis"
+lowestHour = "Švariausia valanda"
+range = "Diapazonas"
+
+[forecast.price]
+average = "Vidurkis"
+lowestHour = "Pigiausia valanda"
+range = "Diapazonas"
+
+[forecast.solar]
+dayAfterTomorrow = "Poryt"
+partly = "dalinai"
+remaining = "likę"
+today = "Šiandien"
+tomorrow = "Rytoj"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Kaina"
+solar = "Saulės"
+
 [header]
 about = "Apie"
 blog = "Tinklaraštis"
@@ -543,6 +568,7 @@ batteryGridChargeActive = "įkraunama iš tinklo"
 batteryGridChargeLimit = "įkrauti iš tinklo, kai"
 batteryHold = "Kaupiklis (užblokuotas)"
 batteryTooltip = "{energy} iš {total} ({soc})"
+forecastTooltip = "prognozė: likusi saulės energijos gamyba šiandien"
 gridImport = "Iš tinklo"
 homePower = "Namo suvartojimas"
 loadpoints = "Įkroviklis | Įkroviklis | {count} įkrovikliai"
diff --git a/i18n/nl.toml b/i18n/nl.toml
index 09d92514..f39e13a6 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -36,6 +36,7 @@ titleEdit = "Editeer Lader Meter"
 
 [config.charger]
 chargers = "EV laders"
+generic = "Algemene integraties"
 ocppHelp = "kopieer dit adres in de configuratie van jouw lader"
 ocppLabel = "OCPP-Server URL"
 switchsockets = "schakelbare stopcontacten"
@@ -78,12 +79,14 @@ energy = "Energie"
 feedinPrice = "Terugleververgoeding"
 gridPrice = "Netprijs"
 hemsType = "Systeem"
+identifier = "RFID-Nummer"
 no = "nee"
 odometer = "Kilometerstand"
 org = "Organisatie"
 phaseCurrents = "Stroomsterkte L1, L2, L3"
 phasePowers = "Vermogen L1, L2, L3"
 phaseVoltages = "Spanning L1, L2, L3"
+phases1p3p = "Faseschakelaar"
 power = "Vermogen"
 powerRange = "Vermogen"
 range = "Actieradius"
@@ -153,6 +156,7 @@ v1Support = "Hulp nodig voor InfluxDB 1.x?"
 v2Support = "Terug naar InfluxDB 2.x"
 
 [config.loadpoint]
+addCharger = "Laadpunt toevoegen"
 cancel = "Annuleren"
 chargerLabel = "Oplader"
 chargerPower11kw = "11 kW"
@@ -160,8 +164,35 @@ chargerPower11kwHelp = "Zal een stroom van 6 tot 16 A gebruiken."
 chargerPower22kw = "22 kW"
 chargerPower22kwHelp = "Zal een stroom van 6 tot 32 A gebruiken."
 chargerPowerCustom = "andere"
+chargerPowerCustomHelp = "“Definieer een aangepast stroombereik.”"
+chargerTypeLabel = "Type laadpunt"
+chargingTitle = "Opladen"
+circuitLabel = "Circuit"
+circuitUnassigned = "niet toegewezen"
+defaultModeHelp = "Laadmodus bij het aansluiten van een voertuig."
+defaultModeHelpKeep = "“Behoudt de laatst geselecteerde laad-modus”"
 defaultModeLabel = "Standaard mode"
 delete = "Wissen"
+electricalSubtitle = "Bij twijfel, raadpleeg een elektricien."
+electricalTitle = "Elektrisch"
+energyMeterHelp = "“Extra meter voor laadstations zonder geïntegreerde meter”"
+energyMeterLabel = "Energiemeter"
+estimateLabel = "“Interpoleer batterijpercentage tussen API updates”"
+maxCurrentHelp = "“Moet groter zijn dan de minimum stroom.”"
+maxCurrentLabel = "“Maximale stroom”"
+minCurrentHelp = "“Kies 6 A enkel wanneer je weet wat je aan het doen bent.”"
+minCurrentLabel = "“Minimale stroom”"
+noVehicles = "“Geen voertuigen ingesteld.”"
+phases1p = "“1-fase”"
+phases3p = "“3-fase”"
+phasesAutomatic = "“Automatische fasen”"
+phasesAutomaticHelp = "“Uw laadstation ondersteunt automatisch wissel tussen laden op 1 of 3-fasen. U kan dit aanpassen van op het startscherm tijdens de laadsessie.”"
+phasesHelp = "“Aantal aangesloten fasen op het laadstation’”"
+phasesLabel = "“Fasen”"
+pollModeAlways = "altijd"
+priorityLabel = "Prioriteit"
+save = "Opslaan"
+showAllSettings = "Alle instellingen tonen"
 
 [config.main]
 addLoadpoint = "Voeg laadpunt toe"
diff --git a/i18n/pt.toml b/i18n/pt.toml
index 3f5ff126..de9ec1c9 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -201,17 +201,46 @@ pollModeConnectedHelp = "Atualizar o estado do veículo em intervalos regulares
 pollModeLabel = "Comportamento de atualização"
 priorityHelp = "Os pontos de carregamento com maior prioridade têm acesso prioritário ao excedente solar."
 priorityLabel = "Prioridade"
+save = "Guardar"
+showAllSettings = "Mostrar todas as definições"
+solarBehaviorCustomHelp = "Defina os seus próprios limiares e latências de ativação e desativação."
+solarBehaviorDefaultHelp = "Apenas carregar com excedente solar. Iniciar após {enableDelay} de excedente. Parar quando não houver excedente suficiente para {disableDelay}."
+solarBehaviorLabel = "Comportamento solar"
+solarModeCustom = "personalizado"
+solarModeMaximum = "máximo solar"
+thresholdDisableDelayLabel = "Diferir desativação"
+thresholdDisableHelpInvalid = "É favor utilizar um valor positivo."
+thresholdDisableHelpPositive = "Interromper o carregamento, quando for utilizada mais de {power} da rede durante {delay}."
+thresholdDisableHelpZero = "Interromper quando a potência de carga mínima não for atingida durante {delay}."
+thresholdDisableLabel = "Desativar a energia da rede"
+thresholdEnableDelayLabel = "Diferir activação"
+thresholdEnableHelpInvalid = "É favor utilizar um valor negativo."
+thresholdEnableHelpNegative = "Iniciar o carregamento, quando {surplus} excedente estiver disponível durante {delay}."
+thresholdEnableHelpZero = "Iniciar quando o excedente de energia de carga mínima estiver disponível durante {delay}."
+thresholdEnableLabel = "Ativar a energia da rede"
+titleAdd = "Adicionar ponto de carregamento"
+titleEdit = "Editar ponto de carregamento"
+titleExample = "Garagem, carport, etc."
+titleLabel = "Título"
+vehicleAutoDetection = "deteção automática"
+vehicleHelpAutoDetection = "Seleciona automaticamente o veículo mais plausível. O cancelamento manual é possível."
+vehicleHelpDefault = "Presumir sempre que este veículo está a carregar aqui. Deteção automática desactivada. A desativação manual é possível."
+vehicleLabel = "Veículo predefinido"
+vehiclesTitle = "Veículos"
 
 [config.main]
+addGrid = "Adicionar contador de rede"
 addLoadpoint = "Adicionar ponto de recarga"
 addPvBattery = "Adicionar solar ou bateria"
+addTariffs = "Adicionar tarifas"
 addVehicle = "Adicionar veículo"
 configured = "configurado"
 edit = "editar"
+name = "Nome"
 title = "Configuração"
 unconfigured = "não configurado"
 vehicles = "Meus Veículos"
-yaml = "Configurado em evcc.yaml. Não editável na UI."
+yaml = "Os dispositivos de evcc.yaml não são editáveis."
 
 [config.messaging]
 description = "Receber mensagens sobre as sessões de carregamento."
@@ -270,6 +299,11 @@ little = "little-endian"
 http = "HTTP (não criptografado)"
 https = "HTTPS (cripto)"
 
+[config.options.status]
+A = "A (não ligado)"
+B = "B (ligado)"
+C = "C (carregamento)"
+
 [config.pv]
 titleAdd = "Adicionar Contador Solar"
 titleEdit = "Editar Contador Solar"
@@ -284,13 +318,16 @@ system = "Sistema"
 vehicles = "Veículos"
 
 [config.sponsor]
-addToken = "Enter token patrocinador"
-changeToken = "Alterar token patrocinador"
+addToken = "Entanir token patrocinador"
+changeToken = "Alterar token"
 description = "O modelo patrocinador nos ajuda a manter o projeto e a construir de forma sustentável novas e emocionantes características. Como patrocinador, você tem acesso a todas as implementações do carregador."
 descriptionToken = "Você obtém o token de {url}. Também oferecemos um token de avaliação para teste."
 error = "O token patrocinador não é válido."
 labelToken = "Token patrocinador"
 title = "Patrocínio"
+tokenRequired = "Tem de configurar um token de patrocinador antes de poder criar este dispositivo."
+tokenRequiredLearnMore = "Mais informações."
+trialToken = "Token de teste"
 
 [config.system]
 logs = "Registos"
@@ -406,6 +443,31 @@ modalUpdateNow = "Instalar agora"
 modalUpdateStarted = "Iniciando a nova versão do evcc..."
 modalUpdateStatusStart = "Instalação iniciada:"
 
+[forecast]
+modalTitle = "Previsões"
+
+[forecast.co2]
+average = "Média"
+lowestHour = "Horário mais limpo "
+range = "Intervalo"
+
+[forecast.price]
+average = "Média"
+lowestHour = "Horário mais barato"
+range = "Intervalo"
+
+[forecast.solar]
+dayAfterTomorrow = "Depois de amanhã"
+partly = "parcialmente"
+remaining = "restante"
+today = "Hoje"
+tomorrow = "Amanhã"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Preço"
+solar = "Solar"
+
 [header]
 about = "Sobre"
 blog = "Blog"
@@ -468,7 +530,9 @@ reset = "Repor senha?"
 title = "Autenticação"
 
 [main]
+startConfiguration = "Iniciar a configuração"
 vehicles = "Estacionamento"
+welcome = "Bem-vindo a bordo!"
 
 [main.chargingPlan]
 active = "Ativo"
@@ -500,6 +564,7 @@ batteryGridChargeActive = "carga da rede ativa"
 batteryGridChargeLimit = "carga da rede quando"
 batteryHold = "Bateria (suspensa)"
 batteryTooltip = "{energy} de {total} ({soc})"
+forecastTooltip = "previsão: produção solar restante de hoje"
 gridImport = "Consumo de rede"
 homePower = "Consumo"
 loadpoints = "Carregador | Carregador | {count} Carregadores"
diff --git a/i18n/sl.toml b/i18n/sl.toml
index 006a5abf..1b977647 100644
--- a/i18n/sl.toml
+++ b/i18n/sl.toml
@@ -128,6 +128,7 @@ save = "Shrani"
 showAdvancedSettings = "Prikaži napredne nastavitve"
 telemetry = "Telemetrija"
 title = "Naslov"
+validateSave = "Preveri in shrani"
 
 [config.grid]
 title = "Omrežni meter"
@@ -154,6 +155,13 @@ title = "InfluxDB"
 v1Support = "Potrebujete podporo za InfluxDB 1.x?"
 v2Support = "Nazaj na InfluxDB 2.x"
 
+[config.loadpoint]
+addCharger = "Dodaj polnilnik"
+cancel = "Prekliči"
+chargerPower11kw = "11 kW"
+chargerPower22kw = "22 kW"
+chargingTitle = "Polnjenje"
+
 [config.main]
 addLoadpoint = "Dodaj polnilno mesto"
 addPvBattery = "Dodaj sončne celice ali baterijo"
diff --git a/i18n/sv.toml b/i18n/sv.toml
index fa417465..10647e0e 100644
--- a/i18n/sv.toml
+++ b/i18n/sv.toml
@@ -447,6 +447,31 @@ modalUpdateNow = "Installera nu"
 modalUpdateStarted = "Starta den nya versionen av evcc..."
 modalUpdateStatusStart = "Installationen har börjat:"
 
+[forecast]
+modalTitle = "Prognos"
+
+[forecast.co2]
+average = "Genomsnitt"
+lowestHour = "Timme med lägst CO₂"
+range = "Intervall"
+
+[forecast.price]
+average = "Genomsnitt"
+lowestHour = "Billigaste timmen"
+range = "Intervall"
+
+[forecast.solar]
+dayAfterTomorrow = "I övermorgon"
+partly = "delvis"
+remaining = "återstående"
+today = "Idag"
+tomorrow = "I morgon"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Pris"
+solar = "Sol"
+
 [header]
 about = "Om"
 blog = "Blogg"
@@ -538,6 +563,7 @@ batteryGridChargeActive = "nätladdning aktiv"
 batteryGridChargeLimit = "nätladdning när"
 batteryHold = "Batteri (låst)"
 batteryTooltip = "{energy} av {total} ({soc})"
+forecastTooltip = "Prognos: återstående solproduktion idag"
 gridImport = "Import från elnät"
 homePower = "Konsumtion"
 loadpoints = "Laddare | Laddare | {count} laddare"
@@ -802,7 +828,7 @@ loadpoint = "Laddplats"
 meterstart = "Mätare start (kWh)"
 meterstop = "Mätare slut (kWh)"
 odometer = "Mätarställning (km)"
-price = "Pris"
+price = "Kostnad"
 priceperkwh = "Pris/kWh"
 solarpercentage = "Sol (%)"
 vehicle = "Fordon"
@@ -815,7 +841,7 @@ filter = "Filter"
 [sessions.group]
 co2 = "Emissioner"
 grid = "Nät"
-price = "Pris"
+price = "Kostnad"
 self = "Sol"
 
 [sessions.groupBy]
@@ -830,7 +856,7 @@ year = "År"
 
 [sessions.type]
 co2 = "CO₂"
-price = "Pris"
+price = "Kostnad"
 solar = "Sol"
 
 [settings]
diff --git a/i18n/tr.toml b/i18n/tr.toml
index 0350a2e8..c8583f06 100644
--- a/i18n/tr.toml
+++ b/i18n/tr.toml
@@ -88,7 +88,7 @@ phases1p3p = "“Faz değiştirme”"
 power = "Güç"
 powerRange = "“Güç”"
 range = "Menzil"
-singlePhase = "“Tek fazlı”"
+singlePhase = "“Tek aşamalı”"
 soc = "Doluluk"
 socLimit = "Doluluk Sınırlaması"
 temp = "Isı"
@@ -182,9 +182,41 @@ maxCurrentLabel = "“Azami akım”"
 minCurrentHelp = "“Sadece ne yaptığını biliyorsan 6 A altına in.”"
 minCurrentLabel = "“Asgari akım”"
 noVehicles = "“Hiçbir araç yapılandırılmamış.”"
-phases1p = "“1 fazlı”"
-phases3p = "“3 fazlı”"
-phasesAutomatic = "“Otomatik fazlar”"
+phases1p = "“1 aşamalı”"
+phases3p = "“3 aşamalı”"
+phasesAutomatic = "“Otomatik aşamalar”"
+phasesAutomaticHelp = "“Doldurma cihazın 1 ve 3 aşamalı doldurma arasında otomatik geçişi destekliyor. Ana perdede doldurma sırasında aşama davranışını ayarlayabilirsin.”"
+phasesHelp = "“Doldurma cihazına bağlı aşama sayısı.”"
+phasesLabel = "“Aşamalar”"
+pollIntervalHelp = "“Araç API güncellemeleri arasındaki süre. Kısa aralıklar aracın bataryasını tüketebilir.”"
+pollIntervalLabel = "“Güncelleme aralığı”"
+pollModeAlways = "“daima”"
+pollModeAlwaysHelp = "“Daima düzenli aralıklarla durum sorgulaması yap.”"
+pollModeCharging = "sadece doldururken"
+pollModeChargingHelp = "“Sadece doldururken araç durum güncellemelerini talep et.”"
+pollModeConnected = "“bağlıyken”"
+pollModeConnectedHelp = "“Bağlıyken araç durumunu düzenli aralıklarla güncelle.”"
+pollModeLabel = "“Güncelleme davranışı”"
+priorityHelp = "“Yüksek öncelikli doldurma noktaları güneş enerjisi fazlasına öncelikli erişim sağlar.”"
+priorityLabel = "“Öncelik”"
+save = "“Kaydet”"
+showAllSettings = "“Tüm ayarları göster”"
+solarBehaviorCustomHelp = "“Kendi etkinleştirme ve devre dışı bırakma eşiklerini ve gecikmelerini tanımla.”"
+solarBehaviorDefaultHelp = "“Sadece güneş enerjisi fazlası ile doldur. Fazlalık {enableDelay} üzerinde ise başla. {disableDelay} için yeterli fazlalık olmadığında dur.”"
+solarBehaviorLabel = "“GES davranışları”"
+solarModeCustom = "“özel”"
+solarModeMaximum = "“sadece güneş”"
+thresholdDisableDelayLabel = "“Kapatma gecikmesi”"
+thresholdDisableHelpInvalid = "“Lütfen pozitif bir değer kullan.”"
+thresholdDisableHelpPositive = "“Şebekeden {delay} için {power}'ten daha fazlası kullanıldığında doldurmayı durdur.”"
+thresholdDisableHelpZero = "“{delay} için asgari doldurma gücüne ulaşılamazsa durdur.”"
+thresholdDisableLabel = "“Şebeke elektriğini kapat”"
+thresholdEnableDelayLabel = "“Açma gecikmesi”"
+thresholdEnableHelpInvalid = "“Lütfen negatif bir değer kullan.”"
+thresholdEnableHelpNegative = "“{delay} için {surplus} fazlalık varsa doldurmaya başla.”"
+thresholdEnableHelpZero = "“{delay} için asgari doldurma gücü fazlası mevcut olduğunda başlat.”"
+thresholdEnableLabel = "“Şebeke gücünü aç”"
+titleAdd = "“ Doldurma Noktası Ekle”"
 
 [config.main]
 addLoadpoint = "Doldurma noktası ekle"
@@ -532,9 +564,9 @@ label = "Asgari dolum oranı"
 label = "Fazlar"
 no1p3pSupport = "Doldurma cihazınız nasıl bağlı?"
 phases_0 = "otomatik geçiş"
-phases_1 = "1 fazlı"
+phases_1 = "1 aşamalı"
 phases_1_hint = "({min}'dan {max}'a kadar)"
-phases_3 = "3 fazlı"
+phases_3 = "3 aşamalı"
 phases_3_hint = "({min}'dan {max}'a kadar)"
 
 [main.mode]
@@ -629,8 +661,8 @@ finished = "Tamamlandı."
 minCharge = "{soc} kadar asgari dolum."
 pvDisable = "Yeterli fazlalık yok. Birazdan duraklatılacak."
 pvEnable = "Fazlalık mevcut. Birazdan başlatılacak."
-scale1p = "Birazdan 1 fazla doldurmaya düşürülecek."
-scale3p = "Birazdan 3 fazla doldurmaya yükseltilecek."
+scale1p = "Birazdan 1 aşamalı doldurmaya düşürülecek."
+scale3p = "Birazdan 3 aşamalı doldurmaya yükseltilecek."
 targetChargeActive = "Doldurma planı yürürlükte. Tahmini bitiş süresi {duration} içerisinde."
 targetChargePlanned = "Doldurma planı {duration} içerisinde başlayacak."
 targetChargeWaitForVehicle = "Doldurma planı hazır. Araç bekleniyor…"
diff --git a/i18n/uk.toml b/i18n/uk.toml
index 6d7ea366..657cee90 100644
--- a/i18n/uk.toml
+++ b/i18n/uk.toml
@@ -448,6 +448,31 @@ modalUpdateNow = "Встановити зараз"
 modalUpdateStarted = "Початок нової версії evcc…"
 modalUpdateStatusStart = "Встановлення розпочато:"
 
+[forecast]
+modalTitle = "Прогноз"
+
+[forecast.co2]
+average = "Середній"
+lowestHour = "Найчистіша година"
+range = "Діапазон"
+
+[forecast.price]
+average = "Середній"
+lowestHour = "Найдешевша година"
+range = "Діапазон"
+
+[forecast.solar]
+dayAfterTomorrow = "Післязавтра"
+partly = "частково"
+remaining = "залишилося"
+today = "Сьогодні"
+tomorrow = "Завтра"
+
+[forecast.type]
+co2 = "CO₂"
+price = "Ціна"
+solar = "Сонячна"
+
 [header]
 about = "Про evcc"
 blog = "Блог"
@@ -539,6 +564,7 @@ batteryGridChargeActive = "активна зарядка мережі"
 batteryGridChargeLimit = "зарядка мережі коли"
 batteryHold = "Акумулятор (locked)"
 batteryTooltip = "{energy} із {total} ({soc})"
+forecastTooltip = "Прогноз: залишкове виробництво сонячної енергії сьогодні"
 gridImport = "Використання мережі"
 homePower = "Споживання"
 loadpoints = "Зарядний пристрій| Зарядний пристрій | {count} зарядні пристрої"
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/cfos.go b/meter/cfos.go
index 7a2f635d..f5c9fc49 100644
--- a/meter/cfos.go
+++ b/meter/cfos.go
@@ -1,6 +1,7 @@
 package meter
 
 import (
+	"context"
 	"encoding/binary"
 
 	"github.com/evcc-io/evcc/api"
@@ -23,11 +24,11 @@ type CfosPowerBrain struct {
 }
 
 func init() {
-	registry.Add("cfos", NewCfosPowerBrainFromConfig)
+	registry.AddCtx("cfos", NewCfosPowerBrainFromConfig)
 }
 
 // NewCfosPowerBrainFromConfig creates a cFos meter from generic config
-func NewCfosPowerBrainFromConfig(other map[string]interface{}) (api.Meter, error) {
+func NewCfosPowerBrainFromConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) {
 	cc := modbus.TcpSettings{
 		ID: 1,
 	}
@@ -36,12 +37,12 @@ func NewCfosPowerBrainFromConfig(other map[string]interface{}) (api.Meter, error
 		return nil, err
 	}
 
-	return NewCfosPowerBrain(cc.URI, cc.ID)
+	return NewCfosPowerBrain(ctx, cc.URI, cc.ID)
 }
 
 // NewCfosPowerBrain creates a cFos meter
-func NewCfosPowerBrain(uri string, id uint8) (*CfosPowerBrain, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+func NewCfosPowerBrain(ctx context.Context, uri string, id uint8) (*CfosPowerBrain, error) {
+	conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
 	}
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/mbmd.go b/meter/mbmd.go
index 059a7bb9..ffd831b0 100644
--- a/meter/mbmd.go
+++ b/meter/mbmd.go
@@ -1,6 +1,7 @@
 package meter
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"strings"
@@ -24,13 +25,13 @@ type ModbusMbmd struct {
 }
 
 func init() {
-	registry.Add("mbmd", NewModbusMbmdFromConfig)
+	registry.AddCtx("mbmd", NewModbusMbmdFromConfig)
 }
 
 //go:generate go tool decorate -f decorateModbusMbmd -b api.Meter -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.PhasePowers,Powers,func() (float64, float64, float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64"
 
 // NewModbusMbmdFromConfig creates api.Meter from config
-func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
+func NewModbusMbmdFromConfig(ctx context.Context, other map[string]interface{}) (api.Meter, error) {
 	cc := struct {
 		Model              string
 		capacity           `mapstructure:",squash"`
@@ -62,7 +63,7 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
 	modbus.Lock()
 	defer modbus.Unlock()
 
-	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
+	conn, err := modbus.NewConnection(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/meter/rct.go b/meter/rct.go
index a42d5234..1ba17a76 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.WithLogger(log.TRACE.Printf), 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/plugin/modbus.go b/plugin/modbus.go
index f1f016af..f73f95e6 100644
--- a/plugin/modbus.go
+++ b/plugin/modbus.go
@@ -45,7 +45,7 @@ func NewModbusFromConfig(ctx context.Context, other map[string]interface{}) (Plu
 	modbus.Lock()
 	defer modbus.Unlock()
 
-	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
+	conn, err := modbus.NewConnection(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/plugin/sunspec.go b/plugin/sunspec.go
index b956fac6..1813fbbb 100644
--- a/plugin/sunspec.go
+++ b/plugin/sunspec.go
@@ -1,6 +1,7 @@
 package plugin
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"math"
@@ -24,11 +25,11 @@ type ModbusSunspec struct {
 }
 
 func init() {
-	registry.Add("sunspec", NewModbusSunspecFromConfig)
+	registry.AddCtx("sunspec", NewModbusSunspecFromConfig)
 }
 
 // NewModbusSunspecFromConfig creates Modbus plugin
-func NewModbusSunspecFromConfig(other map[string]interface{}) (Plugin, error) {
+func NewModbusSunspecFromConfig(ctx context.Context, other map[string]interface{}) (Plugin, error) {
 	cc := struct {
 		modbus.Settings `mapstructure:",squash"`
 		Value           []string
@@ -47,7 +48,7 @@ func NewModbusSunspecFromConfig(other map[string]interface{}) (Plugin, error) {
 	modbus.Lock()
 	defer modbus.Unlock()
 
-	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
+	conn, err := modbus.NewConnection(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
 	if err != nil {
 		return nil, err
 	}
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/server/http_config_device_handler.go b/server/http_config_device_handler.go
index 4de4ef19..dbcffcbb 100644
--- a/server/http_config_device_handler.go
+++ b/server/http_config_device_handler.go
@@ -206,8 +206,8 @@ func deviceStatusHandler(w http.ResponseWriter, r *http.Request) {
 	jsonResult(w, testInstance(instance))
 }
 
-func newDevice[T any](class templates.Class, req map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) (*config.Config, error) {
-	instance, err := newFromConf(context.TODO(), typeTemplate, req)
+func newDevice[T any](ctx context.Context, class templates.Class, req map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) (*config.Config, error) {
+	instance, err := newFromConf(ctx, typeTemplate, req)
 	if err != nil {
 		return nil, err
 	}
@@ -217,7 +217,7 @@ func newDevice[T any](class templates.Class, req map[string]any, newFromConf new
 		return nil, err
 	}
 
-	return &conf, h.Add(config.NewConfigurableDevice[T](&conf, instance))
+	return &conf, h.Add(config.NewConfigurableDevice(&conf, instance))
 }
 
 // newDeviceHandler creates a new device by class
@@ -240,28 +240,33 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) {
 	delete(req, "type")
 
 	var conf *config.Config
+	ctx, cancel, done := startDeviceTimeout()
 
 	switch class {
 	case templates.Charger:
-		conf, err = newDevice(class, req, charger.NewFromConfig, config.Chargers())
+		conf, err = newDevice(ctx, class, req, charger.NewFromConfig, config.Chargers())
 
 	case templates.Meter:
-		conf, err = newDevice(class, req, meter.NewFromConfig, config.Meters())
+		conf, err = newDevice(ctx, class, req, meter.NewFromConfig, config.Meters())
 
 	case templates.Vehicle:
-		conf, err = newDevice(class, req, vehicle.NewFromConfig, config.Vehicles())
+		conf, err = newDevice(ctx, class, req, vehicle.NewFromConfig, config.Vehicles())
 
 	case templates.Circuit:
-		conf, err = newDevice(class, req, func(_ context.Context, _ string, other map[string]interface{}) (api.Circuit, error) {
-			return circuit.NewFromConfig(util.NewLogger("circuit"), other)
+		conf, err = newDevice(ctx, class, req, func(ctx context.Context, _ string, other map[string]interface{}) (api.Circuit, error) {
+			return circuit.NewFromConfig(ctx, util.NewLogger("circuit"), other)
 		}, config.Circuits())
 	}
 
 	if err != nil {
+		cancel()
 		jsonError(w, http.StatusBadRequest, err)
 		return
 	}
 
+	// prevent context from being cancelled
+	close(done)
+
 	setConfigDirty()
 
 	res := struct {
@@ -275,8 +280,8 @@ func newDeviceHandler(w http.ResponseWriter, r *http.Request) {
 	jsonResult(w, res)
 }
 
-func updateDevice[T any](id int, class templates.Class, conf map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) error {
-	dev, instance, merged, err := deviceInstanceFromMergedConfig(id, class, conf, newFromConf, h)
+func updateDevice[T any](ctx context.Context, id int, class templates.Class, conf map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) error {
+	dev, instance, merged, err := deviceInstanceFromMergedConfig(ctx, id, class, conf, newFromConf, h)
 	if err != nil {
 		return err
 	}
@@ -314,29 +319,35 @@ func updateDeviceHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	delete(req, "type")
 
+	ctx, cancel, done := startDeviceTimeout()
+
 	switch class {
 	case templates.Charger:
-		err = updateDevice(id, class, req, charger.NewFromConfig, config.Chargers())
+		err = updateDevice(ctx, id, class, req, charger.NewFromConfig, config.Chargers())
 
 	case templates.Meter:
-		err = updateDevice(id, class, req, meter.NewFromConfig, config.Meters())
+		err = updateDevice(ctx, id, class, req, meter.NewFromConfig, config.Meters())
 
 	case templates.Vehicle:
-		err = updateDevice(id, class, req, vehicle.NewFromConfig, config.Vehicles())
+		err = updateDevice(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles())
 
 	case templates.Circuit:
-		err = updateDevice(id, class, req, func(_ context.Context, _ string, other map[string]interface{}) (api.Circuit, error) {
-			return circuit.NewFromConfig(util.NewLogger("circuit"), other)
+		err = updateDevice(ctx, id, class, req, func(ctx context.Context, _ string, other map[string]interface{}) (api.Circuit, error) {
+			return circuit.NewFromConfig(ctx, util.NewLogger("circuit"), other)
 		}, config.Circuits())
 	}
 
 	setConfigDirty()
 
 	if err != nil {
+		cancel()
 		jsonError(w, http.StatusBadRequest, err)
 		return
 	}
 
+	// prevent context from being cancelled
+	close(done)
+
 	res := struct {
 		ID int `json:"id"`
 	}{
@@ -412,12 +423,12 @@ func deleteDeviceHandler(w http.ResponseWriter, r *http.Request) {
 	jsonResult(w, res)
 }
 
-func testConfig[T any](id int, class templates.Class, conf map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) (T, error) {
+func testConfig[T any](ctx context.Context, id int, class templates.Class, conf map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) (T, error) {
 	if id == 0 {
-		return newFromConf(context.TODO(), typeTemplate, conf)
+		return newFromConf(ctx, typeTemplate, conf)
 	}
 
-	_, instance, _, err := deviceInstanceFromMergedConfig(id, class, conf, newFromConf, h)
+	_, instance, _, err := deviceInstanceFromMergedConfig(ctx, id, class, conf, newFromConf, h)
 
 	return instance, err
 }
@@ -450,25 +461,30 @@ func testConfigHandler(w http.ResponseWriter, r *http.Request) {
 	delete(req, "type")
 
 	var instance any
+	ctx, cancel, done := startDeviceTimeout()
 
 	switch class {
 	case templates.Charger:
-		instance, err = testConfig(id, class, req, charger.NewFromConfig, config.Chargers())
+		instance, err = testConfig(ctx, id, class, req, charger.NewFromConfig, config.Chargers())
 
 	case templates.Meter:
-		instance, err = testConfig(id, class, req, meter.NewFromConfig, config.Meters())
+		instance, err = testConfig(ctx, id, class, req, meter.NewFromConfig, config.Meters())
 
 	case templates.Vehicle:
-		instance, err = testConfig(id, class, req, vehicle.NewFromConfig, config.Vehicles())
+		instance, err = testConfig(ctx, id, class, req, vehicle.NewFromConfig, config.Vehicles())
 
 	case templates.Circuit:
 		err = api.ErrNotAvailable
 	}
 
 	if err != nil {
+		cancel()
 		jsonError(w, http.StatusBadRequest, err)
 		return
 	}
 
+	// prevent context from being cancelled
+	close(done)
+
 	jsonResult(w, testInstance(instance))
 }
diff --git a/server/http_config_helper.go b/server/http_config_helper.go
index d40d37a4..24482631 100644
--- a/server/http_config_helper.go
+++ b/server/http_config_helper.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"slices"
 	"sync"
+	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util/config"
@@ -89,7 +90,24 @@ func mergeMasked(class templates.Class, conf, old map[string]any) (map[string]an
 	return res, nil
 }
 
-func deviceInstanceFromMergedConfig[T any](id int, class templates.Class, conf map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) (config.Device[T], T, map[string]any, error) {
+func startDeviceTimeout() (context.Context, context.CancelFunc, chan struct{}) {
+	done := make(chan struct{})
+	ctx, cancel := context.WithCancel(context.Background())
+
+	go func() {
+		select {
+		case <-time.After(10 * time.Second):
+			// timeout - cancel context
+			cancel()
+		case <-done:
+			// success
+		}
+	}()
+
+	return ctx, cancel, done
+}
+
+func deviceInstanceFromMergedConfig[T any](ctx context.Context, id int, class templates.Class, conf map[string]any, newFromConf newFromConfFunc[T], h config.Handler[T]) (config.Device[T], T, map[string]any, error) {
 	var zero T
 
 	dev, err := h.ByName(config.NameForID(id))
@@ -102,7 +120,7 @@ func deviceInstanceFromMergedConfig[T any](id int, class templates.Class, conf m
 		return nil, zero, nil, err
 	}
 
-	instance, err := newFromConf(context.TODO(), typeTemplate, merged)
+	instance, err := newFromConf(ctx, typeTemplate, merged)
 
 	return dev, instance, merged, err
 }
diff --git a/server/modbus/proxy.go b/server/modbus/proxy.go
index 12123432..d364c397 100644
--- a/server/modbus/proxy.go
+++ b/server/modbus/proxy.go
@@ -1,6 +1,7 @@
 package modbus
 
 import (
+	"context"
 	"fmt"
 	"net"
 
@@ -12,7 +13,7 @@ import (
 )
 
 func StartProxy(port int, config modbus.Settings, readOnly ReadOnlyMode) error {
-	conn, err := modbus.NewConnection(config.URI, config.Device, config.Comset, config.Baudrate, config.Protocol(), config.ID)
+	conn, err := modbus.NewConnection(context.Background(), config.URI, config.Device, config.Comset, config.Baudrate, config.Protocol(), config.ID)
 	if err != nil {
 		return err
 	}
diff --git a/templates/definition/charger/lambda-zewotherm.yaml b/templates/definition/charger/lambda-zewotherm.yaml
index 58c72448..57b8ab64 100644
--- a/templates/definition/charger/lambda-zewotherm.yaml
+++ b/templates/definition/charger/lambda-zewotherm.yaml
@@ -24,7 +24,7 @@ render: |
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register:
-      address: 103 # PV Überschussleistung
+      address: 102 # PV Überschussleistung
       type: holding
       decode: int16
   power:
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
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index d9852b3c..4f4ca847 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -1,6 +1,7 @@
 package modbus
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"strings"
@@ -32,12 +33,12 @@ type TcpSettings struct {
 
 // Settings contains the ModBus settings
 type Settings struct {
-	ID                  uint8
-	SubDevice           int
-	URI, Device, Comset string
-	Baudrate            int
-	UDP                 bool
-	RTU                 *bool // indicates RTU over TCP if true
+	ID                  uint8  `json:",omitempty" yaml:",omitempty"`
+	SubDevice           int    `json:",omitempty" yaml:",omitempty"`
+	URI, Device, Comset string `json:",omitempty" yaml:",omitempty"`
+	Baudrate            int    `json:",omitempty" yaml:",omitempty"`
+	UDP                 bool   `json:",omitempty" yaml:",omitempty"`
+	RTU                 *bool  `json:",omitempty" yaml:",omitempty"`
 }
 
 // Protocol identifies the wire format from the RTU setting
@@ -62,6 +63,7 @@ func (s *Settings) String() string {
 type meterConnection struct {
 	meters.Connection
 	proto Protocol
+	refs  int // count of references; first connection has ref count 0
 	*logger
 }
 
@@ -70,7 +72,24 @@ var (
 	mu          sync.Mutex
 )
 
-func registeredConnection(key string, proto Protocol, newConn meters.Connection) (*meterConnection, error) {
+func unregisterConnection(key string) {
+	mu.Lock()
+	defer mu.Unlock()
+
+	conn, ok := connections[key]
+	if !ok {
+		panic("unregisterConnection: connection not found " + key)
+	}
+
+	if conn.refs > 0 {
+		conn.refs--
+		return
+	}
+
+	delete(connections, key)
+}
+
+func registeredConnection(ctx context.Context, key string, proto Protocol, newConn meters.Connection) (*meterConnection, error) {
 	mu.Lock()
 	defer mu.Unlock()
 
@@ -79,9 +98,16 @@ func registeredConnection(key string, proto Protocol, newConn meters.Connection)
 			return nil, fmt.Errorf("connection already registered with different protocol: %s", key)
 		}
 
+		conn.refs++
+
 		return conn, nil
 	}
 
+	go func() {
+		<-ctx.Done()
+		unregisterConnection(key)
+	}()
+
 	connection := &meterConnection{
 		Connection: newConn,
 		proto:      proto,
@@ -95,8 +121,8 @@ func registeredConnection(key string, proto Protocol, newConn meters.Connection)
 }
 
 // NewConnection creates physical modbus device from config
-func NewConnection(uri, device, comset string, baudrate int, proto Protocol, slaveID uint8) (*Connection, error) {
-	conn, err := physicalConnection(proto, Settings{
+func NewConnection(ctx context.Context, uri, device, comset string, baudrate int, proto Protocol, slaveID uint8) (*Connection, error) {
+	conn, err := physicalConnection(ctx, proto, Settings{
 		URI:      uri,
 		Device:   device,
 		Comset:   comset,
@@ -115,7 +141,7 @@ func NewConnection(uri, device, comset string, baudrate int, proto Protocol, sla
 	return res, nil
 }
 
-func physicalConnection(proto Protocol, cfg Settings) (*meterConnection, error) {
+func physicalConnection(ctx context.Context, proto Protocol, cfg Settings) (*meterConnection, error) {
 	if (cfg.Device != "") == (cfg.URI != "") {
 		return nil, errors.New("invalid modbus configuration: must have either uri or device")
 	}
@@ -135,9 +161,9 @@ func physicalConnection(proto Protocol, cfg Settings) (*meterConnection, error)
 
 		switch proto {
 		case Ascii:
-			return registeredConnection(cfg.Device, proto, meters.NewASCII(cfg.Device, cfg.Baudrate, cfg.Comset))
+			return registeredConnection(ctx, cfg.Device, proto, meters.NewASCII(cfg.Device, cfg.Baudrate, cfg.Comset))
 		default:
-			return registeredConnection(cfg.Device, proto, meters.NewRTU(cfg.Device, cfg.Baudrate, cfg.Comset))
+			return registeredConnection(ctx, cfg.Device, proto, meters.NewRTU(cfg.Device, cfg.Baudrate, cfg.Comset))
 		}
 	}
 
@@ -145,12 +171,12 @@ func physicalConnection(proto Protocol, cfg Settings) (*meterConnection, error)
 
 	switch proto {
 	case Udp:
-		return registeredConnection(uri, proto, meters.NewRTUOverUDP(uri))
+		return registeredConnection(ctx, uri, proto, meters.NewRTUOverUDP(uri))
 	case Rtu:
-		return registeredConnection(uri, proto, meters.NewRTUOverTCP(uri))
+		return registeredConnection(ctx, uri, proto, meters.NewRTUOverTCP(uri))
 	case Ascii:
-		return registeredConnection(uri, proto, meters.NewASCIIOverTCP(uri))
+		return registeredConnection(ctx, uri, proto, meters.NewASCIIOverTCP(uri))
 	default:
-		return registeredConnection(uri, proto, meters.NewTCP(uri))
+		return registeredConnection(ctx, uri, proto, meters.NewTCP(uri))
 	}
 }


Test patch:
diff --git a/assets/js/components/Config/mixins/test.js b/assets/js/components/Config/mixins/test.js
index 1667e20d..9b1990e6 100644
--- a/assets/js/components/Config/mixins/test.js
+++ b/assets/js/components/Config/mixins/test.js
@@ -9,6 +9,7 @@ export default {
       testState: TEST_UNKNOWN,
       testError: null,
       testResult: null,
+      testTimeout: 15000, // 15s
     };
   },
   computed: {
@@ -57,5 +58,21 @@ export default {
       }
       return false;
     },
+    handleCreateError(e) {
+      this.handleError(e, "create failed");
+    },
+    handleUpdateError(e) {
+      this.handleError(e, "update failed");
+    },
+    handleRemoveError(e) {
+      this.handleError(e, "remove failed");
+    },
+    handleError(e, msg) {
+      console.error(e);
+      let message = msg;
+      const { error } = e.response.data || {};
+      if (error) message += `: ${error}`;
+      alert(message);
+    },
   },
 };
diff --git a/charger/ocpp/connector_test.go b/charger/ocpp/connector_test.go
index 95e7b9f2..8250a3e7 100644
--- a/charger/ocpp/connector_test.go
+++ b/charger/ocpp/connector_test.go
@@ -26,7 +26,7 @@ func (suite *connTestSuite) SetupTest() {
 	// setup instance
 	Instance()
 	suite.cp = NewChargePoint(util.NewLogger("foo"), "abc")
-	suite.conn, _ = NewConnector(util.NewLogger("foo"), 1, suite.cp, "")
+	suite.conn, _ = NewConnector(util.NewLogger("foo"), 1, suite.cp, "", Timeout)
 
 	suite.clock = clock.NewMock()
 	suite.conn.clock = suite.clock
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")
+}
diff --git a/server/modbus/proxy_test.go b/server/modbus/proxy_test.go
index 5eb258dc..4947c563 100644
--- a/server/modbus/proxy_test.go
+++ b/server/modbus/proxy_test.go
@@ -1,6 +1,7 @@
 package modbus
 
 import (
+	"context"
 	"encoding/binary"
 	"math/rand"
 	"net"
@@ -34,7 +35,7 @@ func TestConcurrentRead(t *testing.T) {
 
 		go func(id int) {
 			// client
-			conn, err := modbus.NewConnection(l.Addr().String(), "", "", 0, modbus.Tcp, uint8(id))
+			conn, err := modbus.NewConnection(context.TODO(), l.Addr().String(), "", "", 0, modbus.Tcp, uint8(id))
 			require.NoError(t, err)
 
 			for range 50 {
@@ -78,7 +79,7 @@ func TestReadCoils(t *testing.T) {
 	require.NoError(t, err)
 	defer pl.Close()
 
-	downstreamConn, err := modbus.NewConnection(l.Addr().String(), "", "", 0, modbus.Tcp, 1)
+	downstreamConn, err := modbus.NewConnection(context.TODO(), l.Addr().String(), "", "", 0, modbus.Tcp, 1)
 	require.NoError(t, err)
 
 	proxy, _ := mbserver.New(&handler{
@@ -90,7 +91,7 @@ func TestReadCoils(t *testing.T) {
 
 	// test client
 	{
-		conn, err := modbus.NewConnection(pl.Addr().String(), "", "", 0, modbus.Tcp, 1)
+		conn, err := modbus.NewConnection(context.TODO(), pl.Addr().String(), "", "", 0, modbus.Tcp, 1)
 		require.NoError(t, err)
 
 		{ // read