Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Better RTU meter detection (if supported by device) #82

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meters/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type Device interface {

// 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.
Expand Down
2 changes: 1 addition & 1 deletion meters/rs485/abb.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ 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"
}

// wrapTransform validates if reading result is undefined and returns NaN in that case
Expand Down
2 changes: 1 addition & 1 deletion meters/rs485/dzg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion meters/rs485/janitza.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (p *JanitzaProducer) Type() string {

// Description implements Producer interface
func (p *JanitzaProducer) Description() string {
return "Janitza B-Series meters"
return "Janitza B-Series"
}

func (p *JanitzaProducer) snip(iec Measurement) Operation {
Expand Down
2 changes: 1 addition & 1 deletion meters/rs485/mpm3pm.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ 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 {
Expand Down
5 changes: 5 additions & 0 deletions meters/rs485/producer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ type Producer interface {
Probe() Operation
}

// Identificator implements device recognition logic
type Identificator interface {
Identify(bytes []byte) bool
}

// Opcodes map measurements to physical registers
type Opcodes map[meters.Measurement]uint16

Expand Down
51 changes: 37 additions & 14 deletions meters/rs485/rs485.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,9 @@ func (d *rs485) Descriptor() meters.DeviceDescriptor {
}
}

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 {
Expand All @@ -74,11 +68,24 @@ func (d *rs485) query(client modbus.Client, op Operation) (res meters.Measuremen
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{
Expand All @@ -91,15 +98,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
Expand Down
40 changes: 37 additions & 3 deletions meters/rs485/sbc.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ const (
)

type SBCProducer struct {
typ string
phases int
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{
Expand Down Expand Up @@ -46,7 +49,11 @@ func NewSBCProducer() Producer {
Power: 51, // scaler 100
ReactivePower: 52, // scaler 100
}
return &SBCProducer{Opcodes: ops}
return &SBCProducer{
typ: "ALE3", // assume ALE3
phases: 3, // assume 3 phase device
Opcodes: ops,
}
}

// Type implements Producer interface
Expand All @@ -56,7 +63,7 @@ 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
}

// snip creates modbus operation
Expand Down Expand Up @@ -93,8 +100,35 @@ 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":
// single phase
p.phases = 1
case "ALE3", "AWE3":
// three phase direct/ converter
p.phases = 3
default:
return false
}

p.typ = string(bytes[:4])
return true
}

// Probe implements Producer interface
func (p *SBCProducer) Probe() Operation {
return p.snip16(VoltageL1)
// return p.snip16(VoltageL1)
return Operation{
FuncCode: ReadHoldingReg,
OpCode: 6,
ReadLen: 4,
}
}

// Produce implements Producer interface
Expand Down
2 changes: 1 addition & 1 deletion meters/rs485/sdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (p *SDMProducer) Type() string {
}

func (p *SDMProducer) Description() string {
return "Eastron SDM meters"
return "Eastron SDM"
}

func (p *SDMProducer) snip(iec Measurement) Operation {
Expand Down
16 changes: 16 additions & 0 deletions meters/rs485/validator.go
Original file line number Diff line number Diff line change
@@ -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
}