Fix chargeMeter capability lookup for decorated chargers (#28920)

This commit is contained in:
andig 2026-04-08 09:07:19 +02:00 • committed by GitHub
parent 635e17efa0
commit f1408ad10f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 1 deletions

View file

@ -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)

View file

@ -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)