Modbus plugin: split modbus and sunspec (BC) (#11663)
This commit is contained in:
parent
2ea818a977
commit
9517277157
16 changed files with 673 additions and 450 deletions
|
|
@ -12,7 +12,6 @@ import (
|
|||
"github.com/evcc-io/evcc/util/modbus"
|
||||
gridx "github.com/grid-x/modbus"
|
||||
"github.com/volkszaehler/mbmd/meters"
|
||||
"github.com/volkszaehler/mbmd/meters/rs485"
|
||||
"github.com/volkszaehler/mbmd/meters/sunspec"
|
||||
)
|
||||
|
||||
|
|
@ -56,7 +55,7 @@ func ModbusHandlerFactory(conf map[string]interface{}) (TaskHandler, error) {
|
|||
}
|
||||
|
||||
if handler.Register.Address > 0 {
|
||||
handler.op, err = modbus.RegisterOperation(handler.Register)
|
||||
handler.op, err = handler.Register.Operation()
|
||||
}
|
||||
|
||||
return &handler, err
|
||||
|
|
@ -70,7 +69,7 @@ type ModbusHandler struct {
|
|||
Register modbus.Register `mapstructure:",squash"`
|
||||
Values []int
|
||||
Invalid []int
|
||||
op rs485.Operation
|
||||
op modbus.RegisterOperation
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
|
|
@ -80,9 +79,9 @@ func (h *ModbusHandler) testRegister(_ *util.Logger, conn gridx.Client) bool {
|
|||
|
||||
switch h.op.FuncCode {
|
||||
case gridx.FuncCodeReadHoldingRegisters:
|
||||
bytes, err = conn.ReadHoldingRegisters(h.op.OpCode, h.op.ReadLen)
|
||||
bytes, err = conn.ReadHoldingRegisters(h.op.Addr, h.op.Length)
|
||||
case gridx.FuncCodeReadInputRegisters:
|
||||
bytes, err = conn.ReadInputRegisters(h.op.OpCode, h.op.ReadLen)
|
||||
bytes, err = conn.ReadInputRegisters(h.op.Addr, h.op.Length)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -94,7 +93,7 @@ func (h *ModbusHandler) testRegister(_ *util.Logger, conn gridx.Client) bool {
|
|||
}
|
||||
|
||||
var u uint64
|
||||
switch h.op.ReadLen {
|
||||
switch h.op.Length {
|
||||
case 1:
|
||||
u = uint64(binary.BigEndian.Uint16(bytes))
|
||||
case 2:
|
||||
|
|
@ -200,7 +199,7 @@ func (h *ModbusHandler) Test(log *util.Logger, in ResultDetails) (res []ResultDe
|
|||
}
|
||||
|
||||
var ok bool
|
||||
if h.op.OpCode > 0 {
|
||||
if h.op.Addr > 0 {
|
||||
// log.DEBUG.Printf("slave id: %d op: %v", slaveID, h.op)
|
||||
ok = h.testRegister(log, conn.ModbusClient())
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -96,14 +96,16 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
|
|||
device: device,
|
||||
}
|
||||
|
||||
if err := modbus.ParseOperation(device, cc.Power, &m.opPower); err != nil {
|
||||
m.opPower, err = modbus.ParseOperation(device, cc.Power)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid measurement for power: %s", cc.Power)
|
||||
}
|
||||
|
||||
// decorate energy
|
||||
var totalEnergy func() (float64, error)
|
||||
if cc.Energy != "" {
|
||||
if err := modbus.ParseOperation(device, cc.Energy, &m.opEnergy); err != nil {
|
||||
m.opEnergy, err = modbus.ParseOperation(device, cc.Energy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid measurement for energy: %s", cc.Energy)
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +133,8 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
|
|||
// decorate soc
|
||||
var soc func() (float64, error)
|
||||
if cc.Soc != "" {
|
||||
if err := modbus.ParseOperation(device, cc.Soc, &m.opSoc); err != nil {
|
||||
m.opSoc, err = modbus.ParseOperation(device, cc.Soc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid measurement for soc: %s", cc.Soc)
|
||||
}
|
||||
|
||||
|
|
@ -150,9 +153,8 @@ func (m *ModbusMbmd) buildPhaseProviders(readings []string) (func() (float64, fl
|
|||
|
||||
phases := make([]func() (float64, error), 0, 3)
|
||||
for idx, reading := range readings {
|
||||
var opCurrent modbus.Operation
|
||||
|
||||
if err := modbus.ParseOperation(m.device, reading, &opCurrent); err != nil {
|
||||
opCurrent, err := modbus.ParseOperation(m.device, reading)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid measurement [%d]: %s", idx, reading)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package provider
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
|
@ -12,17 +11,14 @@ import (
|
|||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/modbus"
|
||||
gridx "github.com/grid-x/modbus"
|
||||
"github.com/volkszaehler/mbmd/meters"
|
||||
"github.com/volkszaehler/mbmd/meters/sunspec"
|
||||
)
|
||||
|
||||
// Modbus implements modbus RTU and TCP access
|
||||
type Modbus struct {
|
||||
log *util.Logger
|
||||
conn *modbus.Connection
|
||||
device meters.Device
|
||||
op modbus.Operation
|
||||
scale float64
|
||||
log *util.Logger
|
||||
conn *modbus.Connection
|
||||
op modbus.RegisterOperation
|
||||
scale float64
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -32,10 +28,8 @@ func init() {
|
|||
// NewModbusFromConfig creates Modbus plugin
|
||||
func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
|
||||
cc := struct {
|
||||
Model string
|
||||
modbus.Settings `mapstructure:",squash"`
|
||||
Register modbus.Register
|
||||
Value string
|
||||
Scale float64
|
||||
Delay time.Duration
|
||||
ConnectDelay time.Duration
|
||||
|
|
@ -48,12 +42,6 @@ func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// 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, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -77,130 +65,47 @@ func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
|
|||
log := util.NewLogger("modbus")
|
||||
conn.Logger(log.TRACE)
|
||||
|
||||
var device meters.Device
|
||||
var op modbus.Operation
|
||||
|
||||
if (cc.Value == "") == (cc.Register.Decode == "") {
|
||||
return nil, errors.New("either value or register required")
|
||||
if err := cc.Register.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.Model == "" && cc.Value != "" {
|
||||
return nil, errors.New("need device model when value configured")
|
||||
}
|
||||
|
||||
// no registered configured - need device
|
||||
if cc.Register.Decode == "" {
|
||||
device, err = modbus.NewDevice(cc.Model, cc.SubDevice)
|
||||
|
||||
// prepare device
|
||||
if err == nil {
|
||||
err = device.Initialize(conn)
|
||||
|
||||
// silence KOSTAL implementation errors
|
||||
if errors.Is(err, meters.ErrPartiallyOpened) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// model + value configured
|
||||
if cc.Value != "" {
|
||||
if err := modbus.ParseOperation(device, cc.Value, &op); err != nil {
|
||||
return nil, fmt.Errorf("invalid value %s", cc.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// register configured
|
||||
if cc.Register.Decode != "" {
|
||||
if op.MBMD, err = modbus.RegisterOperation(cc.Register); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op, err := cc.Register.Operation()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mb := &Modbus{
|
||||
log: log,
|
||||
conn: conn,
|
||||
device: device,
|
||||
op: op,
|
||||
scale: cc.Scale,
|
||||
log: log,
|
||||
conn: conn,
|
||||
op: op,
|
||||
scale: cc.Scale,
|
||||
}
|
||||
return mb, nil
|
||||
}
|
||||
|
||||
func (m *Modbus) bytesGetter() ([]byte, error) {
|
||||
if op := m.op.MBMD; op.FuncCode != 0 {
|
||||
switch op.FuncCode {
|
||||
case gridx.FuncCodeReadHoldingRegisters:
|
||||
return m.conn.ReadHoldingRegisters(op.OpCode, op.ReadLen)
|
||||
switch m.op.FuncCode {
|
||||
case gridx.FuncCodeReadHoldingRegisters:
|
||||
return m.conn.ReadHoldingRegisters(m.op.Addr, m.op.Length)
|
||||
|
||||
case gridx.FuncCodeReadInputRegisters:
|
||||
return m.conn.ReadInputRegisters(op.OpCode, op.ReadLen)
|
||||
case gridx.FuncCodeReadInputRegisters:
|
||||
return m.conn.ReadInputRegisters(m.op.Addr, m.op.Length)
|
||||
|
||||
case gridx.FuncCodeReadCoils:
|
||||
return m.conn.ReadCoils(op.OpCode, op.ReadLen)
|
||||
case gridx.FuncCodeReadCoils:
|
||||
return m.conn.ReadCoils(m.op.Addr, m.op.Length)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid read function code: %d", op.FuncCode)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid read function code: %d", m.op.FuncCode)
|
||||
}
|
||||
|
||||
return nil, errors.New("expected rtu reading")
|
||||
}
|
||||
|
||||
func (m *Modbus) floatGetter() (f float64, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
var res meters.MeasurementResult
|
||||
|
||||
// if funccode is configured, execute the read directly
|
||||
if op := m.op.MBMD; op.FuncCode != 0 {
|
||||
var bytes []byte
|
||||
if bytes, err = m.bytesGetter(); err != nil {
|
||||
return 0, fmt.Errorf("read failed: %w", err)
|
||||
}
|
||||
|
||||
return m.scale * op.Transform(bytes), nil
|
||||
bytes, err := m.bytesGetter()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read failed: %w", err)
|
||||
}
|
||||
|
||||
// if funccode is not configured, try find the reading on sunspec
|
||||
if dev, ok := m.device.(*sunspec.SunSpec); ok {
|
||||
if m.op.MBMD.IEC61850 != 0 {
|
||||
res, err = dev.QueryOp(m.conn, m.op.MBMD.IEC61850)
|
||||
} else {
|
||||
if res.Value, err = dev.QueryPoint(
|
||||
m.conn,
|
||||
m.op.SunSpec.Model,
|
||||
m.op.SunSpec.Block,
|
||||
m.op.SunSpec.Point,
|
||||
); err != nil {
|
||||
err = fmt.Errorf("model %d block %d point %s: %w", m.op.SunSpec.Model, m.op.SunSpec.Block, m.op.SunSpec.Point, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// silence NaN reading errors by assuming zero
|
||||
if err != nil && errors.Is(err, meters.ErrNaN) {
|
||||
res.Value = 0
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if m.op.MBMD.IEC61850 != 0 {
|
||||
m.log.TRACE.Printf("%s: %v", m.op.MBMD.IEC61850, res.Value)
|
||||
} else {
|
||||
m.log.TRACE.Printf("%d:%d:%s: %v", m.op.SunSpec.Model, m.op.SunSpec.Block, m.op.SunSpec.Point, res.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return m.scale * res.Value, err
|
||||
return m.scale * m.op.Decode(bytes), nil
|
||||
}
|
||||
|
||||
var _ FloatProvider = (*Modbus)(nil)
|
||||
|
|
@ -236,24 +141,6 @@ func (m *Modbus) StringGetter() (func() (string, error), error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// UintFromBytes converts byte slice to bigendian uint value
|
||||
func UintFromBytes(bytes []byte) (u uint64, err error) {
|
||||
switch l := len(bytes); l {
|
||||
case 1:
|
||||
u = uint64(bytes[0])
|
||||
case 2:
|
||||
u = uint64(binary.BigEndian.Uint16(bytes))
|
||||
case 4:
|
||||
u = uint64(binary.BigEndian.Uint32(bytes))
|
||||
case 8:
|
||||
u = binary.BigEndian.Uint64(bytes)
|
||||
default:
|
||||
err = fmt.Errorf("unexpected length: %d", l)
|
||||
}
|
||||
|
||||
return u, err
|
||||
}
|
||||
|
||||
var _ BoolProvider = (*Modbus)(nil)
|
||||
|
||||
// BoolGetter executes configured modbus read operation and implements IntProvider
|
||||
|
|
@ -271,35 +158,58 @@ func (m *Modbus) BoolGetter() (func() (bool, error), error) {
|
|||
|
||||
var _ SetFloatProvider = (*Modbus)(nil)
|
||||
|
||||
// FloatSetter executes configured modbus write operation and implements SetFloatProvider
|
||||
func (m *Modbus) FloatSetter(_ string) (func(float64) error, error) {
|
||||
op := m.op.MBMD
|
||||
if op.FuncCode == 0 {
|
||||
return nil, errors.New("modbus plugin does not support writing to sunspec")
|
||||
func (m *Modbus) writeMultipleRegisters(val uint64) error {
|
||||
val = m.op.Encode(val)
|
||||
|
||||
var err error
|
||||
switch m.op.Length {
|
||||
case 1:
|
||||
var b [2]byte
|
||||
binary.BigEndian.PutUint16(b[:], uint16(val))
|
||||
_, err = m.conn.WriteMultipleRegisters(m.op.Addr, 1, b[:])
|
||||
|
||||
case 2:
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], uint32(val))
|
||||
_, err = m.conn.WriteMultipleRegisters(m.op.Addr, 2, b[:])
|
||||
|
||||
case 4:
|
||||
var b [8]byte
|
||||
binary.BigEndian.PutUint64(b[:], val)
|
||||
_, err = m.conn.WriteMultipleRegisters(m.op.Addr, 4, b[:])
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("invalid write length: %d", m.op.Length)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FloatSetter executes configured modbus write operation and implements SetFloatProvider
|
||||
func (m *Modbus) FloatSetter(_ string) (func(float64) error, error) {
|
||||
// need multiple registers for float
|
||||
if op.FuncCode != gridx.FuncCodeWriteMultipleRegisters {
|
||||
return nil, fmt.Errorf("invalid write function code: %d", op.FuncCode)
|
||||
if m.op.FuncCode != gridx.FuncCodeWriteMultipleRegisters {
|
||||
return nil, fmt.Errorf("invalid write function code: %d", m.op.FuncCode)
|
||||
}
|
||||
|
||||
return func(val float64) error {
|
||||
val = m.scale * val
|
||||
|
||||
var err error
|
||||
switch op.ReadLen {
|
||||
var uval uint64
|
||||
switch m.op.Length {
|
||||
case 2:
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], math.Float32bits(float32(val)))
|
||||
_, err = m.conn.WriteMultipleRegisters(op.OpCode, 2, b[:])
|
||||
|
||||
uval = uint64(math.Float32bits(float32(val)))
|
||||
case 4:
|
||||
var b [8]byte
|
||||
binary.BigEndian.PutUint64(b[:], math.Float64bits(val))
|
||||
_, err = m.conn.WriteMultipleRegisters(op.OpCode, 4, b[:])
|
||||
uval = math.Float64bits(val)
|
||||
}
|
||||
|
||||
var err error
|
||||
switch m.op.FuncCode {
|
||||
case gridx.FuncCodeWriteMultipleRegisters:
|
||||
err = m.writeMultipleRegisters(uval)
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("invalid write length: %d", op.ReadLen)
|
||||
err = fmt.Errorf("invalid write function code: %d", m.op.FuncCode)
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
@ -310,40 +220,16 @@ var _ SetIntProvider = (*Modbus)(nil)
|
|||
|
||||
// IntSetter executes configured modbus write operation and implements SetIntProvider
|
||||
func (m *Modbus) IntSetter(_ string) (func(int64) error, error) {
|
||||
op := m.op.MBMD
|
||||
if op.FuncCode == 0 {
|
||||
return nil, errors.New("modbus plugin does not support writing to sunspec")
|
||||
}
|
||||
|
||||
return func(val int64) error {
|
||||
ival := int64(m.scale * float64(val))
|
||||
|
||||
// if funccode is configured, execute the read directly
|
||||
var err error
|
||||
switch op.FuncCode {
|
||||
switch m.op.FuncCode {
|
||||
case gridx.FuncCodeWriteSingleRegister:
|
||||
_, err = m.conn.WriteSingleRegister(op.OpCode, uint16(ival))
|
||||
_, err = m.conn.WriteSingleRegister(m.op.Addr, uint16(ival))
|
||||
|
||||
case gridx.FuncCodeWriteMultipleRegisters:
|
||||
switch op.ReadLen {
|
||||
case 1:
|
||||
var b [2]byte
|
||||
binary.BigEndian.PutUint16(b[:], uint16(ival))
|
||||
_, err = m.conn.WriteMultipleRegisters(op.OpCode, 1, b[:])
|
||||
|
||||
case 2:
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], uint32(ival))
|
||||
_, err = m.conn.WriteMultipleRegisters(op.OpCode, 2, b[:])
|
||||
|
||||
case 4:
|
||||
var b [8]byte
|
||||
binary.BigEndian.PutUint64(b[:], uint64(ival))
|
||||
_, err = m.conn.WriteMultipleRegisters(op.OpCode, 4, b[:])
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("invalid write length: %d", op.ReadLen)
|
||||
}
|
||||
err = m.writeMultipleRegisters(uint64(ival))
|
||||
|
||||
case gridx.FuncCodeWriteSingleCoil:
|
||||
if ival != 0 {
|
||||
|
|
@ -351,10 +237,10 @@ func (m *Modbus) IntSetter(_ string) (func(int64) error, error) {
|
|||
// and 0x0000 for OFF
|
||||
ival = 0xFF00
|
||||
}
|
||||
_, err = m.conn.WriteSingleCoil(op.OpCode, uint16(ival))
|
||||
_, err = m.conn.WriteSingleCoil(m.op.Addr, uint16(ival))
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("invalid write function code: %d", op.FuncCode)
|
||||
err = fmt.Errorf("invalid write function code: %d", m.op.FuncCode)
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
|
|||
24
provider/modbus_helper.go
Normal file
24
provider/modbus_helper.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// UintFromBytes converts byte slice to bigendian uint value
|
||||
func UintFromBytes(bytes []byte) (u uint64, err error) {
|
||||
switch l := len(bytes); l {
|
||||
case 1:
|
||||
u = uint64(bytes[0])
|
||||
case 2:
|
||||
u = uint64(binary.BigEndian.Uint16(bytes))
|
||||
case 4:
|
||||
u = uint64(binary.BigEndian.Uint32(bytes))
|
||||
case 8:
|
||||
u = binary.BigEndian.Uint64(bytes)
|
||||
default:
|
||||
err = fmt.Errorf("unexpected length: %d", l)
|
||||
}
|
||||
|
||||
return u, err
|
||||
}
|
||||
242
provider/modbus_sunspec.go
Normal file
242
provider/modbus_sunspec.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
sunspec "github.com/andig/gosunspec"
|
||||
"github.com/andig/gosunspec/typelabel"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/modbus"
|
||||
"github.com/volkszaehler/mbmd/meters"
|
||||
sunsdev "github.com/volkszaehler/mbmd/meters/sunspec"
|
||||
)
|
||||
|
||||
// ModbusSunspec implements modbus RTU and TCP access
|
||||
type ModbusSunspec struct {
|
||||
log *util.Logger
|
||||
conn *modbus.Connection
|
||||
device *sunsdev.SunSpec
|
||||
op modbus.SunSpecOperation
|
||||
scale float64
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("sunspec", NewModbusSunspecFromConfig)
|
||||
}
|
||||
|
||||
// NewModbusSunspecFromConfig creates Modbus plugin
|
||||
func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error) {
|
||||
cc := struct {
|
||||
modbus.Settings `mapstructure:",squash"`
|
||||
Value string
|
||||
Scale float64
|
||||
Delay time.Duration
|
||||
ConnectDelay time.Duration
|
||||
Timeout time.Duration
|
||||
}{
|
||||
Scale: 1,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.Tcp, cc.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set non-default timeout
|
||||
if cc.Timeout > 0 {
|
||||
conn.Timeout(cc.Timeout)
|
||||
}
|
||||
|
||||
// set non-default delay
|
||||
if cc.Delay > 0 {
|
||||
conn.Delay(cc.Delay)
|
||||
}
|
||||
|
||||
// set non-default connect delay
|
||||
if cc.ConnectDelay > 0 {
|
||||
conn.ConnectDelay(cc.ConnectDelay)
|
||||
}
|
||||
|
||||
log := util.NewLogger("sunspec")
|
||||
conn.Logger(log.TRACE)
|
||||
|
||||
if cc.Value == "" {
|
||||
return nil, errors.New("value is required")
|
||||
}
|
||||
|
||||
// silence KOSTAL implementation errors
|
||||
device := sunsdev.NewDevice("sunspec", cc.SubDevice)
|
||||
if err := device.Initialize(conn); err != nil && !errors.Is(err, meters.ErrPartiallyOpened) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ops, err := modbus.ParsePoint(cc.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sunspec value: %s", cc.Value)
|
||||
}
|
||||
|
||||
mb := &ModbusSunspec{
|
||||
log: log,
|
||||
conn: conn,
|
||||
device: device,
|
||||
scale: cc.Scale,
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
if _, _, err := device.QueryPointAny(conn, op.Model, op.Block, op.Point); err == nil {
|
||||
mb.op = op
|
||||
return mb, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("sunspec model not found: %v", ops)
|
||||
}
|
||||
|
||||
func (m *ModbusSunspec) floatGetter() (f float64, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
res, err := m.device.QueryPoint(
|
||||
m.conn,
|
||||
m.op.Model,
|
||||
m.op.Block,
|
||||
m.op.Point,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("model %d block %d point %s: %w", m.op.Model, m.op.Block, m.op.Point, err)
|
||||
}
|
||||
|
||||
m.log.TRACE.Printf("model %d block %d point %s: %v", m.op.Model, m.op.Block, m.op.Point, res)
|
||||
|
||||
return m.scale * res, nil
|
||||
}
|
||||
|
||||
var _ FloatProvider = (*Modbus)(nil)
|
||||
|
||||
// FloatGetter executes configured modbus read operation and implements func() (float64, error)
|
||||
func (m *ModbusSunspec) FloatGetter() (func() (f float64, err error), error) {
|
||||
return m.floatGetter, nil
|
||||
}
|
||||
|
||||
var _ IntProvider = (*Modbus)(nil)
|
||||
|
||||
// IntGetter executes configured modbus read operation and implements IntProvider
|
||||
func (m *ModbusSunspec) IntGetter() (func() (int64, error), error) {
|
||||
g, err := m.FloatGetter()
|
||||
|
||||
return func() (int64, error) {
|
||||
res, err := g()
|
||||
return int64(math.Round(res)), err
|
||||
}, err
|
||||
}
|
||||
|
||||
func (m *ModbusSunspec) blockPoint() (block sunspec.Block, point sunspec.Point, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
block, point, err = m.device.QueryPointAny(
|
||||
m.conn,
|
||||
m.op.Model,
|
||||
m.op.Block,
|
||||
m.op.Point,
|
||||
)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("model %d block %d point %s: %w", m.op.Model, m.op.Block, m.op.Point, err)
|
||||
}
|
||||
|
||||
return block, point, err
|
||||
}
|
||||
|
||||
// TODO scale factors
|
||||
|
||||
var _ SetFloatProvider = (*Modbus)(nil)
|
||||
|
||||
// FloatSetter executes configured modbus write operation and implements SetFloatProvider
|
||||
func (m *ModbusSunspec) FloatSetter(_ string) (func(float64) error, error) {
|
||||
block, point, err := m.blockPoint()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
typ := point.Type()
|
||||
|
||||
return func(val float64) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
val = val * m.scale
|
||||
switch typ {
|
||||
case typelabel.Float32:
|
||||
point.SetFloat32(float32(val))
|
||||
default:
|
||||
return fmt.Errorf("invalid point type: %s", typ)
|
||||
}
|
||||
|
||||
return block.Write(m.op.Point)
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ SetIntProvider = (*Modbus)(nil)
|
||||
|
||||
// IntSetter executes configured modbus write operation and implements SetIntProvider
|
||||
func (m *ModbusSunspec) IntSetter(_ string) (func(int64) error, error) {
|
||||
block, point, err := m.blockPoint()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
typ := point.Type()
|
||||
|
||||
return func(val int64) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
val = int64(float64(val) * m.scale)
|
||||
|
||||
switch typ {
|
||||
case typelabel.Bitfield16:
|
||||
point.SetBitfield16(sunspec.Bitfield16(val))
|
||||
case typelabel.Bitfield32:
|
||||
point.SetBitfield32(sunspec.Bitfield32(val))
|
||||
case typelabel.Enum16:
|
||||
point.SetEnum16(sunspec.Enum16(val))
|
||||
case typelabel.Enum32:
|
||||
point.SetEnum32(sunspec.Enum32(val))
|
||||
case typelabel.Int16:
|
||||
point.SetInt16(int16(val))
|
||||
case typelabel.Int32:
|
||||
point.SetInt32(int32(val))
|
||||
case typelabel.Int64:
|
||||
point.SetInt64(val)
|
||||
case typelabel.Uint16:
|
||||
point.SetUint16(uint16(val))
|
||||
case typelabel.Uint32:
|
||||
point.SetUint32(uint32(val))
|
||||
case typelabel.Uint64:
|
||||
point.SetUint64(uint64(val))
|
||||
default:
|
||||
return fmt.Errorf("invalid point type: %s", typ)
|
||||
}
|
||||
|
||||
return block.Write(m.op.Point)
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -36,45 +36,34 @@ render: |
|
|||
power:
|
||||
source: calc
|
||||
add:
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
uri: {{ .host }}:{{ .port }}
|
||||
id: 1
|
||||
model: sunspec
|
||||
value: 160:1:DCW # mppt 1
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
uri: {{ .host }}:{{ .port }}
|
||||
id: 1
|
||||
model: sunspec
|
||||
value: 160:2:DCW # mppt 2
|
||||
energy:
|
||||
source: modbus
|
||||
uri: {{ .host }}:{{ .port }}
|
||||
id: 1
|
||||
model: sunspec
|
||||
value: Export
|
||||
{{- end }}
|
||||
{{- if eq .usage "battery" }}
|
||||
type: custom
|
||||
power:
|
||||
source: calc
|
||||
add:
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
uri: {{ .host }}:{{ .port }}
|
||||
id: 1
|
||||
model: sunspec
|
||||
value: 160:3:DCW # mppt 3 charge
|
||||
scale: -1
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
uri: {{ .host }}:{{ .port }}
|
||||
id: 1
|
||||
model: sunspec
|
||||
value: 160:4:DCW # mppt 4 discharge
|
||||
soc:
|
||||
source: modbus
|
||||
source: sunspec
|
||||
uri: {{ .host }}:{{ .port }}
|
||||
id: 1
|
||||
model: sunspec
|
||||
value: ChargeState
|
||||
value: 124:ChaState
|
||||
{{- if .capacity }}
|
||||
capacity: {{ .capacity }} # kWh
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ linked:
|
|||
requirements:
|
||||
description:
|
||||
de: |
|
||||
Nur ein System kann und darf auf den Wechselrichter zugreifen!
|
||||
Nur ein System kann und darf auf den Wechselrichter zugreifen! Für die aktive Batteriesteuerung muss die externe Batteriesteuerung über Modbus mit dem Handwerkerzugang aktiviert sein.
|
||||
en: |
|
||||
Only a single system may access the inverter!
|
||||
Only a single system may access the inverter! For active battery control, the external battery control via Modbus must be activated using installer access.
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["pv", "battery"]
|
||||
|
|
@ -50,30 +50,25 @@ render: |
|
|||
power:
|
||||
source: calc
|
||||
add: # The add plugin sums up all string values
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: 160:1:DCW # string 1
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: 160:2:DCW # string 2
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: 160:3:DCW # string 3
|
||||
{{- end }}
|
||||
{{- if eq .usage "battery" }}
|
||||
type: custom
|
||||
power:
|
||||
source: modbus
|
||||
source: sunspec
|
||||
{{- include "modbus" . | indent 2 }}
|
||||
model: sunspec
|
||||
value: 802:W # 802 battery control
|
||||
soc:
|
||||
source: modbus
|
||||
source: sunspec
|
||||
{{- include "modbus" . | indent 2 }}
|
||||
model: sunspec
|
||||
value: 802:SoC # 802 battery control
|
||||
limitsoc:
|
||||
source: watchdog
|
||||
|
|
|
|||
|
|
@ -26,10 +26,9 @@ render: |
|
|||
type: custom
|
||||
{{- if eq .usage "grid" }}
|
||||
power:
|
||||
source: modbus
|
||||
source: sunspec
|
||||
{{- include "modbus" . | indent 2 }}
|
||||
timeout: {{ .timeout }}
|
||||
model: sunspec
|
||||
subdevice: 1 # Metering device
|
||||
value: 203:W
|
||||
scale: -1
|
||||
|
|
@ -38,11 +37,10 @@ render: |
|
|||
power:
|
||||
source: calc
|
||||
add:
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
timeout: {{ .timeout }}
|
||||
model: sunspec
|
||||
value: DCPower
|
||||
value: 101|103:DCW
|
||||
- source: modbus
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
timeout: {{ .timeout }}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,19 @@ params:
|
|||
- name: timeout
|
||||
render: |
|
||||
type: custom
|
||||
{{- if eq .usage "grid" }}
|
||||
power:
|
||||
source: modbus
|
||||
source: sunspec
|
||||
{{- include "modbus" . | indent 2 }}
|
||||
timeout: {{ .timeout }}
|
||||
{{- if eq .usage "grid" }}
|
||||
model: sunspec
|
||||
subdevice: 1
|
||||
value: 203:W # sunspec 3-phase meter power reading
|
||||
scale: -1
|
||||
{{- else }}
|
||||
model: sunspec
|
||||
value: Power
|
||||
{{- end }}
|
||||
{{- if eq .usage "pv" }}
|
||||
energy:
|
||||
source: modbus
|
||||
power:
|
||||
source: sunspec
|
||||
{{- include "modbus" . | indent 2 }}
|
||||
timeout: {{ .timeout }}
|
||||
model: sunspec
|
||||
value: Export
|
||||
value: 101|103:W # sunspec inverter AC power reading
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -65,50 +65,45 @@ render: |
|
|||
power:
|
||||
source: calc
|
||||
add:
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: DCPowerS1 # mppt 1
|
||||
- source: modbus
|
||||
value: 160:1:DCW # mppt 1
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: DCPowerS2 # mppt 2
|
||||
value: 160:2:DCW # mppt 2
|
||||
energy:
|
||||
source: calc
|
||||
add:
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: DCEnergyS1 # mppt 1
|
||||
- source: modbus
|
||||
value: 160:1:DCWH # mppt 1
|
||||
scale: 0.001
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: DCEnergyS2 # mppt 2
|
||||
value: 160:2:DCWH # mppt 2
|
||||
scale: 0.001
|
||||
{{- end }}
|
||||
{{- if eq .usage "battery" }}
|
||||
type: custom
|
||||
power:
|
||||
source: calc
|
||||
add:
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: DCPowerS3 # mppt 3 (charge)
|
||||
value: 160:3:DCW # mppt 3 (charge)
|
||||
scale: -1
|
||||
- source: modbus
|
||||
- source: sunspec
|
||||
{{- include "modbus" . | indent 4 }}
|
||||
model: sunspec
|
||||
value: DCPowerS4 # mppt 4 (discharge)
|
||||
value: 160:4:DCW # mppt 4 (discharge)
|
||||
energy:
|
||||
source: modbus
|
||||
source: sunspec
|
||||
{{- include "modbus" . | indent 2 }}
|
||||
model: sunspec
|
||||
value: DCEnergyS4 # mppt 4 (discharge)
|
||||
value: 160:4:DCWH # mppt 4 (discharge)
|
||||
scale: 0.001
|
||||
soc:
|
||||
source: modbus
|
||||
source: sunspec
|
||||
{{- include "modbus" . | indent 2 }}
|
||||
model: sunspec
|
||||
value: ChargeState
|
||||
value: 124:ChaState
|
||||
{{- if .capacity }}
|
||||
capacity: {{ .capacity }} # kWh
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ render: |
|
|||
power: Power
|
||||
energy: Export
|
||||
{{- if eq .usage "battery" }}
|
||||
soc: ChargeState
|
||||
soc: 124:ChaState
|
||||
{{- if .capacity }}
|
||||
capacity: {{ .capacity }} # kWh
|
||||
{{- end }}
|
||||
|
|
|
|||
37
util/modbus/mbmd.go
Normal file
37
util/modbus/mbmd.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"github.com/volkszaehler/mbmd/meters"
|
||||
"github.com/volkszaehler/mbmd/meters/rs485"
|
||||
)
|
||||
|
||||
// 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) (Operation, error) {
|
||||
var (
|
||||
op Operation
|
||||
err error
|
||||
)
|
||||
|
||||
// if measurement cannot be parsed it could be SunSpec model/block/point
|
||||
op.MBMD.IEC61850, err = meters.MeasurementString(measurement)
|
||||
if err != nil {
|
||||
suns, err := ParsePoint(measurement)
|
||||
if err == nil {
|
||||
op.SunSpec = suns[0]
|
||||
}
|
||||
return op, 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 op, err
|
||||
}
|
||||
|
|
@ -3,19 +3,14 @@ package modbus
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/grid-x/modbus"
|
||||
"github.com/volkszaehler/mbmd/encoding"
|
||||
"github.com/volkszaehler/mbmd/meters"
|
||||
"github.com/volkszaehler/mbmd/meters/rs485"
|
||||
"github.com/volkszaehler/mbmd/meters/sunspec"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
type Protocol int
|
||||
|
|
@ -343,164 +338,3 @@ func RS485FindDeviceOp(device *rs485.RS485, measurement meters.Measurement) (op
|
|||
|
||||
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
|
||||
BitMask string
|
||||
}
|
||||
|
||||
// asFloat64 creates a function that returns numerics vales as float64
|
||||
func asFloat64[T constraints.Signed | constraints.Unsigned | constraints.Float](f func([]byte) T) func([]byte) float64 {
|
||||
return func(v []byte) float64 {
|
||||
res := float64(f(v))
|
||||
if math.IsNaN(res) || math.IsInf(res, 0) {
|
||||
res = 0
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = modbus.FuncCodeReadHoldingRegisters
|
||||
case "input":
|
||||
op.FuncCode = modbus.FuncCodeReadInputRegisters
|
||||
case "coil":
|
||||
op.FuncCode = modbus.FuncCodeReadCoils
|
||||
r.Decode = "bool8"
|
||||
case "writesingle", "writeholding":
|
||||
op.FuncCode = modbus.FuncCodeWriteSingleRegister
|
||||
case "writemultiple", "writeholdings":
|
||||
op.FuncCode = modbus.FuncCodeWriteMultipleRegisters
|
||||
case "writecoil":
|
||||
op.FuncCode = modbus.FuncCodeWriteSingleCoil
|
||||
r.Decode = "bool8"
|
||||
default:
|
||||
return rs485.Operation{}, fmt.Errorf("invalid register type: %s", r.Type)
|
||||
}
|
||||
|
||||
switch strings.ToLower(r.Decode) {
|
||||
// 8 bit (coil)
|
||||
case "bool8":
|
||||
op.Transform = decodeBool8
|
||||
op.ReadLen = 1
|
||||
|
||||
// 16 bit
|
||||
case "int16":
|
||||
op.Transform = asFloat64(encoding.Int16)
|
||||
op.ReadLen = 1
|
||||
case "int16nan":
|
||||
op.Transform = decodeNaN16(asFloat64(encoding.Int16), 1<<15, 1<<15-1)
|
||||
op.ReadLen = 1
|
||||
case "uint16":
|
||||
op.Transform = asFloat64(encoding.Uint16)
|
||||
op.ReadLen = 1
|
||||
case "uint16nan":
|
||||
op.Transform = decodeNaN16(asFloat64(encoding.Uint16), 1<<16-1)
|
||||
op.ReadLen = 1
|
||||
case "bool16":
|
||||
mask, err := decodeMask(r.BitMask)
|
||||
if err != nil {
|
||||
return op, err
|
||||
}
|
||||
op.Transform = decodeBool16(mask)
|
||||
op.ReadLen = 1
|
||||
|
||||
// 32 bit
|
||||
case "int32":
|
||||
op.Transform = asFloat64(encoding.Int32)
|
||||
case "int32nan":
|
||||
op.Transform = decodeNaN32(asFloat64(encoding.Int32), 1<<31, 1<<31-1)
|
||||
case "int32s":
|
||||
op.Transform = asFloat64(encoding.Int32LswFirst)
|
||||
case "uint32":
|
||||
op.Transform = asFloat64(encoding.Uint32)
|
||||
case "uint32s":
|
||||
op.Transform = asFloat64(encoding.Uint32LswFirst)
|
||||
case "uint32nan":
|
||||
op.Transform = decodeNaN32(asFloat64(encoding.Uint32), 1<<32-1)
|
||||
case "float32", "ieee754":
|
||||
op.Transform = asFloat64(encoding.Float32)
|
||||
case "float32s", "ieee754s":
|
||||
op.Transform = asFloat64(encoding.Float32LswFirst)
|
||||
|
||||
// 64 bit
|
||||
case "uint64":
|
||||
op.Transform = asFloat64(encoding.Uint64)
|
||||
op.ReadLen = 4
|
||||
case "uint64nan":
|
||||
op.Transform = decodeNaN64(asFloat64(encoding.Uint64), 1<<64-1)
|
||||
op.ReadLen = 4
|
||||
case "float64":
|
||||
op.Transform = encoding.Float64
|
||||
op.ReadLen = 4
|
||||
|
||||
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, 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
package modbus
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParsePoint(t *testing.T) {
|
||||
tc := []struct {
|
||||
in string
|
||||
model, block int
|
||||
point string
|
||||
err bool
|
||||
in string
|
||||
ops []SunSpecOperation
|
||||
}{
|
||||
{"103:W", 103, 0, "W", false},
|
||||
{"802:1:V", 802, 1, "V", false},
|
||||
{"802::V", 802, 1, "V", true},
|
||||
{"103:W", []SunSpecOperation{{103, 0, "W"}}},
|
||||
{"802:1:V", []SunSpecOperation{{802, 1, "V"}}},
|
||||
{"101|103:DCW", []SunSpecOperation{{101, 0, "DCW"}, {103, 0, "DCW"}}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Log(tc)
|
||||
|
||||
model, block, point, err := ParsePoint(tc.in)
|
||||
|
||||
if (err != nil) != tc.err {
|
||||
t.Errorf("unexpected error: %d:%d:%s %v", model, block, point, err)
|
||||
}
|
||||
|
||||
if !tc.err && (model != tc.model || block != tc.block || point != tc.point) {
|
||||
t.Errorf("unexpected result: %d:%d:%s", model, block, point)
|
||||
}
|
||||
ops, err := ParsePoint(tc.in)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.ops, ops)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
180
util/modbus/register.go
Normal file
180
util/modbus/register.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/grid-x/modbus"
|
||||
"github.com/volkszaehler/mbmd/encoding"
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// Register contains the ModBus register configuration
|
||||
type Register struct {
|
||||
Address uint16 // Length uint16
|
||||
Type string
|
||||
Decode string // TODO deprecated, use Encoding
|
||||
Encoding string
|
||||
BitMask string
|
||||
}
|
||||
|
||||
func (r Register) Error() error {
|
||||
if r.Address == 0 {
|
||||
return errors.New("address is required")
|
||||
}
|
||||
if r.Type == "" {
|
||||
return errors.New("type is required")
|
||||
}
|
||||
if r.Decode == "" && r.Encoding == "" {
|
||||
return errors.New("encoding is required")
|
||||
}
|
||||
if r.Decode != "" && r.Encoding != "" {
|
||||
return errors.New("must not have decide when encoding is specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Register) encoding() string {
|
||||
if r.Encoding != "" {
|
||||
return r.Encoding
|
||||
}
|
||||
return r.Decode
|
||||
}
|
||||
|
||||
// Operation creates a modbus operation from a register definition
|
||||
func (r Register) Operation() (RegisterOperation, error) {
|
||||
op := RegisterOperation{
|
||||
Addr: r.Address,
|
||||
}
|
||||
|
||||
switch strings.ToLower(r.Type) {
|
||||
case "holding":
|
||||
op.FuncCode = modbus.FuncCodeReadHoldingRegisters
|
||||
case "input":
|
||||
op.FuncCode = modbus.FuncCodeReadInputRegisters
|
||||
case "coil":
|
||||
op.FuncCode = modbus.FuncCodeReadCoils
|
||||
case "writesingle", "writeholding":
|
||||
op.FuncCode = modbus.FuncCodeWriteSingleRegister
|
||||
case "writemultiple", "writeholdings":
|
||||
op.FuncCode = modbus.FuncCodeWriteMultipleRegisters
|
||||
case "writecoil":
|
||||
op.FuncCode = modbus.FuncCodeWriteSingleCoil
|
||||
default:
|
||||
return RegisterOperation{}, fmt.Errorf("invalid register type: %s", r.Type)
|
||||
}
|
||||
|
||||
if op.IsRead() {
|
||||
switch strings.ToLower(r.encoding()) {
|
||||
// 8 bit (coil)
|
||||
case "bool8":
|
||||
op.Decode = decodeBool8
|
||||
op.Length = 1
|
||||
|
||||
// 16 bit
|
||||
case "int16":
|
||||
op.Decode = asFloat64(encoding.Int16)
|
||||
op.Length = 1
|
||||
case "int16nan":
|
||||
op.Decode = decodeNaN16(asFloat64(encoding.Int16), 1<<15, 1<<15-1)
|
||||
op.Length = 1
|
||||
case "uint16":
|
||||
op.Decode = asFloat64(encoding.Uint16)
|
||||
op.Length = 1
|
||||
case "uint16nan":
|
||||
op.Decode = decodeNaN16(asFloat64(encoding.Uint16), 1<<16-1)
|
||||
op.Length = 1
|
||||
case "bool16":
|
||||
mask, err := decodeMask(r.BitMask)
|
||||
if err != nil {
|
||||
return op, err
|
||||
}
|
||||
op.Decode = decodeBool16(mask)
|
||||
op.Length = 1
|
||||
|
||||
// 32 bit
|
||||
case "int32":
|
||||
op.Decode = asFloat64(encoding.Int32)
|
||||
op.Length = 2
|
||||
case "int32nan":
|
||||
op.Decode = decodeNaN32(asFloat64(encoding.Int32), 1<<31, 1<<31-1)
|
||||
op.Length = 2
|
||||
case "int32s":
|
||||
op.Decode = asFloat64(encoding.Int32LswFirst)
|
||||
op.Length = 2
|
||||
case "uint32":
|
||||
op.Decode = asFloat64(encoding.Uint32)
|
||||
op.Length = 2
|
||||
case "uint32s":
|
||||
op.Decode = asFloat64(encoding.Uint32LswFirst)
|
||||
op.Length = 2
|
||||
case "uint32nan":
|
||||
op.Decode = decodeNaN32(asFloat64(encoding.Uint32), 1<<32-1)
|
||||
op.Length = 2
|
||||
case "float32", "ieee754":
|
||||
op.Decode = asFloat64(encoding.Float32)
|
||||
op.Length = 2
|
||||
case "float32s", "ieee754s":
|
||||
op.Decode = asFloat64(encoding.Float32LswFirst)
|
||||
op.Length = 2
|
||||
|
||||
// 64 bit
|
||||
case "uint64":
|
||||
op.Decode = asFloat64(encoding.Uint64)
|
||||
op.Length = 4
|
||||
case "uint64nan":
|
||||
op.Decode = decodeNaN64(asFloat64(encoding.Uint64), 1<<64-1)
|
||||
op.Length = 4
|
||||
case "float64":
|
||||
op.Decode = encoding.Float64
|
||||
op.Length = 4
|
||||
|
||||
default:
|
||||
return RegisterOperation{}, fmt.Errorf("invalid register decoding: %s", r.Decode)
|
||||
}
|
||||
} else {
|
||||
switch strings.ToLower(r.encoding()) {
|
||||
case "int32s", "uint32s", "float32s", "ieee754s":
|
||||
op.Encode = func(v uint64) uint64 {
|
||||
return v&0xFFFF<<16 | v&0xFFFF0000>>16
|
||||
}
|
||||
|
||||
default:
|
||||
op.Encode = func(v uint64) uint64 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
|
||||
// asFloat64 creates a function that returns numerics vales as float64
|
||||
func asFloat64[T constraints.Signed | constraints.Unsigned | constraints.Float](f func([]byte) T) func([]byte) float64 {
|
||||
return func(v []byte) float64 {
|
||||
res := float64(f(v))
|
||||
if math.IsNaN(res) || math.IsInf(res, 0) {
|
||||
res = 0
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
type RegisterOperation struct {
|
||||
FuncCode uint8
|
||||
Addr uint16
|
||||
Length uint16
|
||||
Encode func(uint64) uint64
|
||||
Decode func([]byte) float64
|
||||
}
|
||||
|
||||
func (op RegisterOperation) IsRead() bool {
|
||||
return !slices.Contains([]uint8{
|
||||
modbus.FuncCodeWriteSingleRegister,
|
||||
modbus.FuncCodeWriteMultipleRegisters,
|
||||
modbus.FuncCodeWriteSingleCoil,
|
||||
}, op.FuncCode)
|
||||
}
|
||||
51
util/modbus/sunspec.go
Normal file
51
util/modbus/sunspec.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SunSpecOperation is a sunspec modbus operation
|
||||
type SunSpecOperation struct {
|
||||
Model, Block int
|
||||
Point string
|
||||
}
|
||||
|
||||
// ParsePoint parses sunspec point from string
|
||||
func ParsePoint(selector string) ([]SunSpecOperation, error) {
|
||||
el := strings.Split(selector, ":")
|
||||
if len(el) < 2 || len(el) > 3 {
|
||||
return nil, fmt.Errorf("invalid sunspec format: %s", selector)
|
||||
}
|
||||
|
||||
models := strings.Split(el[0], "|")
|
||||
if len(models) == 0 {
|
||||
return nil, fmt.Errorf("missing sunspec model: %s", selector)
|
||||
}
|
||||
|
||||
var res []SunSpecOperation
|
||||
for _, m := range models {
|
||||
model, err := strconv.Atoi(m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sunspec model: %s", selector)
|
||||
}
|
||||
|
||||
var block int
|
||||
if len(el) == 3 {
|
||||
// block is the middle element
|
||||
block, err = strconv.Atoi(el[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sunspec block: %s", selector)
|
||||
}
|
||||
}
|
||||
|
||||
res = append(res, SunSpecOperation{
|
||||
Model: model,
|
||||
Block: block,
|
||||
Point: el[len(el)-1],
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue