Sunspec: read model tree only once (BC) (#16832)

This commit is contained in:
andig 2024-10-24 14:58:08 +02:00 • committed by GitHub
parent a804a77523
commit 47c77f4254
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 135 additions and 123 deletions

2
go.mod
View file

@ -90,7 +90,7 @@ require (
github.com/teslamotors/vehicle-command v0.2.1
github.com/traefik/yaegi v0.16.1
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20240727104742-3191c0dbfb9e
github.com/volkszaehler/mbmd v0.0.0-20241024124559-c21fb12c007d
github.com/writeas/go-strip-markdown/v2 v2.1.1
gitlab.com/bboehmke/sunny v0.16.0
go.uber.org/mock v0.4.0

4
go.sum
View file

@ -671,8 +671,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/volkszaehler/mbmd v0.0.0-20240727104742-3191c0dbfb9e h1:d0KoYuIPXTI/vn85axsoD7w616oUalAzmDLZwv9yB9Q=
github.com/volkszaehler/mbmd v0.0.0-20240727104742-3191c0dbfb9e/go.mod h1:MA3vKWI4KozcpWvD9dwN6CL+WqwdK9XGXT5Iv49IsUM=
github.com/volkszaehler/mbmd v0.0.0-20241024124559-c21fb12c007d h1:K49ROfxojl+nq1k+eJG5QdYgBs1pjBnC1jQj/fypeiw=
github.com/volkszaehler/mbmd v0.0.0-20241024124559-c21fb12c007d/go.mod h1:egeAZti78O7U6QcdDPze5NHiiLdPSviw6YA7rsNYSMQ=
github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=

View file

@ -3,6 +3,7 @@ package meter
import (
"errors"
"fmt"
"strings"
"time"
"github.com/evcc-io/evcc/api"
@ -10,16 +11,15 @@ import (
"github.com/evcc-io/evcc/util/modbus"
"github.com/volkszaehler/mbmd/meters"
"github.com/volkszaehler/mbmd/meters/rs485"
"github.com/volkszaehler/mbmd/meters/sunspec"
)
// ModbusMbmd is an api.Meter implementation with configurable getters and setters.
type ModbusMbmd struct {
conn *modbus.Connection
device meters.Device
opPower modbus.Operation
opEnergy modbus.Operation
opSoc modbus.Operation
device *rs485.RS485
opPower rs485.Operation
opEnergy rs485.Operation
opSoc rs485.Operation
}
func init() {
@ -53,7 +53,7 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
// assume RTU if not set and this is a known RS485 meter model
if cc.RTU == nil {
if rtu := modbus.IsRS485(cc.Model); rtu {
if rtu := isRS485(cc.Model); rtu {
cc.RTU = &rtu
}
}
@ -76,17 +76,7 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
conn.Logger(log.TRACE)
// prepare device
device, err := modbus.NewDevice(cc.Model, cc.SubDevice)
if err == nil {
err = device.Initialize(conn)
// silence Kostal implementation errors
if errors.Is(err, meters.ErrPartiallyOpened) {
err = nil
}
}
device, err := rs485.NewDevice(strings.ToUpper(cc.Model))
if err != nil {
return nil, err
}
@ -96,7 +86,7 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
device: device,
}
m.opPower, err = modbus.ParseOperation(device, cc.Power)
m.opPower, err = rs485FindDeviceOp(device, cc.Power)
if err != nil {
return nil, fmt.Errorf("invalid measurement for power: %s", cc.Power)
}
@ -104,7 +94,7 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
// decorate energy
var totalEnergy func() (float64, error)
if cc.Energy != "" {
m.opEnergy, err = modbus.ParseOperation(device, cc.Energy)
m.opEnergy, err = rs485FindDeviceOp(device, cc.Energy)
if err != nil {
return nil, fmt.Errorf("invalid measurement for energy: %s", cc.Energy)
}
@ -133,7 +123,7 @@ func NewModbusMbmdFromConfig(other map[string]interface{}) (api.Meter, error) {
// decorate soc
var soc func() (float64, error)
if cc.Soc != "" {
m.opSoc, err = modbus.ParseOperation(device, cc.Soc)
m.opSoc, err = rs485FindDeviceOp(device, cc.Soc)
if err != nil {
return nil, fmt.Errorf("invalid measurement for soc: %s", cc.Soc)
}
@ -155,7 +145,7 @@ func (m *ModbusMbmd) buildPhaseProviders(readings []string) (func() (float64, fl
var phases [3]func() (float64, error)
for idx, reading := range readings {
opCurrent, err := modbus.ParseOperation(m.device, reading)
opCurrent, err := rs485FindDeviceOp(m.device, reading)
if err != nil {
return nil, fmt.Errorf("invalid measurement [%d]: %s", idx, reading)
}
@ -169,26 +159,8 @@ func (m *ModbusMbmd) buildPhaseProviders(readings []string) (func() (float64, fl
}
// floatGetter executes configured modbus read operation and implements func() (float64, error)
func (m *ModbusMbmd) floatGetter(op modbus.Operation) (float64, error) {
var res meters.MeasurementResult
var err error
if dev, ok := m.device.(*rs485.RS485); ok {
res, err = dev.QueryOp(m.conn, op.MBMD)
}
if dev, ok := m.device.(*sunspec.SunSpec); ok {
if op.MBMD.IEC61850 != 0 {
res, err = dev.QueryOp(m.conn, op.MBMD.IEC61850)
} else {
res.Value, err = dev.QueryPoint(
m.conn,
op.SunSpec.Model,
op.SunSpec.Block,
op.SunSpec.Point,
)
}
}
func (m *ModbusMbmd) floatGetter(op rs485.Operation) (float64, error) {
res, err := m.device.QueryOp(m.conn, op)
// silence NaN reading errors by assuming zero
if err != nil && errors.Is(err, meters.ErrNaN) {

37
meter/mbmd_operation.go Normal file
View file

@ -0,0 +1,37 @@
package meter
import (
"fmt"
"strings"
"github.com/volkszaehler/mbmd/meters"
"github.com/volkszaehler/mbmd/meters/rs485"
)
// isRS485 determines if model is a known MBMD rs485 device model
func isRS485(model string) bool {
for k := range rs485.Producers {
if strings.EqualFold(model, k) {
return true
}
}
return false
}
// rs485FindDeviceOp checks is RS485 device supports operation
func rs485FindDeviceOp(device *rs485.RS485, name string) (op rs485.Operation, err error) {
measurement, err := meters.MeasurementString(name)
if err != nil {
return rs485.Operation{}, fmt.Errorf("invalid measurement: %s", name)
}
ops := device.Producer().Produce()
for _, op := range ops {
if op.IEC61850 == measurement {
return op, nil
}
}
return op, fmt.Errorf("unsupported measurement: %s", measurement.String())
}

View file

@ -68,10 +68,25 @@ func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error)
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
devices := sunspecDevices.Get(conn)
if devices == nil {
devices, err = sunsdev.DeviceTree(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

55
provider/sunspec_cache.go Normal file
View file

@ -0,0 +1,55 @@
package provider
import (
"strconv"
gosunspec "github.com/andig/gosunspec"
"github.com/evcc-io/evcc/util/modbus"
"github.com/volkszaehler/mbmd/meters/sunspec"
)
var sunspecDevices = sunspecDeviceCache{
data: make(map[string][]gosunspec.Device),
}
// sunspecDeviceCache is a cache for sunspec connection's device tree
type sunspecDeviceCache struct {
data map[string][]gosunspec.Device
}
func (c *sunspecDeviceCache) Get(conn *modbus.Connection) []gosunspec.Device {
return c.data[conn.Addr()]
}
func (c *sunspecDeviceCache) Put(conn *modbus.Connection, devices []gosunspec.Device) {
c.data[conn.Addr()] = devices
}
var sunspecSubDevices = sunspecSubDeviceCache{
data: make(map[string][]*sunspec.SunSpec),
}
// sunspecSubDeviceCache is a cache for a sunspec devices's models
type sunspecSubDeviceCache struct {
data map[string][]*sunspec.SunSpec
}
func (c *sunspecSubDeviceCache) Get(conn *modbus.Connection, subDevice int) *sunspec.SunSpec {
addr := sunspecSubdeviceAddr(conn, subDevice)
for _, dev := range c.data[addr] {
if dev.Descriptor().SubDevice == subDevice {
return dev
}
}
return nil
}
func (c *sunspecSubDeviceCache) Put(conn *modbus.Connection, subDevice int, dev *sunspec.SunSpec) {
addr := sunspecSubdeviceAddr(conn, subDevice)
c.data[addr] = append(c.data[addr], dev)
}
func sunspecSubdeviceAddr(conn *modbus.Connection, subDevice int) string {
return conn.String() + "::" + strconv.Itoa(subDevice)
}

View file

@ -1,6 +1,7 @@
package modbus
import (
"fmt"
"time"
"github.com/volkszaehler/mbmd/meters"
@ -10,10 +11,15 @@ import (
type Connection struct {
*logger
meters.Connection
slaveID uint8 // duplicated from meters.Connection
logical meters.Logger
delay time.Duration
}
func (c *Connection) Addr() string {
return fmt.Sprintf("%s::%d", c.Connection.String(), c.slaveID)
}
func (c *Connection) Logger(logger meters.Logger) {
c.logical = logger
}
@ -24,6 +30,7 @@ func (c *Connection) Delay(delay time.Duration) {
func (c *Connection) Clone(slaveID uint8) *Connection {
return &Connection{
slaveID: slaveID,
Connection: c.Connection.Clone(slaveID),
logger: c.logger,
}

View file

@ -1,35 +0,0 @@
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 {
if op.SunSpec, err = ParsePoint(measurement); err != nil {
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
}

View file

@ -8,8 +8,6 @@ import (
"github.com/evcc-io/evcc/util"
"github.com/volkszaehler/mbmd/meters"
"github.com/volkszaehler/mbmd/meters/rs485"
"github.com/volkszaehler/mbmd/meters/sunspec"
)
type Protocol int
@ -109,6 +107,7 @@ func NewConnection(uri, device, comset string, baudrate int, proto Protocol, sla
}
res := &Connection{
slaveID: slaveID,
Connection: conn.Clone(slaveID),
logger: conn.logger,
}
@ -155,41 +154,3 @@ func physicalConnection(proto Protocol, cfg Settings) (*meterConnection, error)
return registeredConnection(uri, proto, meters.NewTCP(uri))
}
}
// NewDevice creates physical modbus device from config
func NewDevice(model string, subdevice int) (device meters.Device, err error) {
if IsRS485(model) {
device, err = rs485.NewDevice(strings.ToUpper(model))
} else {
device = sunspec.NewDevice(strings.ToUpper(model), subdevice)
}
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.EqualFold(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())
}