Repository: evcc-io/evcc
Pull request #15180 (2024-08-01 14:51:16)

Update versicharge.yaml wegen FW Update
Richtige Engergiewerte ab 2.135.
siehe auch Pull request 
https://github.com/evcc-io/evcc/commit/1515a67fc0e7446a076e9c0690bdb98c76d122e3

premultiply: Also ich verstehe den Text hinter dem Komma zumindest nicht.
andig: Raus damit- sieht man ja in den Features. Oder- falls vorhanden- einfach ausprägen?

Patch:
diff --git a/charger/abb.go b/charger/abb.go
index 6d24b399..3fb47ce7 100644
--- a/charger/abb.go
+++ b/charger/abb.go
@@ -65,7 +65,7 @@ func NewABBFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewABB(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewABB(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewABB creates ABB charger
diff --git a/charger/alphatec.go b/charger/alphatec.go
index 56fa6dee..7e76df63 100644
--- a/charger/alphatec.go
+++ b/charger/alphatec.go
@@ -55,7 +55,7 @@ func NewAlphatecFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewAlphatec(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewAlphatec(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewAlphatec creates Alphatec charger
diff --git a/charger/config.go b/charger/config.go
index 0c0617ea..2dcc7e68 100644
--- a/charger/config.go
+++ b/charger/config.go
@@ -1,31 +1,21 @@
 package charger
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
 	"github.com/evcc-io/evcc/api"
+	reg "github.com/evcc-io/evcc/util/registry"
 )
 
-type chargerRegistry map[string]func(map[string]interface{}) (api.Charger, error)
+var registry = reg.New[api.Charger]("charger")
 
-func (r chargerRegistry) Add(name string, factory func(map[string]interface{}) (api.Charger, error)) {
-	if _, exists := r[name]; exists {
-		panic(fmt.Sprintf("cannot register duplicate charger type: %s", name))
-	}
-	r[name] = factory
+// Types returns the list of types
+func Types() []string {
+	return registry.Types()
 }
 
-func (r chargerRegistry) Get(name string) (func(map[string]interface{}) (api.Charger, error), error) {
-	factory, exists := r[name]
-	if !exists {
-		return nil, fmt.Errorf("invalid charger type: %s", name)
-	}
-	return factory, nil
-}
-
-var registry chargerRegistry = make(map[string]func(map[string]interface{}) (api.Charger, error))
-
 // NewFromConfig creates charger from configuration
 func NewFromConfig(typ string, other map[string]interface{}) (api.Charger, error) {
 	factory, err := registry.Get(strings.ToLower(typ))
@@ -33,7 +23,7 @@ func NewFromConfig(typ string, other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	v, err := factory(other)
+	v, err := factory(context.TODO(), other)
 	if err != nil {
 		err = fmt.Errorf("cannot create charger type '%s': %w", typ, err)
 	}
diff --git a/charger/delta.go b/charger/delta.go
index 68005f96..42371ee6 100644
--- a/charger/delta.go
+++ b/charger/delta.go
@@ -76,7 +76,7 @@ func NewDeltaFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewDelta(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID, cc.Connector)
+	return NewDelta(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID, cc.Connector)
 }
 
 // NewDelta creates Delta charger
diff --git a/charger/evsedin.go b/charger/evsedin.go
index 049b4e9d..277cc8f0 100644
--- a/charger/evsedin.go
+++ b/charger/evsedin.go
@@ -39,7 +39,7 @@ func NewEvseDINFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewEvseDIN(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewEvseDIN(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewEvseDIN creates EVSE DIN charger
diff --git a/charger/heidelberg-ec.go b/charger/heidelberg-ec.go
index 1a3c8829..f0ce71e4 100644
--- a/charger/heidelberg-ec.go
+++ b/charger/heidelberg-ec.go
@@ -70,7 +70,7 @@ func NewHeidelbergECFromConfig(other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	return NewHeidelbergEC(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewHeidelbergEC(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewHeidelbergEC creates HeidelbergEC charger
diff --git a/charger/mennekes-compact.go b/charger/mennekes-compact.go
index 36415328..d334ab35 100644
--- a/charger/mennekes-compact.go
+++ b/charger/mennekes-compact.go
@@ -83,7 +83,7 @@ func NewMennekesCompactFromConfig(other map[string]interface{}) (api.Charger, er
 		return nil, err
 	}
 
-	return NewMennekesCompact(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID, cc.Timeout)
+	return NewMennekesCompact(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID, cc.Timeout)
 }
 
 // NewMennekesCompact creates Mennekes charger
diff --git a/charger/obo.go b/charger/obo.go
index 6b3c0342..314adda8 100644
--- a/charger/obo.go
+++ b/charger/obo.go
@@ -39,7 +39,7 @@ func NewOboFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewObo(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewObo(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewObo creates OBO Bettermann charger
diff --git a/charger/phoenix-ev-ser.go b/charger/phoenix-ev-ser.go
index e612fd58..3e5366d1 100644
--- a/charger/phoenix-ev-ser.go
+++ b/charger/phoenix-ev-ser.go
@@ -34,7 +34,7 @@ func NewPhoenixEVSerFromConfig(other map[string]interface{}) (api.Charger, error
 		return nil, err
 	}
 
-	return NewPhoenixEVSer(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewPhoenixEVSer(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewPhoenixEVSer creates a Phoenix charger
diff --git a/charger/prachtalpha.go b/charger/prachtalpha.go
index 989c17d8..6bdeb75e 100644
--- a/charger/prachtalpha.go
+++ b/charger/prachtalpha.go
@@ -66,7 +66,7 @@ func NewPrachtAlphaFromConfig(other map[string]interface{}) (api.Charger, error)
 		return nil, err
 	}
 
-	return NewPrachtAlpha(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID, cc.Timeout, cc.Connector)
+	return NewPrachtAlpha(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID, cc.Timeout, cc.Connector)
 }
 
 // NewPrachtAlpha creates PrachtAlpha charger
diff --git a/charger/pulsares.go b/charger/pulsares.go
index be66ed37..73122eca 100644
--- a/charger/pulsares.go
+++ b/charger/pulsares.go
@@ -62,7 +62,7 @@ func NewPulsaresFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	wb, err := NewPulsares(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	wb, err := NewPulsares(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/smartevse.go b/charger/smartevse.go
index 91e286a6..d8e9c717 100644
--- a/charger/smartevse.go
+++ b/charger/smartevse.go
@@ -77,7 +77,7 @@ func NewsmartEVSEFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewsmartEVSE(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewsmartEVSE(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewsmartEVSE creates a new charger
diff --git a/charger/solax.go b/charger/solax.go
index 818022e5..e7708a66 100644
--- a/charger/solax.go
+++ b/charger/solax.go
@@ -70,7 +70,7 @@ func NewSolaxFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewSolax(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewSolax(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewSolax creates Solax charger
diff --git a/charger/sungrow.go b/charger/sungrow.go
index f696f0fe..03a29f28 100644
--- a/charger/sungrow.go
+++ b/charger/sungrow.go
@@ -75,7 +75,7 @@ func NewSungrowFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewSungrow(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	return NewSungrow(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Protocol(), cc.ID)
 }
 
 // NewSungrow creates Sungrow charger
diff --git a/cmd/setup.go b/cmd/setup.go
index 7dc7764f..96011aea 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -204,6 +204,8 @@ NEXT:
 }
 
 func configureMeters(static []config.Named, names ...string) error {
+	g, _ := errgroup.WithContext(context.Background())
+
 	for i, cc := range static {
 		if cc.Name == "" {
 			return fmt.Errorf("cannot create meter %d: missing name", i+1)
@@ -217,14 +219,18 @@ func configureMeters(static []config.Named, names ...string) error {
 			log.WARN.Printf("create meter %d: %v", i+1, err)
 		}
 
-		instance, err := meter.NewFromConfig(cc.Type, cc.Other)
-		if err != nil {
-			return &DeviceError{cc.Name, fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)}
-		}
+		g.Go(func() error {
+			instance, err := meter.NewFromConfig(cc.Type, cc.Other)
+			if err != nil {
+				return &DeviceError{cc.Name, fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)}
+			}
 
-		if err := config.Meters().Add(config.NewStaticDevice(cc, instance)); err != nil {
-			return &DeviceError{cc.Name, err}
-		}
+			if err := config.Meters().Add(config.NewStaticDevice(cc, instance)); err != nil {
+				return &DeviceError{cc.Name, err}
+			}
+
+			return nil
+		})
 	}
 
 	// append devices from database
@@ -234,25 +240,29 @@ func configureMeters(static []config.Named, names ...string) error {
 	}
 
 	for _, conf := range configurable {
-		cc := conf.Named()
+		g.Go(func() error {
+			cc := conf.Named()
 
-		if len(names) > 0 && !slices.Contains(names, cc.Name) {
-			return nil
-		}
+			if len(names) > 0 && !slices.Contains(names, cc.Name) {
+				return nil
+			}
 
-		// TOTO add fake devices
+			// TODO add fake devices
 
-		instance, err := meter.NewFromConfig(cc.Type, cc.Other)
-		if err != nil {
-			return &DeviceError{cc.Name, fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)}
-		}
+			instance, err := meter.NewFromConfig(cc.Type, cc.Other)
+			if err != nil {
+				return &DeviceError{cc.Name, fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)}
+			}
 
-		if err := config.Meters().Add(config.NewConfigurableDevice(conf, instance)); err != nil {
-			return &DeviceError{cc.Name, err}
-		}
+			if err := config.Meters().Add(config.NewConfigurableDevice(conf, instance)); err != nil {
+				return &DeviceError{cc.Name, err}
+			}
+
+			return nil
+		})
 	}
 
-	return nil
+	return g.Wait()
 }
 
 func configureChargers(static []config.Named, names ...string) error {
@@ -299,7 +309,7 @@ func configureChargers(static []config.Named, names ...string) error {
 				return nil
 			}
 
-			// TOTO add fake devices
+			// TODO add fake devices
 
 			instance, err := charger.NewFromConfig(cc.Type, cc.Other)
 			if err != nil {
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 5f32ced5..4e6cb7e3 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -862,6 +862,11 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64) error {
 		lp.setAndPublishEnabled(enabled)
 		lp.chargerSwitched = lp.clock.Now()
 
+		// ensure we always re-set current when enabling charger
+		if !enabled {
+			lp.chargeCurrent = 0
+		}
+
 		lp.bus.Publish(evChargeCurrent, chargeCurrent)
 
 		// start/stop vehicle wake-up timer
diff --git a/go.mod b/go.mod
index 8b84e7c2..17b14b1c 100644
--- a/go.mod
+++ b/go.mod
@@ -89,7 +89,7 @@ require (
 	github.com/teslamotors/vehicle-command v0.0.2
 	github.com/traefik/yaegi v0.16.1
 	github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
-	github.com/volkszaehler/mbmd v0.0.0-20240611142726-33463eb0324e
+	github.com/volkszaehler/mbmd v0.0.0-20240727104742-3191c0dbfb9e
 	github.com/writeas/go-strip-markdown/v2 v2.1.1
 	gitlab.com/bboehmke/sunny v0.16.0
 	go.uber.org/mock v0.4.0
@@ -197,6 +197,8 @@ require (
 
 replace gopkg.in/yaml.v3 => github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467
 
+replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20240503125516-9fd99fe0e438
+
 replace github.com/enbility/spine-go => github.com/enbility/spine-go v0.0.0-20240726200332-a983de1e34b8
 
 replace github.com/enbility/ship-go => github.com/enbility/ship-go v0.0.0-20240731093131-37b1302bca66
diff --git a/go.sum b/go.sum
index 5fa521d3..39dc84fe 100644
--- a/go.sum
+++ b/go.sum
@@ -132,6 +132,8 @@ github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b/go.mod h1:Bjz
 github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evcc-io/modbus v0.0.0-20240503125516-9fd99fe0e438 h1:I14MN2UauarS2Lo+prnNXEj31MxJS1mEfLUrEnOUBsw=
+github.com/evcc-io/modbus v0.0.0-20240503125516-9fd99fe0e438/go.mod h1:WpbUAyptAAi0VAriSRopZa6uhiJOJCTz7KFvgGtNRXc=
 github.com/evcc-io/ocpp-go v0.0.0-20240730071053-d69e53b0fce9 h1:FLv1vmLnfc8DanI5U1qOTe1Zr0OgZ/tuOvHALtZ2sOI=
 github.com/evcc-io/ocpp-go v0.0.0-20240730071053-d69e53b0fce9/go.mod h1:ZynYDWGw6CslG3vyPuucLsy6AyE+h3XXYlr39jhNiQY=
 github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701 h1:3JplY3KS6KMDVDNAU+3+KWmSWmoHIU34qwuIpW6SiHk=
@@ -276,10 +278,6 @@ github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilf
 github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
-github.com/grid-x/modbus v0.0.0-20210714071042-7af2b65ec03b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw=
-github.com/grid-x/modbus v0.0.0-20240503115206-582f2ab60a18 h1:8V5xRtdD70kGC4/IHqFq+kcBSWr4k6nscAUgWwJ6A5k=
-github.com/grid-x/modbus v0.0.0-20240503115206-582f2ab60a18/go.mod h1:WpbUAyptAAi0VAriSRopZa6uhiJOJCTz7KFvgGtNRXc=
-github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk=
 github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa h1:Rsn6ARgNkXrsXJIzhkE4vQr5Gbx2LvtEMv4BJOK4LyU=
 github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@@ -662,8 +660,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
 github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
-github.com/volkszaehler/mbmd v0.0.0-20240611142726-33463eb0324e h1:1iAo0qalenDcyo9ySgRWjDuMTpfyA/rBDKklMMNahZk=
-github.com/volkszaehler/mbmd v0.0.0-20240611142726-33463eb0324e/go.mod h1:p1nUKfszvbZ+mMMsMi1vaKGA/eBAXC34VSKCk5slyG4=
+github.com/volkszaehler/mbmd v0.0.0-20240727104742-3191c0dbfb9e h1:d0KoYuIPXTI/vn85axsoD7w616oUalAzmDLZwv9yB9Q=
+github.com/volkszaehler/mbmd v0.0.0-20240727104742-3191c0dbfb9e/go.mod h1:MA3vKWI4KozcpWvD9dwN6CL+WqwdK9XGXT5Iv49IsUM=
 github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
 github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
diff --git a/meter/config.go b/meter/config.go
index 68555c6b..3a122899 100644
--- a/meter/config.go
+++ b/meter/config.go
@@ -1,31 +1,21 @@
 package meter
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
 	"github.com/evcc-io/evcc/api"
+	reg "github.com/evcc-io/evcc/util/registry"
 )
 
-type meterRegistry map[string]func(map[string]interface{}) (api.Meter, error)
+var registry = reg.New[api.Meter]("meter")
 
-func (r meterRegistry) Add(name string, factory func(map[string]interface{}) (api.Meter, error)) {
-	if _, exists := r[name]; exists {
-		panic(fmt.Sprintf("cannot register duplicate meter type: %s", name))
-	}
-	r[name] = factory
+// Types returns the list of types
+func Types() []string {
+	return registry.Types()
 }
 
-func (r meterRegistry) Get(name string) (func(map[string]interface{}) (api.Meter, error), error) {
-	factory, exists := r[name]
-	if !exists {
-		return nil, fmt.Errorf("invalid meter type: %s", name)
-	}
-	return factory, nil
-}
-
-var registry meterRegistry = make(map[string]func(map[string]interface{}) (api.Meter, error))
-
 // NewFromConfig creates meter from configuration
 func NewFromConfig(typ string, other map[string]interface{}) (api.Meter, error) {
 	factory, err := registry.Get(strings.ToLower(typ))
@@ -33,7 +23,7 @@ func NewFromConfig(typ string, other map[string]interface{}) (api.Meter, error)
 		return nil, err
 	}
 
-	v, err := factory(other)
+	v, err := factory(context.TODO(), other)
 	if err != nil {
 		err = fmt.Errorf("cannot create meter type '%s': %w", typ, err)
 	}
diff --git a/meter/mbmd.go b/meter/mbmd.go
index 55164484..29876ea5 100644
--- a/meter/mbmd.go
+++ b/meter/mbmd.go
@@ -57,20 +57,19 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
 		cc.RTU = &b
 	}
 
-	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	modbus.Lock()
+	defer modbus.Unlock()
+
+	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
 	if err != nil {
 		return nil, err
 	}
 
-	// set non-default delay
-	if cc.Delay > 0 {
-		conn.Delay(cc.Delay)
-	}
-
 	// set non-default timeout
-	if cc.Timeout > 0 {
-		conn.Timeout(cc.Timeout)
-	}
+	conn.Timeout(cc.Timeout)
+
+	// set non-default delay
+	conn.Delay(cc.Delay)
 
 	log := util.NewLogger("modbus")
 	conn.Logger(log.TRACE)
diff --git a/provider/modbus.go b/provider/modbus.go
index b92f55d0..51ac94b3 100644
--- a/provider/modbus.go
+++ b/provider/modbus.go
@@ -41,25 +41,22 @@ func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
 		return nil, err
 	}
 
-	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+	modbus.Lock()
+	defer modbus.Unlock()
+
+	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
 	if err != nil {
 		return nil, err
 	}
 
 	// set non-default timeout
-	if cc.Timeout > 0 {
-		conn.Timeout(cc.Timeout)
-	}
+	conn.Timeout(cc.Timeout)
 
 	// set non-default delay
-	if cc.Delay > 0 {
-		conn.Delay(cc.Delay)
-	}
+	conn.Delay(cc.Delay)
 
 	// set non-default connect delay
-	if cc.ConnectDelay > 0 {
-		conn.ConnectDelay(cc.ConnectDelay)
-	}
+	conn.ConnectDelay(cc.ConnectDelay)
 
 	log := util.NewLogger("modbus")
 	conn.Logger(log.TRACE)
diff --git a/provider/modbus_sunspec.go b/provider/modbus_sunspec.go
index 39d7aec1..6b5df688 100644
--- a/provider/modbus_sunspec.go
+++ b/provider/modbus_sunspec.go
@@ -44,25 +44,22 @@ func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error)
 		return nil, err
 	}
 
+	modbus.Lock()
+	defer modbus.Unlock()
+
 	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.Tcp, cc.ID)
 	if err != nil {
 		return nil, err
 	}
 
 	// set non-default timeout
-	if cc.Timeout > 0 {
-		conn.Timeout(cc.Timeout)
-	}
+	conn.Timeout(cc.Timeout)
 
 	// set non-default delay
-	if cc.Delay > 0 {
-		conn.Delay(cc.Delay)
-	}
+	conn.Delay(cc.Delay)
 
 	// set non-default connect delay
-	if cc.ConnectDelay > 0 {
-		conn.ConnectDelay(cc.ConnectDelay)
-	}
+	conn.ConnectDelay(cc.ConnectDelay)
 
 	log := util.NewLogger("sunspec")
 	conn.Logger(log.TRACE)
diff --git a/server/modbus/handler.go b/server/modbus/handler.go
index caae01aa..d5a3b9e0 100644
--- a/server/modbus/handler.go
+++ b/server/modbus/handler.go
@@ -98,7 +98,7 @@ LOOP:
 
 func (h *handler) HandleDiscreteInputs(req *mbserver.DiscreteInputsRequest) ([]bool, error) {
 	h.log.TRACE.Printf("read discrete: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
-	b, err := h.conn.ReadDiscreteInputsWithSlave(req.UnitId, req.Addr, req.Quantity)
+	b, err := h.conn.Clone(req.UnitId).ReadDiscreteInputs(req.Addr, req.Quantity)
 	return h.bytesToBoolResult("read discrete", req.Quantity, b, err)
 }
 
@@ -120,24 +120,24 @@ func (h *handler) HandleCoils(req *mbserver.CoilsRequest) ([]bool, error) {
 				u = 0xFF00
 			}
 
-			b, err := h.conn.WriteSingleCoilWithSlave(req.UnitId, req.Addr, u)
+			b, err := h.conn.Clone(req.UnitId).WriteSingleCoil(req.Addr, u)
 			return h.bytesToBoolResult("write coil", req.Quantity, b, err)
 		}
 
 		h.log.TRACE.Printf("write coils: id %d addr %d qty %d val %v", req.UnitId, req.Addr, req.Quantity, req.Args)
 		args := coilsToBytes(req.Args)
-		b, err := h.conn.WriteMultipleCoilsWithSlave(req.UnitId, req.Addr, req.Quantity, args)
+		b, err := h.conn.Clone(req.UnitId).WriteMultipleCoils(req.Addr, req.Quantity, args)
 		return h.bytesToBoolResult("write coils", req.Quantity, b, err)
 	}
 
 	h.log.TRACE.Printf("read coils: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
-	b, err := h.conn.ReadCoilsWithSlave(req.UnitId, req.Addr, req.Quantity)
+	b, err := h.conn.Clone(req.UnitId).ReadCoils(req.Addr, req.Quantity)
 	return h.bytesToBoolResult("read coils", req.Quantity, b, err)
 }
 
 func (h *handler) HandleInputRegisters(req *mbserver.InputRegistersRequest) ([]uint16, error) {
 	h.log.TRACE.Printf("read input: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
-	b, err := h.conn.ReadInputRegistersWithSlave(req.UnitId, req.Addr, req.Quantity)
+	b, err := h.conn.Clone(req.UnitId).ReadInputRegisters(req.Addr, req.Quantity)
 	return h.exceptionToUint16AndError("read input", b, err)
 }
 
@@ -154,16 +154,16 @@ func (h *handler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest)
 
 		if req.WriteFuncCode == gridx.FuncCodeWriteSingleRegister {
 			h.log.TRACE.Printf("write holding: id %d addr %d val %04x", req.UnitId, req.Addr, req.Args[0])
-			b, err := h.conn.WriteSingleRegisterWithSlave(req.UnitId, req.Addr, req.Args[0])
+			b, err := h.conn.Clone(req.UnitId).WriteSingleRegister(req.Addr, req.Args[0])
 			return h.exceptionToUint16AndError("write holding", b, err)
 		}
 
 		h.log.TRACE.Printf("write holdings: id %d addr %d qty %d val %0x", req.UnitId, req.Addr, req.Quantity, asBytes(req.Args))
-		b, err := h.conn.WriteMultipleRegistersWithSlave(req.UnitId, req.Addr, req.Quantity, asBytes(req.Args))
+		b, err := h.conn.Clone(req.UnitId).WriteMultipleRegisters(req.Addr, req.Quantity, asBytes(req.Args))
 		return h.exceptionToUint16AndError("write multiple holding", b, err)
 	}
 
 	h.log.TRACE.Printf("read holdings: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
-	b, err := h.conn.ReadHoldingRegistersWithSlave(req.UnitId, req.Addr, req.Quantity)
+	b, err := h.conn.Clone(req.UnitId).ReadHoldingRegisters(req.Addr, req.Quantity)
 	return h.exceptionToUint16AndError("read holding", b, err)
 }
diff --git a/server/modbus/proxy.go b/server/modbus/proxy.go
index 096b6d7a..12123432 100644
--- a/server/modbus/proxy.go
+++ b/server/modbus/proxy.go
@@ -12,7 +12,7 @@ import (
 )
 
 func StartProxy(port int, config modbus.Settings, readOnly ReadOnlyMode) error {
-	conn, err := modbus.NewConnection(config.URI, config.Device, config.Comset, config.Baudrate, modbus.ProtocolFromRTU(config.RTU), config.ID)
+	conn, err := modbus.NewConnection(config.URI, config.Device, config.Comset, config.Baudrate, config.Protocol(), config.ID)
 	if err != nil {
 		return err
 	}
diff --git a/tariff/config.go b/tariff/config.go
index 34dda811..a1cc7a94 100644
--- a/tariff/config.go
+++ b/tariff/config.go
@@ -1,31 +1,21 @@
 package tariff
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
 	"github.com/evcc-io/evcc/api"
+	reg "github.com/evcc-io/evcc/util/registry"
 )
 
-type tariffRegistry map[string]func(map[string]interface{}) (api.Tariff, error)
+var registry = reg.New[api.Tariff]("tariff")
 
-func (r tariffRegistry) Add(name string, factory func(map[string]interface{}) (api.Tariff, error)) {
-	if _, exists := r[name]; exists {
-		panic(fmt.Sprintf("cannot register duplicate tariff type: %s", name))
-	}
-	r[name] = factory
+// Types returns the list of types
+func Types() []string {
+	return registry.Types()
 }
 
-func (r tariffRegistry) Get(name string) (func(map[string]interface{}) (api.Tariff, error), error) {
-	factory, exists := r[name]
-	if !exists {
-		return nil, fmt.Errorf("invalid tariff type: %s", name)
-	}
-	return factory, nil
-}
-
-var registry tariffRegistry = make(map[string]func(map[string]interface{}) (api.Tariff, error))
-
 // NewFromConfig creates tariff from configuration
 func NewFromConfig(typ string, other map[string]interface{}) (api.Tariff, error) {
 	factory, err := registry.Get(strings.ToLower(typ))
@@ -33,7 +23,7 @@ func NewFromConfig(typ string, other map[string]interface{}) (api.Tariff, error)
 		return nil, err
 	}
 
-	v, err := factory(other)
+	v, err := factory(context.TODO(), other)
 	if err != nil {
 		err = fmt.Errorf("cannot create tariff type '%s': %w", typ, err)
 	}
diff --git a/templates/definition/charger/versicharge.yaml b/templates/definition/charger/versicharge.yaml
index 2100ae86..838dcfe7 100644
--- a/templates/definition/charger/versicharge.yaml
+++ b/templates/definition/charger/versicharge.yaml
@@ -6,8 +6,8 @@ products:
 requirements:
   evcc: ["sponsorship"]
   description:
-    de: Erfordert Firmware >= 2.121.5
-    en: Requires firmware >= 2.121.5
+    de: Erfordert Firmware >= 2.135
+    en: Requires firmware >= 2.135
 params:
   - name: modbus
     choice: ["tcpip"]
diff --git a/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml
index 74fe50e7..a2598422 100644
--- a/templates/definition/meter/goodwe-hybrid.yaml
+++ b/templates/definition/meter/goodwe-hybrid.yaml
@@ -8,7 +8,7 @@ params:
   - name: usage
     choice: ["grid", "pv", "battery"]
   - name: modbus
-    choice: ["rs485", "tcpip"]
+    choice: ["rs485", "tcpip", "udp"]
     baudrate: 9600
     id: 247
   - name: capacity
diff --git a/util/modbus/connection.go b/util/modbus/connection.go
new file mode 100644
index 00000000..672a44ff
--- /dev/null
+++ b/util/modbus/connection.go
@@ -0,0 +1,113 @@
+package modbus
+
+import (
+	"sync"
+	"time"
+
+	"github.com/grid-x/modbus"
+	"github.com/volkszaehler/mbmd/meters"
+)
+
+// Connection is a logical modbus connection per slave ID sharing a physical connection
+type Connection struct {
+	meters.Connection
+	mu      sync.Mutex
+	logger  *logger
+	logical meters.Logger
+	delay   time.Duration
+}
+
+func (c *Connection) Delay(delay time.Duration) {
+	c.delay = delay
+}
+
+func (c *Connection) Clone(slaveID uint8) *Connection {
+	return &Connection{
+		Connection: c.Connection.Clone(slaveID),
+		logger:     c.logger,
+	}
+}
+
+// TODO resolve conflicts
+func (c *Connection) ConnectDelay(delay time.Duration) {
+	if delay > 0 {
+		c.Connection.ConnectDelay(delay)
+	}
+}
+
+// TODO resolve conflicts
+func (c *Connection) Timeout(timeout time.Duration) {
+	if timeout > 0 {
+		_ = c.Connection.Timeout(timeout)
+	}
+}
+
+func (c *Connection) Logger(l modbus.Logger) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.logical = l
+}
+
+func (c *Connection) prepare() {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	time.Sleep(c.delay)
+	c.logger.Logger(c.logical)
+}
+
+func (c *Connection) ReadCoils(address, quantity uint16) ([]byte, error) {
+	c.prepare()
+	return c.ModbusClient().ReadCoils(address, quantity)
+}
+
+func (c *Connection) WriteSingleCoil(address, value uint16) ([]byte, error) {
+	c.prepare()
+	return c.ModbusClient().WriteSingleCoil(address, value)
+}
+
+func (c *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) {
+	c.prepare()
+	return c.ModbusClient().ReadInputRegisters(address, quantity)
+}
+
+func (c *Connection) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) {
+	c.prepare()
+	return c.ModbusClient().ReadHoldingRegisters(address, quantity)
+}
+
+func (c *Connection) WriteSingleRegister(address, value uint16) ([]byte, error) {
+	c.prepare()
+	return c.ModbusClient().WriteSingleRegister(address, value)
+}
+
+func (c *Connection) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) {
+	c.prepare()
+	return c.ModbusClient().WriteMultipleRegisters(address, quantity, value)
+}
+
+func (c *Connection) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) {
+	c.prepare()
+	return c.ModbusClient().ReadDiscreteInputs(address, quantity)
+}
+
+func (c *Connection) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) {
+	c.prepare()
+	return c.ModbusClient().WriteMultipleCoils(address, quantity, value)
+}
+
+func (c *Connection) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) {
+	c.prepare()
+	return c.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value)
+}
+
+func (c *Connection) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) {
+	c.prepare()
+	return c.ModbusClient().MaskWriteRegister(address, andMask, orMask)
+}
+
+func (c *Connection) ReadFIFOQueue(address uint16) (results []byte, err error) {
+	c.prepare()
+	return c.ModbusClient().ReadFIFOQueue(address)
+}
diff --git a/util/modbus/log.go b/util/modbus/log.go
new file mode 100644
index 00000000..59495eb2
--- /dev/null
+++ b/util/modbus/log.go
@@ -0,0 +1,29 @@
+package modbus
+
+import (
+	"sync"
+
+	"github.com/grid-x/modbus"
+	"github.com/volkszaehler/mbmd/meters"
+)
+
+type logger struct {
+	mu     sync.RWMutex
+	logger meters.Logger
+}
+
+func (l *logger) Logger(logger modbus.Logger) {
+	l.mu.Lock()
+	defer l.mu.Unlock()
+
+	l.logger = logger
+}
+
+func (l *logger) Printf(format string, v ...interface{}) {
+	l.mu.RLock()
+	defer l.mu.RUnlock()
+
+	if l.logger != nil {
+		l.logger.Printf(format, v...)
+	}
+}
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index d8833e30..9201e1d3 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"strings"
 	"sync"
-	"time"
 
 	"github.com/evcc-io/evcc/util"
 	"github.com/volkszaehler/mbmd/meters"
@@ -19,6 +18,7 @@ const (
 	Tcp Protocol = iota
 	Rtu
 	Ascii
+	Udp
 
 	CoilOn uint16 = 0xFF00
 )
@@ -38,9 +38,22 @@ type Settings struct {
 	SubDevice           int
 	URI, Device, Comset string
 	Baudrate            int
+	UDP                 bool
 	RTU                 *bool // indicates RTU over TCP if true
 }
 
+// Protocol identifies the wire format from the RTU setting
+func (s Settings) Protocol() Protocol {
+	switch {
+	case s.UDP:
+		return Udp
+	case s.RTU != nil && *s.RTU:
+		return Rtu
+	default:
+		return Tcp
+	}
+}
+
 func (s *Settings) String() string {
 	if s.URI != "" {
 		return s.URI
@@ -48,180 +61,6 @@ func (s *Settings) String() string {
 	return s.Device
 }
 
-// Connection decorates a meters.Connection with transparent slave id and error handling
-type Connection struct {
-	slaveID uint8
-	mu      sync.Mutex
-	conn    meters.Connection
-	delay   time.Duration
-}
-
-func (mb *Connection) prepare(slaveID uint8) {
-	mb.conn.Slave(slaveID)
-	if mb.delay > 0 {
-		time.Sleep(mb.delay)
-	}
-}
-
-func (mb *Connection) handle(res []byte, err error) ([]byte, error) {
-	if err != nil {
-		mb.conn.Close()
-	}
-	return res, err
-}
-
-// Delay sets delay so use between subsequent modbus operations
-func (mb *Connection) Delay(delay time.Duration) {
-	mb.delay = delay
-}
-
-// ConnectDelay sets the initial delay after connecting before starting communication
-func (mb *Connection) ConnectDelay(delay time.Duration) {
-	mb.conn.ConnectDelay(delay)
-}
-
-// Logger sets logger implementation
-func (mb *Connection) Logger(logger meters.Logger) {
-	mb.conn.Logger(logger)
-}
-
-// Timeout sets the connection timeout (not idle timeout)
-func (mb *Connection) Timeout(timeout time.Duration) {
-	mb.conn.Timeout(timeout)
-}
-
-// ReadCoils wraps the underlying implementation
-func (mb *Connection) ReadCoilsWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().ReadCoils(address, quantity))
-}
-
-// WriteSingleCoil wraps the underlying implementation
-func (mb *Connection) WriteSingleCoilWithSlave(slaveID uint8, address, value uint16) ([]byte, error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().WriteSingleCoil(address, value))
-}
-
-// ReadInputRegisters wraps the underlying implementation
-func (mb *Connection) ReadInputRegistersWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().ReadInputRegisters(address, quantity))
-}
-
-// ReadHoldingRegisters wraps the underlying implementation
-func (mb *Connection) ReadHoldingRegistersWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().ReadHoldingRegisters(address, quantity))
-}
-
-// WriteSingleRegister wraps the underlying implementation
-func (mb *Connection) WriteSingleRegisterWithSlave(slaveID uint8, address, value uint16) ([]byte, error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().WriteSingleRegister(address, value))
-}
-
-// WriteMultipleRegisters wraps the underlying implementation
-func (mb *Connection) WriteMultipleRegistersWithSlave(slaveID uint8, address, quantity uint16, value []byte) ([]byte, error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().WriteMultipleRegisters(address, quantity, value))
-}
-
-// ReadDiscreteInputs wraps the underlying implementation
-func (mb *Connection) ReadDiscreteInputsWithSlave(slaveID uint8, address, quantity uint16) (results []byte, err error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().ReadDiscreteInputs(address, quantity))
-}
-
-// WriteMultipleCoils wraps the underlying implementation
-func (mb *Connection) WriteMultipleCoilsWithSlave(slaveID uint8, address, quantity uint16, value []byte) (results []byte, err error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().WriteMultipleCoils(address, quantity, value))
-}
-
-// ReadWriteMultipleRegisters wraps the underlying implementation
-func (mb *Connection) ReadWriteMultipleRegistersWithSlave(slaveID uint8, readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value))
-}
-
-// MaskWriteRegister wraps the underlying implementation
-func (mb *Connection) MaskWriteRegisterWithSlave(slaveID uint8, address, andMask, orMask uint16) (results []byte, err error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().MaskWriteRegister(address, andMask, orMask))
-}
-
-// ReadFIFOQueue wraps the underlying implementation
-func (mb *Connection) ReadFIFOQueueWithSlave(slaveID uint8, address uint16) (results []byte, err error) {
-	mb.mu.Lock()
-	defer mb.mu.Unlock()
-	mb.prepare(slaveID)
-	return mb.handle(mb.conn.ModbusClient().ReadFIFOQueue(address))
-}
-
-func (mb *Connection) ReadCoils(address, quantity uint16) ([]byte, error) {
-	return mb.ReadCoilsWithSlave(mb.slaveID, address, quantity)
-}
-
-func (mb *Connection) WriteSingleCoil(address, value uint16) ([]byte, error) {
-	return mb.WriteSingleCoilWithSlave(mb.slaveID, address, value)
-}
-
-func (mb *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) {
-	return mb.ReadInputRegistersWithSlave(mb.slaveID, address, quantity)
-}
-
-func (mb *Connection) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) {
-	return mb.ReadHoldingRegistersWithSlave(mb.slaveID, address, quantity)
-}
-
-func (mb *Connection) WriteSingleRegister(address, value uint16) ([]byte, error) {
-	return mb.WriteSingleRegisterWithSlave(mb.slaveID, address, value)
-}
-
-func (mb *Connection) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) {
-	return mb.WriteMultipleRegistersWithSlave(mb.slaveID, address, quantity, value)
-}
-
-func (mb *Connection) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) {
-	return mb.ReadDiscreteInputsWithSlave(mb.slaveID, address, quantity)
-}
-
-func (mb *Connection) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) {
-	return mb.WriteMultipleCoilsWithSlave(mb.slaveID, address, quantity, value)
-}
-
-func (mb *Connection) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) {
-	return mb.ReadWriteMultipleRegistersWithSlave(mb.slaveID, readAddress, readQuantity, writeAddress, writeQuantity, value)
-}
-
-func (mb *Connection) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) {
-	return mb.MaskWriteRegisterWithSlave(mb.slaveID, address, andMask, orMask)
-}
-
-func (mb *Connection) ReadFIFOQueue(address uint16) (results []byte, err error) {
-	return mb.ReadFIFOQueueWithSlave(mb.slaveID, address)
-}
-
 var (
 	connections = make(map[string]meters.Connection)
 	mu          sync.Mutex
@@ -240,65 +79,69 @@ func registeredConnection(key string, newConn meters.Connection) meters.Connecti
 	return newConn
 }
 
