diff --git a/README.md b/README.md index dbcf537a3..c78b18758 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,21 @@ chargers: ### Meter -Meters provide data about power and energy consumption. Meter has a single implementation where meter readings- power and energy- can be configured to be delivered by [plugin](#plugins). +Meters provide data about power and energy consumption. Available meter implementations are: + +- `modbus`: ModBus meters as supported by [MBMD](https://github.com/volkszaehler/mbmd#supported-devices). Configuration is similar to the [ModBus plugin](#modbus-read-only) where `power` and `energy` specify the MBMD measurement value to use: + + ```yaml + - name: pv + type: modbus + model: sdm + uri: rs485.fritz.box:23 + id: 2 + power: PowerL1 + energy: Export + ``` + +- `default`: default meter implementation where meter readings- `power` and `energy` are configured using [plugin](#plugins) ### Vehicle @@ -206,31 +220,29 @@ The meter configuration consists of the actual physical connection and the value #### Physical connection -Three different types are supported: +If the device is physically connected using an RS485 adapter, `device` and serial configuration `baudrate`, `comset` must be specified: -- `modbus-rtu`: use this type if the device is physically connected using an RS485 adapter. Requires adapter name in `device` and serial configuration `baudrate`, `comset`. Example: +```yaml +type: modbus +device: /dev/ttyUSB0 +baudrate: 9600 +comset: "8N1" +``` - ```yaml - type: modbus-rtu - device: /dev/ttyUSB0 - baudrate: 9600 - comset: "8N1" - ``` +If the device is a grid inverter or a Modbus meter connected via TCP, `uri` must be specified: -- `modbus-tcprtu`: use this type if the device is physically connected using an RS485/Ethernet adapter. Requires adapter address in `uri`. Adapter serial configuration must be done directly on the adapter. Example: +```yaml +type: modbus +uri: 192.168.0.11:502 +``` - ```yaml - type: modbus-rtu - uri: 192.168.0.10:502 - ``` +If the device is a Modbus RTU device connected using an RS485/Ethernet adapter, set `rtu: true`. The serial configuration must be done directly on the adapter. Example: -- `modbus-tcp`: use this type if the device is a grid inverter or other Modbus TCP meter connected via TCP. Requires the device address and port in `uri`. Example: - - ```yaml - type: modbus-tcp - uri: 192.168.0.11:502 - meter: kostal # "sunspec" or any grid inverter brand name - ``` +```yaml +type: modbus +uri: 192.168.0.10:502 +rtu: true +``` #### Logical connection @@ -239,16 +251,16 @@ The meter device type `meter` and the device's slave id `id` are always required ```yaml type: ... uri/device: ... -meter: sdm +model: sdm id: 3 -value: power +value: Power ``` -Supported meter types are all supported by [MBMD](https://github.com/volkszaehler/mbmd#supported-devices): +Supported meter models are the same as supported by [MBMD](https://github.com/volkszaehler/mbmd#supported-devices): - RTU: - `ABB` ABB A/B-Series meters - - `BE` Bernecker Engineering MPM3PM meters + - `MPM` Bernecker Engineering MPM3PM meters - `DZG` DZG Metering GmbH DVH4013 meters - `INEPRO` Inepro Metering Pro 380 - `JANITZA` Janitza B-Series meters diff --git a/core/wrapper/chargerater_test.go b/core/wrapper/chargerater_test.go index 88c41b937..a79194a19 100644 --- a/core/wrapper/chargerater_test.go +++ b/core/wrapper/chargerater_test.go @@ -44,7 +44,7 @@ func TestWrappedMeter(t *testing.T) { mm := mock.NewMockMeter(ctrl) me := mock.NewMockMeterEnergy(ctrl) - cm := &meter.CompositeMeter{Meter: mm, MeterEnergy: me} + cm := &meter.WrappedMeterEnergy{Meter: mm, MeterEnergy: me} me.EXPECT(). TotalEnergy(). diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 010cb60ee..291a77255 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -61,12 +61,17 @@ meters: type: script # use script cmd: /bin/sh -c "echo 0" # actual command timeout: 3s # kill script after 3 seconds -- name: sma-grid +- name: sma type: sma # SMA Home Manager 2.0 or SMA Energy Meter 30 - uri: 192.168.1.4 # IP Address of the device -- name: sma-pv - type: sma # SMA Home Manager 2.0 or SMA Energy Meter 30 - uri: 192.168.1.4 # IP Address of the device + uri: 192.168.1.4 # IP address of the device +- name: sdm + type: modbus + model: sdm + uri: rs485.fritz.box:23 + rtu: true # rs485 device connected using ethernet adapter + id: 2 + power: Power # default values, optionally override + energy: Sum # default values, optionally override chargers: - name: wallbe diff --git a/go.mod b/go.mod index dfaace171..4d77de489 100644 --- a/go.mod +++ b/go.mod @@ -26,10 +26,11 @@ require ( github.com/spf13/viper v1.6.3 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/technoweenie/multipartstreamer v1.0.1 // indirect - github.com/volkszaehler/mbmd v0.0.0-20200420184732-6539a8fcdcbb + github.com/volkszaehler/mbmd v0.0.0-20200427071855-e83718d438c1 golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 // indirect + golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect - golang.org/x/tools v0.0.0-20200421042724-cfa8b22178d2 + golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b ) replace github.com/spf13/viper => github.com/andig/viper v1.6.3-0.20200308172723-deb8393798ec diff --git a/go.sum b/go.sum index 3985d1439..0b4cbd3eb 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY= +github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo= github.com/andig/gosunspec v0.0.0-20200112114436-368765cf4ec1 h1:Fe5Mw5GBsmi1lDsYfRsE++f/PDk0p+TdbR6iQhu1RlM= github.com/andig/gosunspec v0.0.0-20200112114436-368765cf4ec1/go.mod h1:YkshK8WMzYn1iXAZzHUO75gIqhMSan2ctgBVtBkRIyA= github.com/andig/viper v1.6.3-0.20200308172723-deb8393798ec h1:wjG6C1Qnz3GhKcQLXHfdv0Fjxu/MMDLoi+m7U6V+W20= @@ -186,6 +188,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU= +github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= @@ -254,10 +258,10 @@ github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkT github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/volkszaehler/mbmd v0.0.0-20200420092504-612973da25e0 h1:VXSJkD8PdXY4fgm5AYU5wQXbXE47PGGKMpTOoLfnGUg= -github.com/volkszaehler/mbmd v0.0.0-20200420092504-612973da25e0/go.mod h1:JM+zNF1VW2M1UEdy5DDjGccMsxPtcuy/yyj3DdfXC4A= -github.com/volkszaehler/mbmd v0.0.0-20200420184732-6539a8fcdcbb h1:gqxcgZEK+wCaK6x89OClHYGADka4en6eA5Ugv9xMOPE= -github.com/volkszaehler/mbmd v0.0.0-20200420184732-6539a8fcdcbb/go.mod h1:JM+zNF1VW2M1UEdy5DDjGccMsxPtcuy/yyj3DdfXC4A= +github.com/volkszaehler/mbmd v0.0.0-20200425162258-8528afbf6802 h1:F9nAK3ctHkvBM3Dg6Ov7JLyJCLCDGXXIClpb7ZDwRyI= +github.com/volkszaehler/mbmd v0.0.0-20200425162258-8528afbf6802/go.mod h1:9tE184AIdCL9o9u+nOc2fWJ64NED05Rc63CrLRbDLw0= +github.com/volkszaehler/mbmd v0.0.0-20200427071855-e83718d438c1 h1:uC/JWhovJ4Hb1a2wBUqrEJKkKekWjfbiCfozph7iNgk= +github.com/volkszaehler/mbmd v0.0.0-20200427071855-e83718d438c1/go.mod h1:9tE184AIdCL9o9u+nOc2fWJ64NED05Rc63CrLRbDLw0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -313,6 +317,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -361,6 +367,7 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -371,8 +378,8 @@ golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c h1:JzwTM5XxGxiCwZEIZQPG46csyhWQxQlu2uSi3bEza34= golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200421042724-cfa8b22178d2 h1:fVXd7Kf16vo/CTZCrBFb6BeEhaSbBtsPz9oWmWAoyl8= -golang.org/x/tools v0.0.0-20200421042724-cfa8b22178d2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b h1:zSzQJAznWxAh9fZxiPy2FZo+ZZEYoYFYYDYdOrU7AaM= +golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/meter/config.go b/meter/config.go index 6a70cbf6d..3215a5e3b 100644 --- a/meter/config.go +++ b/meter/config.go @@ -14,6 +14,8 @@ func NewFromConfig(log *util.Logger, typ string, other map[string]interface{}) a switch strings.ToLower(typ) { case "default", "configurable": c = NewConfigurableFromConfig(log, other) + case "modbus": + c = NewModbusFromConfig(log, other) case "sma": c = NewSMAFromConfig(log, other) default: diff --git a/meter/meter.go b/meter/meter.go index 5745c537f..f6f10f361 100644 --- a/meter/meter.go +++ b/meter/meter.go @@ -6,8 +6,8 @@ import ( "github.com/andig/evcc/util" ) -// CompositeMeter decorates a Meter with MeterEnergy. -type CompositeMeter struct { +// WrappedMeterEnergy decorates a Meter with MeterEnergy +type WrappedMeterEnergy struct { api.Meter api.MeterEnergy } @@ -24,7 +24,7 @@ func NewConfigurableFromConfig(log *util.Logger, other map[string]interface{}) a // decorate Meter with MeterEnergy if cc.Energy != nil { - m = &CompositeMeter{ + m = &WrappedMeterEnergy{ Meter: m, MeterEnergy: NewMeterEnergy(provider.NewFloatGetterFromConfig(log, *cc.Energy)), } diff --git a/meter/modbus.go b/meter/modbus.go new file mode 100644 index 000000000..9b5c014b2 --- /dev/null +++ b/meter/modbus.go @@ -0,0 +1,137 @@ +package meter + +import ( + "github.com/andig/evcc/api" + "github.com/andig/evcc/provider" + "github.com/andig/evcc/provider/modbus" + "github.com/andig/evcc/util" + "github.com/volkszaehler/mbmd/meters" + "github.com/volkszaehler/mbmd/meters/rs485" + "github.com/volkszaehler/mbmd/meters/sunspec" +) + +// Modbus is an api.Meter implementation with configurable getters and setters. +type Modbus struct { + log *util.Logger + conn meters.Connection + device meters.Device + slaveID uint8 + opPower rs485.Operation + opEnergy rs485.Operation +} + +// NewModbusFromConfig creates api.Meter from config +func NewModbusFromConfig(log *util.Logger, other map[string]interface{}) api.Meter { + cc := struct { + provider.ModbusSettings `mapstructure:",squash"` + Power, Energy string + }{} + util.DecodeOther(log, other, &cc) + + // 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 := modbus.NewConnection(log, cc.URI, cc.Device, cc.Comset, cc.Baudrate, *cc.RTU) + device, err := modbus.NewDevice(log, cc.Model, *cc.RTU) + + log = util.NewLogger("modb") + conn.Logger(log.TRACE) + + // prepare device + if err == nil { + conn.Slave(cc.ID) + err = device.Initialize(conn.ModbusClient()) + + // silence Kostal implementation errors + if _, partial := err.(meters.SunSpecPartiallyInitialized); partial { + err = nil + } + } + if err != nil { + log.FATAL.Fatal(err) + } + + m := &Modbus{ + log: log, + conn: conn, + device: device, + slaveID: cc.ID, + } + + // power reading + if cc.Power == "" { + cc.Power = "Power" + } + + powerM, err := meters.MeasurementString(cc.Power) + if err != nil { + log.FATAL.Fatalf("invalid measurement for power: %s", cc.Power) + } + + // for RS485 check if producer supports the measurement + m.opPower = rs485.Operation{IEC61850: powerM} + if dev, ok := device.(*rs485.RS485); ok { + m.opPower = modbus.RS485FindDeviceOp(dev, powerM) + + if m.opPower.IEC61850 == 0 { + log.FATAL.Fatalf("unsupported measurement for power: %s", cc.Power) + } + } + + // energy reading + if cc.Energy == "" { + cc.Energy = "Sum" + } + + energyM, err := meters.MeasurementString(cc.Energy) + if err != nil { + log.FATAL.Fatalf("invalid measurement for energy: %s", cc.Energy) + } + + // for RS485 check if producer supports the measurement + m.opEnergy = rs485.Operation{IEC61850: energyM} + if dev, ok := device.(*rs485.RS485); ok { + m.opEnergy = modbus.RS485FindDeviceOp(dev, energyM) + + if m.opEnergy.IEC61850 == 0 { + log.FATAL.Fatalf("unsupported measurement for energy: %s", cc.Energy) + } + } + + return m +} + +// floatGetter executes configured modbus read operation and implements provider.FloatGetter +func (m *Modbus) floatGetter(op rs485.Operation) (float64, error) { + m.conn.Slave(m.slaveID) + + var res meters.MeasurementResult + var err error + + if dev, ok := m.device.(*rs485.RS485); ok { + res, err = dev.QueryOp(m.conn.ModbusClient(), op) + } + + if dev, ok := m.device.(*sunspec.SunSpec); ok { + res, err = dev.QueryOp(m.conn.ModbusClient(), op.IEC61850) + } + + if err == nil { + m.log.TRACE.Printf("%+v", res) + } + + return res.Value, err +} + +// CurrentPower implements the Meter.CurrentPower interface +func (m *Modbus) CurrentPower() (float64, error) { + return m.floatGetter(m.opPower) +} + +// TotalEnergy implements the Meter.TotalEnergy interface +func (m *Modbus) TotalEnergy() (float64, error) { + return m.floatGetter(m.opEnergy) +} diff --git a/provider/config.go b/provider/config.go index 00c428c3a..3b150db8f 100644 --- a/provider/config.go +++ b/provider/config.go @@ -72,7 +72,7 @@ func NewFloatGetterFromConfig(log *util.Logger, config Config) (res FloatGetter) if pc.Cache > 0 { res = NewCached(log, res, pc.Cache).FloatGetter() } - case "modbus-rtu", "modbus-tcp", "modbus-rtuovertcp", "modbus-tcprtu", "modbus-rtutcp": + case "modbus": res = FloatGetter(NewModbusFromConfig(log, config.Type, config.Other).FloatGetter) default: log.FATAL.Fatalf("invalid provider type %s", config.Type) diff --git a/provider/modbus.go b/provider/modbus.go index 8a2be0bb5..122f640ec 100644 --- a/provider/modbus.go +++ b/provider/modbus.go @@ -2,8 +2,8 @@ package provider import ( "math" - "strings" + "github.com/andig/evcc/provider/modbus" "github.com/andig/evcc/util" "github.com/volkszaehler/mbmd/meters" "github.com/volkszaehler/mbmd/meters/rs485" @@ -19,49 +19,28 @@ type Modbus struct { op rs485.Operation } -var connections map[string]meters.Connection - -func modbusConnection(key string, newConn meters.Connection) meters.Connection { - if connections == nil { - connections = make(map[string]meters.Connection) - } - - if conn, ok := connections[key]; ok { - return conn - } - - connections[key] = newConn - return newConn +type ModbusSettings struct { + Model string + modbus.Connection `mapstructure:",squash"` } // NewModbusFromConfig creates Modbus plugin func NewModbusFromConfig(log *util.Logger, typ string, other map[string]interface{}) *Modbus { cc := struct { - URI, Device, Comset string - Meter, Value string - Baudrate int - ID uint8 + ModbusSettings `mapstructure:",squash"` + Value string }{} util.DecodeOther(log, other, &cc) - var conn meters.Connection - var device meters.Device - var err error - - switch strings.ToLower(typ) { - case "modbus-rtu": - conn = modbusConnection(cc.Device, meters.NewRTU(cc.Device, cc.Baudrate, cc.Comset)) - device, err = rs485.NewDevice(strings.ToUpper(cc.Meter)) - case "modbus-rtuovertcp", "modbus-tcprtu", "modbus-rtutcp": - conn = modbusConnection(cc.URI, meters.NewRTUOverTCP(cc.URI)) - device, err = rs485.NewDevice(strings.ToUpper(cc.Meter)) - case "modbus-tcp": - conn = modbusConnection(cc.URI, meters.NewTCP(cc.URI)) - device = sunspec.NewDevice(strings.ToUpper(cc.Meter)) - default: - log.FATAL.Fatalf("invalid provider type %s", typ) + // 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 := modbus.NewConnection(log, cc.URI, cc.Device, cc.Comset, cc.Baudrate, *cc.RTU) + device, err := modbus.NewDevice(log, cc.Model, *cc.RTU) + log = util.NewLogger("modb") conn.Logger(log.TRACE) @@ -79,18 +58,18 @@ func NewModbusFromConfig(log *util.Logger, typ string, other map[string]interfac log.FATAL.Fatal(err) } - measurement := meters.UnStringMeasurement(cc.Value) - if measurement == 0 { + measurement, err := meters.MeasurementString(cc.Value) + if err != nil { log.FATAL.Fatalf("invalid measurement %s", cc.Value) } // for RS485 check if producer supports the measurement op := rs485.Operation{IEC61850: measurement} if dev, ok := device.(*rs485.RS485); ok { - op = rs485FindOp(dev, measurement) + op = modbus.RS485FindDeviceOp(dev, measurement) if op.IEC61850 == 0 { - log.FATAL.Fatalf("invalid value %s", measurement) + log.FATAL.Fatalf("unsupported measurement value: %s", measurement) } } @@ -103,19 +82,6 @@ func NewModbusFromConfig(log *util.Logger, typ string, other map[string]interfac } } -func rs485FindOp(device *rs485.RS485, measurement meters.Measurement) (op rs485.Operation) { - ops := device.Producer().Produce() - - for _, o := range ops { - if o.IEC61850 == measurement { - op = o - break - } - } - - return op -} - // FloatGetter executes configured modbus read operation and implements provider.FloatGetter func (m *Modbus) FloatGetter() (float64, error) { m.conn.Slave(m.slaveID) diff --git a/provider/modbus/modbus.go b/provider/modbus/modbus.go new file mode 100644 index 000000000..fd3850c84 --- /dev/null +++ b/provider/modbus/modbus.go @@ -0,0 +1,93 @@ +package modbus + +import ( + "strings" + + "github.com/andig/evcc/util" + "github.com/volkszaehler/mbmd/meters" + "github.com/volkszaehler/mbmd/meters/rs485" + "github.com/volkszaehler/mbmd/meters/sunspec" +) + +// Connection contains the ModBus connection configuration +type Connection struct { + ID uint8 + URI, Device, Comset string + Baudrate int + RTU *bool // indicates RTU over TCP if true +} + +var connections map[string]meters.Connection + +func registeredConnection(key string, newConn meters.Connection) meters.Connection { + if connections == nil { + connections = make(map[string]meters.Connection) + } + + if conn, ok := connections[key]; ok { + return conn + } + + connections[key] = newConn + return newConn +} + +// NewConnection creates physical modbus device from config +func NewConnection(log *util.Logger, uri, device, comset string, baudrate int, rtu bool) (conn meters.Connection) { + if device != "" { + conn = registeredConnection(device, meters.NewRTU(device, baudrate, comset)) + } + + if uri != "" { + if rtu { + conn = registeredConnection(uri, meters.NewRTUOverTCP(uri)) + } else { + conn = registeredConnection(uri, meters.NewTCP(uri)) + } + } + + if conn == nil { + log.FATAL.Fatalf("config: invalid modbus configuration: need either uri or device") + } + + return conn +} + +// NewDevice creates physical modbus device from config +func NewDevice(log *util.Logger, model string, isRS485 bool) (device meters.Device, err error) { + if isRS485 { + device, err = rs485.NewDevice(strings.ToUpper(model)) + } else { + device = sunspec.NewDevice(strings.ToUpper(model)) + } + + if device == nil { + log.FATAL.Fatalf("config: invalid modbus configuration: need either uri or device") + } + + return device, err +} + +// IsRS485 determines if model is a known MBMD rs485 device model +func IsRS485(model string) bool { + for k := range rs485.Producers { + if strings.ToUpper(model) == k { + return true + } + } + return false +} + +// RS485FindDeviceOp checks is RS485 device supports operation +func RS485FindDeviceOp(device *rs485.RS485, measurement meters.Measurement) (op rs485.Operation) { + ops := device.Producer().Produce() + + for _, o := range ops { + if o.IEC61850 == measurement { + op = o + break + } + } + + return op +}