From f41ad70ac418ae5dd2fc9503c2990a1ac72347e6 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 15 May 2026 16:30:25 +0200 Subject: [PATCH] Loadpoint: fix charge meter method decoration (#29912) --- core/capable_test.go | 46 +++++++++++++++++++++++++++++++++++++++----- core/caps.go | 32 ++++++++++++++++++++++++++++++ core/loadpoint.go | 23 +++++++--------------- 3 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 core/caps.go diff --git a/core/capable_test.go b/core/capable_test.go index 40b9b9157..40a925c34 100644 --- a/core/capable_test.go +++ b/core/capable_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/api/implement" "github.com/stretchr/testify/assert" ) @@ -81,11 +82,7 @@ func TestCapsWrapping(t *testing.T) { var m api.Meter if mt, ok := api.Cap[api.Meter](c); ok { - if c, ok := c.(api.Capable); ok { - m = &capableMeter{Meter: mt, Capable: c} - } else { - m = mt - } + m = &capableMeter{Meter: mt, source: c} } { @@ -104,3 +101,42 @@ func TestCapsWrapping(t *testing.T) { assert.True(t, api.HasCap[api.Battery](m), "missing battery cap") } } + +// staticPhaseCharger emulates a charger like DaheimLaden: it embeds an +// implement.Caps registry (so it satisfies api.Capable) but exposes +// api.Meter and api.PhaseCurrents as static struct methods rather than +// registering them in the registry. +type staticPhaseCharger struct { + implement.Caps +} + +var ( + _ api.Capable = (*staticPhaseCharger)(nil) + _ api.Meter = (*staticPhaseCharger)(nil) + _ api.PhaseCurrents = (*staticPhaseCharger)(nil) +) + +func (*staticPhaseCharger) CurrentPower() (float64, error) { return 0, nil } +func (*staticPhaseCharger) Currents() (float64, float64, float64, error) { return 1, 2, 3, nil } + +// TestCapableMeterStaticInterface guards against the regression in +// https://github.com/evcc-io/evcc/issues/29877: a charger that embeds +// implement.Caps but implements PhaseCurrents as a static method must +// still expose that capability through the capableMeter wrapper. +func TestCapableMeterStaticInterface(t *testing.T) { + c := &staticPhaseCharger{Caps: implement.New()} + + var m api.Meter + if mt, ok := api.Cap[api.Meter](c); ok { + m = &capableMeter{Meter: mt, source: c} + } + + assert.True(t, api.HasCap[api.PhaseCurrents](m), + "PhaseCurrents must remain discoverable on capableMeter when implemented statically") + + pc, ok := api.Cap[api.PhaseCurrents](m) + assert.True(t, ok) + i1, i2, i3, err := pc.Currents() + assert.NoError(t, err) + assert.Equal(t, []float64{1, 2, 3}, []float64{i1, i2, i3}) +} diff --git a/core/caps.go b/core/caps.go new file mode 100644 index 000000000..7965a2037 --- /dev/null +++ b/core/caps.go @@ -0,0 +1,32 @@ +package core + +import ( + "reflect" + + "github.com/evcc-io/evcc/api" +) + +// capableMeter wraps a meter with capability lookup from its source. +// This preserves capability checks (like MeterEnergy, PhaseCurrents, PhaseVoltages) when +// the meter was extracted from a decorated charger's capability registry. +type capableMeter struct { + api.Meter + source any +} + +// Capability implements the api.Capable interface. It first consults the +// source's capability registry (for decorated capabilities), then falls back +// to a direct type assertion on the source so statically-implemented +// interfaces (e.g. PhaseCurrents on the DaheimLaden charger) remain +// discoverable through the wrapper (https://github.com/evcc-io/evcc/issues/29877). +func (m *capableMeter) Capability(typ reflect.Type) (any, bool) { + if c, ok := m.source.(api.Capable); ok { + if impl, ok := c.Capability(typ); ok { + return impl, true + } + } + if v := reflect.ValueOf(m.source); v.IsValid() && v.Type().Implements(typ) { + return m.source, true + } + return nil, false +} diff --git a/core/loadpoint.go b/core/loadpoint.go index 870c57014..f56f1bcd8 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -402,14 +402,6 @@ func (lp *Loadpoint) requestUpdate() { } } -// capableMeter wraps a meter with capability lookup from its source. -// This preserves capability checks (like MeterEnergy, PhaseCurrents, PhaseVoltages) when -// the meter was extracted from a decorated charger's capability registry. -type capableMeter struct { - api.Meter - api.Capable -} - // configureChargerType ensures that chargeMeter, Rate and Timer can use charger capabilities func (lp *Loadpoint) configureChargerType(charger api.Charger) { var integrated bool @@ -419,14 +411,13 @@ func (lp *Loadpoint) configureChargerType(charger api.Charger) { integrated = true if mt, ok := api.Cap[api.Meter](charger); ok { - // preserve charger's capability registry so that subsequent - // capability checks on chargeMeter (e.g. MeterEnergy, PhaseCurrents) - // still work for decorated chargers (https://github.com/evcc-io/evcc/issues/28915) - if c, ok := charger.(api.Capable); ok { - lp.chargeMeter = &capableMeter{Meter: mt, Capable: c} - } else { - lp.chargeMeter = mt - } + // preserve charger's capability registry and static interface + // implementations so that subsequent capability checks on + // chargeMeter (e.g. MeterEnergy, PhaseCurrents) still work for + // decorated chargers (https://github.com/evcc-io/evcc/issues/28915) + // and for chargers that statically implement these interfaces + // (https://github.com/evcc-io/evcc/issues/29877). + lp.chargeMeter = &capableMeter{Meter: mt, source: charger} } else { mt := new(wrapper.ChargeMeter) _ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentWrappedMeterHandler)