evcc-io/meter/mbmd.go
2025-10-31 15:25:53 +00:00

191 lines
4.9 KiB
Go

package meter
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"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"
)
// ModbusMbmd is an api.Meter implementation with configurable getters and setters.
type ModbusMbmd struct {
conn *modbus.Connection
device *rs485.RS485
opPower rs485.Operation
opEnergy rs485.Operation
opSoc rs485.Operation
}
func init() {
registry.AddCtx("mbmd", NewModbusMbmdFromConfig)
}
//go:generate go tool decorate -f decorateModbusMbmd -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhasePowers,Powers,func() (float64, float64, float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64"
// NewModbusMbmdFromConfig creates api.Meter from config
func NewModbusMbmdFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) {
cc := struct {
Model string
batteryCapacity `mapstructure:",squash"`
modbus.Settings `mapstructure:",squash"`
Power, Energy, Soc string
Currents []string
Voltages []string
Powers []string
Delay time.Duration
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 {
if rtu := isRS485(cc.Model); rtu {
cc.RTU = &rtu
}
}
modbus.Lock()
defer modbus.Unlock()
conn, err := modbus.NewConnection(ctx, cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.Settings.Protocol(), cc.ID)
if err != nil {
return nil, err
}
// set non-default timeout
conn.Timeout(cc.Timeout)
// set non-default delay
conn.Delay(cc.Delay)
log := util.NewLogger("modbus")
conn.Logger(log.TRACE)
// prepare device
device, err := rs485.NewDevice(strings.ToUpper(cc.Model))
if err != nil {
return nil, err
}
m := &ModbusMbmd{
conn: conn,
device: device,
}
ops := device.Producer().Produce()
m.opPower, err = rs485FindDeviceOp(ops, cc.Power)
if err != nil {
return nil, fmt.Errorf("invalid measurement for power: %s", cc.Power)
}
// decorate energy
var totalEnergy func() (float64, error)
if cc.Energy != "" {
m.opEnergy, err = rs485FindDeviceOp(ops, cc.Energy)
if err != nil {
return nil, fmt.Errorf("invalid measurement for energy: %s", cc.Energy)
}
totalEnergy = m.totalEnergy
}
// decorate currents
currentsG, err := m.buildPhaseProviders(ops, cc.Currents)
if err != nil {
return nil, fmt.Errorf("currents: %w", err)
}
// decorate voltages
voltagesG, err := m.buildPhaseProviders(ops, cc.Voltages)
if err != nil {
return nil, fmt.Errorf("voltages: %w", err)
}
// decorate powers
powersG, err := m.buildPhaseProviders(ops, cc.Powers)
if err != nil {
return nil, fmt.Errorf("powers: %w", err)
}
// decorate soc
var soc func() (float64, error)
if cc.Soc != "" {
m.opSoc, err = rs485FindDeviceOp(ops, cc.Soc)
if err != nil {
return nil, fmt.Errorf("invalid measurement for soc: %s", cc.Soc)
}
soc = m.soc
}
return decorateModbusMbmd(m, totalEnergy, currentsG, voltagesG, powersG, soc, cc.batteryCapacity.Decorator()), nil
}
func (m *ModbusMbmd) 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 {
opCurrent, err := rs485FindDeviceOp(ops, reading)
if err != nil {
return nil, fmt.Errorf("invalid measurement [%d]: %s", idx, reading)
}
phases[idx] = func() (float64, error) {
return m.floatGetter(opCurrent)
}
}
return measurement.CombinePhases(phases), nil
}
// floatGetter executes configured modbus read operation and implements func() (float64, error)
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) {
res.Value = 0
err = nil
}
return res.Value, err
}
// CurrentPower implements the api.Meter interface
func (m *ModbusMbmd) CurrentPower() (float64, error) {
return m.floatGetter(m.opPower)
}
// totalEnergy implements the api.MeterEnergy interface
func (m *ModbusMbmd) totalEnergy() (float64, error) {
return m.floatGetter(m.opEnergy)
}
// soc implements the api.Battery interface
func (m *ModbusMbmd) soc() (float64, error) {
return m.floatGetter(m.opSoc)
}