Meter (SunSpec): read max AC power from nameplate (#32732)

This commit is contained in:
andig 2026-08-20 20:14:13 +02:00 • committed by GitHub
parent fb35c0905f
commit e2c59fc6e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 91 additions and 24 deletions

View file

@ -24,7 +24,7 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M
measurement.Curtailer `mapstructure:",squash"` // optional
// pv
pvMaxACPower `mapstructure:",squash"`
pvMaxACPowerCtx `mapstructure:",squash"`
// battery
batteryCapacityCtx `mapstructure:",squash"`
@ -127,7 +127,7 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M
implement.May(m, implement.PhaseCurrents(currentsG))
implement.May(m, implement.PhaseVoltages(voltagesG))
implement.May(m, implement.PhasePowers(powersG))
implement.May(m, implement.MaxACPowerGetter(cc.pvMaxACPower.Decorator()))
implement.May(m, implement.MaxACPowerGetter(cc.pvMaxACPowerCtx.Decorator(ctx)))
return m, nil
}

View file

@ -22,6 +22,27 @@ func TestPV(t *testing.T) {
_, ok := api.Cap[api.MaxACPowerGetter](m)
assert.True(t, ok, "MaxACPowerGetter")
}
func TestPVMaxACPowerPlugin(t *testing.T) {
newMeter := func(maxacpower any) api.Meter {
m, err := NewConfigurableFromConfig(t.Context(), map[string]any{
"power": map[string]any{"source": "const", "value": 1000},
"maxacpower": maxacpower,
})
require.NoError(t, err)
return m
}
m := newMeter(map[string]any{"source": "const", "value": 5000})
mp, ok := api.Cap[api.MaxACPowerGetter](m)
require.True(t, ok, "MaxACPowerGetter")
assert.Equal(t, 5000.0, mp.MaxACPower())
// device without rating must not be decorated
m = newMeter(map[string]any{"source": "error", "error": "ErrNotAvailable"})
_, ok = api.Cap[api.MaxACPowerGetter](m)
assert.False(t, ok, "MaxACPowerGetter")
}
func TestBattery(t *testing.T) {
m, err := NewConfigurableFromConfig(t.Context(), map[string]any{
"power": map[string]any{

View file

@ -43,11 +43,11 @@ func resolveFloat(ctx context.Context, v any) (func() float64, error) {
case nil:
return nil, nil
case int:
return staticCapacity(float64(v)), nil
return staticFloat(float64(v)), nil
case int64:
return staticCapacity(float64(v)), nil
return staticFloat(float64(v)), nil
case float64:
return staticCapacity(v), nil
return staticFloat(v), nil
default:
var cfg plugin.Config
if err := util.DecodeOther(v, &cfg); err != nil {
@ -67,7 +67,7 @@ func resolveFloat(ctx context.Context, v any) (func() float64, error) {
}
}
func staticCapacity(f float64) func() float64 {
func staticFloat(f float64) func() float64 {
if f == 0 {
return nil
}

View file

@ -1,5 +1,11 @@
package meter
import (
"context"
"github.com/evcc-io/evcc/util"
)
type pvMaxACPower struct {
MaxACPower float64
}
@ -8,10 +14,23 @@ type pvMaxACPower struct {
// Decorator returns the max AC power decorator
func (m *pvMaxACPower) Decorator() func() float64 {
if m.MaxACPower == 0 {
return nil
}
return func() float64 {
return m.MaxACPower
}
return staticFloat(m.MaxACPower)
}
type pvMaxACPowerCtx struct {
MaxACPower any // static W value or float plugin
}
// var _ api.MaxACPowerGetter = (*pvMaxACPowerCtx)(nil)
// Decorator returns the max AC power decorator. MaxACPower may be a static value or a
// float plugin like the SunSpec nameplate rating. Since the rating is constant, the
// plugin is read once. Unavailable or zero means not configured.
func (m *pvMaxACPowerCtx) Decorator(ctx context.Context) func() float64 {
get, err := resolveFloat(ctx, m.MaxACPower)
if err != nil {
// not all devices expose the rating- must not fail configuration
util.NewLogger("meter").WARN.Printf("maxacpower: %v", err)
}
return staticFloat(floatOr0(get))
}