From f1408ad10fa5cfb7ac0e3a3438f96bf90b2b56f1 Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 Apr 2026 09:07:19 +0200 Subject: [PATCH] Fix chargeMeter capability lookup for decorated chargers (#28920) --- api/capable_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++++ core/loadpoint.go | 17 ++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/api/capable_test.go b/api/capable_test.go index a365299ea..565dd596e 100644 --- a/api/capable_test.go +++ b/api/capable_test.go @@ -66,6 +66,52 @@ func TestCap_CapableRegistryLookup(t *testing.T) { assert.False(t, ok) } +// decoratedCharger simulates a real decorated charger where Meter is NOT +// directly embedded but only available through the capability registry. +type decoratedCharger struct { + caps map[reflect.Type]any +} + +func (d *decoratedCharger) Capability(typ reflect.Type) (any, bool) { + c, ok := d.caps[typ] + return c, ok +} + +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. + decorated := &decoratedCharger{ + caps: map[reflect.Type]any{ + reflect.TypeFor[Meter](): &testMeterImpl{}, + reflect.TypeFor[MeterEnergy](): &testMeterEnergyImpl{}, + }, + } + + // extract Meter from decorated source (slow path: from caps registry) + 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") + + // Fix: wrapping extracted meter with source's Capable preserves registry + type capableMeter struct { + Meter + Capable + } + wrapped := &capableMeter{Meter: mt, Capable: decorated} + + me, ok := Cap[MeterEnergy](wrapped) + require.True(t, ok, "wrapped meter should find MeterEnergy via Capable") + + energy, err := me.TotalEnergy() + assert.NoError(t, err) + assert.Equal(t, 99.0, energy) +} + func TestCap_NilValue(t *testing.T) { _, ok := Cap[MeterEnergy](nil) assert.False(t, ok) diff --git a/core/loadpoint.go b/core/loadpoint.go index 00a51a489..4177e6a34 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -395,6 +395,14 @@ 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 @@ -404,7 +412,14 @@ func (lp *Loadpoint) configureChargerType(charger api.Charger) { integrated = true if mt, ok := api.Cap[api.Meter](charger); ok { - lp.chargeMeter = mt + // 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 + } } else { mt := new(wrapper.ChargeMeter) _ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentWrappedMeterHandler)