evcc-io/util/modbus/modbus.go

208 lines
5.1 KiB
Go

package modbus
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/volkszaehler/mbmd/meters"
"github.com/volkszaehler/mbmd/meters/rs485"
"github.com/volkszaehler/mbmd/meters/sunspec"
)
// Settings combine physical modbus configuration and MBMD model
type Settings struct {
Model string
Connection `mapstructure:",squash"`
}
// 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(uri, device, comset string, baudrate int, rtu bool) (conn meters.Connection, err error) {
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 {
return nil, errors.New("invalid modbus configuration: need either uri or device")
}
return conn, nil
}
// NewDevice creates physical modbus device from config
func NewDevice(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 {
err = errors.New("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, err error) {
ops := device.Producer().Produce()
for _, op := range ops {
if op.IEC61850 == measurement {
return op, nil
}
}
return op, fmt.Errorf("unsupported measurement: %s", measurement.String())
}
// Register contains the ModBus register configuration
type Register struct {
Address uint16 // Length uint16
Type string
Decode string
}
// RegisterOperation creates a read operation from a register definition
func RegisterOperation(r Register) (rs485.Operation, error) {
op := rs485.Operation{
OpCode: r.Address,
ReadLen: 2,
}
switch strings.ToLower(r.Type) {
case "holding":
op.FuncCode = rs485.ReadHoldingReg
case "input":
op.FuncCode = rs485.ReadInputReg
default:
return rs485.Operation{}, fmt.Errorf("invalid register type: %s", r.Type)
}
switch strings.ToLower(r.Decode) {
case "float32", "ieee754":
op.Transform = rs485.RTUIeee754ToFloat64
case "float32s", "ieee754s":
op.Transform = rs485.RTUIeee754ToFloat64Swapped
case "float64":
op.Transform = rs485.RTUUint64ToFloat64
op.ReadLen = 4
case "uint16":
op.Transform = rs485.RTUUint16ToFloat64
op.ReadLen = 1
case "uint32":
op.Transform = rs485.RTUUint32ToFloat64
case "uint32s":
op.Transform = rs485.RTUUint32ToFloat64Swapped
case "uint64":
op.Transform = rs485.RTUUint64ToFloat64
op.ReadLen = 4
case "int16":
op.Transform = rs485.RTUInt16ToFloat64
op.ReadLen = 1
case "int32":
op.Transform = rs485.RTUInt32ToFloat64
case "int32s":
op.Transform = rs485.RTUInt32ToFloat64Swapped
default:
return rs485.Operation{}, fmt.Errorf("invalid register decoding: %s", r.Decode)
}
return op, nil
}
// SunSpecOperation is a sunspec modbus operation
type SunSpecOperation struct {
Model, Block int
Point string
}
// ParsePoint parses sunspec point from string
func ParsePoint(selector string) (model int, block int, point string, err error) {
err = fmt.Errorf("invalid point: %s", selector)
el := strings.Split(selector, ":")
if len(el) < 2 || len(el) > 3 {
return
}
if model, err = strconv.Atoi(el[0]); err != nil {
return
}
if len(el) == 3 {
// block is the middle element
if block, err = strconv.Atoi(el[1]); err != nil {
return
}
}
point = el[len(el)-1]
return model, block, point, nil
}
// Operation is a register-based or sunspec modbus operation
type Operation struct {
MBMD rs485.Operation
SunSpec SunSpecOperation
}
// ParseOperation parses an MBMD measurement or SunsSpec point definition into a modbus operation
func ParseOperation(dev meters.Device, measurement string, op *Operation) (err error) {
// if measurement cannot be parsed it could be SunSpec model/block/point
if op.MBMD.IEC61850, err = meters.MeasurementString(measurement); err != nil {
op.SunSpec.Model, op.SunSpec.Block, op.SunSpec.Point, err = ParsePoint(measurement)
return err
}
// for RS485 check if producer supports the measurement
if dev, ok := dev.(*rs485.RS485); ok {
op.MBMD, err = RS485FindDeviceOp(dev, op.MBMD.IEC61850)
}
return err
}