evcc-io/plugin/sunspec.go

311 lines
7.5 KiB
Go

package plugin
import (
"context"
"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
mask uint64
}
func init() {
registry.AddCtx("sunspec", NewModbusSunspecFromConfig)
}
// NewModbusSunspecFromConfig creates Modbus plugin
func NewModbusSunspecFromConfig(ctx context.Context, other map[string]any) (Plugin, error) {
cc := struct {
modbus.Settings `mapstructure:",squash"`
Value []string
Scale float64
BitMask string
ConnectDelay time.Duration
}{
Scale: 1,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
var mask uint64
if cc.BitMask != "" {
var err error
if mask, err = modbus.DecodeMask(cc.BitMask); err != nil {
return nil, err
}
}
modbus.Lock()
defer modbus.Unlock()
conn, err := cc.Settings.Connection(ctx)
if err != nil {
return nil, err
}
// set non-default connect delay
conn.ConnectDelay(cc.ConnectDelay)
log := util.NewLogger("sunspec")
conn.Logger(log.TRACE)
if len(cc.Value) == 0 {
return nil, errors.New("value is required")
}
devices := sunspecDevices.Get(conn)
if devices == nil {
// the device tree captures the client gosunspec uses for all further
// block reads and writes, hence wrap it in the deduplicating cache
devices, err = sunsdev.DeviceTree(newSunspecCachedClient(conn))
if err != nil && !errors.Is(err, meters.ErrPartiallyOpened) {
return nil, err
}
sunspecDevices.Put(conn, devices)
}
device := sunspecSubDevices.Get(conn, cc.SubDevice)
if device == nil {
// silence KOSTAL implementation errors
device = sunsdev.NewDevice("sunspec", cc.SubDevice)
if err := device.InitializeWithTree(devices); err != nil {
return nil, err
}
sunspecSubDevices.Put(conn, cc.SubDevice, device)
}
var ops []modbus.SunSpecOperation
for _, val := range cc.Value {
op, err := modbus.ParsePoint(val)
if err != nil {
return nil, fmt.Errorf("invalid sunspec value: %s", cc.Value)
}
ops = append(ops, op)
}
mb := &ModbusSunspec{
log: log,
conn: conn,
device: device,
scale: cc.Scale,
mask: mask,
}
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)
}
// recoverToError converts a panic into *err, for sunspec point access that panics on type mismatch.
func recoverToError(err *error) {
if r := recover(); r != nil {
*err = fmt.Errorf("panic: %v", r)
}
}
func (m *ModbusSunspec) floatGetter() (f float64, err error) {
defer recoverToError(&err)
res, err := m.device.QueryPoint(
m.conn,
m.op.Model,
m.op.Block,
m.op.Point,
)
if err != nil && !errors.Is(err, meters.ErrNaN) {
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 _ FloatGetter = (*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 _ IntGetter = (*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
}
var _ BoolGetter = (*ModbusSunspec)(nil)
// BoolGetter treats any non-zero raw value as true, ANDing an optional bitmask in first.
// Reads the point directly since FloatGetter's ScaledValue panics on enum/bitfield points.
func (m *ModbusSunspec) BoolGetter() (func() (bool, error), error) {
return func() (res bool, err error) {
defer recoverToError(&err)
_, point, err := m.blockPoint()
if err != nil {
return false, err
}
val, err := pointInt64(point)
if err != nil {
return false, fmt.Errorf("model %d block %d point %s: %w", m.op.Model, m.op.Block, m.op.Point, err)
}
return sunspecBool(val, m.mask), nil
}, nil
}
// pointInt64 reads a sunspec point's raw (unscaled) value as int64.
func pointInt64(point sunspec.Point) (int64, error) {
switch point.Type() {
case typelabel.Bitfield16:
return int64(point.Bitfield16()), nil
case typelabel.Bitfield32:
return int64(point.Bitfield32()), nil
case typelabel.Enum16:
return int64(point.Enum16()), nil
case typelabel.Enum32:
return int64(point.Enum32()), nil
case typelabel.Int16:
return int64(point.Int16()), nil
case typelabel.Int32:
return int64(point.Int32()), nil
case typelabel.Int64:
return point.Int64(), nil
case typelabel.Uint16:
return int64(point.Uint16()), nil
case typelabel.Uint32:
return int64(point.Uint32()), nil
case typelabel.Uint64:
return int64(point.Uint64()), nil
default:
return 0, fmt.Errorf("unsupported type: %s", point.Type())
}
}
func sunspecBool(val int64, mask uint64) bool {
if mask != 0 {
return uint64(val)&mask != 0
}
return val != 0
}
func (m *ModbusSunspec) blockPoint() (block sunspec.Block, point sunspec.Point, err error) {
defer recoverToError(&err)
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
}
var _ FloatSetter = (*Modbus)(nil)
// FloatSetter executes configured modbus write operation and implements FloatSetter
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 recoverToError(&err)
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 _ IntSetter = (*Modbus)(nil)
// IntSetter executes configured modbus write operation and implements IntSetter
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 recoverToError(&err)
val = int64(float64(val) * m.scale)
// SetValue is used to include the scale factor when writing
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.SetValue(int16(val))
case typelabel.Int32:
point.SetValue(int32(val))
case typelabel.Int64:
point.SetValue(val)
case typelabel.Uint16:
point.SetValue(uint16(val))
case typelabel.Uint32:
point.SetValue(uint32(val))
case typelabel.Uint64:
point.SetValue(uint64(val))
default:
return fmt.Errorf("invalid point type: %s", typ)
}
return block.Write(m.op.Point)
}, nil
}