-// ProtocolFromRTU identifies the wire format from the RTU setting
-func ProtocolFromRTU(rtu *bool) Protocol {
-	if rtu != nil && *rtu {
-		return Rtu
+// 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{
+		URI:      uri,
+		Device:   device,
+		Comset:   comset,
+		Baudrate: baudrate,
+	})
+	if err != nil {
+		return nil, err
 	}
-	return Tcp
+
+	res := &Connection{
+		Connection: conn.Clone(slaveID),
+		logger:     new(logger),
+	}
+
+	return res, nil
 }
 
-// NewConnection creates physical modbus device from config
-func NewConnection(uri, device, comset string, baudrate int, proto Protocol, slaveID uint8) (*Connection, error) {
+func physicalConnection(proto Protocol, cfg Settings) (meters.Connection, error) {
 	var conn meters.Connection
 
-	if device != "" && uri != "" {
-		return nil, errors.New("invalid modbus configuration: can only have either uri or device")
+	if (cfg.Device != "") == (cfg.URI != "") {
+		return nil, errors.New("invalid modbus configuration: must have either uri or device")
 	}
 
-	if device != "" {
-		switch strings.ToUpper(comset) {
+	if cfg.Device != "" {
+		switch strings.ToUpper(cfg.Comset) {
 		case "8N1", "8E1", "8N2":
 		case "80":
-			comset = "8E1"
+			cfg.Comset = "8E1"
 		default:
-			return nil, fmt.Errorf("invalid comset: %s", comset)
+			return nil, fmt.Errorf("invalid comset: %s", cfg.Comset)
 		}
 
-		if baudrate == 0 {
+		if cfg.Baudrate == 0 {
 			return nil, errors.New("invalid modbus configuration: need baudrate and comset")
 		}
 
 		if proto == Ascii {
-			conn = registeredConnection(device, meters.NewASCII(device, baudrate, comset))
+			conn = registeredConnection(cfg.Device, meters.NewASCII(cfg.Device, cfg.Baudrate, cfg.Comset))
 		} else {
-			conn = registeredConnection(device, meters.NewRTU(device, baudrate, comset))
+			conn = registeredConnection(cfg.Device, meters.NewRTU(cfg.Device, cfg.Baudrate, cfg.Comset))
 		}
 	}
 
-	if uri != "" {
-		uri = util.DefaultPort(uri, 502)
+	if cfg.URI != "" {
+		cfg.URI = util.DefaultPort(cfg.URI, 502)
 
 		switch proto {
+		case Udp:
+			conn = registeredConnection(cfg.URI, meters.NewRTUOverUDP(cfg.URI))
 		case Rtu:
-			conn = registeredConnection(uri, meters.NewRTUOverTCP(uri))
+			conn = registeredConnection(cfg.URI, meters.NewRTUOverTCP(cfg.URI))
 		case Ascii:
-			conn = registeredConnection(uri, meters.NewASCIIOverTCP(uri))
+			conn = registeredConnection(cfg.URI, meters.NewASCIIOverTCP(cfg.URI))
 		default:
-			conn = registeredConnection(uri, meters.NewTCP(uri))
+			conn = registeredConnection(cfg.URI, meters.NewTCP(cfg.URI))
 		}
 	}
 
-	if conn == nil {
-		return nil, errors.New("invalid modbus configuration: need either uri or device")
-	}
-
-	slaveConn := &Connection{
-		slaveID: slaveID,
-		conn:    conn,
-	}
-
-	return slaveConn, nil
+	return conn, nil
 }
 
 // NewDevice creates physical modbus device from config
diff --git a/util/modbus/mutex.go b/util/modbus/mutex.go
new file mode 100644
index 00000000..4592bddb
--- /dev/null
+++ b/util/modbus/mutex.go
@@ -0,0 +1,13 @@
+package modbus
+
+import "sync"
+
+var mu2 sync.Mutex
+
+func Lock() {
+	mu2.Lock()
+}
+
+func Unlock() {
+	mu2.Unlock()
+}
diff --git a/util/registry/registry.go b/util/registry/registry.go
new file mode 100644
index 00000000..3394f1b3
--- /dev/null
+++ b/util/registry/registry.go
@@ -0,0 +1,49 @@
+package registry
+
+import (
+	"context"
+	"fmt"
+
+	"golang.org/x/exp/maps"
+)
+
+type (
+	factoryFunc[T any] func(context.Context, map[string]any) (T, error)
+
+	registry[T any] struct {
+		typ  string
+		data map[string]factoryFunc[T]
+	}
+)
+
+func (r registry[T]) Add(name string, factory func(map[string]any) (T, error)) {
+	r.AddCtx(name, func(_ context.Context, cc map[string]any) (T, error) {
+		return factory(cc)
+	})
+}
+
+func (r registry[T]) AddCtx(name string, factory factoryFunc[T]) {
+	if _, exists := r.data[name]; exists {
+		panic(fmt.Sprintf("cannot register duplicate %s type: %s", r.typ, name))
+	}
+	r.data[name] = factory
+}
+
+func (r registry[T]) Get(name string) (factoryFunc[T], error) {
+	factory, exists := r.data[name]
+	if !exists {
+		return nil, fmt.Errorf("invalid %s type: %s", r.typ, name)
+	}
+	return factory, nil
+}
+
+func (r registry[T]) Types() []string {
+	return maps.Keys(r.data)
+}
+
+func New[T any](typ string) registry[T] {
+	return registry[T]{
+		typ:  typ,
+		data: make(map[string]factoryFunc[T]),
+	}
+}
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index 93982c69..79f29d93 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -435,6 +435,7 @@ modbus:
   interfaces:
     rs485: ["rs485serial", "rs485tcpip"]
     tcpip: ["tcpip"]
+    udp: ["udp"]
   types:
     rs485serial:
       description:
@@ -476,6 +477,18 @@ modbus:
         - reference: true
           name: port
           default: 502
+    udp:
+      description:
+        generic: UDP
+      params:
+        - reference: true
+          referencename: modbusid
+          name: id
+        - reference: true
+          name: host
+        - reference: true
+          name: port
+          default: 502
 
 devicegroups:
   generic:
diff --git a/util/templates/documentation.go b/util/templates/documentation.go
index 004d7a18..c041af52 100644
--- a/util/templates/documentation.go
+++ b/util/templates/documentation.go
@@ -3,6 +3,7 @@ package templates
 import (
 	"bytes"
 	_ "embed"
+	"slices"
 	"strconv"
 	"strings"
 	"text/template"
@@ -77,6 +78,17 @@ func (t *Template) RenderDocumentation(product Product, lang string) ([]byte, er
 		filteredParams = append(filteredParams, param)
 	}
 
+	// all advanced params should be sorted to the end
+	slices.SortStableFunc(filteredParams, func(i, j Param) int {
+		if i.IsAdvanced() && !j.IsAdvanced() {
+			return 1
+		}
+		if !i.IsAdvanced() && j.IsAdvanced() {
+			return -1
+		}
+		return 0
+	})
+
 	data := map[string]interface{}{
 		"Template":               t.Template,
 		"ProductBrand":           product.Brand,
diff --git a/util/templates/modbus.tpl b/util/templates/modbus.tpl
index aea82255..ea374c4c 100644
--- a/util/templates/modbus.tpl
+++ b/util/templates/modbus.tpl
@@ -13,6 +13,11 @@ rtu: true
 # Modbus TCP
 uri: {{ .host }}:{{ .port }}
 rtu: false
+{{- else if or (eq .modbus "udp") .udp }}
+# Modbus UDP
+uri: {{ .host }}:{{ if (ne .port "502") }}{{ .port }}{{ else }}8899{{ end }}
+udp: true
+rtu: true
 {{- else }}
 # configuration error - should not happen
 modbusConnectionTypeNotDefined: {{ .modbus }}
diff --git a/util/templates/types.go b/util/templates/types.go
index 8a598bb2..c95838f5 100644
--- a/util/templates/types.go
+++ b/util/templates/types.go
@@ -17,9 +17,11 @@ const (
 
 	ModbusChoiceRS485    = "rs485"
 	ModbusChoiceTCPIP    = "tcpip"
+	ModbusChoiceUDP      = "udp"
 	ModbusKeyRS485Serial = "rs485serial"
 	ModbusKeyRS485TCPIP  = "rs485tcpip"
 	ModbusKeyTCPIP       = "tcpip"
+	ModbusKeyUDP         = "udp"
 
 	ModbusParamNameId       = "id"
 	ModbusParamNameDevice   = "device"
@@ -37,7 +39,7 @@ const (
 	RenderModeInstance
 )
 
-var ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP}
+var ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP, ModbusChoiceUDP}
 
 const (
 	CapabilityISO151182      = "iso151182"       // ISO 15118-2 support
@@ -62,7 +64,7 @@ var predefinedTemplateProperties = []string{
 	"type", "template", "name",
 	ModbusParamNameId, ModbusParamNameDevice, ModbusParamNameBaudrate, ModbusParamNameComset,
 	ModbusParamNameURI, ModbusParamNameHost, ModbusParamNamePort, ModbusParamNameRTU,
-	ModbusKeyTCPIP, ModbusKeyRS485Serial, ModbusKeyRS485TCPIP,
+	ModbusKeyTCPIP, ModbusKeyUDP, ModbusKeyRS485Serial, ModbusKeyRS485TCPIP,
 }
 
 // TextLanguage contains language-specific texts
diff --git a/vehicle/config.go b/vehicle/config.go
index ca692e59..2590013d 100644
--- a/vehicle/config.go
+++ b/vehicle/config.go
@@ -1,12 +1,14 @@
 package vehicle
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
+	reg "github.com/evcc-io/evcc/util/registry"
 )
 
 const (
@@ -14,32 +16,11 @@ const (
 	interval = 15 * time.Minute // refresh interval when charging
 )
 
-type vehicleRegistry map[string]func(map[string]interface{}) (api.Vehicle, error)
+var registry = reg.New[api.Vehicle]("vehicle")
 
-func (r vehicleRegistry) Add(name string, factory func(map[string]interface{}) (api.Vehicle, error)) {
-	if _, exists := r[name]; exists {
-		panic(fmt.Sprintf("cannot register duplicate vehicle type: %s", name))
-	}
-	r[name] = factory
-}
-
-func (r vehicleRegistry) Get(name string) (func(map[string]interface{}) (api.Vehicle, error), error) {
-	factory, exists := r[name]
-	if !exists {
-		return nil, fmt.Errorf("invalid vehicle type: %s", name)
-	}
-	return factory, nil
-}
-
-var registry vehicleRegistry = make(map[string]func(map[string]interface{}) (api.Vehicle, error))
-
-// Types returns the list of vehicle types
+// Types returns the list of types
 func Types() []string {
-	var res []string
-	for typ := range registry {
-		res = append(res, typ)
-	}
-	return res
+	return registry.Types()
 }
 
 // NewFromConfig creates vehicle from configuration
@@ -63,7 +44,7 @@ func NewFromConfig(typ string, other map[string]interface{}) (api.Vehicle, error
 		return nil, err
 	}
 
-	v, err := factory(cc.Other)
+	v, err := factory(context.TODO(), cc.Other)
 	if err != nil {
 		err = fmt.Errorf("cannot create vehicle type '%s': %w", typ, err)
 	}
diff --git a/vehicle/mercedes/api.go b/vehicle/mercedes/api.go
index 1ba5546d..47acfb02 100644
--- a/vehicle/mercedes/api.go
+++ b/vehicle/mercedes/api.go
@@ -97,6 +97,10 @@ func (v *API) Status(vin string) (StatusResponse, error) {
 		res.EvInfo.Battery.ChargingStatus = 3
 	}
 
+	if val, ok := message.Attributes["maxSoc"]; ok && val != nil {
+		res.EvInfo.Battery.SocLimit = int(val.GetIntValue())
+	}
+
 	if val, ok := message.Attributes["selectedChargeProgram"]; ok && val != nil {
 		selectedChargeProgram := val.GetIntValue()
 		res.EvInfo.Battery.SelectedChargeProgram = int(selectedChargeProgram)
@@ -108,10 +112,7 @@ func (v *API) Status(vin string) (StatusResponse, error) {
 				}
 			}
 		}
-	} else {
-		if val, ok := message.Attributes["maxSoc"]; ok && val != nil {
-			res.EvInfo.Battery.SocLimit = int(val.GetIntValue())
-		}
 	}
+
 	return res, err
 }


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 2c74d474..66a51208 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -448,7 +448,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 	lp.Update(-5000, false, time.Time{}, false, false, 0, nil, nil)
 	ctrl.Finish()
 
-	t.Log("soc has fallen below target - soc update prevented by timer")
+	t.Log("soc has risen below target - soc update prevented by timer")
 	clock.Add(5 * time.Minute)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
@@ -460,6 +460,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 	vehicle.EXPECT().Soc().Return(85.0, nil)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
+	charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
 	charger.EXPECT().Enable(true).Return(nil)
 	lp.Update(-5000, false, time.Time{}, false, false, 0, nil, nil)
 	ctrl.Finish()
diff --git a/meter/template_test.go b/meter/template_test.go
index 9a4067be..eaf8d134 100644
--- a/meter/template_test.go
+++ b/meter/template_test.go
@@ -13,9 +13,10 @@ var acceptable = []string{
 	"missing token",
 	"mqtt not configured",
 	"not a SunSpec device",
-	"missing credentials", // sockets
-	"power: timeout",      // sockets
-	"missing password",    // Powerwall
+	"connect: connection refused", // sockets
+	"missing credentials",         // sockets
+	"power: timeout",              // sockets
+	"missing password",            // Powerwall
 	"connect: no route to host",
 	"connect: connection refused",
 	"connect: network is unreachable",
diff --git a/server/modbus/proxy_test.go b/server/modbus/proxy_test.go
index 5bd72f7c..84cb382c 100644
--- a/server/modbus/proxy_test.go
+++ b/server/modbus/proxy_test.go
@@ -27,21 +27,21 @@ func TestConcurrentRead(t *testing.T) {
 	require.NoError(t, srv.Start(l))
 	defer func() { _ = srv.Stop() }()
 
-	// client
-	conn, err := modbus.NewConnection(l.Addr().String(), "", "", 0, modbus.Tcp, 1)
-	require.NoError(t, err)
-
 	var wg sync.WaitGroup
 
 	for i := 1; i <= 10; i++ {
 		wg.Add(1)
 
 		go func(id int) {
+			// client
+			conn, err := modbus.NewConnection(l.Addr().String(), "", "", 0, modbus.Tcp, uint8(id))
+			require.NoError(t, err)
+
 			for i := 0; i < 50; i++ {
 				addr := uint16(rand.Int31n(200) + 1)
 				qty := uint16(rand.Int31n(32) + 1)
 
-				b, err := conn.ReadInputRegistersWithSlave(uint8(id), addr, qty)
+				b, err := conn.ReadInputRegisters(addr, qty)
 				require.NoError(t, err)
 
 				if err == nil {
@@ -94,24 +94,24 @@ func TestReadCoils(t *testing.T) {
 		require.NoError(t, err)
 
 		{ // read
-			b, err := conn.ReadCoilsWithSlave(1, 1, 1)
+			b, err := conn.ReadCoils(1, 1)
 			require.NoError(t, err)
 			assert.Equal(t, []byte{0x01}, b)
 
-			b, err = conn.ReadCoilsWithSlave(1, 1, 2)
+			b, err = conn.ReadCoils(1, 2)
 			require.NoError(t, err)
 			assert.Equal(t, []byte{0x03}, b)
 
-			b, err = conn.ReadCoilsWithSlave(1, 1, 9)
+			b, err = conn.ReadCoils(1, 9)
 			require.NoError(t, err)
 			assert.Equal(t, []byte{0xFF, 0x01}, b)
 		}
 		{ // write
-			b, err := conn.WriteSingleCoilWithSlave(1, 1, 0xFF00)
+			b, err := conn.WriteSingleCoil(1, 0xFF00)
 			require.NoError(t, err)
 			assert.Equal(t, []byte{0xFF, 0x00}, b)
 
-			b, err = conn.WriteMultipleCoilsWithSlave(1, 1, 9, []byte{0xFF, 0x01})
+			b, err = conn.WriteMultipleCoils(1, 9, []byte{0xFF, 0x01})
 			require.NoError(t, err)
 			assert.Equal(t, []byte{0x00, 0x09}, b)
 		}
diff --git a/util/templates/render_testing.go b/util/templates/render_testing.go
index 5ad7b912..ab6a63b6 100644
--- a/util/templates/render_testing.go
+++ b/util/templates/render_testing.go
@@ -48,6 +48,8 @@ func TestClass(t *testing.T, class Class, instantiate func(t *testing.T, values
 			// we only test one modbus setup
 			if slices.Contains(modbusChoices, ModbusChoiceTCPIP) {
 				values[ModbusKeyTCPIP] = true
+			} else if slices.Contains(modbusChoices, ModbusChoiceUDP) {
+				values[ModbusKeyUDP] = true
 			} else {
 				values[ModbusKeyRS485TCPIP] = true
 			}