Loadpoint: fix charge meter method decoration (#29912)

This commit is contained in:
andig 2026-05-15 16:30:25 +02:00 • committed by GitHub
parent 394553c198
commit f41ad70ac4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 21 deletions

View file

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

32
core/caps.go Normal file
View file

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

View file

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