evcc-io/meter/mbmd.go

199 lines
4.8 KiB
Go

package meter
import (
"context"
"errors"
"fmt"
"strings"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/api/implement"
"github.com/evcc-io/evcc/meter/measurement"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/modbus"
"github.com/volkszaehler/mbmd/meters"
"github.com/volkszaehler/mbmd/meters/rs485"
)
// Mbmd is an api.Meter implementation with configurable getters and setters.
type Mbmd struct {
conn *modbus.Connection
device *rs485.RS485
}
func init() {
registry.AddCtx("mbmd", NewMbmdFromConfig)
}
// NewMbmdFromConfig creates api.Meter from config
func NewMbmdFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) {
cc := struct {
Model string
batteryCapacity `mapstructure:",squash"`
batteryPowerLimits `mapstructure:",squash"`
batterySocLimits `mapstructure:",squash"`
modbus.Settings `mapstructure:",squash"`
Power, Energy, Soc string
ReturnEnergy string
Currents []string
Voltages []string
Powers []string
}{
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 {
if rtu := isRS485(cc.Model); rtu {
cc.RTU = &rtu
}
}
modbus.Lock()
defer modbus.Unlock()
conn, err := cc.Settings.Connection(ctx)
if err != nil {
return nil, err
}
log := util.NewLogger("modbus")
conn.Logger(log.TRACE)
// prepare device
device, err := rs485.NewDevice(strings.ToUpper(cc.Model))
if err != nil {
return nil, err
}
mbmd := &Mbmd{
conn: conn,
device: device,
}
ops := device.Producer().Produce()
powerG, err := mbmd.deviceOp(ops, cc.Power)
if err != nil {
return nil, fmt.Errorf("invalid measurement for power: %s", cc.Power)
}
m, _ := NewConfigurable(powerG)
// decorate energy
if cc.Energy != "" {
totalEnergy, err := mbmd.deviceOp(ops, cc.Energy)
if err != nil {
return nil, fmt.Errorf("invalid measurement for energy: %s", cc.Energy)
}
implement.Has(m, implement.MeterEnergy(totalEnergy))
}
// decorate return energy
if cc.ReturnEnergy != "" {
returnEnergy, err := mbmd.deviceOp(ops, cc.ReturnEnergy)
if err != nil {
return nil, fmt.Errorf("invalid measurement for returnenergy: %s", cc.ReturnEnergy)
}
implement.Has(m, implement.MeterReturnEnergy(returnEnergy))
}
// decorate soc
if cc.Soc != "" {
soc, err := mbmd.deviceOp(ops, cc.Soc)
if err != nil {
return nil, fmt.Errorf("invalid measurement for soc: %s", cc.Soc)
}
implement.Has(m, implement.Battery(soc))
implement.May(m, implement.BatteryCapacity(cc.batteryCapacity.Decorator()))
implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator()))
implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator()))
return m, nil
}
// decorate currents
currentsG, err := mbmd.buildPhaseProviders(ops, cc.Currents)
if err != nil {
return nil, fmt.Errorf("currents: %w", err)
}
implement.May(m, implement.PhaseCurrents(currentsG))
// decorate voltages
voltagesG, err := mbmd.buildPhaseProviders(ops, cc.Voltages)
if err != nil {
return nil, fmt.Errorf("voltages: %w", err)
}
implement.May(m, implement.PhaseVoltages(voltagesG))
// decorate powers
powersG, err := mbmd.buildPhaseProviders(ops, cc.Powers)
if err != nil {
return nil, fmt.Errorf("powers: %w", err)
}
implement.May(m, implement.PhasePowers(powersG))
return m, nil
}
// deviceOp checks is RS485 device supports operation
func (m *Mbmd) deviceOp(ops []rs485.Operation, name string) (func() (float64, error), error) {
// leading minus sign?
name, invert := strings.CutPrefix(name, "-")
measurement, err := meters.MeasurementString(name)
if err != nil {
return nil, fmt.Errorf("invalid measurement: %s", name)
}
for _, op := range ops {
if op.IEC61850 == measurement {
return func() (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) {
res.Value = 0
err = nil
}
if invert {
return -res.Value, err
}
return res.Value, err
}, nil
}
}
return nil, fmt.Errorf("unsupported measurement: %s", measurement.String())
}
func (m *Mbmd) buildPhaseProviders(ops []rs485.Operation, readings []string) (func() (float64, float64, float64, error), error) {
if len(readings) == 0 {
return nil, nil
}
if len(readings) != 3 {
return nil, errors.New("need one per phase, total three")
}
var phases [3]func() (float64, error)
for idx, reading := range readings {
g, err := m.deviceOp(ops, reading)
if err != nil {
return nil, fmt.Errorf("invalid measurement [%d]: %s", idx, reading)
}
phases[idx] = g
}
return measurement.CombinePhases(phases), nil
}