Add MeterReturnEnergy (#29805)

This commit is contained in:
andig 2026-06-02 13:53:13 +02:00 • committed by GitHub
parent 869ac1d6eb
commit 7b5df178ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 196 additions and 57 deletions

View file

@ -9,7 +9,7 @@ import (
"golang.org/x/oauth2"
)
//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff
//go:generate go tool mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff
// Meter provides total active power in W
type Meter interface {
@ -21,6 +21,11 @@ type MeterEnergy interface {
TotalEnergy() (float64, error)
}
// MeterReturnEnergy provides total returned energy in kWh
type MeterReturnEnergy interface {
ReturnEnergy() (float64, error)
}
// PhaseCurrents provides per-phase current A
type PhaseCurrents interface {
Currents() (float64, float64, float64, error)

View file

@ -273,6 +273,21 @@ func (i *iMeterEnergy) TotalEnergy() (float64, error) {
return i.meterEnergy0()
}
func MeterReturnEnergy(meterReturnEnergy0 func() (float64, error)) api.MeterReturnEnergy {
if meterReturnEnergy0 == nil {
return nil
}
return &iMeterReturnEnergy{meterReturnEnergy0}
}
type iMeterReturnEnergy struct {
meterReturnEnergy0 func() (float64, error)
}
func (i *iMeterReturnEnergy) ReturnEnergy() (float64, error) {
return i.meterReturnEnergy0()
}
func PhaseCurrents(phaseCurrents0 func() (float64, float64, float64, error)) api.PhaseCurrents {
if phaseCurrents0 == nil {
return nil

View file

@ -1,9 +1,9 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff)
// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff)
//
// Generated by this command:
//
// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff
// mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,CurrentGetter,PhaseSwitcher,PhaseGetter,FeatureDescriber,Identifier,Meter,MeterEnergy,MeterReturnEnergy,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff
//
// Package api is a generated GoMock package.
@ -448,6 +448,45 @@ func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy))
}
// MockMeterReturnEnergy is a mock of MeterReturnEnergy interface.
type MockMeterReturnEnergy struct {
ctrl *gomock.Controller
recorder *MockMeterReturnEnergyMockRecorder
isgomock struct{}
}
// MockMeterReturnEnergyMockRecorder is the mock recorder for MockMeterReturnEnergy.
type MockMeterReturnEnergyMockRecorder struct {
mock *MockMeterReturnEnergy
}
// NewMockMeterReturnEnergy creates a new mock instance.
func NewMockMeterReturnEnergy(ctrl *gomock.Controller) *MockMeterReturnEnergy {
mock := &MockMeterReturnEnergy{ctrl: ctrl}
mock.recorder = &MockMeterReturnEnergyMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMeterReturnEnergy) EXPECT() *MockMeterReturnEnergyMockRecorder {
return m.recorder
}
// ReturnEnergy mocks base method.
func (m *MockMeterReturnEnergy) ReturnEnergy() (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReturnEnergy")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReturnEnergy indicates an expected call of ReturnEnergy.
func (mr *MockMeterReturnEnergyMockRecorder) ReturnEnergy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReturnEnergy", reflect.TypeOf((*MockMeterReturnEnergy)(nil).ReturnEnergy))
}
// MockPhaseCurrents is a mock of PhaseCurrents interface.
type MockPhaseCurrents struct {
ctrl *gomock.Controller

View file

@ -146,13 +146,13 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.C
implement.May(c, implement.Battery(soc))
implement.May(c, implement.SocLimiter(limitsoc))
// decorate measurements
powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(c, implement.Meter(powerG))
implement.May(c, implement.MeterEnergy(energyG))
implement.May(c, implement.MeterReturnEnergy(returnG))
currentsG, voltagesG, _, err := cc.Phases.Configure(ctx)
if err != nil {

View file

@ -82,12 +82,13 @@ func NewHeatpumpFromConfig(ctx context.Context, other map[string]any) (api.Charg
return nil, err
}
powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(res, implement.Meter(powerG))
implement.May(res, implement.MeterEnergy(energyG))
implement.May(res, implement.MeterReturnEnergy(returnG))
tempG, limitTempG, err := cc.Temperature.Configure(ctx)
if err != nil {

View file

@ -10,22 +10,29 @@ import (
type Energy struct {
Power *plugin.Config // optional
Energy *plugin.Config // optional
ReturnEnergy *plugin.Config // optional
}
func (cc *Energy) Configure(ctx context.Context) (
func() (float64, error),
func() (float64, error),
func() (float64, error),
error,
) {
powerG, err := cc.Power.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("power: %w", err)
return nil, nil, nil, fmt.Errorf("power: %w", err)
}
energyG, err := cc.Energy.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("energy: %w", err)
return nil, nil, nil, fmt.Errorf("energy: %w", err)
}
return powerG, energyG, nil
returnG, err := cc.ReturnEnergy.FloatGetter(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("returnEnergy: %w", err)
}
return powerG, energyG, returnG, nil
}

View file

@ -61,12 +61,13 @@ func NewSgReadyRelayFromConfig(ctx context.Context, other map[string]any) (api.C
return nil, err
}
powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(res, implement.Meter(powerG))
implement.May(res, implement.MeterEnergy(energyG))
implement.May(res, implement.MeterReturnEnergy(returnG))
tempG, limitTempG, err := cc.Temperature.Configure(ctx)
if err != nil {

View file

@ -107,12 +107,13 @@ func NewSgReadyFromConfig(ctx context.Context, other map[string]any) (api.Charge
return nil, err
}
powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
implement.May(res, implement.Meter(powerG))
implement.May(res, implement.MeterEnergy(energyG))
implement.May(res, implement.MeterReturnEnergy(returnG))
tempG, limitTempG, err := cc.Temperature.Configure(ctx)
if err != nil {

View file

@ -92,6 +92,13 @@ func (d *dumper) Dump(name string, v any) {
})
}
if v, ok := api.Cap[api.MeterReturnEnergy](v); ok {
d.measureTime(w, "Return Energy", func() (string, error) {
energy, err := v.ReturnEnergy()
return fmt.Sprintf("%.1fkWh", energy), err
})
}
if v, ok := api.Cap[api.PhaseCurrents](v); ok {
d.measureTime(w, "Current L1..L3", func() (string, error) {
i1, i2, i3, err := v.Currents()

View file

@ -75,6 +75,7 @@ func generate(out io.Writer) error {
reflect.TypeFor[api.MaxACPowerGetter](),
reflect.TypeFor[api.Meter](),
reflect.TypeFor[api.MeterEnergy](),
reflect.TypeFor[api.MeterReturnEnergy](),
reflect.TypeFor[api.PhaseCurrents](),
reflect.TypeFor[api.PhaseGetter](),
reflect.TypeFor[api.PhasePowers](),

View file

@ -10,22 +10,29 @@ import (
type Energy struct {
Power plugin.Config
Energy *plugin.Config // optional
ReturnEnergy *plugin.Config // optional
}
func (cc *Energy) Configure(ctx context.Context) (
func() (float64, error),
func() (float64, error),
func() (float64, error),
error,
) {
powerG, err := cc.Power.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("power: %w", err)
return nil, nil, nil, fmt.Errorf("power: %w", err)
}
energyG, err := cc.Energy.FloatGetter(ctx)
if err != nil {
return nil, nil, fmt.Errorf("energy: %w", err)
return nil, nil, nil, fmt.Errorf("energy: %w", err)
}
return powerG, energyG, nil
returnG, err := cc.ReturnEnergy.FloatGetter(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("returnEnergy: %w", err)
}
return powerG, energyG, returnG, nil
}

View file

@ -44,13 +44,14 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M
return nil, err
}
powerG, energyG, err := cc.Energy.Configure(ctx)
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
if err != nil {
return nil, err
}
m, _ := NewConfigurable(powerG)
implement.May(m, implement.MeterEnergy(energyG))
implement.May(m, implement.MeterReturnEnergy(returnG))
// dim/curtail
if err := cc.Dimmer.Implement(ctx, m); err != nil {

View file

@ -47,6 +47,12 @@ func NewMovingAverageFromConfig(ctx context.Context, other map[string]any) (api.
totalEnergy = m.TotalEnergy
}
// decorate return energy reading
var returnEnergy func() (float64, error)
if m, ok := api.Cap[api.MeterReturnEnergy](m); ok {
returnEnergy = m.ReturnEnergy
}
// decorate battery reading
var batterySoc func() (float64, error)
if m, ok := api.Cap[api.Battery](m); ok {
@ -72,6 +78,7 @@ func NewMovingAverageFromConfig(ctx context.Context, other map[string]any) (api.
}
implement.May(meter, implement.MeterEnergy(totalEnergy))
implement.May(meter, implement.MeterReturnEnergy(returnEnergy))
if batterySoc != nil {
implement.Has(meter, implement.Battery(batterySoc))

View file

@ -6,22 +6,24 @@ import (
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
)
type Generation interface {
CurrentPower() (float64, error)
Enabled() (bool, error)
Enable(bool) error
TotalEnergy() (float64, error)
api.Meter
api.MeterEnergy
api.MeterReturnEnergy
}
type Phases interface {
Currents() (float64, float64, float64, error)
Voltages() (float64, float64, float64, error)
Powers() (float64, float64, float64, error)
api.PhaseCurrents
api.PhaseVoltages
api.PhasePowers
}
// Connection is the Shelly connection

View file

@ -125,6 +125,25 @@ func (c *gen1) TotalEnergy() (float64, error) {
return c.energy(energy) / 1000, nil
}
func (c *gen1) ReturnEnergy() (float64, error) {
var energy float64
res, err := c.status.Get()
if err != nil {
return 0, err
}
switch {
case c.channel < len(res.Meters):
energy = res.Meters[c.channel].Total_Returned
case c.channel < len(res.EMeters):
energy = res.EMeters[c.channel].Total_Returned
default:
return 0, errors.New("invalid channel, missing power meter")
}
return c.energy(energy) / 1000, nil
}
// gen1Energy in kWh
func (c *gen1) energy(energy float64) float64 {
// Gen 1 Shelly EM devices are providing Watt hours, Gen 1 Shelly PM devices are providing Watt minutes

View file

@ -219,7 +219,7 @@ func (c *gen2) Enable(enable bool) error {
return c.execEnableCmd(c.switchchannel, "Switch.Set", enable, &res)
}
// TotalEnergy implements the api.Meter interface
// TotalEnergy implements the api.MeterEnergy interface
func (c *gen2) TotalEnergy() (float64, error) {
switch {
case c.hasEM1Endpoint():
@ -239,6 +239,26 @@ func (c *gen2) TotalEnergy() (float64, error) {
}
}
// ReturnEnergy implements the api.MeterReturnEnergy interface
func (c *gen2) ReturnEnergy() (float64, error) {
switch {
case c.hasEM1Endpoint():
res, err := c.em1data()
return res.TotalActRetEnergy / 1000, err
case c.hasEMEndpoint():
res, err := c.emdata()
return res.TotalActRet / 1000, err
case c.hasSwitchEndpoint():
res, err := c.switchstatus.Get()
return res.Ret_Aenergy.Total / 1000, err
default:
return 0, fmt.Errorf("unknown shelly model: %s", c.model)
}
}
// Currents implements the api.PhaseCurrents interface
func (c *gen2) Currents() (float64, float64, float64, error) {
switch {

View file

@ -51,32 +51,28 @@ func NewMeterFromConfig(ctx context.Context, other map[string]any) (Plugin, erro
var _ FloatGetter = (*meterPlugin)(nil)
func (o *meterPlugin) FloatGetter() (func() (float64, error), error) {
err := fmt.Errorf("unsupported method: %s", o.method.String())
switch o.method {
case Energy:
if !api.HasCap[api.MeterEnergy](o.meter) {
return nil, err
}
case Soc:
if !api.HasCap[api.Battery](o.meter) {
return nil, err
}
}
return func() (float64, error) {
switch o.method {
case Energy:
m, _ := api.Cap[api.MeterEnergy](o.meter)
if m, ok := api.Cap[api.MeterEnergy](o.meter); ok {
f, err := m.TotalEnergy()
return f * o.scale, err
}
case ReturnEnergy:
if m, ok := api.Cap[api.MeterReturnEnergy](o.meter); ok {
f, err := m.ReturnEnergy()
return f * o.scale, err
}
case Soc:
m, _ := api.Cap[api.Battery](o.meter)
if m, ok := api.Cap[api.Battery](o.meter); ok {
f, err := m.Soc()
return f * o.scale, err
default:
}
case Power:
f, err := o.meter.CurrentPower()
return f * o.scale, err
}
return 0, fmt.Errorf("unsupported method: %s", o.method.String())
}, nil
}

View file

@ -6,6 +6,7 @@ type Method int
const (
_ Method = iota
Energy
ReturnEnergy
Power
Soc
)

View file

@ -7,11 +7,11 @@ import (
"strings"
)
const _MethodName = "EnergyPowerSoc"
const _MethodName = "EnergyReturnEnergyPowerSoc"
var _MethodIndex = [...]uint8{0, 6, 11, 14}
var _MethodIndex = [...]uint8{0, 6, 18, 23, 26}
const _MethodLowerName = "energypowersoc"
const _MethodLowerName = "energyreturnenergypowersoc"
func (i Method) String() string {
i -= 1
@ -26,25 +26,29 @@ func (i Method) String() string {
func _MethodNoOp() {
var x [1]struct{}
_ = x[Energy-(1)]
_ = x[Power-(2)]
_ = x[Soc-(3)]
_ = x[ReturnEnergy-(2)]
_ = x[Power-(3)]
_ = x[Soc-(4)]
}
var _MethodValues = []Method{Energy, Power, Soc}
var _MethodValues = []Method{Energy, ReturnEnergy, Power, Soc}
var _MethodNameToValueMap = map[string]Method{
_MethodName[0:6]: Energy,
_MethodLowerName[0:6]: Energy,
_MethodName[6:11]: Power,
_MethodLowerName[6:11]: Power,
_MethodName[11:14]: Soc,
_MethodLowerName[11:14]: Soc,
_MethodName[6:18]: ReturnEnergy,
_MethodLowerName[6:18]: ReturnEnergy,
_MethodName[18:23]: Power,
_MethodLowerName[18:23]: Power,
_MethodName[23:26]: Soc,
_MethodLowerName[23:26]: Soc,
}
var _MethodNames = []string{
_MethodName[0:6],
_MethodName[6:11],
_MethodName[11:14],
_MethodName[6:18],
_MethodName[18:23],
_MethodName[23:26],
}
// MethodString retrieves an enum value from the enum constants string name.

View file

@ -279,6 +279,11 @@ func testInstance(instance any) map[string]testResult {
makeResult("energy", val, err)
}
if dev, ok := api.Cap[api.MeterReturnEnergy](instance); ok {
val, err := dev.ReturnEnergy()
makeResult("returnEnergy", val, err)
}
if dev, ok := api.Cap[api.Battery](instance); ok {
val, err := dev.Soc()
key := "soc"