Repository: evcc-io/evcc
Pull request #4830 (2022-10-15 10:42:37)

Parallelise setup of chargers and vehicles
This is useful with multiple slow devices like OCPP chargers and multiple vehicle apis.

- [ ] pass context to charger creation to allow aborting when first instance has failed


Patch:
diff --git a/cmd/config.go b/cmd/config.go
index 963ad479..97f2c1b9 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -1,12 +1,14 @@
 package cmd
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"net/http"
 	"sort"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/dustin/go-humanize"
@@ -18,11 +20,13 @@ import (
 	"github.com/evcc-io/evcc/server"
 	autoauth "github.com/evcc-io/evcc/server/auth"
 	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
 	"github.com/evcc-io/evcc/vehicle"
 	"github.com/evcc-io/evcc/vehicle/wrapper"
 	"github.com/gorilla/handlers"
 	"github.com/gorilla/mux"
 	"golang.org/x/exp/maps"
+	"golang.org/x/sync/errgroup"
 )
 
 var conf = config{
@@ -47,13 +51,14 @@ type config struct {
 	Network      networkConfig
 	Log          string
 	SponsorToken string
-	Telemetry    bool
 	Plant        string // telemetry plant id
+	Telemetry    bool
 	Metrics      bool
 	Profile      bool
 	Levels       map[string]string
 	Interval     time.Duration
 	Mqtt         mqttConfig
+	ModbusProxy  []proxyConfig
 	Database     dbConfig
 	Javascript   map[string]interface{}
 	Influx       server.InfluxConfig
@@ -73,6 +78,12 @@ type mqttConfig struct {
 	Topic       string
 }
 
+type proxyConfig struct {
+	Port            int
+	ReadOnly        bool
+	modbus.Settings `mapstructure:",squash"`
+}
+
 type dbConfig struct {
 	Type string
 	Dsn  string
@@ -196,64 +207,85 @@ func (cp *ConfigProvider) configureMeters(conf config) error {
 }
 
 func (cp *ConfigProvider) configureChargers(conf config) error {
+	var mu sync.Mutex
+	g, _ := errgroup.WithContext(context.Background())
+
 	cp.chargers = make(map[string]api.Charger)
 	for id, cc := range conf.Chargers {
 		if cc.Name == "" {
 			return fmt.Errorf("cannot create %s charger: missing name", humanize.Ordinal(id+1))
 		}
 
-		c, err := charger.NewFromConfig(cc.Type, cc.Other)
-		if err != nil {
-			err = fmt.Errorf("cannot create charger '%s': %w", cc.Name, err)
-			return err
-		}
+		cc := cc
 
-		if _, exists := cp.chargers[cc.Name]; exists {
-			return fmt.Errorf("duplicate charger name: %s already defined and must be unique", cc.Name)
-		}
+		g.Go(func() error {
+			c, err := charger.NewFromConfig(cc.Type, cc.Other)
+			if err != nil {
+				return fmt.Errorf("cannot create charger '%s': %w", cc.Name, err)
+			}
 
-		cp.chargers[cc.Name] = c
+			mu.Lock()
+			defer mu.Unlock()
+
+			if _, exists := cp.chargers[cc.Name]; exists {
+				return fmt.Errorf("duplicate charger name: %s already defined and must be unique", cc.Name)
+			}
+
+			cp.chargers[cc.Name] = c
+			return nil
+		})
 	}
 
-	return nil
+	return g.Wait()
 }
 
 func (cp *ConfigProvider) configureVehicles(conf config) error {
+	var mu sync.Mutex
+	g, _ := errgroup.WithContext(context.Background())
+
 	cp.vehicles = make(map[string]api.Vehicle)
 	for id, cc := range conf.Vehicles {
 		if cc.Name == "" {
 			return fmt.Errorf("cannot create %s vehicle: missing name", humanize.Ordinal(id+1))
 		}
 
-		// ensure vehicle config has title
-		var ccWithTitle struct {
-			Title string
-			Other map[string]interface{} `mapstructure:",remain"`
-		}
+		cc := cc
 
-		if err := util.DecodeOther(cc.Other, &ccWithTitle); err != nil {
-			return err
-		}
+		g.Go(func() error {
+			// ensure vehicle config has title
+			var ccWithTitle struct {
+				Title string
+				Other map[string]interface{} `mapstructure:",remain"`
+			}
 
-		if ccWithTitle.Title == "" {
-			//lint:ignore SA1019 as Title is safe on ascii
-			cc.Other["title"] = strings.Title(cc.Name)
-		}
+			if err := util.DecodeOther(cc.Other, &ccWithTitle); err != nil {
+				return err
+			}
 
-		v, err := vehicle.NewFromConfig(cc.Type, cc.Other)
-		if err != nil {
-			// wrap any created errors to prevent fatals
-			v, _ = wrapper.New(v, err)
-		}
+			if ccWithTitle.Title == "" {
+				//lint:ignore SA1019 as Title is safe on ascii
+				cc.Other["title"] = strings.Title(cc.Name)
+			}
 
-		if _, exists := cp.vehicles[cc.Name]; exists {
-			return fmt.Errorf("duplicate vehicle name: %s already defined and must be unique", cc.Name)
-		}
+			v, err := vehicle.NewFromConfig(cc.Type, cc.Other)
+			if err != nil {
+				// wrap any created errors to prevent fatals
+				v, _ = wrapper.New(v, err)
+			}
+
+			mu.Lock()
+			defer mu.Unlock()
 
-		cp.vehicles[cc.Name] = v
+			if _, exists := cp.vehicles[cc.Name]; exists {
+				return fmt.Errorf("duplicate vehicle name: %s already defined and must be unique", cc.Name)
+			}
+
+			cp.vehicles[cc.Name] = v
+			return nil
+		})
 	}
 
-	return nil
+	return g.Wait()
 }
 
 // webControl handles routing for devices. For now only api.AuthProvider related routes
diff --git a/cmd/setup.go b/cmd/setup.go
index b51e4303..38260311 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -20,6 +20,7 @@ import (
 	"github.com/evcc-io/evcc/server"
 	"github.com/evcc-io/evcc/server/db"
 	"github.com/evcc-io/evcc/server/db/settings"
+	"github.com/evcc-io/evcc/server/modbus"
 	"github.com/evcc-io/evcc/tariff"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/locale"
@@ -106,6 +107,15 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) {
 		err = configureMQTT(conf.Mqtt)
 	}
 
+	// setup modbus proxy listeners
+	if err == nil {
+		for _, cfg := range conf.ModbusProxy {
+			if err = modbus.StartProxy(cfg.Port, cfg.Settings, cfg.ReadOnly); err != nil {
+				break
+			}
+		}
+	}
+
 	// setup javascript VMs
 	if err == nil {
 		err = configureJavascript(conf.Javascript)
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index f0e7611a..4067c749 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -37,6 +37,14 @@ levels:
   cache: error
   db: error
 
+# modbus proxy for allowing external programs to reuse the evcc modbus connection
+# each entry will start a proxy instance at the given port speaking Modbus TCP and
+# relaying to the given modbus downstream device (either TCP or RTU, RS485 or TCP)
+modbusproxy:
+  #  - port: 5200
+  #    uri: localhost:502
+  #    readonly: true
+
 # meter definitions
 # name can be freely chosen and is used as reference when assigning meters to site and loadpoints
 # for documentation see https://docs.evcc.io/docs/devices/meters
diff --git a/go.mod b/go.mod
index bf0e8f4c..a63c9d30 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/Masterminds/sprig/v3 v3.2.2
 	github.com/PuerkitoBio/goquery v1.8.0
 	github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71
+	github.com/andig/mbserver v0.0.0-20221101145037-fcaa2dfef9fb
 	github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
 	github.com/avast/retry-go/v3 v3.1.1
 	github.com/aws/aws-sdk-go v1.44.127
@@ -86,6 +87,7 @@ require (
 	golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
 	golang.org/x/net v0.1.0
 	golang.org/x/oauth2 v0.1.0
+	golang.org/x/sync v0.1.0
 	golang.org/x/text v0.4.0
 	google.golang.org/api v0.101.0
 	google.golang.org/grpc v1.50.1
@@ -168,7 +170,6 @@ require (
 	go.opencensus.io v0.23.0 // indirect
 	golang.org/x/crypto v0.1.0 // indirect
 	golang.org/x/mod v0.6.0 // indirect
-	golang.org/x/sync v0.1.0 // indirect
 	golang.org/x/sys v0.1.0 // indirect
 	golang.org/x/term v0.1.0 // indirect
 	golang.org/x/tools v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index eb9d5c2d..5ec4c0b0 100644
--- a/go.sum
+++ b/go.sum
@@ -98,6 +98,8 @@ github.com/andig/go-powerwall v0.2.1-0.20220205120646-e5220ad9a9a0 h1:9fQ/afZNwq
 github.com/andig/go-powerwall v0.2.1-0.20220205120646-e5220ad9a9a0/go.mod h1:NA12RXBKXFQFx3Nb9xMTQjHIphu1Fl64i/gQMuRdMZc=
 github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71 h1:tnjVNZjuz+CK6fdc7ohJpMHjcEGFI5APp0l5T5Ocr/Y=
 github.com/andig/gosunspec v0.0.0-20211108155140-af2e73b86e71/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k=
+github.com/andig/mbserver v0.0.0-20221101145037-fcaa2dfef9fb h1:cw8kFzV0kbG4E4Bv4UOmbgNkhCEZ0p7frNCFO4LKzm0=
+github.com/andig/mbserver v0.0.0-20221101145037-fcaa2dfef9fb/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM=
 github.com/andig/rct v0.0.0-20221101081802-96d01efdc68c h1:iXNLsesR2rTRPmr+QbjGgRTAOue8QpIkibakicNR7Qg=
 github.com/andig/rct v0.0.0-20221101081802-96d01efdc68c/go.mod h1:0lfd2mmBnBzIvuzYtdhG+2371u+cUfIxsYErm4P9KRI=
 github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
@@ -292,6 +294,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
+github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
 github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
@@ -774,6 +777,7 @@ github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR
 github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc=
 github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/simonvetter/modbus v1.6.0 h1:RDHJevtc7LDIVoHAbhDun8fy+QwnGe+ZU+sLm9ZZzjc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
diff --git a/server/modbus/handler.go b/server/modbus/handler.go
new file mode 100644
index 00000000..6c2ac1ad
--- /dev/null
+++ b/server/modbus/handler.go
@@ -0,0 +1,141 @@
+package modbus
+
+import (
+	"encoding/binary"
+	"errors"
+
+	"github.com/andig/mbserver"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	gridx "github.com/grid-x/modbus"
+)
+
+type handler struct {
+	log      *util.Logger
+	readOnly bool
+	mbserver.RequestHandler
+	conn *modbus.Connection
+}
+
+func bytesAsUint16(b []byte) []uint16 {
+	u := make([]uint16, 0, len(b)/2)
+	for i := 0; i < len(b)/2; i++ {
+		u = append(u, binary.BigEndian.Uint16(b[2*i:]))
+	}
+	return u
+}
+
+func asBytes(u []uint16) []byte {
+	b := make([]byte, 2*len(u))
+	for i, u := range u {
+		binary.BigEndian.PutUint16(b[2*i:], u)
+	}
+	return b
+}
+
+func bytesAsBool(b []byte) []bool {
+	var res []bool
+	for _, c := range bytesAsUint16(b) {
+		if c != 0 {
+			res = append(res, true)
+			continue
+		}
+		res = append(res, false)
+	}
+	return res
+}
+
+func boolAsBytes(b []bool) []byte {
+	res := make([]byte, 2*len(b))
+	for i, bb := range b {
+		if bb {
+			binary.BigEndian.PutUint16(res[2*i:], 0xFF00)
+		}
+	}
+	return res
+}
+
+func (h *handler) logResult(op string, b []byte, err error) {
+	if err == nil {
+		h.log.TRACE.Printf(op+" response: %0x", b)
+	} else {
+		h.log.TRACE.Printf(op+" response: %v", err)
+	}
+}
+
+func (h *handler) exceptionToUint16AndError(op string, b []byte, err error) ([]uint16, error) {
+	h.logResult(op, b, err)
+
+	var modbusError *gridx.Error
+	if errors.As(err, &modbusError) {
+		err = mbserver.MapExceptionCodeToError(modbusError.ExceptionCode)
+	}
+
+	return bytesAsUint16(b), err
+}
+
+func (h *handler) exceptionToBoolAndError(op string, b []byte, err error) ([]bool, error) {
+	h.logResult(op, b, err)
+
+	var modbusError *gridx.Error
+	if errors.As(err, &modbusError) {
+		err = mbserver.MapExceptionCodeToError(modbusError.ExceptionCode)
+	}
+
+	return bytesAsBool(b), err
+}
+
+func (h *handler) HandleCoils(req *mbserver.CoilsRequest) ([]bool, error) {
+	if req.IsWrite {
+		if h.readOnly {
+			return nil, mbserver.ErrIllegalFunction
+		}
+
+		if req.Quantity == 1 {
+			h.log.TRACE.Printf("write coil: id: %d addr: %d val: %t", req.UnitId, req.Addr, req.Args[0])
+			var u uint16
+			if req.Args[0] {
+				u = 0xFF00
+			}
+
+			b, err := h.conn.WriteSingleCoilWithSlave(req.UnitId, req.Addr, u)
+			return h.exceptionToBoolAndError("write coil", b, err)
+		}
+
+		h.log.TRACE.Printf("write multiple coils: id: %d addr: %d qty: %d val: %v", req.UnitId, req.Addr, req.Quantity, req.Args)
+		b, err := h.conn.WriteMultipleCoilsWithSlave(req.UnitId, req.Addr, req.Quantity, boolAsBytes(req.Args))
+		return h.exceptionToBoolAndError("write multiple coils", b, err)
+	}
+
+	h.log.TRACE.Printf("read coil: id: %d addr: %d qty: %d", req.UnitId, req.Addr, req.Quantity)
+	b, err := h.conn.ReadCoilsWithSlave(req.UnitId, req.Addr, req.Quantity)
+	return h.exceptionToBoolAndError("read coil", b, err)
+}
+
+func (h *handler) HandleInputRegisters(req *mbserver.InputRegistersRequest) (res []uint16, err 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)
+	return h.exceptionToUint16AndError("read input", b, err)
+}
+
+func (h *handler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest) (res []uint16, err error) {
+	if req.IsWrite {
+		if h.readOnly {
+			return nil, mbserver.ErrIllegalFunction
+		}
+
+		if req.Quantity == 1 {
+			h.log.TRACE.Printf("write holding: id: %d addr: %d val: %0x", req.UnitId, req.Addr, req.Args[0])
+			b, err := h.conn.WriteSingleRegisterWithSlave(req.UnitId, req.Addr, req.Args[0])
+			return h.exceptionToUint16AndError("write holding", b, err)
+		}
+
+		h.log.TRACE.Printf("write multiple holding: 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))
+		return h.exceptionToUint16AndError("write multiple holding", b, err)
+	}
+
+	h.log.TRACE.Printf("read holding: id: %d addr: %d qty: %d", req.UnitId, req.Addr, req.Quantity)
+	b, err := h.conn.ReadHoldingRegistersWithSlave(req.UnitId, req.Addr, req.Quantity)
+	return h.exceptionToUint16AndError("read holding", b, err)
+}
diff --git a/server/modbus/proxy.go b/server/modbus/proxy.go
new file mode 100644
index 00000000..7a4593b1
--- /dev/null
+++ b/server/modbus/proxy.go
@@ -0,0 +1,45 @@
+package modbus
+
+import (
+	"fmt"
+	"net"
+
+	"github.com/andig/mbserver"
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/evcc-io/evcc/util/sponsor"
+)
+
+func StartProxy(port int, config modbus.Settings, readOnly bool) error {
+	conn, err := modbus.NewConnection(config.URI, config.Device, config.Comset, config.Baudrate, modbus.ProtocolFromRTU(config.RTU), config.ID)
+	if err != nil {
+		return err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return api.ErrSponsorRequired
+	}
+
+	h := &handler{
+		log:            util.NewLogger(fmt.Sprintf("proxy-%d", port)),
+		readOnly:       readOnly,
+		RequestHandler: new(mbserver.DummyHandler),
+		conn:           conn,
+	}
+
+	l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
+	if err != nil {
+		return err
+	}
+
+	h.log.DEBUG.Printf("modbus proxy for %s listening at :%d", config.String(), port)
+
+	srv, err := mbserver.New(h)
+
+	if err == nil {
+		err = srv.Start(l)
+	}
+
+	return err
+}
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index a715f756..382803ac 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -46,15 +46,23 @@ type Settings struct {
 	RTU                 *bool // indicates RTU over TCP if true
 }
 
+func (s *Settings) String() string {
+	if s.URI != "" {
+		return s.URI
+	}
+	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() {
-	mb.conn.Slave(mb.slaveID)
+func (mb *Connection) prepare(slaveID uint8) {
+	mb.conn.Slave(slaveID)
 	if mb.delay > 0 {
 		time.Sleep(mb.delay)
 	}
@@ -88,71 +96,137 @@ func (mb *Connection) Timeout(timeout time.Duration) {
 }
 
 // ReadCoils wraps the underlying implementation
-func (mb *Connection) ReadCoils(address, quantity uint16) ([]byte, error) {
-	mb.prepare()
+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) WriteSingleCoil(address, quantity uint16) ([]byte, error) {
-	mb.prepare()
-	return mb.handle(mb.conn.ModbusClient().WriteSingleCoil(address, quantity))
+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) ReadInputRegisters(address, quantity uint16) ([]byte, error) {
-	mb.prepare()
+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) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) {
-	mb.prepare()
+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) WriteSingleRegister(address, value uint16) ([]byte, error) {
-	mb.prepare()
+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) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) {
-	mb.prepare()
+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) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) {
-	mb.prepare()
+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) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) {
-	mb.prepare()
+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) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) {
-	mb.prepare()
+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) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) {
-	mb.prepare()
+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) ReadFIFOQueue(address uint16) (results []byte, err error) {
-	mb.prepare()
+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, quantity uint16) ([]byte, error) {
+	return mb.WriteSingleCoilWithSlave(mb.slaveID, address, quantity)
+}
+
+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


Test patch:
diff --git a/server/modbus/proxy_test.go b/server/modbus/proxy_test.go
new file mode 100644
index 00000000..c325cf82
--- /dev/null
+++ b/server/modbus/proxy_test.go
@@ -0,0 +1,70 @@
+package modbus
+
+import (
+	"encoding/binary"
+	"math/rand"
+	"net"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/andig/mbserver"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestProxyRead(t *testing.T) {
+	l, err := net.Listen("tcp", ":0")
+	assert.NoError(t, err)
+	defer l.Close()
+
+	t.Log(l.Addr().String())
+
+	conn, err := modbus.NewConnection(l.Addr().String(), "", "", 0, modbus.Tcp, 1)
+	assert.NoError(t, err)
+
+	h := &echoHandler{
+		id:             0,
+		RequestHandler: new(mbserver.DummyHandler),
+		conn:           conn,
+	}
+
+	srv, _ := mbserver.New(h)
+	assert.NoError(t, srv.Start(l))
+	defer func() { _ = srv.Stop() }()
+
+	var wg sync.WaitGroup
+
+	for i := 1; i <= 10; i++ {
+		wg.Add(1)
+
+		go func(id int) {
+			for i := 0; i < 50; i++ {
+				addr := uint16(rand.Int31n(200) + 1)
+
+				b, err := conn.ReadInputRegistersWithSlave(uint8(id), addr, 1)
+				assert.NoError(t, err)
+
+				if err == nil {
+					assert.Equal(t, addr^uint16(id), binary.BigEndian.Uint16(b))
+				}
+
+				time.Sleep(time.Duration(rand.Int31n(1000)) * time.Microsecond)
+			}
+
+			wg.Done()
+		}(i)
+	}
+
+	wg.Wait()
+}
+
+type echoHandler struct {
+	id int
+	mbserver.RequestHandler
+	conn *modbus.Connection
+}
+
+func (h *echoHandler) HandleInputRegisters(req *mbserver.InputRegistersRequest) (res []uint16, err error) {
+	return []uint16{req.Addr ^ uint16(req.UnitId)}, err
+}