Add ModBus meters in addition to modbus providers (#58)

This commit is contained in:
andig 2020-04-29 13:15:29 +02:00 • committed by GitHub
parent 697c3c410a
commit d60252234b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 317 additions and 94 deletions

View file

@ -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

View file

@ -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().

View file

@ -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

5
go.mod
View file

@ -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

19
go.sum
View file

@ -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=

View file

@ -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:

View file

@ -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)),
}

137
meter/modbus.go Normal file
View file

@ -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)
}

View file

@ -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)

View file

@ -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)

93
provider/modbus/modbus.go Normal file
View file

@ -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
}