api: split MeterEnergy into MeterImport and MeterExport (#29788)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
andig 2026-05-09 16:20:18 +02:00 • committed by GitHub
parent 37e5659936
commit 7a5b1e7560
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
128 changed files with 570 additions and 435 deletions

View file

@ -9,16 +9,21 @@ 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,MeterImport,MeterExport,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff
// Meter provides total active power in W
type Meter interface {
CurrentPower() (float64, error)
}
// MeterEnergy provides total energy in kWh
type MeterEnergy interface {
TotalEnergy() (float64, error)
// MeterImport provides total import in kWh
type MeterImport interface {
ImportEnergy() (float64, error)
}
// MeterExport provides total export in kWh
type MeterExport interface {
ExportEnergy() (float64, error)
}
// PhaseCurrents provides per-phase current A

View file

@ -19,9 +19,9 @@ func (d *decoratedMeter) Capability(typ reflect.Type) (any, bool) {
return c, ok
}
type testMeterEnergyImpl struct{}
type testMeterImportImpl struct{}
func (t *testMeterEnergyImpl) TotalEnergy() (float64, error) {
func (t *testMeterImportImpl) ImportEnergy() (float64, error) {
return 99.0, nil
}
@ -32,13 +32,13 @@ func (t *testMeterImpl) CurrentPower() (float64, error) {
}
func TestCap_DirectTypeAssertion(t *testing.T) {
// concrete type that directly implements MeterEnergy
impl := &testMeterEnergyImpl{}
// concrete type that directly implements MeterImport
impl := &testMeterImportImpl{}
me, ok := Cap[MeterEnergy](impl)
me, ok := Cap[MeterImport](impl)
require.True(t, ok)
energy, err := me.TotalEnergy()
energy, err := me.ImportEnergy()
assert.NoError(t, err)
assert.Equal(t, 99.0, energy)
}
@ -49,15 +49,15 @@ func TestCap_CapableRegistryLookup(t *testing.T) {
decorated := &decoratedMeter{
Meter: base,
caps: map[reflect.Type]any{
reflect.TypeFor[MeterEnergy](): &testMeterEnergyImpl{},
reflect.TypeFor[MeterImport](): &testMeterImportImpl{},
},
}
// should find MeterEnergy via registry
me, ok := Cap[MeterEnergy](decorated)
// should find MeterImport via registry
me, ok := Cap[MeterImport](decorated)
require.True(t, ok)
energy, err := me.TotalEnergy()
energy, err := me.ImportEnergy()
assert.NoError(t, err)
assert.Equal(t, 99.0, energy)
@ -81,11 +81,11 @@ func TestCap_ExtractedCapabilityLosesRegistry(t *testing.T) {
// Reproduces https://github.com/evcc-io/evcc/issues/28915
// When a Meter is extracted from a decorated charger via Cap[Meter],
// the extracted impl does NOT carry the Capable interface, so
// subsequent Cap[MeterEnergy] on the extracted value fails.
// subsequent Cap[MeterImport] on the extracted value fails.
decorated := &decoratedCharger{
caps: map[reflect.Type]any{
reflect.TypeFor[Meter](): &testMeterImpl{},
reflect.TypeFor[MeterEnergy](): &testMeterEnergyImpl{},
reflect.TypeFor[MeterImport](): &testMeterImportImpl{},
},
}
@ -93,9 +93,9 @@ func TestCap_ExtractedCapabilityLosesRegistry(t *testing.T) {
mt, ok := Cap[Meter](decorated)
require.True(t, ok)
// Bug: extracted meter cannot find MeterEnergy because it's a standalone impl
_, ok = Cap[MeterEnergy](mt)
assert.False(t, ok, "extracted meter should NOT have MeterEnergy capability")
// Bug: extracted meter cannot find MeterImport because it's a standalone impl
_, ok = Cap[MeterImport](mt)
assert.False(t, ok, "extracted meter should NOT have MeterImport capability")
// Fix: wrapping extracted meter with source's Capable preserves registry
type capableMeter struct {
@ -104,32 +104,32 @@ func TestCap_ExtractedCapabilityLosesRegistry(t *testing.T) {
}
wrapped := &capableMeter{Meter: mt, Capable: decorated}
me, ok := Cap[MeterEnergy](wrapped)
require.True(t, ok, "wrapped meter should find MeterEnergy via Capable")
me, ok := Cap[MeterImport](wrapped)
require.True(t, ok, "wrapped meter should find MeterImport via Capable")
energy, err := me.TotalEnergy()
energy, err := me.ImportEnergy()
assert.NoError(t, err)
assert.Equal(t, 99.0, energy)
}
func TestCap_NilValue(t *testing.T) {
_, ok := Cap[MeterEnergy](nil)
_, ok := Cap[MeterImport](nil)
assert.False(t, ok)
}
func TestCap_DirectTakesPrecedence(t *testing.T) {
// type that both directly implements AND has registry
type directAndCapable struct {
testMeterEnergyImpl
testMeterImportImpl
caps map[reflect.Type]any //nolint:unused
}
v := &directAndCapable{}
me, ok := Cap[MeterEnergy](v)
me, ok := Cap[MeterImport](v)
require.True(t, ok)
energy, err := me.TotalEnergy()
energy, err := me.ImportEnergy()
assert.NoError(t, err)
assert.Equal(t, 99.0, energy)
}

View file

@ -203,19 +203,34 @@ func (i *iChargeState) Status() (api.ChargeStatus, error) {
return i.chargeState0()
}
func MeterEnergy(meterEnergy0 func() (float64, error)) api.MeterEnergy {
if meterEnergy0 == nil {
func MeterImport(meterImport0 func() (float64, error)) api.MeterImport {
if meterImport0 == nil {
return nil
}
return &iMeterEnergy{meterEnergy0}
return &iMeterImport{meterImport0}
}
type iMeterEnergy struct {
meterEnergy0 func() (float64, error)
type iMeterImport struct {
meterImport0 func() (float64, error)
}
func (i *iMeterEnergy) TotalEnergy() (float64, error) {
return i.meterEnergy0()
func (i *iMeterImport) ImportEnergy() (float64, error) {
return i.meterImport0()
}
func MeterExport(meterExport0 func() (float64, error)) api.MeterExport {
if meterExport0 == nil {
return nil
}
return &iMeterExport{meterExport0}
}
type iMeterExport struct {
meterExport0 func() (float64, error)
}
func (i *iMeterExport) ExportEnergy() (float64, error) {
return i.meterExport0()
}
func PhaseCurrents(phaseCurrents0 func() (float64, float64, float64, error)) api.PhaseCurrents {

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,MeterImport,MeterExport,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,MeterImport,MeterExport,PhaseCurrents,Vehicle,ConnectionTimer,ChargeRater,Battery,BatteryController,BatterySocLimiter,Circuit,Dimmer,Tariff
//
// Package api is a generated GoMock package.
@ -409,43 +409,82 @@ func (mr *MockMeterMockRecorder) CurrentPower() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentPower", reflect.TypeOf((*MockMeter)(nil).CurrentPower))
}
// MockMeterEnergy is a mock of MeterEnergy interface.
type MockMeterEnergy struct {
// MockMeterImport is a mock of MeterImport interface.
type MockMeterImport struct {
ctrl *gomock.Controller
recorder *MockMeterEnergyMockRecorder
recorder *MockMeterImportMockRecorder
isgomock struct{}
}
// MockMeterEnergyMockRecorder is the mock recorder for MockMeterEnergy.
type MockMeterEnergyMockRecorder struct {
mock *MockMeterEnergy
// MockMeterImportMockRecorder is the mock recorder for MockMeterImport.
type MockMeterImportMockRecorder struct {
mock *MockMeterImport
}
// NewMockMeterEnergy creates a new mock instance.
func NewMockMeterEnergy(ctrl *gomock.Controller) *MockMeterEnergy {
mock := &MockMeterEnergy{ctrl: ctrl}
mock.recorder = &MockMeterEnergyMockRecorder{mock}
// NewMockMeterImport creates a new mock instance.
func NewMockMeterImport(ctrl *gomock.Controller) *MockMeterImport {
mock := &MockMeterImport{ctrl: ctrl}
mock.recorder = &MockMeterImportMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMeterEnergy) EXPECT() *MockMeterEnergyMockRecorder {
func (m *MockMeterImport) EXPECT() *MockMeterImportMockRecorder {
return m.recorder
}
// TotalEnergy mocks base method.
func (m *MockMeterEnergy) TotalEnergy() (float64, error) {
// ImportEnergy mocks base method.
func (m *MockMeterImport) ImportEnergy() (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TotalEnergy")
ret := m.ctrl.Call(m, "ImportEnergy")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TotalEnergy indicates an expected call of TotalEnergy.
func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call {
// ImportEnergy indicates an expected call of ImportEnergy.
func (mr *MockMeterImportMockRecorder) ImportEnergy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportEnergy", reflect.TypeOf((*MockMeterImport)(nil).ImportEnergy))
}
// MockMeterExport is a mock of MeterExport interface.
type MockMeterExport struct {
ctrl *gomock.Controller
recorder *MockMeterExportMockRecorder
isgomock struct{}
}
// MockMeterExportMockRecorder is the mock recorder for MockMeterExport.
type MockMeterExportMockRecorder struct {
mock *MockMeterExport
}
// NewMockMeterExport creates a new mock instance.
func NewMockMeterExport(ctrl *gomock.Controller) *MockMeterExport {
mock := &MockMeterExport{ctrl: ctrl}
mock.recorder = &MockMeterExportMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMeterExport) EXPECT() *MockMeterExportMockRecorder {
return m.recorder
}
// ExportEnergy mocks base method.
func (m *MockMeterExport) ExportEnergy() (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExportEnergy")
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExportEnergy indicates an expected call of ExportEnergy.
func (mr *MockMeterExportMockRecorder) ExportEnergy() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportEnergy", reflect.TypeOf((*MockMeterExport)(nil).ExportEnergy))
}
// MockPhaseCurrents is a mock of PhaseCurrents interface.