diff --git a/go.mod b/go.mod index 4809102f9..e2b6ca890 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index f2d5b6d11..08f2ade6d 100644 --- a/go.sum +++ b/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= diff --git a/meter/mbmd.go b/meter/mbmd.go index a0497ebc4..6a0dcbf4b 100644 --- a/meter/mbmd.go +++ b/meter/mbmd.go @@ -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) { diff --git a/meter/mbmd_operation.go b/meter/mbmd_operation.go new file mode 100644 index 000000000..b0099e5b2 --- /dev/null +++ b/meter/mbmd_operation.go @@ -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()) +} diff --git a/provider/modbus_sunspec.go b/provider/sunspec.go similarity index 91% rename from provider/modbus_sunspec.go rename to provider/sunspec.go index 5f16b44f4..30c278511 100644 --- a/provider/modbus_sunspec.go +++ b/provider/sunspec.go @@ -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 diff --git a/provider/sunspec_cache.go b/provider/sunspec_cache.go new file mode 100644 index 000000000..402182ab7 --- /dev/null +++ b/provider/sunspec_cache.go @@ -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) +} diff --git a/util/modbus/connection.go b/util/modbus/connection.go index 2f0a8ed03..eadacafe9 100644 --- a/util/modbus/connection.go +++ b/util/modbus/connection.go @@ -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, } diff --git a/util/modbus/mbmd.go b/util/modbus/mbmd.go deleted file mode 100644 index ec9693eb8..000000000 --- a/util/modbus/mbmd.go +++ /dev/null @@ -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 -} diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go index 3514db37d..d9852b3cf 100644 --- a/util/modbus/modbus.go +++ b/util/modbus/modbus.go @@ -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()) -}