Repository: evcc-io/evcc
Pull request #11915 (2024-01-28 14:24:04)

Docs: Improved tesla templates
- more detailed description
- add "latest" marker for clearer distinction in docs/configure/configui


Patch:
diff --git a/charger/warp2.go b/charger/warp2.go
index dd15be57..ed361514 100644
--- a/charger/warp2.go
+++ b/charger/warp2.go
@@ -145,12 +145,12 @@ func NewWarp2(mqttconf mqtt.Config, topic, emTopic string, timeout time.Duration
 		return nil, err
 	}
 
-	wb.emStateG, err = to.StringGetter(mq("%s/energy_manager/state", emTopic))
+	wb.emStateG, err = to.StringGetter(mq("%s/power_manager/state", emTopic))
 	if err != nil {
 		return nil, err
 	}
 	wb.phasesS, err = provider.NewMqtt(log, client,
-		fmt.Sprintf("%s/energy_manager/external_control_update", emTopic), 0).
+		fmt.Sprintf("%s/power_manager/external_control_update", emTopic), 0).
 		WithPayload(`{ "phases_wanted": ${phases} }`).
 		IntSetter("phases")
 	if err != nil {
diff --git a/cmd/detect/tasks/modbus.go b/cmd/detect/tasks/modbus.go
index ab4d4538..c9bf5e4c 100644
--- a/cmd/detect/tasks/modbus.go
+++ b/cmd/detect/tasks/modbus.go
@@ -12,7 +12,6 @@ import (
 	"github.com/evcc-io/evcc/util/modbus"
 	gridx "github.com/grid-x/modbus"
 	"github.com/volkszaehler/mbmd/meters"
-	"github.com/volkszaehler/mbmd/meters/rs485"
 	"github.com/volkszaehler/mbmd/meters/sunspec"
 )
 
@@ -56,7 +55,7 @@ func ModbusHandlerFactory(conf map[string]interface{}) (TaskHandler, error) {
 	}
 
 	if handler.Register.Address > 0 {
-		handler.op, err = modbus.RegisterOperation(handler.Register)
+		handler.op, err = handler.Register.Operation()
 	}
 
 	return &handler, err
@@ -70,7 +69,7 @@ type ModbusHandler struct {
 	Register modbus.Register `mapstructure:",squash"`
 	Values   []int
 	Invalid  []int
-	op       rs485.Operation
+	op       modbus.RegisterOperation
 	Timeout  time.Duration
 }
 
@@ -80,9 +79,9 @@ func (h *ModbusHandler) testRegister(_ *util.Logger, conn gridx.Client) bool {
 
 	switch h.op.FuncCode {
 	case gridx.FuncCodeReadHoldingRegisters:
-		bytes, err = conn.ReadHoldingRegisters(h.op.OpCode, h.op.ReadLen)
+		bytes, err = conn.ReadHoldingRegisters(h.op.Addr, h.op.Length)
 	case gridx.FuncCodeReadInputRegisters:
-		bytes, err = conn.ReadInputRegisters(h.op.OpCode, h.op.ReadLen)
+		bytes, err = conn.ReadInputRegisters(h.op.Addr, h.op.Length)
 	}
 
 	if err != nil {
@@ -94,7 +93,7 @@ func (h *ModbusHandler) testRegister(_ *util.Logger, conn gridx.Client) bool {
 	}
 
 	var u uint64
-	switch h.op.ReadLen {
+	switch h.op.Length {
 	case 1:
 		u = uint64(binary.BigEndian.Uint16(bytes))
 	case 2:
@@ -200,7 +199,7 @@ func (h *ModbusHandler) Test(log *util.Logger, in ResultDetails) (res []ResultDe
 		}
 
 		var ok bool
-		if h.op.OpCode > 0 {
+		if h.op.Addr > 0 {
 			// log.DEBUG.Printf("slave id: %d op: %v", slaveID, h.op)
 			ok = h.testRegister(log, conn.ModbusClient())
 		} else {
diff --git a/meter/mbmd.go b/meter/mbmd.go
index e869ed6d..fbff8601 100644
--- a/meter/mbmd.go
+++ b/meter/mbmd.go
@@ -96,14 +96,16 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
 		device: device,
 	}
 
-	if err := modbus.ParseOperation(device, cc.Power, &m.opPower); err != nil {
+	m.opPower, err = modbus.ParseOperation(device, cc.Power)
+	if err != nil {
 		return nil, fmt.Errorf("invalid measurement for power: %s", cc.Power)
 	}
 
 	// decorate energy
 	var totalEnergy func() (float64, error)
 	if cc.Energy != "" {
-		if err := modbus.ParseOperation(device, cc.Energy, &m.opEnergy); err != nil {
+		m.opEnergy, err = modbus.ParseOperation(device, cc.Energy)
+		if err != nil {
 			return nil, fmt.Errorf("invalid measurement for energy: %s", cc.Energy)
 		}
 
@@ -131,7 +133,8 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
 	// decorate soc
 	var soc func() (float64, error)
 	if cc.Soc != "" {
-		if err := modbus.ParseOperation(device, cc.Soc, &m.opSoc); err != nil {
+		m.opSoc, err = modbus.ParseOperation(device, cc.Soc)
+		if err != nil {
 			return nil, fmt.Errorf("invalid measurement for soc: %s", cc.Soc)
 		}
 
@@ -150,9 +153,8 @@ func (m *ModbusMbmd) buildPhaseProviders(readings []string) (func() (float64, fl
 
 		phases := make([]func() (float64, error), 0, 3)
 		for idx, reading := range readings {
-			var opCurrent modbus.Operation
-
-			if err := modbus.ParseOperation(m.device, reading, &opCurrent); err != nil {
+			opCurrent, err := modbus.ParseOperation(m.device, reading)
+			if err != nil {
 				return nil, fmt.Errorf("invalid measurement [%d]: %s", idx, reading)
 			}
 
diff --git a/provider/modbus.go b/provider/modbus.go
index 6ece5b61..ccd00b87 100644
--- a/provider/modbus.go
+++ b/provider/modbus.go
@@ -3,7 +3,6 @@ package provider
 import (
 	"bytes"
 	"encoding/binary"
-	"errors"
 	"fmt"
 	"math"
 	"strings"
@@ -12,17 +11,14 @@ import (
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/modbus"
 	gridx "github.com/grid-x/modbus"
-	"github.com/volkszaehler/mbmd/meters"
-	"github.com/volkszaehler/mbmd/meters/sunspec"
 )
 
 // Modbus implements modbus RTU and TCP access
 type Modbus struct {
-	log    *util.Logger
-	conn   *modbus.Connection
-	device meters.Device
-	op     modbus.Operation
-	scale  float64
+	log   *util.Logger
+	conn  *modbus.Connection
+	op    modbus.RegisterOperation
+	scale float64
 }
 
 func init() {
@@ -32,10 +28,8 @@ func init() {
 // NewModbusFromConfig creates Modbus plugin
 func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
 	cc := struct {
-		Model           string
 		modbus.Settings `mapstructure:",squash"`
 		Register        modbus.Register
-		Value           string
 		Scale           float64
 		Delay           time.Duration
 		ConnectDelay    time.Duration
@@ -48,12 +42,6 @@ func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
 		return nil, err
 	}
 
-	// assume RTU if not set and this is a known RS485 meter model
-	if cc.RTU == nil {
-		b := modbus.IsRS485(cc.Model)
-		cc.RTU = &b
-	}
-
 	conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
 	if err != nil {
 		return nil, err
@@ -77,130 +65,47 @@ func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
 	log := util.NewLogger("modbus")
 	conn.Logger(log.TRACE)
 
-	var device meters.Device
-	var op modbus.Operation
-
-	if (cc.Value == "") == (cc.Register.Decode == "") {
-		return nil, errors.New("either value or register required")
-	}
-
-	if cc.Model == "" && cc.Value != "" {
-		return nil, errors.New("need device model when value configured")
-	}
-
-	// no registered configured - need device
-	if cc.Register.Decode == "" {
-		device, err = modbus.NewDevice(cc.Model, cc.SubDevice)
-
-		// prepare device
-		if err == nil {
-			err = device.Initialize(conn)
-
-			// silence KOSTAL implementation errors
-			if errors.Is(err, meters.ErrPartiallyOpened) {
-				err = nil
-			}
-		}
-
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	// model + value configured
-	if cc.Value != "" {
-		if err := modbus.ParseOperation(device, cc.Value, &op); err != nil {
-			return nil, fmt.Errorf("invalid value %s", cc.Value)
-		}
+	if err := cc.Register.Error(); err != nil {
+		return nil, err
 	}
 
-	// register configured
-	if cc.Register.Decode != "" {
-		if op.MBMD, err = modbus.RegisterOperation(cc.Register); err != nil {
-			return nil, err
-		}
+	op, err := cc.Register.Operation()
+	if err != nil {
+		return nil, err
 	}
 
 	mb := &Modbus{
-		log:    log,
-		conn:   conn,
-		device: device,
-		op:     op,
-		scale:  cc.Scale,
+		log:   log,
+		conn:  conn,
+		op:    op,
+		scale: cc.Scale,
 	}
 	return mb, nil
 }
 
 func (m *Modbus) bytesGetter() ([]byte, error) {
-	if op := m.op.MBMD; op.FuncCode != 0 {
-		switch op.FuncCode {
-		case gridx.FuncCodeReadHoldingRegisters:
-			return m.conn.ReadHoldingRegisters(op.OpCode, op.ReadLen)
+	switch m.op.FuncCode {
+	case gridx.FuncCodeReadHoldingRegisters:
+		return m.conn.ReadHoldingRegisters(m.op.Addr, m.op.Length)
 
-		case gridx.FuncCodeReadInputRegisters:
-			return m.conn.ReadInputRegisters(op.OpCode, op.ReadLen)
+	case gridx.FuncCodeReadInputRegisters:
+		return m.conn.ReadInputRegisters(m.op.Addr, m.op.Length)
 
-		case gridx.FuncCodeReadCoils:
-			return m.conn.ReadCoils(op.OpCode, op.ReadLen)
+	case gridx.FuncCodeReadCoils:
+		return m.conn.ReadCoils(m.op.Addr, m.op.Length)
 
-		default:
-			return nil, fmt.Errorf("invalid read function code: %d", op.FuncCode)
-		}
+	default:
+		return nil, fmt.Errorf("invalid read function code: %d", m.op.FuncCode)
 	}
-
-	return nil, errors.New("expected rtu reading")
 }
 
 func (m *Modbus) floatGetter() (f float64, err error) {
-	defer func() {
-		if r := recover(); r != nil {
-			err = fmt.Errorf("panic: %v", r)
-		}
-	}()
-
-	var res meters.MeasurementResult
-
-	// if funccode is configured, execute the read directly
-	if op := m.op.MBMD; op.FuncCode != 0 {
-		var bytes []byte
-		if bytes, err = m.bytesGetter(); err != nil {
-			return 0, fmt.Errorf("read failed: %w", err)
-		}
-
-		return m.scale * op.Transform(bytes), nil
-	}
-
-	// if funccode is not configured, try find the reading on sunspec
-	if dev, ok := m.device.(*sunspec.SunSpec); ok {
-		if m.op.MBMD.IEC61850 != 0 {
-			res, err = dev.QueryOp(m.conn, m.op.MBMD.IEC61850)
-		} else {
-			if res.Value, err = dev.QueryPoint(
-				m.conn,
-				m.op.SunSpec.Model,
-				m.op.SunSpec.Block,
-				m.op.SunSpec.Point,
-			); err != nil {
-				err = fmt.Errorf("model %d block %d point %s: %w", m.op.SunSpec.Model, m.op.SunSpec.Block, m.op.SunSpec.Point, err)
-			}
-		}
-	}
-
-	// silence NaN reading errors by assuming zero
-	if err != nil && errors.Is(err, meters.ErrNaN) {
-		res.Value = 0
-		err = nil
-	}
-
-	if err == nil {
-		if m.op.MBMD.IEC61850 != 0 {
-			m.log.TRACE.Printf("%s: %v", m.op.MBMD.IEC61850, res.Value)
-		} else {
-			m.log.TRACE.Printf("%d:%d:%s: %v", m.op.SunSpec.Model, m.op.SunSpec.Block, m.op.SunSpec.Point, res.Value)
-		}
+	bytes, err := m.bytesGetter()
+	if err != nil {
+		return 0, fmt.Errorf("read failed: %w", err)
 	}
 
-	return m.scale * res.Value, err
+	return m.scale * m.op.Decode(bytes), nil
 }
 
 var _ FloatProvider = (*Modbus)(nil)
@@ -236,24 +141,6 @@ func (m *Modbus) StringGetter() (func() (string, error), error) {
 	}, nil
 }
 
-// UintFromBytes converts byte slice to bigendian uint value
-func UintFromBytes(bytes []byte) (u uint64, err error) {
-	switch l := len(bytes); l {
-	case 1:
-		u = uint64(bytes[0])
-	case 2:
-		u = uint64(binary.BigEndian.Uint16(bytes))
-	case 4:
-		u = uint64(binary.BigEndian.Uint32(bytes))
-	case 8:
-		u = binary.BigEndian.Uint64(bytes)
-	default:
-		err = fmt.Errorf("unexpected length: %d", l)
-	}
-
-	return u, err
-}
-
 var _ BoolProvider = (*Modbus)(nil)
 
 // BoolGetter executes configured modbus read operation and implements IntProvider
@@ -271,35 +158,58 @@ func (m *Modbus) BoolGetter() (func() (bool, error), error) {
 
 var _ SetFloatProvider = (*Modbus)(nil)
 
-// FloatSetter executes configured modbus write operation and implements SetFloatProvider
-func (m *Modbus) FloatSetter(_ string) (func(float64) error, error) {
-	op := m.op.MBMD
-	if op.FuncCode == 0 {
-		return nil, errors.New("modbus plugin does not support writing to sunspec")
+func (m *Modbus) writeMultipleRegisters(val uint64) error {
+	val = m.op.Encode(val)
+
+	var err error
+	switch m.op.Length {
+	case 1:
+		var b [2]byte
+		binary.BigEndian.PutUint16(b[:], uint16(val))
+		_, err = m.conn.WriteMultipleRegisters(m.op.Addr, 1, b[:])
+
+	case 2:
+		var b [4]byte
+		binary.BigEndian.PutUint32(b[:], uint32(val))
+		_, err = m.conn.WriteMultipleRegisters(m.op.Addr, 2, b[:])
+
+	case 4:
+		var b [8]byte
+		binary.BigEndian.PutUint64(b[:], val)
+		_, err = m.conn.WriteMultipleRegisters(m.op.Addr, 4, b[:])
+
+	default:
+		err = fmt.Errorf("invalid write length: %d", m.op.Length)
 	}
 
+	return err
+}
+
+// FloatSetter executes configured modbus write operation and implements SetFloatProvider
+func (m *Modbus) FloatSetter(_ string) (func(float64) error, error) {
 	// need multiple registers for float
-	if op.FuncCode != gridx.FuncCodeWriteMultipleRegisters {
-		return nil, fmt.Errorf("invalid write function code: %d", op.FuncCode)
+	if m.op.FuncCode != gridx.FuncCodeWriteMultipleRegisters {
+		return nil, fmt.Errorf("invalid write function code: %d", m.op.FuncCode)
 	}
 
 	return func(val float64) error {
 		val = m.scale * val
 
-		var err error
-		switch op.ReadLen {
+		var uval uint64
+		switch m.op.Length {
 		case 2:
-			var b [4]byte
-			binary.BigEndian.PutUint32(b[:], math.Float32bits(float32(val)))
-			_, err = m.conn.WriteMultipleRegisters(op.OpCode, 2, b[:])
-
+			uval = uint64(math.Float32bits(float32(val)))
 		case 4:
-			var b [8]byte
-			binary.BigEndian.PutUint64(b[:], math.Float64bits(val))
-			_, err = m.conn.WriteMultipleRegisters(op.OpCode, 4, b[:])
+			uval = math.Float64bits(val)
+		}
+
+		var err error
+		switch m.op.FuncCode {
+		case gridx.FuncCodeWriteMultipleRegisters:
+			err = m.writeMultipleRegisters(uval)
 
 		default:
-			err = fmt.Errorf("invalid write length: %d", op.ReadLen)
+			err = fmt.Errorf("invalid write function code: %d", m.op.FuncCode)
 		}
 
 		return err
@@ -310,40 +220,16 @@ var _ SetIntProvider = (*Modbus)(nil)
 
 // IntSetter executes configured modbus write operation and implements SetIntProvider
 func (m *Modbus) IntSetter(_ string) (func(int64) error, error) {
-	op := m.op.MBMD
-	if op.FuncCode == 0 {
-		return nil, errors.New("modbus plugin does not support writing to sunspec")
-	}
-
 	return func(val int64) error {
 		ival := int64(m.scale * float64(val))
 
-		// if funccode is configured, execute the read directly
 		var err error
-		switch op.FuncCode {
+		switch m.op.FuncCode {
 		case gridx.FuncCodeWriteSingleRegister:
-			_, err = m.conn.WriteSingleRegister(op.OpCode, uint16(ival))
+			_, err = m.conn.WriteSingleRegister(m.op.Addr, uint16(ival))
 
 		case gridx.FuncCodeWriteMultipleRegisters:
-			switch op.ReadLen {
-			case 1:
-				var b [2]byte
-				binary.BigEndian.PutUint16(b[:], uint16(ival))
-				_, err = m.conn.WriteMultipleRegisters(op.OpCode, 1, b[:])
-
-			case 2:
-				var b [4]byte
-				binary.BigEndian.PutUint32(b[:], uint32(ival))
-				_, err = m.conn.WriteMultipleRegisters(op.OpCode, 2, b[:])
-
-			case 4:
-				var b [8]byte
-				binary.BigEndian.PutUint64(b[:], uint64(ival))
-				_, err = m.conn.WriteMultipleRegisters(op.OpCode, 4, b[:])
-
-			default:
-				err = fmt.Errorf("invalid write length: %d", op.ReadLen)
-			}
+			err = m.writeMultipleRegisters(uint64(ival))
 
 		case gridx.FuncCodeWriteSingleCoil:
 			if ival != 0 {
@@ -351,10 +237,10 @@ func (m *Modbus) IntSetter(_ string) (func(int64) error, error) {
 				// and 0x0000 for OFF
 				ival = 0xFF00
 			}
-			_, err = m.conn.WriteSingleCoil(op.OpCode, uint16(ival))
+			_, err = m.conn.WriteSingleCoil(m.op.Addr, uint16(ival))
 
 		default:
-			err = fmt.Errorf("invalid write function code: %d", op.FuncCode)
+			err = fmt.Errorf("invalid write function code: %d", m.op.FuncCode)
 		}
 
 		return err
diff --git a/provider/modbus_helper.go b/provider/modbus_helper.go
new file mode 100644
index 00000000..f48dd277
--- /dev/null
+++ b/provider/modbus_helper.go
@@ -0,0 +1,24 @@
+package provider
+
+import (
+	"encoding/binary"
+	"fmt"
+)
+
+// UintFromBytes converts byte slice to bigendian uint value
+func UintFromBytes(bytes []byte) (u uint64, err error) {
+	switch l := len(bytes); l {
+	case 1:
+		u = uint64(bytes[0])
+	case 2:
+		u = uint64(binary.BigEndian.Uint16(bytes))
+	case 4:
+		u = uint64(binary.BigEndian.Uint32(bytes))
+	case 8:
+		u = binary.BigEndian.Uint64(bytes)
+	default:
+		err = fmt.Errorf("unexpected length: %d", l)
+	}
+
+	return u, err
+}
diff --git a/provider/modbus_sunspec.go b/provider/modbus_sunspec.go
new file mode 100644
index 00000000..45f8a99b
--- /dev/null
+++ b/provider/modbus_sunspec.go
@@ -0,0 +1,242 @@
+package provider
+
+import (
+	"errors"
+	"fmt"
+	"math"
+	"time"
+
+	sunspec "github.com/andig/gosunspec"
+	"github.com/andig/gosunspec/typelabel"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/volkszaehler/mbmd/meters"
+	sunsdev "github.com/volkszaehler/mbmd/meters/sunspec"
+)
+
+// ModbusSunspec implements modbus RTU and TCP access
+type ModbusSunspec struct {
+	log    *util.Logger
+	conn   *modbus.Connection
+	device *sunsdev.SunSpec
+	op     modbus.SunSpecOperation
+	scale  float64
+}
+
+func init() {
+	registry.Add("sunspec", NewModbusSunspecFromConfig)
+}
+
+// NewModbusSunspecFromConfig creates Modbus plugin
+func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error) {
+	cc := struct {
+		modbus.Settings `mapstructure:",squash"`
+		Value           string
+		Scale           float64
+		Delay           time.Duration
+		ConnectDelay    time.Duration
+		Timeout         time.Duration
+	}{
+		Scale: 1,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	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)
+	}
+
+	// set non-default delay
+	if cc.Delay > 0 {
+		conn.Delay(cc.Delay)
+	}
+
+	// set non-default connect delay
+	if cc.ConnectDelay > 0 {
+		conn.ConnectDelay(cc.ConnectDelay)
+	}
+
+	log := util.NewLogger("sunspec")
+	conn.Logger(log.TRACE)
+
+	if cc.Value == "" {
+		return nil, errors.New("value is required")
+	}
+
+	// silence KOSTAL implementation errors
+	device := sunsdev.NewDevice("sunspec", cc.SubDevice)
+	if err := device.Initialize(conn); err != nil && !errors.Is(err, meters.ErrPartiallyOpened) {
+		return nil, err
+	}
+
+	ops, err := modbus.ParsePoint(cc.Value)
+	if err != nil {
+		return nil, fmt.Errorf("invalid sunspec value: %s", cc.Value)
+	}
+
+	mb := &ModbusSunspec{
+		log:    log,
+		conn:   conn,
+		device: device,
+		scale:  cc.Scale,
+	}
+
+	for _, op := range ops {
+		if _, _, err := device.QueryPointAny(conn, op.Model, op.Block, op.Point); err == nil {
+			mb.op = op
+			return mb, nil
+		}
+	}
+
+	return nil, fmt.Errorf("sunspec model not found: %v", ops)
+}
+
+func (m *ModbusSunspec) floatGetter() (f float64, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("panic: %v", r)
+		}
+	}()
+
+	res, err := m.device.QueryPoint(
+		m.conn,
+		m.op.Model,
+		m.op.Block,
+		m.op.Point,
+	)
+	if err != nil {
+		return 0, fmt.Errorf("model %d block %d point %s: %w", m.op.Model, m.op.Block, m.op.Point, err)
+	}
+
+	m.log.TRACE.Printf("model %d block %d point %s: %v", m.op.Model, m.op.Block, m.op.Point, res)
+
+	return m.scale * res, nil
+}
+
+var _ FloatProvider = (*Modbus)(nil)
+
+// FloatGetter executes configured modbus read operation and implements func() (float64, error)
+func (m *ModbusSunspec) FloatGetter() (func() (f float64, err error), error) {
+	return m.floatGetter, nil
+}
+
+var _ IntProvider = (*Modbus)(nil)
+
+// IntGetter executes configured modbus read operation and implements IntProvider
+func (m *ModbusSunspec) IntGetter() (func() (int64, error), error) {
+	g, err := m.FloatGetter()
+
+	return func() (int64, error) {
+		res, err := g()
+		return int64(math.Round(res)), err
+	}, err
+}
+
+func (m *ModbusSunspec) blockPoint() (block sunspec.Block, point sunspec.Point, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("panic: %v", r)
+		}
+	}()
+
+	block, point, err = m.device.QueryPointAny(
+		m.conn,
+		m.op.Model,
+		m.op.Block,
+		m.op.Point,
+	)
+	if err != nil {
+		err = fmt.Errorf("model %d block %d point %s: %w", m.op.Model, m.op.Block, m.op.Point, err)
+	}
+
+	return block, point, err
+}
+
+// TODO scale factors
+
+var _ SetFloatProvider = (*Modbus)(nil)
+
+// FloatSetter executes configured modbus write operation and implements SetFloatProvider
+func (m *ModbusSunspec) FloatSetter(_ string) (func(float64) error, error) {
+	block, point, err := m.blockPoint()
+	if err != nil {
+		return nil, err
+	}
+
+	typ := point.Type()
+
+	return func(val float64) (err error) {
+		defer func() {
+			if r := recover(); r != nil {
+				err = fmt.Errorf("panic: %v", r)
+			}
+		}()
+
+		val = val * m.scale
+		switch typ {
+		case typelabel.Float32:
+			point.SetFloat32(float32(val))
+		default:
+			return fmt.Errorf("invalid point type: %s", typ)
+		}
+
+		return block.Write(m.op.Point)
+	}, nil
+}
+
+var _ SetIntProvider = (*Modbus)(nil)
+
+// IntSetter executes configured modbus write operation and implements SetIntProvider
+func (m *ModbusSunspec) IntSetter(_ string) (func(int64) error, error) {
+	block, point, err := m.blockPoint()
+	if err != nil {
+		return nil, err
+	}
+
+	typ := point.Type()
+
+	return func(val int64) (err error) {
+		defer func() {
+			if r := recover(); r != nil {
+				err = fmt.Errorf("panic: %v", r)
+			}
+		}()
+
+		val = int64(float64(val) * m.scale)
+
+		switch typ {
+		case typelabel.Bitfield16:
+			point.SetBitfield16(sunspec.Bitfield16(val))
+		case typelabel.Bitfield32:
+			point.SetBitfield32(sunspec.Bitfield32(val))
+		case typelabel.Enum16:
+			point.SetEnum16(sunspec.Enum16(val))
+		case typelabel.Enum32:
+			point.SetEnum32(sunspec.Enum32(val))
+		case typelabel.Int16:
+			point.SetInt16(int16(val))
+		case typelabel.Int32:
+			point.SetInt32(int32(val))
+		case typelabel.Int64:
+			point.SetInt64(val)
+		case typelabel.Uint16:
+			point.SetUint16(uint16(val))
+		case typelabel.Uint32:
+			point.SetUint32(uint32(val))
+		case typelabel.Uint64:
+			point.SetUint64(uint64(val))
+		default:
+			return fmt.Errorf("invalid point type: %s", typ)
+		}
+
+		return block.Write(m.op.Point)
+	}, nil
+}
diff --git a/templates/definition/charger/tinkerforge-warp.yaml b/templates/definition/charger/tinkerforge-warp.yaml
index 8c380533..a294e13a 100644
--- a/templates/definition/charger/tinkerforge-warp.yaml
+++ b/templates/definition/charger/tinkerforge-warp.yaml
@@ -11,8 +11,8 @@ products:
 capabilities: ["mA", "1p3p", "rfid"]
 requirements:
   description:
-    en: Firmware v2 required. Automatic phase switching requires the additional WARP Energy Manager.
-    de: Firmware v2 erforderlich. Für automatische Phasenumschaltung wird zusätzlich der WARP Energy Manager benötigt.
+    en: WARP Firmware v2 required. Automatic phase switching requires the additional WARP Energy Manager.
+    de: WARP Firmware v2 erforderlich. Für automatische Phasenumschaltung wird zusätzlich der WARP Energy Manager benötigt.
   uri: https://docs.evcc.io/docs/devices/chargers#tinkerforge
 params:
   - preset: mqtt
@@ -20,8 +20,8 @@ params:
     default: warp
   - name: energymanager
     help:
-      de: EnergyManager MQTT Topic (falls installiert)
-      en: EnergyManager MQTT topic if installed
+      de: WEM Firmware v2 erforderlich. EnergyManager MQTT Topic (falls installiert)
+      en: WEM Firmware v2 required. EnergyManager MQTT topic (if installed)
 render: |
   type: warp2
   {{ include "mqtt" . }}
diff --git a/templates/definition/meter/fronius-gen24.yaml b/templates/definition/meter/fronius-gen24.yaml
index cb1f5021..2c2ae464 100644
--- a/templates/definition/meter/fronius-gen24.yaml
+++ b/templates/definition/meter/fronius-gen24.yaml
@@ -36,45 +36,34 @@ render: |
   power:
     source: calc
     add:
-    - source: modbus
+    - source: sunspec
       uri: {{ .host }}:{{ .port }}
       id: 1
-      model: sunspec
       value: 160:1:DCW # mppt 1
-    - source: modbus
+    - source: sunspec
       uri: {{ .host }}:{{ .port }}
       id: 1
-      model: sunspec
       value: 160:2:DCW # mppt 2
-  energy:
-    source: modbus
-    uri: {{ .host }}:{{ .port }}
-    id: 1
-    model: sunspec
-    value: Export
   {{- end }}
   {{- if eq .usage "battery" }}
   type: custom
   power:
     source: calc
     add:
-    - source: modbus
+    - source: sunspec
       uri: {{ .host }}:{{ .port }}
       id: 1
-      model: sunspec
       value: 160:3:DCW # mppt 3 charge
       scale: -1
-    - source: modbus
+    - source: sunspec
       uri: {{ .host }}:{{ .port }}
       id: 1
-      model: sunspec
       value: 160:4:DCW # mppt 4 discharge
   soc:
-    source: modbus
+    source: sunspec
     uri: {{ .host }}:{{ .port }}
     id: 1
-    model: sunspec
-    value: ChargeState
+    value: 124:ChaState
   {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/homematic.yaml b/templates/definition/meter/homematic.yaml
index 2e472b6f..a055be3f 100644
--- a/templates/definition/meter/homematic.yaml
+++ b/templates/definition/meter/homematic.yaml
@@ -4,7 +4,7 @@ products:
 group: switchsockets
 params:
   - name: usage
-    choice: ["grid", "pv", "charge"]
+    choice: ["pv", "charge"]
   - name: host
   - name: device
     description:
diff --git a/templates/definition/meter/kostal-plenticore.yaml b/templates/definition/meter/kostal-plenticore.yaml
index 00a476c7..c641b13d 100644
--- a/templates/definition/meter/kostal-plenticore.yaml
+++ b/templates/definition/meter/kostal-plenticore.yaml
@@ -13,9 +13,9 @@ linked:
 requirements:
   description:
     de: |
-      Nur ein System kann und darf auf den Wechselrichter zugreifen!
+      Nur ein System kann und darf auf den Wechselrichter zugreifen! Für die aktive Batteriesteuerung muss die externe Batteriesteuerung über Modbus mit dem Handwerkerzugang aktiviert sein.
     en: |
-      Only a single system may access the inverter!
+      Only a single system may access the inverter! For active battery control, the external battery control via Modbus must be activated using installer access.
 params:
   - name: usage
     choice: ["pv", "battery"]
@@ -50,30 +50,25 @@ render: |
   power:
     source: calc
     add: # The add plugin sums up all string values
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
       value: 160:1:DCW # string 1
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
       value: 160:2:DCW # string 2
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
       value: 160:3:DCW # string 3
   {{- end }}
   {{- if eq .usage "battery" }}
   type: custom
   power:
-    source: modbus
+    source: sunspec
     {{- include "modbus" . | indent 2 }}
-    model: sunspec
     value: 802:W # 802 battery control
   soc:
-    source: modbus
+    source: sunspec
     {{- include "modbus" . | indent 2 }}
-    model: sunspec
     value: 802:SoC # 802 battery control
   limitsoc:
     source: watchdog
diff --git a/templates/definition/meter/sma-hybrid.yaml b/templates/definition/meter/sma-hybrid.yaml
index db12fb90..9d3ad722 100644
--- a/templates/definition/meter/sma-hybrid.yaml
+++ b/templates/definition/meter/sma-hybrid.yaml
@@ -83,7 +83,7 @@ render: |
       decode: uint32nan
   batterymode:
     source: watchdog
-    timeout: {{ .watchdog }} # re-write at timeout/2
+    timeout: {{ .watchdog }}
     reset: 1 # reset watchdog on normal
     set:
       source: switch
diff --git a/templates/definition/meter/sma-inverter-modbus.yaml b/templates/definition/meter/sma-inverter-modbus.yaml
index 3cac3e92..f596b13c 100644
--- a/templates/definition/meter/sma-inverter-modbus.yaml
+++ b/templates/definition/meter/sma-inverter-modbus.yaml
@@ -2,14 +2,10 @@ template: sma-inverter-modbus
 products:
   - brand: SMA
     description:
-      generic: Sunny Island (Modbus)
-  - brand: SMA
-    description:
-      generic: Sunny Boy Storage (Modbus)
-capabilities: ["battery-control"]
+      generic: Wechselrichter (Modbus)
 params:
   - name: usage
-    choice: ["battery"]
+    choice: ["pv"]
   - name: modbus
     choice: ["tcpip"]
     port: 502
@@ -17,12 +13,6 @@ params:
     help:
       en: The Modbus TCP-Server needs to be enabled on this inverter
       de: Der Modbus TCP-Server muss an diesem Wechselrichter aktiviert sein
-  - name: capacity
-    advanced: true
-  - name: watchdog
-    type: duration
-    default: 60s
-    advanced: true
 render: |
   type: custom
   power:
@@ -40,55 +30,3 @@ render: |
       type: holding
       decode: uint64nan
     scale: 0.001
-  {{- if eq .usage "battery" }}
-  soc:
-    source: modbus
-    {{- include "modbus" . | indent 2 }}
-    register:
-      address: 30845 # SMA Modbus Profile: Bat.ChaStt
-      type: holding
-      decode: uint32nan
-  batterymode:
-    source: watchdog
-    timeout: {{ .watchdog }} # re-write at timeout/2
-    reset: 1 # reset watchdog on normal
-    set:
-      source: switch
-      switch:
-      - case: 1 # normal
-        set:
-          source: const
-          value: 10000 # Maximale Wirkleistung
-          set:
-            source: modbus
-            {{- include "modbus" . | indent 10 }}
-            register:
-              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
-              type: writemultiple
-              decode: int32
-      - case: 2 # hold
-        set:
-          source: const
-          value: 0 # Maximale Wirkleistung
-          set:
-            source: modbus
-            {{- include "modbus" . | indent 10 }}
-            register:
-              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
-              type: writemultiple
-              decode: int32
-      - case: 3 # charge
-        set:
-          source: const
-          value: -10000 # Maximale Wirkleistung
-          set:
-            source: modbus
-            {{- include "modbus" . | indent 10 }}
-            register:
-              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
-              type: writemultiple
-              decode: int32
-  {{- if .capacity }}
-  capacity: {{ .capacity }} # kWh
-  {{- end }}
-  {{- end }}
diff --git a/templates/definition/meter/sma-sbs-modbus.yaml b/templates/definition/meter/sma-sbs-modbus.yaml
new file mode 100644
index 00000000..8a3a89e9
--- /dev/null
+++ b/templates/definition/meter/sma-sbs-modbus.yaml
@@ -0,0 +1,89 @@
+template: sma-sbs-modbus
+products:
+  - brand: SMA
+    description:
+      generic: Sunny Boy Storage (Modbus)
+capabilities: ["battery-control"]
+params:
+  - name: usage
+    choice: ["battery"]
+  - name: modbus
+    choice: ["tcpip"]
+    port: 502
+    id: 3
+    help:
+      en: The Modbus TCP-Server needs to be enabled on this inverter
+      de: Der Modbus TCP-Server muss an diesem Wechselrichter aktiviert sein
+  - name: capacity
+    advanced: true
+  - name: watchdog
+    type: duration
+    default: 60s
+    advanced: true
+render: |
+  type: custom
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 30775 # SMA Modbus Profile: GridMs.TotW
+      type: input
+      decode: int32nan
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 30513 # SMA Modbus Profile: Metering.TotWhOut
+      type: holding
+      decode: uint64nan
+    scale: 0.001
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 30845 # SMA Modbus Profile: Bat.ChaStt
+      type: holding
+      decode: uint32nan
+  batterymode:
+    source: watchdog
+    timeout: {{ .watchdog }}
+    reset: 1 # reset watchdog on normal
+    set:
+      source: switch
+      switch:
+      - case: 1 # normal
+        set:
+          source: const
+          value: 10000 # Maximale Wirkleistung 100.00%
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
+              type: writemultiple
+              decode: int32
+      - case: 2 # hold
+        set:
+          source: const
+          value: 0 # Maximale Wirkleistung 0.00%
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
+              type: writemultiple
+              decode: int32
+      - case: 3 # charge
+        set:
+          source: const
+          value: -10000 # Maximale Wirkleistung -100.00%
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
+              type: writemultiple
+              decode: int32
+  {{- if .capacity }}
+  capacity: {{ .capacity }} # kWh
+  {{- end }}
diff --git a/templates/definition/meter/sma-si-modbus.yaml b/templates/definition/meter/sma-si-modbus.yaml
new file mode 100644
index 00000000..a55e7300
--- /dev/null
+++ b/templates/definition/meter/sma-si-modbus.yaml
@@ -0,0 +1,111 @@
+template: sma-si-modbus
+products:
+  - brand: SMA
+    description:
+      generic: Sunny Island (Modbus)
+capabilities: ["battery-control"]
+params:
+  - name: usage
+    choice: ["battery"]
+  - name: modbus
+    choice: ["tcpip"]
+    port: 502
+    id: 3
+    help:
+      en: The Modbus TCP-Server needs to be enabled on this inverter
+      de: Der Modbus TCP-Server muss an diesem Wechselrichter aktiviert sein
+  - name: capacity
+    advanced: true
+  - name: watchdog
+    type: duration
+    default: 60s
+    advanced: true
+render: |
+  type: custom
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 30775 # SMA Modbus Profile: GridMs.TotW
+      type: input
+      decode: int32nan
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 30513 # SMA Modbus Profile: Metering.TotWhOut
+      type: holding
+      decode: uint64nan
+    scale: 0.001
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 30845 # SMA Modbus Profile: Bat.ChaStt
+      type: holding
+      decode: uint32nan
+  batterymode:
+    source: watchdog
+    timeout: {{ .watchdog }}
+    reset: 1 # reset watchdog on normal
+    set:
+      source: switch
+      switch:
+      - case: 1 # normal
+        set:
+          source: const
+          value: 803 # inaktiv (Ina)
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 40151 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WCtlComAct
+              type: writemultiple
+              decode: uint32
+      - case: 2 # hold
+        set:
+          source: sequence
+          set:
+          - source: const
+            value: 0 # Wirkleistungsvorgabe
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40149 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSpt
+                type: writemultiple
+                decode: int32
+          - source: const
+            value: 802 # aktiv (Act)
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40151 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WCtlComAct
+                type: writemultiple
+                decode: uint32
+      - case: 3 # charge
+        set:
+          source: sequence
+          set:
+          - source: const
+            value: -2147483648 # Wirkleistungsvorgabe
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40149 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSpt
+                type: writemultiple
+                decode: int32
+          - source: const
+            value: 802 # aktiv (Act)
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40151 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WCtlComAct
+                type: writemultiple
+                decode: uint32
+  {{- if .capacity }}
+  capacity: {{ .capacity }} # kWh
+  {{- end }}
diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml
index a8da2664..e20eaa75 100644
--- a/templates/definition/meter/solaredge-hybrid.yaml
+++ b/templates/definition/meter/solaredge-hybrid.yaml
@@ -26,10 +26,9 @@ render: |
   type: custom
   {{- if eq .usage "grid" }}
   power:
-    source: modbus
+    source: sunspec
     {{- include "modbus" . | indent 2 }}
     timeout: {{ .timeout }}
-    model: sunspec
     subdevice: 1 # Metering device
     value: 203:W
     scale: -1
@@ -38,11 +37,10 @@ render: |
   power:
     source: calc
     add:
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
       timeout: {{ .timeout }}
-      model: sunspec
-      value: DCPower
+      value: 101|103:DCW
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       timeout: {{ .timeout }}
diff --git a/templates/definition/meter/solaredge-inverter.yaml b/templates/definition/meter/solaredge-inverter.yaml
index 31ea7974..d3fe7e0f 100644
--- a/templates/definition/meter/solaredge-inverter.yaml
+++ b/templates/definition/meter/solaredge-inverter.yaml
@@ -19,24 +19,19 @@ params:
   - name: timeout
 render: |
   type: custom
+  {{- if eq .usage "grid" }}
   power:
-    source: modbus
+    source: sunspec
     {{- include "modbus" . | indent 2 }}
     timeout: {{ .timeout }}
-  {{- if eq .usage "grid" }}
-    model: sunspec
     subdevice: 1
     value: 203:W # sunspec 3-phase meter power reading
     scale: -1
-  {{- else }}
-    model: sunspec
-    value: Power
   {{- end }}
   {{- if eq .usage "pv" }}
-  energy:
-    source: modbus
+  power:
+    source: sunspec
     {{- include "modbus" . | indent 2 }}
     timeout: {{ .timeout }}
-    model: sunspec
-    value: Export
+    value: 101|103:W # sunspec inverter AC power reading
   {{- end }}
diff --git a/templates/definition/meter/sunspec-hybrid.yaml b/templates/definition/meter/sunspec-hybrid.yaml
index 37474a58..1a4f46df 100644
--- a/templates/definition/meter/sunspec-hybrid.yaml
+++ b/templates/definition/meter/sunspec-hybrid.yaml
@@ -65,50 +65,45 @@ render: |
   power:
     source: calc
     add:
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
-      value: DCPowerS1 # mppt 1
-    - source: modbus
+      value: 160:1:DCW # mppt 1
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
-      value: DCPowerS2 # mppt 2
+      value: 160:2:DCW # mppt 2
   energy:
     source: calc
     add:
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
-      value: DCEnergyS1 # mppt 1
-    - source: modbus
+      value: 160:1:DCWH # mppt 1
+      scale: 0.001
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
-      value: DCEnergyS2 # mppt 2
+      value: 160:2:DCWH # mppt 2
+      scale: 0.001
   {{- end }}
   {{- if eq .usage "battery" }}
   type: custom
   power:
     source: calc
     add:
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
-      value: DCPowerS3 # mppt 3 (charge)
+      value: 160:3:DCW # mppt 3 (charge)
       scale: -1
-    - source: modbus
+    - source: sunspec
       {{- include "modbus" . | indent 4 }}
-      model: sunspec
-      value: DCPowerS4 # mppt 4 (discharge)
+      value: 160:4:DCW # mppt 4 (discharge)
   energy:
-    source: modbus
+    source: sunspec
     {{- include "modbus" . | indent 2 }}
-    model: sunspec
-    value: DCEnergyS4 # mppt 4 (discharge)
+    value: 160:4:DCWH # mppt 4 (discharge)
+    scale: 0.001
   soc:
-    source: modbus
+    source: sunspec
     {{- include "modbus" . | indent 2 }}
-    model: sunspec
-    value: ChargeState
+    value: 124:ChaState
   {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/sunspec-inverter.yaml b/templates/definition/meter/sunspec-inverter.yaml
index 85d723d3..031227db 100644
--- a/templates/definition/meter/sunspec-inverter.yaml
+++ b/templates/definition/meter/sunspec-inverter.yaml
@@ -63,7 +63,7 @@ render: |
   power: Power
   energy: Export
   {{- if eq .usage "battery" }}
-  soc: ChargeState
+  soc: 124:ChaState
   {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/vehicle/tesla-command.yaml b/templates/definition/vehicle/tesla-command.yaml
index c6708da8..9b7275c5 100644
--- a/templates/definition/vehicle/tesla-command.yaml
+++ b/templates/definition/vehicle/tesla-command.yaml
@@ -2,15 +2,20 @@ template: tesla-command
 products:
   - brand: Tesla
     description:
-      generic: Vehicle-Command API
+      de: aktuell
+      en: latest
 requirements:
   description:
     de: |
-      Es wird ein `access` und ein `refresh` Token für die Kommunikation mit der Tesla API erstellt werden.
-      Die Tokens werden unter https://tesla.evcc.io erstellt.
+      Du benötigst ein `access` und ein `refresh` Token.
+      Diese kannst du über [tesla.evcc.io](https://tesla.evcc.io) erstellt.
+
+      **Hinweis:** Dies ist die von Tesla offiziell unterstützte [FleetAPI](https://developer.tesla.com/docs/fleet-api).
     en: |
-      You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
-      Tokens are generated using https://tesla.evcc.io.
+      You need an `access` and a `refresh` token.
+      Please generate them through [tesla.evcc.io](https://tesla.evcc.io).
+
+      **Note:** This is Tesla's officially supported [FleetAPI](https://developer.tesla.com/docs/fleet-api).
 params:
   - name: title
   - name: icon
diff --git a/templates/definition/vehicle/tesla.yaml b/templates/definition/vehicle/tesla.yaml
index b16be511..07c3713b 100644
--- a/templates/definition/vehicle/tesla.yaml
+++ b/templates/definition/vehicle/tesla.yaml
@@ -1,6 +1,9 @@
 template: tesla
 products:
   - brand: Tesla
+    description:
+      de: veraltet
+      en: legacy
 requirements:
   description:
     de: |
@@ -10,6 +13,8 @@ requirements:
       - [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone)
       - [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens)
       - [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth)
+
+      **Hinweis:** Diese API ist von Tesla [nicht offiziell unterstützt](https://tesla-api.timdorr.com) und wird Stück für Stück abgeschaltet.**
     en: |
       You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
 
@@ -17,6 +22,8 @@ requirements:
       - [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone)
       - [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens)
       - [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth)
+
+      **Note:** This API is [not officially supported](https://tesla-api.timdorr.com) by Tesla and will be deprecated step by step.
 params:
   - name: title
   - name: icon
diff --git a/templates/definition/vehicle/volvo-connected.yaml b/templates/definition/vehicle/volvo-connected.yaml
index 3caf2b93..3baf3582 100644
--- a/templates/definition/vehicle/volvo-connected.yaml
+++ b/templates/definition/vehicle/volvo-connected.yaml
@@ -1,6 +1,9 @@
 template: volvo-connected
 products:
   - brand: Volvo
+    description:
+      de: aktuell
+      en: latest
 params:
   - preset: vehicle-base
   - preset: vehicle-identify
diff --git a/templates/definition/vehicle/volvo.yaml b/templates/definition/vehicle/volvo.yaml
index afed3c3b..16fb436c 100644
--- a/templates/definition/vehicle/volvo.yaml
+++ b/templates/definition/vehicle/volvo.yaml
@@ -2,7 +2,8 @@ template: volvo
 products:
   - brand: Volvo
     description:
-      generic: legacy
+      de: veraltet
+      en: legacy
 params:
   - preset: vehicle-base
   - preset: vehicle-identify
diff --git a/util/modbus/mbmd.go b/util/modbus/mbmd.go
new file mode 100644
index 00000000..7ceeece8
--- /dev/null
+++ b/util/modbus/mbmd.go
@@ -0,0 +1,37 @@
+package modbus
+
+import (
+	"github.com/volkszaehler/mbmd/meters"
+	"github.com/volkszaehler/mbmd/meters/rs485"
+)
+
+// Operation is a register-based or sunspec modbus operation
+type Operation struct {
+	MBMD    rs485.Operation
+	SunSpec SunSpecOperation
+}
+
+// ParseOperation parses an MBMD measurement or SunsSpec point definition into a modbus operation
+func ParseOperation(dev meters.Device, measurement string) (Operation, error) {
+	var (
+		op  Operation
+		err error
+	)
+
+	// if measurement cannot be parsed it could be SunSpec model/block/point
+	op.MBMD.IEC61850, err = meters.MeasurementString(measurement)
+	if err != nil {
+		suns, err := ParsePoint(measurement)
+		if err == nil {
+			op.SunSpec = suns[0]
+		}
+		return op, err
+	}
+
+	// for RS485 check if producer supports the measurement
+	if dev, ok := dev.(*rs485.RS485); ok {
+		op.MBMD, err = RS485FindDeviceOp(dev, op.MBMD.IEC61850)
+	}
+
+	return op, err
+}
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index 7600be79..c71dbc67 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -3,19 +3,14 @@ package modbus
 import (
 	"errors"
 	"fmt"
-	"math"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
 
 	"github.com/evcc-io/evcc/util"
-	"github.com/grid-x/modbus"
-	"github.com/volkszaehler/mbmd/encoding"
 	"github.com/volkszaehler/mbmd/meters"
 	"github.com/volkszaehler/mbmd/meters/rs485"
 	"github.com/volkszaehler/mbmd/meters/sunspec"
-	"golang.org/x/exp/constraints"
 )
 
 type Protocol int
@@ -343,164 +338,3 @@ func RS485FindDeviceOp(device *rs485.RS485, measurement meters.Measurement) (op
 
 	return op, fmt.Errorf("unsupported measurement: %s", measurement.String())
 }
-
-// Register contains the ModBus register configuration
-type Register struct {
-	Address uint16 // Length  uint16
-	Type    string
-	Decode  string
-	BitMask string
-}
-
-// asFloat64 creates a function that returns numerics vales as float64
-func asFloat64[T constraints.Signed | constraints.Unsigned | constraints.Float](f func([]byte) T) func([]byte) float64 {
-	return func(v []byte) float64 {
-		res := float64(f(v))
-		if math.IsNaN(res) || math.IsInf(res, 0) {
-			res = 0
-		}
-		return res
-	}
-}
-
-// RegisterOperation creates a read operation from a register definition
-func RegisterOperation(r Register) (rs485.Operation, error) {
-	op := rs485.Operation{
-		OpCode:  r.Address,
-		ReadLen: 2,
-	}
-
-	switch strings.ToLower(r.Type) {
-	case "holding":
-		op.FuncCode = modbus.FuncCodeReadHoldingRegisters
-	case "input":
-		op.FuncCode = modbus.FuncCodeReadInputRegisters
-	case "coil":
-		op.FuncCode = modbus.FuncCodeReadCoils
-		r.Decode = "bool8"
-	case "writesingle", "writeholding":
-		op.FuncCode = modbus.FuncCodeWriteSingleRegister
-	case "writemultiple", "writeholdings":
-		op.FuncCode = modbus.FuncCodeWriteMultipleRegisters
-	case "writecoil":
-		op.FuncCode = modbus.FuncCodeWriteSingleCoil
-		r.Decode = "bool8"
-	default:
-		return rs485.Operation{}, fmt.Errorf("invalid register type: %s", r.Type)
-	}
-
-	switch strings.ToLower(r.Decode) {
-	// 8 bit (coil)
-	case "bool8":
-		op.Transform = decodeBool8
-		op.ReadLen = 1
-
-	// 16 bit
-	case "int16":
-		op.Transform = asFloat64(encoding.Int16)
-		op.ReadLen = 1
-	case "int16nan":
-		op.Transform = decodeNaN16(asFloat64(encoding.Int16), 1<<15, 1<<15-1)
-		op.ReadLen = 1
-	case "uint16":
-		op.Transform = asFloat64(encoding.Uint16)
-		op.ReadLen = 1
-	case "uint16nan":
-		op.Transform = decodeNaN16(asFloat64(encoding.Uint16), 1<<16-1)
-		op.ReadLen = 1
-	case "bool16":
-		mask, err := decodeMask(r.BitMask)
-		if err != nil {
-			return op, err
-		}
-		op.Transform = decodeBool16(mask)
-		op.ReadLen = 1
-
-	// 32 bit
-	case "int32":
-		op.Transform = asFloat64(encoding.Int32)
-	case "int32nan":
-		op.Transform = decodeNaN32(asFloat64(encoding.Int32), 1<<31, 1<<31-1)
-	case "int32s":
-		op.Transform = asFloat64(encoding.Int32LswFirst)
-	case "uint32":
-		op.Transform = asFloat64(encoding.Uint32)
-	case "uint32s":
-		op.Transform = asFloat64(encoding.Uint32LswFirst)
-	case "uint32nan":
-		op.Transform = decodeNaN32(asFloat64(encoding.Uint32), 1<<32-1)
-	case "float32", "ieee754":
-		op.Transform = asFloat64(encoding.Float32)
-	case "float32s", "ieee754s":
-		op.Transform = asFloat64(encoding.Float32LswFirst)
-
-	// 64 bit
-	case "uint64":
-		op.Transform = asFloat64(encoding.Uint64)
-		op.ReadLen = 4
-	case "uint64nan":
-		op.Transform = decodeNaN64(asFloat64(encoding.Uint64), 1<<64-1)
-		op.ReadLen = 4
-	case "float64":
-		op.Transform = encoding.Float64
-		op.ReadLen = 4
-
-	default:
-		return rs485.Operation{}, fmt.Errorf("invalid register decoding: %s", r.Decode)
-	}
-
-	return op, nil
-}
-
-// SunSpecOperation is a sunspec modbus operation
-type SunSpecOperation struct {
-	Model, Block int
-	Point        string
-}
-
-// ParsePoint parses sunspec point from string
-func ParsePoint(selector string) (model, block int, point string, err error) {
-	err = fmt.Errorf("invalid point: %s", selector)
-
-	el := strings.Split(selector, ":")
-	if len(el) < 2 || len(el) > 3 {
-		return
-	}
-
-	if model, err = strconv.Atoi(el[0]); err != nil {
-		return
-	}
-
-	if len(el) == 3 {
-		// block is the middle element
-		if block, err = strconv.Atoi(el[1]); err != nil {
-			return
-		}
-	}
-
-	point = el[len(el)-1]
-
-	return model, block, point, nil
-}
-
-// Operation is a register-based or sunspec modbus operation
-type Operation struct {
-	MBMD    rs485.Operation
-	SunSpec SunSpecOperation
-}
-
-// ParseOperation parses an MBMD measurement or SunsSpec point definition into a modbus operation
-func ParseOperation(dev meters.Device, measurement string, op *Operation) (err error) {
-	// if measurement cannot be parsed it could be SunSpec model/block/point
-	if op.MBMD.IEC61850, err = meters.MeasurementString(measurement); err != nil {
-		op.SunSpec.Model, op.SunSpec.Block, op.SunSpec.Point, err = ParsePoint(measurement)
-		return err
-	}
-
-	// for RS485 check if producer supports the measurement
-	if dev, ok := dev.(*rs485.RS485); ok {
-		op.MBMD, err = RS485FindDeviceOp(dev, op.MBMD.IEC61850)
-	}
-
-	return err
-}
diff --git a/util/modbus/register.go b/util/modbus/register.go
new file mode 100644
index 00000000..359215e7
--- /dev/null
+++ b/util/modbus/register.go
@@ -0,0 +1,180 @@
+package modbus
+
+import (
+	"errors"
+	"fmt"
+	"math"
+	"slices"
+	"strings"
+
+	"github.com/grid-x/modbus"
+	"github.com/volkszaehler/mbmd/encoding"
+	"golang.org/x/exp/constraints"
+)
+
+// Register contains the ModBus register configuration
+type Register struct {
+	Address  uint16 // Length  uint16
+	Type     string
+	Decode   string // TODO deprecated, use Encoding
+	Encoding string
+	BitMask  string
+}
+
+func (r Register) Error() error {
+	if r.Address == 0 {
+		return errors.New("address is required")
+	}
+	if r.Type == "" {
+		return errors.New("type is required")
+	}
+	if r.Decode == "" && r.Encoding == "" {
+		return errors.New("encoding is required")
+	}
+	if r.Decode != "" && r.Encoding != "" {
+		return errors.New("must not have decide when encoding is specified")
+	}
+	return nil
+}
+
+func (r Register) encoding() string {
+	if r.Encoding != "" {
+		return r.Encoding
+	}
+	return r.Decode
+}
+
+// Operation creates a modbus operation from a register definition
+func (r Register) Operation() (RegisterOperation, error) {
+	op := RegisterOperation{
+		Addr: r.Address,
+	}
+
+	switch strings.ToLower(r.Type) {
+	case "holding":
+		op.FuncCode = modbus.FuncCodeReadHoldingRegisters
+	case "input":
+		op.FuncCode = modbus.FuncCodeReadInputRegisters
+	case "coil":
+		op.FuncCode = modbus.FuncCodeReadCoils
+	case "writesingle", "writeholding":
+		op.FuncCode = modbus.FuncCodeWriteSingleRegister
+	case "writemultiple", "writeholdings":
+		op.FuncCode = modbus.FuncCodeWriteMultipleRegisters
+	case "writecoil":
+		op.FuncCode = modbus.FuncCodeWriteSingleCoil
+	default:
+		return RegisterOperation{}, fmt.Errorf("invalid register type: %s", r.Type)
+	}
+
+	if op.IsRead() {
+		switch strings.ToLower(r.encoding()) {
+		// 8 bit (coil)
+		case "bool8":
+			op.Decode = decodeBool8
+			op.Length = 1
+
+		// 16 bit
+		case "int16":
+			op.Decode = asFloat64(encoding.Int16)
+			op.Length = 1
+		case "int16nan":
+			op.Decode = decodeNaN16(asFloat64(encoding.Int16), 1<<15, 1<<15-1)
+			op.Length = 1
+		case "uint16":
+			op.Decode = asFloat64(encoding.Uint16)
+			op.Length = 1
+		case "uint16nan":
+			op.Decode = decodeNaN16(asFloat64(encoding.Uint16), 1<<16-1)
+			op.Length = 1
+		case "bool16":
+			mask, err := decodeMask(r.BitMask)
+			if err != nil {
+				return op, err
+			}
+			op.Decode = decodeBool16(mask)
+			op.Length = 1
+
+		// 32 bit
+		case "int32":
+			op.Decode = asFloat64(encoding.Int32)
+			op.Length = 2
+		case "int32nan":
+			op.Decode = decodeNaN32(asFloat64(encoding.Int32), 1<<31, 1<<31-1)
+			op.Length = 2
+		case "int32s":
+			op.Decode = asFloat64(encoding.Int32LswFirst)
+			op.Length = 2
+		case "uint32":
+			op.Decode = asFloat64(encoding.Uint32)
+			op.Length = 2
+		case "uint32s":
+			op.Decode = asFloat64(encoding.Uint32LswFirst)
+			op.Length = 2
+		case "uint32nan":
+			op.Decode = decodeNaN32(asFloat64(encoding.Uint32), 1<<32-1)
+			op.Length = 2
+		case "float32", "ieee754":
+			op.Decode = asFloat64(encoding.Float32)
+			op.Length = 2
+		case "float32s", "ieee754s":
+			op.Decode = asFloat64(encoding.Float32LswFirst)
+			op.Length = 2
+
+		// 64 bit
+		case "uint64":
+			op.Decode = asFloat64(encoding.Uint64)
+			op.Length = 4
+		case "uint64nan":
+			op.Decode = decodeNaN64(asFloat64(encoding.Uint64), 1<<64-1)
+			op.Length = 4
+		case "float64":
+			op.Decode = encoding.Float64
+			op.Length = 4
+
+		default:
+			return RegisterOperation{}, fmt.Errorf("invalid register decoding: %s", r.Decode)
+		}
+	} else {
+		switch strings.ToLower(r.encoding()) {
+		case "int32s", "uint32s", "float32s", "ieee754s":
+			op.Encode = func(v uint64) uint64 {
+				return v&0xFFFF<<16 | v&0xFFFF0000>>16
+			}
+
+		default:
+			op.Encode = func(v uint64) uint64 {
+				return v
+			}
+		}
+	}
+
+	return op, nil
+}
+
+// asFloat64 creates a function that returns numerics vales as float64
+func asFloat64[T constraints.Signed | constraints.Unsigned | constraints.Float](f func([]byte) T) func([]byte) float64 {
+	return func(v []byte) float64 {
+		res := float64(f(v))
+		if math.IsNaN(res) || math.IsInf(res, 0) {
+			res = 0
+		}
+		return res
+	}
+}
+
+type RegisterOperation struct {
+	FuncCode uint8
+	Addr     uint16
+	Length   uint16
+	Encode   func(uint64) uint64
+	Decode   func([]byte) float64
+}
+
+func (op RegisterOperation) IsRead() bool {
+	return !slices.Contains([]uint8{
+		modbus.FuncCodeWriteSingleRegister,
+		modbus.FuncCodeWriteMultipleRegisters,
+		modbus.FuncCodeWriteSingleCoil,
+	}, op.FuncCode)
+}
diff --git a/util/modbus/sunspec.go b/util/modbus/sunspec.go
new file mode 100644
index 00000000..363ef73c
--- /dev/null
+++ b/util/modbus/sunspec.go
@@ -0,0 +1,51 @@
+package modbus
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+)
+
+// SunSpecOperation is a sunspec modbus operation
+type SunSpecOperation struct {
+	Model, Block int
+	Point        string
+}
+
+// ParsePoint parses sunspec point from string
+func ParsePoint(selector string) ([]SunSpecOperation, error) {
+	el := strings.Split(selector, ":")
+	if len(el) < 2 || len(el) > 3 {
+		return nil, fmt.Errorf("invalid sunspec format: %s", selector)
+	}
+
+	models := strings.Split(el[0], "|")
+	if len(models) == 0 {
+		return nil, fmt.Errorf("missing sunspec model: %s", selector)
+	}
+
+	var res []SunSpecOperation
+	for _, m := range models {
+		model, err := strconv.Atoi(m)
+		if err != nil {
+			return nil, fmt.Errorf("invalid sunspec model: %s", selector)
+		}
+
+		var block int
+		if len(el) == 3 {
+			// block is the middle element
+			block, err = strconv.Atoi(el[1])
+			if err != nil {
+				return nil, fmt.Errorf("invalid sunspec block: %s", selector)
+			}
+		}
+
+		res = append(res, SunSpecOperation{
+			Model: model,
+			Block: block,
+			Point: el[len(el)-1],
+		})
+	}
+
+	return res, nil
+}


Test patch:
diff --git a/util/modbus/modbus_test.go b/util/modbus/modbus_test.go
index c462f495..bf87c71e 100644
--- a/util/modbus/modbus_test.go
+++ b/util/modbus/modbus_test.go
@@ -1,30 +1,26 @@
 package modbus
 
-import "testing"
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
 
 func TestParsePoint(t *testing.T) {
 	tc := []struct {
-		in           string
-		model, block int
-		point        string
-		err          bool
+		in  string
+		ops []SunSpecOperation
 	}{
-		{"103:W", 103, 0, "W", false},
-		{"802:1:V", 802, 1, "V", false},
-		{"802::V", 802, 1, "V", true},
+		{"103:W", []SunSpecOperation{{103, 0, "W"}}},
+		{"802:1:V", []SunSpecOperation{{802, 1, "V"}}},
+		{"101|103:DCW", []SunSpecOperation{{101, 0, "DCW"}, {103, 0, "DCW"}}},
 	}
 
 	for _, tc := range tc {
 		t.Log(tc)
 
-		model, block, point, err := ParsePoint(tc.in)
-
-		if (err != nil) != tc.err {
-			t.Errorf("unexpected error: %d:%d:%s %v", model, block, point, err)
-		}
-
-		if !tc.err && (model != tc.model || block != tc.block || point != tc.point) {
-			t.Errorf("unexpected result: %d:%d:%s", model, block, point)
-		}
+		ops, err := ParsePoint(tc.in)
+		require.NoError(t, err)
+		require.Equal(t, tc.ops, ops)
 	}
 }