Sunspec: read model tree only once (BC) (#16832)
This commit is contained in:
parent
a804a77523
commit
47c77f4254
9 changed files with 135 additions and 123 deletions
2
go.mod
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
37
meter/mbmd_operation.go
Normal 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())
|
||||
}
|
||||
|
|
@ -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
55
provider/sunspec_cache.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue