diff --git a/api/api.go b/api/api.go index d92a7e65bd0..989caf5d624 100644 --- a/api/api.go +++ b/api/api.go @@ -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/exported energy in kWh +type MeterReturnEnergy interface { + ReturnEnergy() (float64, error) +} + // PhaseCurrents provides per-phase current A type PhaseCurrents interface { Currents() (float64, float64, float64, error) diff --git a/api/implement/implementations.go b/api/implement/implementations.go index 70a5fc630de..c7c4776c212 100644 --- a/api/implement/implementations.go +++ b/api/implement/implementations.go @@ -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 diff --git a/api/mock.go b/api/mock.go index 4591c1e3866..f260aaa32cc 100644 --- a/api/mock.go +++ b/api/mock.go @@ -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 diff --git a/charger/charger.go b/charger/charger.go index 5e47a74da35..7d7fce0b236 100644 --- a/charger/charger.go +++ b/charger/charger.go @@ -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 { diff --git a/charger/easee_test.go b/charger/easee_test.go index a9905ad4294..cc251f2ead9 100644 --- a/charger/easee_test.go +++ b/charger/easee_test.go @@ -624,7 +624,7 @@ func TestChargingSession_AtStartup_ProtectsSessionEnergy(t *testing.T) { assert.Equal(t, 9173.5, total) // totalEnergy updated } -func TestLifetimeEnergy_DoesNotDecreaseTotalEnergy(t *testing.T) { +func TestLifetimeEnergy_DoesNotDecreaseImportEnergy(t *testing.T) { e := newEasee() e.totalEnergy = 9173.5 diff --git a/charger/heatpump.go b/charger/heatpump.go index 0eb44b2ec9c..f523c4de622 100644 --- a/charger/heatpump.go +++ b/charger/heatpump.go @@ -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 { diff --git a/charger/measurement/energy.go b/charger/measurement/energy.go index ed173643c6f..56c86af8291 100644 --- a/charger/measurement/energy.go +++ b/charger/measurement/energy.go @@ -8,24 +8,31 @@ import ( ) type Energy struct { - Power *plugin.Config // optional - Energy *plugin.Config // optional + 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) + } + + returnG, err := cc.ReturnEnergy.FloatGetter(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("returnEnergy: %w", err) } - return powerG, energyG, nil + return powerG, energyG, returnG, nil } diff --git a/charger/sgready-relay.go b/charger/sgready-relay.go index 62e17cc4aa8..92336b2a417 100644 --- a/charger/sgready-relay.go +++ b/charger/sgready-relay.go @@ -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 { diff --git a/charger/sgready.go b/charger/sgready.go index bc152b4f50e..d18012c6f80 100644 --- a/charger/sgready.go +++ b/charger/sgready.go @@ -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 { diff --git a/cmd/dumper.go b/cmd/dumper.go index 6d6e1752180..39c5ff7cb6c 100644 --- a/cmd/dumper.go +++ b/cmd/dumper.go @@ -86,12 +86,19 @@ func (d *dumper) Dump(name string, v any) { } if v, ok := api.Cap[api.MeterEnergy](v); ok { - d.measureTime(w, "Energy", func() (string, error) { + d.measureTime(w, "Import", func() (string, error) { energy, err := v.TotalEnergy() return fmt.Sprintf("%.1fkWh", energy), err }) } + if v, ok := api.Cap[api.MeterReturnEnergy](v); ok { + d.measureTime(w, "Export", 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() diff --git a/cmd/implement/implement.go b/cmd/implement/implement.go index 19b7e7ca096..ec034e1f4e1 100644 --- a/cmd/implement/implement.go +++ b/cmd/implement/implement.go @@ -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](), diff --git a/core/loadpoint.go b/core/loadpoint.go index 870c57014c0..bba58de4b2c 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -403,7 +403,7 @@ func (lp *Loadpoint) requestUpdate() { } // capableMeter wraps a meter with capability lookup from its source. -// This preserves capability checks (like MeterEnergy, PhaseCurrents, PhaseVoltages) when +// This preserves capability checks (like MeterEnergy, MeterReturnEnergy, PhaseCurrents, PhaseVoltages) when // the meter was extracted from a decorated charger's capability registry. type capableMeter struct { api.Meter diff --git a/core/metrics/accumulator_test.go b/core/metrics/accumulator_test.go index 62b4962142a..0de3ee1d477 100644 --- a/core/metrics/accumulator_test.go +++ b/core/metrics/accumulator_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMeterEnergyMeterTotal(t *testing.T) { +func TestMeterImportMeterTotal(t *testing.T) { clock := clock.NewMock() clock.Set(now.BeginningOfDay()) @@ -23,7 +23,7 @@ func TestMeterEnergyMeterTotal(t *testing.T) { assert.Equal(t, 1.0, me.Imported()) } -func TestMeterEnergyAddPower(t *testing.T) { +func TestMeterImportAddPower(t *testing.T) { clock := clock.NewMock() clock.Set(now.BeginningOfDay()) diff --git a/core/site.go b/core/site.go index 8f3c39d1475..bf7ec0d8b87 100644 --- a/core/site.go +++ b/core/site.go @@ -370,7 +370,7 @@ func meterCapabilities(name string, meter any) string { panic("not a meter: " + name) } - energy := api.HasCap[api.MeterEnergy](meter) + energy := api.HasCap[api.MeterEnergy](meter) || api.HasCap[api.MeterReturnEnergy](meter) currents := api.HasCap[api.PhaseCurrents](meter) name += ":" @@ -516,10 +516,19 @@ func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) [ site.log.ERROR.Printf("%s %d power: %v", key, i+1, err) } - // energy (production) + // energy (production for pv/battery, consumption for aux/ext) var energy float64 - if m, ok := api.Cap[api.MeterEnergy](meter); err == nil && ok { - energy, err = m.TotalEnergy() + if err == nil { + switch key { + case "pv", "battery": + if m, ok := api.Cap[api.MeterReturnEnergy](meter); ok { + energy, err = m.ReturnEnergy() + } + default: + if m, ok := api.Cap[api.MeterEnergy](meter); ok { + energy, err = m.TotalEnergy() + } + } if err != nil { site.log.ERROR.Printf("%s %d energy: %v", key, i+1, err) } diff --git a/meter/homematic.go b/meter/homematic.go index ef71a453de4..76f825b35b1 100644 --- a/meter/homematic.go +++ b/meter/homematic.go @@ -68,7 +68,7 @@ var _ api.MeterEnergy = (*CCU)(nil) // TotalEnergy implements the api.MeterEnergy interface func (c *CCU) TotalEnergy() (float64, error) { if c.usage == "grid" { - return c.conn.GridTotalEnergy() + return c.conn.GridImportEnergy() } return c.conn.TotalEnergy() } diff --git a/meter/homematic/connection.go b/meter/homematic/connection.go index 59083d1f869..c200f2a487d 100644 --- a/meter/homematic/connection.go +++ b/meter/homematic/connection.go @@ -109,8 +109,8 @@ func (c *Connection) GridCurrentPower() (float64, error) { return res.FloatValue("IEC_POWER"), err } -// GridTotalEnergy reads the homematic HM-ES-TX-WM grid meterchannel energy in kWh -func (c *Connection) GridTotalEnergy() (float64, error) { +// GridImportEnergy reads the homematic HM-ES-TX-WM grid meterchannel energy in kWh +func (c *Connection) GridImportEnergy() (float64, error) { res, err := c.meterG.Get() return res.FloatValue("IEC_ENERGY_COUNTER"), err } diff --git a/meter/measurement/energy.go b/meter/measurement/energy.go index 658a91460ac..f7f3d3cfdc0 100644 --- a/meter/measurement/energy.go +++ b/meter/measurement/energy.go @@ -8,24 +8,31 @@ import ( ) type Energy struct { - Power plugin.Config - Energy *plugin.Config // optional + 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) + } + + returnG, err := cc.ReturnEnergy.FloatGetter(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("returnEnergy: %w", err) } - return powerG, energyG, nil + return powerG, energyG, returnG, nil } diff --git a/meter/meter.go b/meter/meter.go index a7f9599336b..c2fb1d51f1a 100644 --- a/meter/meter.go +++ b/meter/meter.go @@ -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)) // decorate soc socG, err := cc.Soc.FloatGetter(ctx) diff --git a/meter/meter_average.go b/meter/meter_average.go index f217f75ae99..472e5a81b22 100644 --- a/meter/meter_average.go +++ b/meter/meter_average.go @@ -41,10 +41,16 @@ func NewMovingAverageFromConfig(ctx context.Context, other map[string]any) (api. meter, _ := NewConfigurable(mav.CurrentPower) - // decorate energy reading - var totalEnergy func() (float64, error) + // decorate import reading + var importEnergy func() (float64, error) if m, ok := api.Cap[api.MeterEnergy](m); ok { - totalEnergy = m.TotalEnergy + importEnergy = m.TotalEnergy + } + + // decorate export reading + var exportEnergy func() (float64, error) + if m, ok := api.Cap[api.MeterReturnEnergy](m); ok { + exportEnergy = m.ReturnEnergy } // decorate battery reading @@ -71,7 +77,8 @@ func NewMovingAverageFromConfig(ctx context.Context, other map[string]any) (api. powers = m.Powers } - implement.May(meter, implement.MeterEnergy(totalEnergy)) + implement.May(meter, implement.MeterEnergy(importEnergy)) + implement.May(meter, implement.MeterReturnEnergy(exportEnergy)) if batterySoc != nil { implement.Has(meter, implement.Battery(batterySoc)) diff --git a/meter/powerwall.go b/meter/powerwall.go index c84d1e2df41..a1a905e0fca 100644 --- a/meter/powerwall.go +++ b/meter/powerwall.go @@ -142,10 +142,14 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, refres m.energySite = energySite } - if m.usage == "load" || m.usage == "solar" { + if m.usage == "load" { implement.Has(m, implement.MeterEnergy(m.totalEnergy)) } + if m.usage == "solar" { + implement.Has(m, implement.MeterReturnEnergy(m.totalEnergy)) + } + if usage == "battery" { implement.Has(m, implement.Battery(m.batterySoc)) implement.May(m, implement.BatterySocLimiter(batterySocLimits.Decorator())) @@ -193,7 +197,7 @@ func (m *PowerWall) CurrentPower() (float64, error) { return 0, fmt.Errorf("invalid usage: %s", m.usage) } -// totalEnergy implements the api.MeterEnergy interface +// totalEnergy returns import (load) or export (solar) energy depending on usage func (m *PowerWall) totalEnergy() (float64, error) { res, err := m.meterG() if err != nil { diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go index 1806140238d..abe6d2b0b34 100644 --- a/meter/shelly/connection.go +++ b/meter/shelly/connection.go @@ -16,6 +16,7 @@ type Generation interface { Enabled() (bool, error) Enable(bool) error TotalEnergy() (float64, error) + ReturnEnergy() (float64, error) } type Phases interface { diff --git a/meter/shelly/gen1.go b/meter/shelly/gen1.go index 512111f28c6..3856a584021 100644 --- a/meter/shelly/gen1.go +++ b/meter/shelly/gen1.go @@ -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 diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go index 58209b2e15c..102c2f0f1f4 100644 --- a/meter/shelly/gen2.go +++ b/meter/shelly/gen2.go @@ -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.MeterEnergy 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 { diff --git a/meter/sma.go b/meter/sma.go index 2ae5d59fecb..8c8454bcb2a 100644 --- a/meter/sma.go +++ b/meter/sma.go @@ -87,7 +87,7 @@ func NewSMA(uri, password, iface string, serial uint32, scale float64, usage str go sm.device.Run() if usage == "battery" { - implement.May(sm, implement.MeterEnergy(sm.TotalEnergy)) + implement.May(sm, implement.MeterReturnEnergy(sm.ReturnEnergy)) implement.Has(sm, implement.Battery(sm.soc)) implement.May(sm, implement.BatteryCapacity(capacity)) implement.May(sm, implement.BatterySocLimiter(batterySocLimits)) @@ -103,10 +103,10 @@ func (sm *SMA) CurrentPower() (float64, error) { return sm.scale * (sma.AsFloat(values[sunny.ActivePowerPlus]) - sma.AsFloat(values[sunny.ActivePowerMinus])), err } -var _ api.MeterEnergy = (*SMA)(nil) +var _ api.MeterReturnEnergy = (*SMA)(nil) -// TotalEnergy implements the api.MeterEnergy interface -func (sm *SMA) TotalEnergy() (float64, error) { +// ReturnEnergy implements the api.MeterReturnEnergy interface +func (sm *SMA) ReturnEnergy() (float64, error) { values, err := sm.device.Values() if sm.scale < 0 { return sma.AsFloat(values[sunny.ActiveEnergyMinus]) / 3600000, err diff --git a/plugin/meter.go b/plugin/meter.go index a59d89fa7d8..ff1e3b40467 100644 --- a/plugin/meter.go +++ b/plugin/meter.go @@ -55,7 +55,7 @@ func (o *meterPlugin) FloatGetter() (func() (float64, error), error) { switch o.method { case Energy: - if !api.HasCap[api.MeterEnergy](o.meter) { + if !api.HasCap[api.MeterEnergy](o.meter) && !api.HasCap[api.MeterReturnEnergy](o.meter) { return nil, err } case Soc: @@ -67,8 +67,12 @@ func (o *meterPlugin) FloatGetter() (func() (float64, error), error) { return func() (float64, error) { switch o.method { case Energy: - m, _ := api.Cap[api.MeterEnergy](o.meter) - f, err := m.TotalEnergy() + if m, ok := api.Cap[api.MeterEnergy](o.meter); ok { + f, err := m.TotalEnergy() + return f * o.scale, err + } + m, _ := api.Cap[api.MeterReturnEnergy](o.meter) + f, err := m.ReturnEnergy() return f * o.scale, err case Soc: m, _ := api.Cap[api.Battery](o.meter) diff --git a/server/http_config_helper.go b/server/http_config_helper.go index 921449c556b..4a300e84eed 100644 --- a/server/http_config_helper.go +++ b/server/http_config_helper.go @@ -277,6 +277,9 @@ func testInstance(instance any) map[string]testResult { if dev, ok := api.Cap[api.MeterEnergy](instance); ok { val, err := dev.TotalEnergy() makeResult("energy", val, err) + } else if dev, ok := api.Cap[api.MeterReturnEnergy](instance); ok { + val, err := dev.ReturnEnergy() + makeResult("energy", val, err) } if dev, ok := api.Cap[api.Battery](instance); ok {