package meter import ( "context" "errors" "fmt" "strings" "time" "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 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 } 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 }