diff --git a/cmd/confighandler.go b/cmd/confighandler.go index c9e6e323..d9f0f8ed 100644 --- a/cmd/confighandler.go +++ b/cmd/confighandler.go @@ -132,6 +132,7 @@ func createConnection(device string, rtu bool, baudrate int, comset string) (res return res } +// ConnectionManager returns connection manager for spec or creates new one func (conf *DeviceConfigHandler) ConnectionManager(connSpec string, rtu bool, baudrate int, comset string) meters.Manager { manager, ok := conf.Managers[connSpec] if !ok { diff --git a/cmd/scan.go b/cmd/scan.go index 0decade2..e8aee243 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -41,21 +41,6 @@ func addDesc(s *string, key string, val string) { } } -// validator checks if value is in range of reference values -type validator struct { - refs []float64 -} - -func (v validator) check(f float64) bool { - tolerance := 0.1 // 10% - for _, ref := range v.refs { - if f >= (1-tolerance)*ref && f <= (1+tolerance)*ref { - return true - } - } - return false -} - func scan(cmd *cobra.Command, args []string) { if len(args) > 0 { log.Fatalf("excess arguments, aborting: %v", args) @@ -93,9 +78,6 @@ func scan(cmd *cobra.Command, args []string) { deviceList := make(map[int]meters.Device) log.Printf("starting bus scan on %s", adapter) - // validate against 110V and 230V to make detection reliable - v := validator{[]float64{110, 230}} - SCAN: // loop over all valid slave adresses for deviceID := 1; deviceID <= 247; deviceID++ { @@ -111,13 +93,11 @@ SCAN: log.Println(err) // log error but continue } - mr, err := dev.Probe(client) - if err == nil && v.check(mr.Value) { - log.Printf("device %d: %s type device found, %s: %.2f\r\n", + match, err := dev.Probe(client) + if match && err == nil { + log.Printf("device %d: %s type device found\r\n", deviceID, dev.Descriptor().Manufacturer, - mr.Measurement, - mr.Value, ) deviceList[deviceID] = dev diff --git a/meters/device.go b/meters/device.go index 9a3082ba..41d841f9 100644 --- a/meters/device.go +++ b/meters/device.go @@ -21,17 +21,17 @@ type DeviceDescriptor struct { // Device is a modbus device that can be described, probed and queried type Device interface { - // Initialize prepares the device for usage. Any setup or initilization should be done here. + // Initialize prepares the device for usage. Any setup or initialization should be done here. // It requires that the client has the correct device id applied. Initialize(client modbus.Client) error // Descriptor returns the device descriptor. Since this method does not have - // bus access the descriptor should be preared during initilization. + // bus access the descriptor should be preared during initialization. Descriptor() DeviceDescriptor // Probe tests if a basic register, typically VoltageL1, can be read. // It requires that the client has the correct device id applied. - Probe(client modbus.Client) (MeasurementResult, error) + Probe(client modbus.Client) (bool, error) // Query retrieves all registers that the device supports. // It requires that the client has the correct device id applied. diff --git a/meters/rs485/abb.go b/meters/rs485/abb.go index 22d81ddf..e65194fc 100644 --- a/meters/rs485/abb.go +++ b/meters/rs485/abb.go @@ -67,7 +67,11 @@ func (p *ABBProducer) Type() string { // Description implements Producer interface func (p *ABBProducer) Description() string { - return "ABB A/B-Series meters" + return "ABB A/B-Series" +} + +func (p *ABBProducer) Initialize(client modbusClient, descriptor *DeviceDescriptor) error { + return initializeMID(client, descriptor) } // wrapTransform validates if reading result is undefined and returns NaN in that case @@ -95,7 +99,7 @@ func (p *ABBProducer) snip(iec Measurement, readlen uint16, sign signedness, tra nanAwareTransform := wrapTransform(2*readlen, sign, transform) snip := Operation{ - FuncCode: ReadHoldingReg, + FuncCode: readHoldingReg, OpCode: p.Opcodes[iec], ReadLen: readlen, Transform: nanAwareTransform, diff --git a/meters/rs485/dzg.go b/meters/rs485/dzg.go index 6511af70..3ed5e0db 100644 --- a/meters/rs485/dzg.go +++ b/meters/rs485/dzg.go @@ -59,7 +59,7 @@ func (p *DZGProducer) Type() string { // Description implements Producer interface func (p *DZGProducer) Description() string { - return "DZG Metering GmbH DVH4013 meters" + return "DZG Metering GmbH DVH4013" } func (p *DZGProducer) snip(iec Measurement, scaler ...float64) Operation { @@ -69,7 +69,7 @@ func (p *DZGProducer) snip(iec Measurement, scaler ...float64) Operation { } snip := Operation{ - FuncCode: ReadHoldingReg, + FuncCode: readHoldingReg, OpCode: p.Opcode(iec), ReadLen: 2, IEC61850: iec, diff --git a/meters/rs485/inepro.go b/meters/rs485/inepro.go index db1660b2..ebbb5f04 100644 --- a/meters/rs485/inepro.go +++ b/meters/rs485/inepro.go @@ -1,6 +1,11 @@ package rs485 -import . "github.com/volkszaehler/mbmd/meters" +import ( + "encoding/binary" + "fmt" + + . "github.com/volkszaehler/mbmd/meters" +) func init() { Register(NewIneproProducer) @@ -102,9 +107,32 @@ func (p *IneproProducer) Description() string { return "Inepro Metering Pro 380" } +// initialize implements initializer interface +func (p *IneproProducer) initialize(client modbusClient, descriptor *DeviceDescriptor) error { + // serial + if bytes, err := client.ReadHoldingRegisters(0x4000, 2); err == nil { + descriptor.Serial = fmt.Sprintf("%d", binary.BigEndian.Uint32(bytes)) + } + // firmware + if bytes, err := client.ReadHoldingRegisters(0x4007, 2); err == nil { + descriptor.Version = fmt.Sprintf("%.2f", RTUIeee754ToFloat64(bytes)) + } + // hardware + if bytes, err := client.ReadHoldingRegisters(0x4009, 2); err == nil { + descriptor.Options += fmt.Sprintf("HW %.2f", RTUIeee754ToFloat64(bytes)) + } + // current rating + if bytes, err := client.ReadHoldingRegisters(0x400B, 1); err == nil { + descriptor.Options += fmt.Sprintf(" %dA", binary.BigEndian.Uint16(bytes)) + } + + // assume success + return nil +} + func (p *IneproProducer) snip(iec Measurement, scaler ...float64) Operation { snip := Operation{ - FuncCode: ReadHoldingReg, + FuncCode: readHoldingReg, OpCode: p.Opcodes[iec], ReadLen: 2, IEC61850: iec, diff --git a/meters/rs485/janitza.go b/meters/rs485/janitza.go index 87bb6a48..8471af85 100644 --- a/meters/rs485/janitza.go +++ b/meters/rs485/janitza.go @@ -1,6 +1,8 @@ package rs485 -import . "github.com/volkszaehler/mbmd/meters" +import ( + . "github.com/volkszaehler/mbmd/meters" +) func init() { Register(NewJanitzaProducer) @@ -51,12 +53,16 @@ func (p *JanitzaProducer) Type() string { // Description implements Producer interface func (p *JanitzaProducer) Description() string { - return "Janitza B-Series meters" + return "Janitza MID B-Series" +} + +func (p *JanitzaProducer) Initialize(client modbusClient, descriptor *DeviceDescriptor) error { + return initializeMID(client, descriptor) } func (p *JanitzaProducer) snip(iec Measurement) Operation { snip := Operation{ - FuncCode: ReadHoldingReg, + FuncCode: readHoldingReg, OpCode: p.Opcode(iec), ReadLen: 2, IEC61850: iec, diff --git a/meters/rs485/mpm3pm.go b/meters/rs485/mpm3pm.go index b1dd500c..cffed891 100644 --- a/meters/rs485/mpm3pm.go +++ b/meters/rs485/mpm3pm.go @@ -55,12 +55,12 @@ func (p *MPM3MPProducer) Type() string { // Description implements Producer interface func (p *MPM3MPProducer) Description() string { - return "Bernecker Engineering MPM3PM meters" + return "Bernecker Engineering MPM3PM" } func (p *MPM3MPProducer) snip(iec Measurement, readlen uint16, transform RTUTransform, scaler ...float64) Operation { snip := Operation{ - FuncCode: ReadInputReg, + FuncCode: readInputReg, OpCode: p.Opcodes[iec], ReadLen: readlen, Transform: transform, diff --git a/meters/rs485/rs485.go b/meters/rs485/rs485.go index 456ae63a..11fb31b3 100644 --- a/meters/rs485/rs485.go +++ b/meters/rs485/rs485.go @@ -1,6 +1,7 @@ package rs485 import ( + "encoding/binary" "fmt" "time" @@ -10,14 +11,55 @@ import ( ) const ( - ReadHoldingReg = 3 - ReadInputReg = 4 + // modbus operation types + readHoldingReg = 3 + readInputReg = 4 ) +// modbusClient is the minimal interface that is usable by the initializer interface. +// It is used to keep the producers free of modbus implementation dependencies. +type modbusClient interface { + ReadHoldingRegisters(address, quantity uint16) (results []byte, err error) + ReadInputRegisters(address, quantity uint16) (results []byte, err error) +} + +// identificator implements device recognition logic +type identificator interface { + identify(bytes []byte) bool +} + +// initializer can be implemented by producers to perform bus operations for +// device initialization +type initializer interface { + // initialize prepares the device for usage. Any setup or initialization should + // be done here. It requires that the client has the correct device id applied. + initialize(client modbusClient, descriptor *meters.DeviceDescriptor) error +} + +// MID meters initialization method used by Janitza and ABB +func initializeMID(client modbusClient, descriptor *meters.DeviceDescriptor) error { + // serial + if bytes, err := client.ReadHoldingRegisters(0x8900, 2); err == nil { + descriptor.Serial = fmt.Sprintf("%4x", binary.BigEndian.Uint32(bytes)) + } + // firmware + if bytes, err := client.ReadHoldingRegisters(0x8908, 8); err == nil { + descriptor.Version = string(bytes) + } + // type + if bytes, err := client.ReadHoldingRegisters(0x8960, 6); err == nil { + descriptor.Model = string(bytes) + } + + // assume success + return nil +} + type rs485 struct { - producer Producer - ops chan Operation - inflight Operation + producer Producer + descriptor meters.DeviceDescriptor + ops chan Operation + inflight Operation } // NewDevice creates a device who's type must exist in the producer registry @@ -43,42 +85,56 @@ func NewDevice(typeid string) (meters.Device, error) { return nil, fmt.Errorf("unknown meter type %s", typeid) } -// Initialize prepares the device for usage. Any setup or initilization should be done here. +// Initialize prepares the device for usage. Any setup or initialization should be done here. func (d *rs485) Initialize(client modbus.Client) error { + d.descriptor = meters.DeviceDescriptor{ + Manufacturer: d.producer.Type(), + Model: d.producer.Description(), + } + + // does device support initializing itself? + if p, ok := d.producer.(initializer); ok { + return p.initialize(client, &d.descriptor) + } + return nil } // Descriptor returns the device descriptor. Since this method doe not have bus access the descriptor should be preared -// during initilization. +// during initialization. func (d *rs485) Descriptor() meters.DeviceDescriptor { - return meters.DeviceDescriptor{ - Manufacturer: d.producer.Type(), - Model: d.producer.Description(), - } + return d.descriptor } -func (d *rs485) query(client modbus.Client, op Operation) (res meters.MeasurementResult, err error) { - var bytes []byte - +func (d *rs485) rawQuery(client modbus.Client, op Operation) (bytes []byte, err error) { if op.ReadLen == 0 { - return res, fmt.Errorf("invalid meter operation %v", op) - } - - if op.Transform == nil { - return res, fmt.Errorf("transformation not defined: %v", op) + return bytes, fmt.Errorf("invalid meter operation %v", op) } switch op.FuncCode { - case ReadHoldingReg: + case readHoldingReg: bytes, err = client.ReadHoldingRegisters(op.OpCode, op.ReadLen) - case ReadInputReg: + case readInputReg: bytes, err = client.ReadInputRegisters(op.OpCode, op.ReadLen) default: - return res, fmt.Errorf("unknown function code %d", op.FuncCode) + return bytes, fmt.Errorf("unknown function code %d", op.FuncCode) } if err != nil { - return res, errors.Wrap(err, "read failed") + return bytes, errors.Wrap(err, "read failed") + } + + return bytes, nil +} + +func (d *rs485) query(client modbus.Client, op Operation) (res meters.MeasurementResult, err error) { + if op.Transform == nil { + return res, fmt.Errorf("transformation not defined: %v", op) + } + + bytes, err := d.rawQuery(client, op) + if err != nil { + return res, err } res = meters.MeasurementResult{ @@ -91,15 +147,31 @@ func (d *rs485) query(client modbus.Client, op Operation) (res meters.Measuremen } // Probe is called by the handler after preparing the bus by setting the device id -func (d *rs485) Probe(client modbus.Client) (res meters.MeasurementResult, err error) { +func (d *rs485) Probe(client modbus.Client) (res bool, err error) { op := d.producer.Probe() - res, err = d.query(client, op) + // use specific identificator for devices that are able to recognize + // themselves reliably + if idf, ok := d.producer.(identificator); ok { + bytes, err := d.rawQuery(client, op) + if err != nil { + return false, err + } + + match := idf.identify(bytes) + return match, nil + } + + // use default validator looking for 110/230V + measurement, err := d.query(client, op) if err != nil { - return res, err + return false, err } - return res, nil + v := validator{[]float64{110, 230}} + match := v.validate(measurement.Value) + + return match, nil } // Query is called by the handler after preparing the bus by setting the device id and waiting for rate limit diff --git a/meters/rs485/sbc.go b/meters/rs485/sbc.go index 522f8876..6f494dfb 100644 --- a/meters/rs485/sbc.go +++ b/meters/rs485/sbc.go @@ -1,6 +1,12 @@ package rs485 -import . "github.com/volkszaehler/mbmd/meters" +import ( + "encoding/binary" + "errors" + "fmt" + + . "github.com/volkszaehler/mbmd/meters" +) func init() { Register(NewSBCProducer) @@ -11,12 +17,14 @@ const ( ) type SBCProducer struct { + typ string Opcodes } func NewSBCProducer() Producer { /** * Opcodes for Saia Burgess ALE3 + * https://www.sbc-support.com/uploads/tx_srcproducts/26-527_ENG_DS_EnergyMeter-ALE3-with-Modbus_01.pdf * http://datenblatt.stark-elektronik.de/saia_burgess/DE_DS_Energymeter-ALE3-with-Modbus.pdf */ ops := Opcodes{ @@ -46,7 +54,10 @@ func NewSBCProducer() Producer { Power: 51, // scaler 100 ReactivePower: 52, // scaler 100 } - return &SBCProducer{Opcodes: ops} + return &SBCProducer{ + typ: "ALE3", // assume ALE3 + Opcodes: ops, + } } // Type implements Producer interface @@ -56,13 +67,46 @@ func (p *SBCProducer) Type() string { // Description implements Producer interface func (p *SBCProducer) Description() string { - return "Saia Burgess Controls ALE3 meters" + return "Saia Burgess " + p.typ +} + +func (p *SBCProducer) initialize(client modbusClient, descriptor *DeviceDescriptor) error { + // model + op := p.Probe() + b, err := client.ReadHoldingRegisters(op.OpCode, op.ReadLen) + if err != nil { + return err + } + + if !p.identify(b) { + return errors.New("could not recognize configured SBC device") + } + + descriptor.Model = p.Description() + + // fw version + b, err = client.ReadHoldingRegisters(0, 1) + if err != nil { + return err + } + + descriptor.Version = fmt.Sprintf("%.1f", float64(binary.BigEndian.Uint32(b))/10) + + // serial number + b, err = client.ReadHoldingRegisters(16, 2) + if err != nil { + return err + } + + descriptor.Serial = fmt.Sprintf("%d", binary.BigEndian.Uint32(b)) + + return nil } // snip creates modbus operation func (p *SBCProducer) snip(iec Measurement, readlen uint16) Operation { return Operation{ - FuncCode: ReadHoldingReg, + FuncCode: readHoldingReg, OpCode: p.Opcode(iec) - 1, // adjust according to docs ReadLen: readlen, IEC61850: iec, @@ -93,12 +137,47 @@ func (p *SBCProducer) snip32(iec Measurement, scaler ...float64) Operation { return snip } +// identify implements Identifier interface +func (p *SBCProducer) identify(bytes []byte) bool { + if len(bytes) < 4 { + return false + } + + switch string(bytes[:4]) { + case "ALD1", "ALE3", "AWE3": + p.typ = string(bytes[:4]) + default: + return false + } + + return true +} + +// Probe implements Producer interface func (p *SBCProducer) Probe() Operation { - return p.snip16(VoltageL1) + return Operation{ + FuncCode: readHoldingReg, + OpCode: 6, + ReadLen: 4, + } } // Produce implements Producer interface func (p *SBCProducer) Produce() (res []Operation) { + // single-phase meter + if p.typ == "ALD1" { + res = append(res, + p.snip16(VoltageL1), + p.snip16(CurrentL1, 10), + p.snip16(PowerL1, 100), + p.snip16(ReactivePowerL1, 100), + p.snip16(CosphiL1, 100), + p.snip32(Import, 100), + ) + + return res + } + for _, op := range []Measurement{ VoltageL1, VoltageL2, VoltageL3, } { @@ -112,7 +191,9 @@ func (p *SBCProducer) Produce() (res []Operation) { } for _, op := range []Measurement{ + Power, ReactivePower, PowerL1, PowerL2, PowerL3, + ReactivePowerL1, ReactivePowerL2, ReactivePowerL3, CosphiL1, CosphiL2, CosphiL3, } { res = append(res, p.snip16(op, 100)) diff --git a/meters/rs485/sdm.go b/meters/rs485/sdm.go index 2c284f81..350f4a07 100644 --- a/meters/rs485/sdm.go +++ b/meters/rs485/sdm.go @@ -70,12 +70,12 @@ func (p *SDMProducer) Type() string { } func (p *SDMProducer) Description() string { - return "Eastron SDM meters" + return "Eastron SDM" } func (p *SDMProducer) snip(iec Measurement) Operation { operation := Operation{ - FuncCode: ReadInputReg, + FuncCode: readInputReg, OpCode: p.Opcode(iec), ReadLen: 2, IEC61850: iec, diff --git a/meters/rs485/validator.go b/meters/rs485/validator.go new file mode 100644 index 00000000..3607e457 --- /dev/null +++ b/meters/rs485/validator.go @@ -0,0 +1,16 @@ +package rs485 + +// validator checks if value is in range of reference values +type validator struct { + refs []float64 +} + +func (v validator) validate(f float64) bool { + tolerance := 0.1 // 10% + for _, ref := range v.refs { + if f >= (1-tolerance)*ref && f <= (1+tolerance)*ref { + return true + } + } + return false +} diff --git a/meters/sunspec/sunspec.go b/meters/sunspec/sunspec.go index d0a066f4..d5cf0273 100644 --- a/meters/sunspec/sunspec.go +++ b/meters/sunspec/sunspec.go @@ -11,7 +11,6 @@ import ( _ "github.com/andig/gosunspec/models" // device tree parsing requires all models "github.com/andig/gosunspec/models/model1" - "github.com/andig/gosunspec/models/model101" "github.com/pkg/errors" "github.com/volkszaehler/mbmd/meters" ) @@ -137,42 +136,23 @@ func (d *sunSpec) Descriptor() meters.DeviceDescriptor { return d.descriptor } -func (d *sunSpec) Probe(client modbus.Client) (res meters.MeasurementResult, err error) { - if d.notInitilized() { - return res, errors.New("sunspec: not initialized") +func (d *sunSpec) Probe(client modbus.Client) (bool, error) { + if d.notInitialized() { + return false, errors.New("sunspec: not initialized") } for _, model := range d.models { - if model.Id() != 101 && model.Id() != 103 { - continue - } - - b := model.MustBlock(0) - if err = b.Read(); err != nil { - return - } - - pointID := model101.PhVphA - p := b.MustPoint(pointID) - - v := p.ScaledValue() - if math.IsNaN(v) { - return res, errors.Wrapf(err, "sunspec: could not read probe snip") - } - - mr := meters.MeasurementResult{ - Measurement: meters.Current, - Value: v, - Timestamp: time.Now(), + for id := range modelMap { + if id == model.Id() { + return true, nil + } } - - return mr, nil } - return res, fmt.Errorf("sunspec: could not find model for probe snip") + return false, fmt.Errorf("sunspec: could not find suitable model") } -func (d *sunSpec) notInitilized() bool { +func (d *sunSpec) notInitialized() bool { return len(d.models) == 0 } @@ -199,7 +179,7 @@ func (d *sunSpec) convertPoint(b sunspec.Block, blockID int, pointID string, m m } func (d *sunSpec) Query(client modbus.Client) (res []meters.MeasurementResult, err error) { - if d.notInitilized() { + if d.notInitialized() { return res, errors.New("sunspec: not initialized") }