From 03f62e559314f4671170d48441062e93539db1dc Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 2 Aug 2024 21:32:45 +0200 Subject: [PATCH] Modbus: add UDP and allow concurrent access (#13676) --- charger/abb.go | 2 +- charger/alphatec.go | 2 +- charger/delta.go | 2 +- charger/evsedin.go | 2 +- charger/heidelberg-ec.go | 2 +- charger/mennekes-compact.go | 2 +- charger/obo.go | 2 +- charger/phoenix-ev-ser.go | 2 +- charger/prachtalpha.go | 2 +- charger/pulsares.go | 2 +- charger/smartevse.go | 2 +- charger/solax.go | 2 +- charger/sungrow.go | 2 +- cmd/setup.go | 56 ++-- go.mod | 4 +- go.sum | 10 +- meter/mbmd.go | 17 +- meter/template_test.go | 7 +- provider/modbus.go | 17 +- provider/modbus_sunspec.go | 15 +- server/modbus/handler.go | 16 +- server/modbus/proxy.go | 2 +- server/modbus/proxy_test.go | 20 +- templates/definition/meter/goodwe-hybrid.yaml | 2 +- util/modbus/connection.go | 113 ++++++++ util/modbus/log.go | 29 ++ util/modbus/modbus.go | 261 ++++-------------- util/modbus/mutex.go | 13 + util/templates/defaults.yaml | 13 + util/templates/modbus.tpl | 5 + util/templates/render_testing.go | 2 + util/templates/types.go | 6 +- 32 files changed, 329 insertions(+), 305 deletions(-) create mode 100644 util/modbus/connection.go create mode 100644 util/modbus/log.go create mode 100644 util/modbus/mutex.go diff --git a/charger/abb.go b/charger/abb.go index 6d24b3996..3fb47ce7e 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 56fa6dee2..7e76df630 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/delta.go b/charger/delta.go index 68005f96d..42371ee6a 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 049b4e9d6..277cc8f0b 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 1a3c88299..f0ce71e48 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 36415328a..d334ab358 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 6b3c0342e..314adda87 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 e612fd580..3e5366d1f 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 989c17d88..6bdeb75e1 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 be66ed378..73122ecaa 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 91e286a60..d8e9c7179 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 818022e55..e7708a664 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 f696f0fe9..03a29f28e 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 7dc7764f5..96011aead 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 + } + + // 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)} + } + + if err := config.Meters().Add(config.NewConfigurableDevice(conf, instance)); err != nil { + return &DeviceError{cc.Name, err} + } - if len(names) > 0 && !slices.Contains(names, cc.Name) { return nil - } - - // TOTO 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)} - } - - if err := config.Meters().Add(config.NewConfigurableDevice(conf, instance)); err != nil { - return &DeviceError{cc.Name, err} - } + }) } - 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/go.mod b/go.mod index 8b84e7c21..17b14b1c0 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 5fa521d36..39dc84feb 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/mbmd.go b/meter/mbmd.go index 551644843..29876ea5e 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/meter/template_test.go b/meter/template_test.go index 9a4067bea..eaf8d134b 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/provider/modbus.go b/provider/modbus.go index b92f55d02..51ac94b31 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 39d7aec11..6b5df6886 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 caae01aa9..d5a3b9e05 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 096b6d7ac..121234325 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/server/modbus/proxy_test.go b/server/modbus/proxy_test.go index 5bd72f7c1..84cb382c6 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/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml index 74fe50e79..a25984222 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 000000000..672a44ffd --- /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 000000000..59495eb20 --- /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 d8833e30c..9201e1d3b 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 - } - return Tcp -} - // NewConnection creates physical modbus device from config func NewConnection(uri, device, comset string, baudrate int, proto Protocol, slaveID uint8) (*Connection, error) { - var conn meters.Connection - - if device != "" && uri != "" { - return nil, errors.New("invalid modbus configuration: can only have either uri or device") + conn, err := physicalConnection(proto, Settings{ + URI: uri, + Device: device, + Comset: comset, + Baudrate: baudrate, + }) + if err != nil { + return nil, err } - if device != "" { - switch strings.ToUpper(comset) { + res := &Connection{ + Connection: conn.Clone(slaveID), + logger: new(logger), + } + + return res, nil +} + +func physicalConnection(proto Protocol, cfg Settings) (meters.Connection, error) { + var conn meters.Connection + + if (cfg.Device != "") == (cfg.URI != "") { + return nil, errors.New("invalid modbus configuration: must have either uri or device") + } + + 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 000000000..4592bddb6 --- /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/templates/defaults.yaml b/util/templates/defaults.yaml index 93982c698..79f29d936 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/modbus.tpl b/util/templates/modbus.tpl index aea822557..ea374c4c9 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/render_testing.go b/util/templates/render_testing.go index 5ad7b912c..ab6a63b68 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 } diff --git a/util/templates/types.go b/util/templates/types.go index 8a598bb22..c95838f58 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