Add ModBus meters in addition to modbus providers (#58)
This commit is contained in:
parent
697c3c410a
commit
d60252234b
11 changed files with 317 additions and 94 deletions
62
README.md
62
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
|
||||
|
|
|
|||
|
|
@ -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().
|
||||
|
|
|
|||
|
|
@ -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
5
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
|
||||
|
|
|
|||
19
go.sum
19
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=
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
137
meter/modbus.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
93
provider/modbus/modbus.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue