sunspec: add bool getter (#31482)
This commit is contained in:
parent
6cce4b789f
commit
c3692046b9
4 changed files with 195 additions and 23 deletions
|
|
@ -22,6 +22,7 @@ type ModbusSunspec struct {
|
|||
device *sunsdev.SunSpec
|
||||
op modbus.SunSpecOperation
|
||||
scale float64
|
||||
mask uint64
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -34,6 +35,7 @@ func NewModbusSunspecFromConfig(ctx context.Context, other map[string]any) (Plug
|
|||
modbus.Settings `mapstructure:",squash"`
|
||||
Value []string
|
||||
Scale float64
|
||||
BitMask string
|
||||
Delay time.Duration
|
||||
ConnectDelay time.Duration
|
||||
Timeout time.Duration
|
||||
|
|
@ -45,6 +47,14 @@ func NewModbusSunspecFromConfig(ctx context.Context, other map[string]any) (Plug
|
|||
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()
|
||||
|
||||
|
|
@ -104,6 +114,7 @@ func NewModbusSunspecFromConfig(ctx context.Context, other map[string]any) (Plug
|
|||
conn: conn,
|
||||
device: device,
|
||||
scale: cc.Scale,
|
||||
mask: mask,
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
|
|
@ -116,12 +127,15 @@ func NewModbusSunspecFromConfig(ctx context.Context, other map[string]any) (Plug
|
|||
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 func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
defer recoverToError(&err)
|
||||
|
||||
res, err := m.device.QueryPoint(
|
||||
m.conn,
|
||||
|
|
@ -157,12 +171,65 @@ func (m *ModbusSunspec) IntGetter() (func() (int64, error), error) {
|
|||
}, 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)
|
||||
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,
|
||||
|
|
@ -189,11 +256,7 @@ func (m *ModbusSunspec) FloatSetter(_ string) (func(float64) error, error) {
|
|||
typ := point.Type()
|
||||
|
||||
return func(val float64) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
defer recoverToError(&err)
|
||||
|
||||
val = val * m.scale
|
||||
switch typ {
|
||||
|
|
@ -219,11 +282,7 @@ func (m *ModbusSunspec) IntSetter(_ string) (func(int64) error, error) {
|
|||
typ := point.Type()
|
||||
|
||||
return func(val int64) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
defer recoverToError(&err)
|
||||
|
||||
val = int64(float64(val) * m.scale)
|
||||
|
||||
|
|
|
|||
113
plugin/sunspec_test.go
Normal file
113
plugin/sunspec_test.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
sunspec "github.com/andig/gosunspec"
|
||||
"github.com/andig/gosunspec/memory"
|
||||
"github.com/andig/gosunspec/models/model704"
|
||||
"github.com/evcc-io/evcc/util/modbus"
|
||||
"github.com/stretchr/testify/require"
|
||||
sunsdev "github.com/volkszaehler/mbmd/meters/sunspec"
|
||||
)
|
||||
|
||||
func TestSunspecBool(t *testing.T) {
|
||||
cases := []struct {
|
||||
res int64
|
||||
mask uint64
|
||||
want bool
|
||||
}{
|
||||
{0, 0, false},
|
||||
{1, 0, true},
|
||||
{2, 0, true},
|
||||
{0b0100, 0b0010, false}, // masked-out bit
|
||||
{0b0110, 0b0010, true}, // masked-in bit
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
if got := sunspecBool(c.res, c.mask); got != c.want {
|
||||
t.Errorf("sunspecBool(%v, %v) = %v, want %v", c.res, c.mask, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newSunspecTestDevice builds an in-memory SunSpec model 704 (DER AC Controls)
|
||||
// device; mbmd never touches the modbus.Client argument, so no connection is needed.
|
||||
func newSunspecTestDevice(t *testing.T) (*sunsdev.SunSpec, sunspec.Block) {
|
||||
t.Helper()
|
||||
|
||||
slab, err := memory.NewSlabBuilder().AddModel(model704.ModelID).Build()
|
||||
require.NoError(t, err)
|
||||
|
||||
arr, err := memory.Open(slab)
|
||||
require.NoError(t, err)
|
||||
|
||||
devices := arr.Collect(sunspec.AllDevices)
|
||||
require.NotEmpty(t, devices)
|
||||
|
||||
block := devices[0].MustModel(sunspec.ModelId(model704.ModelID)).MustBlock(0)
|
||||
|
||||
dev := sunsdev.NewDevice("test")
|
||||
require.NoError(t, dev.InitializeWithTree(devices))
|
||||
|
||||
return dev, block
|
||||
}
|
||||
|
||||
// TestSunspecBoolGetterEnum exercises BoolGetter against a real enum16 point
|
||||
// (WMaxLimPctEna, SunSpec 704), the curtailment-enabled flag.
|
||||
func TestSunspecBoolGetterEnum(t *testing.T) {
|
||||
dev, block := newSunspecTestDevice(t)
|
||||
|
||||
for _, c := range []struct {
|
||||
val sunspec.Enum16
|
||||
mask uint64
|
||||
want bool
|
||||
}{
|
||||
{0, 0, false},
|
||||
{1, 0, true},
|
||||
{0b11, 0b10, true}, // masked-in bit
|
||||
{0b11, 0b100, false}, // masked-out bit
|
||||
} {
|
||||
block.MustPoint(model704.WMaxLimPctEna).SetEnum16(c.val)
|
||||
require.NoError(t, block.Write(model704.WMaxLimPctEna))
|
||||
|
||||
mb := &ModbusSunspec{
|
||||
device: dev,
|
||||
op: modbus.SunSpecOperation{Model: model704.ModelID, Point: model704.WMaxLimPctEna},
|
||||
mask: c.mask,
|
||||
}
|
||||
|
||||
g, err := mb.BoolGetter()
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := g()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.want, got, "value %v mask %v", c.val, c.mask)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSunspecBoolGetterInt exercises BoolGetter against a real scaled int-like
|
||||
// point (WMaxLimPct, a uint16 with a SunSpec scale factor).
|
||||
func TestSunspecBoolGetterInt(t *testing.T) {
|
||||
dev, block := newSunspecTestDevice(t)
|
||||
|
||||
mb := &ModbusSunspec{
|
||||
device: dev,
|
||||
op: modbus.SunSpecOperation{Model: model704.ModelID, Point: model704.WMaxLimPct},
|
||||
}
|
||||
|
||||
g, err := mb.BoolGetter()
|
||||
require.NoError(t, err)
|
||||
|
||||
block.MustPoint(model704.WMaxLimPct).SetUint16(0)
|
||||
require.NoError(t, block.Write(model704.WMaxLimPct))
|
||||
got, err := g()
|
||||
require.NoError(t, err)
|
||||
require.False(t, got)
|
||||
|
||||
block.MustPoint(model704.WMaxLimPct).SetUint16(50)
|
||||
require.NoError(t, block.Write(model704.WMaxLimPct))
|
||||
got, err = g()
|
||||
require.NoError(t, err)
|
||||
require.True(t, got)
|
||||
}
|
||||
|
|
@ -17,8 +17,8 @@ func Backoff() *backoff.ExponentialBackOff {
|
|||
return backoff.NewExponentialBackOff(backoff.WithInitialInterval(20*time.Millisecond), backoff.WithMaxElapsedTime(10*time.Second))
|
||||
}
|
||||
|
||||
// decodeMask converts a bit mask in decimal or hex format to uint64
|
||||
func decodeMask(mask string) (uint64, error) {
|
||||
// DecodeMask converts a bit mask in decimal or hex format to uint64
|
||||
func DecodeMask(mask string) (uint64, error) {
|
||||
mask = strings.ToLower(mask)
|
||||
|
||||
if mask == "" {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func (r Register) DecodeFunc() (func([]byte) float64, error) {
|
|||
case "uint16nan":
|
||||
return decodeNaN16(asFloat64(encoding.Uint16), 1<<16-1), nil
|
||||
case "bool16":
|
||||
mask, err := decodeMask(r.BitMask)
|
||||
mask, err := DecodeMask(r.BitMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue