174 lines
4 KiB
Go
174 lines
4 KiB
Go
package meter
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/modbus"
|
|
"github.com/volkszaehler/mbmd/meters"
|
|
"github.com/volkszaehler/mbmd/meters/rs485"
|
|
"github.com/volkszaehler/mbmd/meters/sunspec"
|
|
)
|
|
|
|
// Modbus is an api.Meter implementation with configurable getters and setters.
|
|
type Modbus struct {
|
|
log *util.Logger
|
|
conn *modbus.Connection
|
|
device meters.Device
|
|
opPower modbus.Operation
|
|
opEnergy modbus.Operation
|
|
opSoC modbus.Operation
|
|
}
|
|
|
|
func init() {
|
|
registry.Add("modbus", NewModbusFromConfig)
|
|
}
|
|
|
|
//go:generate go run ../cmd/tools/decorate.go -f decorateModbus -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,SoC,func() (float64, error)"
|
|
|
|
// NewModbusFromConfig creates api.Meter from config
|
|
func NewModbusFromConfig(other map[string]interface{}) (api.Meter, error) {
|
|
cc := struct {
|
|
Model string
|
|
modbus.Settings `mapstructure:",squash"`
|
|
Power, Energy, SoC string
|
|
Timeout time.Duration
|
|
}{
|
|
Power: "Power",
|
|
Settings: modbus.Settings{
|
|
ID: 1,
|
|
},
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// assume RTU if not set and this is a known RS485 meter model
|
|
if cc.RTU == nil {
|
|
b := modbus.IsRS485(cc.Model)
|
|
cc.RTU = &b
|
|
}
|
|
|
|
format := modbus.TcpFormat
|
|
if cc.RTU != nil && *cc.RTU {
|
|
format = modbus.RtuFormat
|
|
}
|
|
|
|
conn, err := modbus.NewConnection(cc.URI, cc.Device, cc.Comset, cc.Baudrate, format, cc.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// set non-default timeout
|
|
if cc.Timeout > 0 {
|
|
conn.Timeout(cc.Timeout)
|
|
}
|
|
|
|
log := util.NewLogger("modbus")
|
|
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
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := &Modbus{
|
|
log: log,
|
|
conn: conn,
|
|
device: device,
|
|
}
|
|
|
|
cc.Power = modbus.ReadingName(cc.Power)
|
|
if err := modbus.ParseOperation(device, cc.Power, &m.opPower); err != nil {
|
|
return nil, fmt.Errorf("invalid measurement for power: %s", cc.Power)
|
|
}
|
|
|
|
// decorate energy reading
|
|
var totalEnergy func() (float64, error)
|
|
if cc.Energy != "" {
|
|
cc.Energy = modbus.ReadingName(cc.Energy)
|
|
if err := modbus.ParseOperation(device, cc.Energy, &m.opEnergy); err != nil {
|
|
return nil, fmt.Errorf("invalid measurement for energy: %s", cc.Energy)
|
|
}
|
|
|
|
totalEnergy = m.totalEnergy
|
|
}
|
|
|
|
// decorate soc reading
|
|
var soc func() (float64, error)
|
|
if cc.SoC != "" {
|
|
cc.SoC = modbus.ReadingName(cc.SoC)
|
|
if err := modbus.ParseOperation(device, cc.SoC, &m.opSoC); err != nil {
|
|
return nil, fmt.Errorf("invalid measurement for soc: %s", cc.SoC)
|
|
}
|
|
|
|
soc = m.soc
|
|
}
|
|
|
|
return decorateModbus(m, totalEnergy, soc), nil
|
|
}
|
|
|
|
// floatGetter executes configured modbus read operation and implements func() (float64, error)
|
|
func (m *Modbus) 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, err = dev.QueryPoint(
|
|
m.conn,
|
|
op.SunSpec.Model,
|
|
op.SunSpec.Block,
|
|
op.SunSpec.Point,
|
|
)
|
|
}
|
|
}
|
|
|
|
// silence NaN reading errors by assuming zero
|
|
if err != nil && errors.Is(err, meters.ErrNaN) {
|
|
res.Value = 0
|
|
err = nil
|
|
}
|
|
|
|
if err == nil {
|
|
m.log.TRACE.Printf("%+v", res)
|
|
}
|
|
|
|
return res.Value, err
|
|
}
|
|
|
|
// CurrentPower implements the api.Meter interface
|
|
func (m *Modbus) CurrentPower() (float64, error) {
|
|
return m.floatGetter(m.opPower)
|
|
}
|
|
|
|
// totalEnergy implements the api.MeterEnergy interface
|
|
func (m *Modbus) totalEnergy() (float64, error) {
|
|
return m.floatGetter(m.opEnergy)
|
|
}
|
|
|
|
// soc implements the api.Battery interface
|
|
func (m *Modbus) soc() (float64, error) {
|
|
return m.floatGetter(m.opSoC)
|
|
}
|