Huawei: clamp battery charge/discharge power to inverter maximum (#30776)

This commit is contained in:
andig 2026-06-13 17:56:40 +02:00 • committed by GitHub
parent 3aac67bdd0
commit eadcd31257
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 225 additions and 18 deletions

99
plugin/cached.go Normal file
View file

@ -0,0 +1,99 @@
package plugin
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/util"
)
func init() {
registry.AddCtx("cached", NewCachedFromConfig)
}
// cachedPlugin caches the wrapped reading for the configured duration
type cachedPlugin struct {
ctx context.Context
clock clock.Clock
cache time.Duration
value Config
}
// NewCachedFromConfig creates cached provider
func NewCachedFromConfig(ctx context.Context, other map[string]any) (Plugin, error) {
var cc struct {
Cache time.Duration
Value Config
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.Cache <= 0 {
return nil, errors.New("cache duration is required")
}
return &cachedPlugin{
ctx: ctx,
clock: clock.New(),
cache: cc.Cache,
value: cc.Value,
}, nil
}
func cachedGetter[T any](o *cachedPlugin, valuer func(ctx context.Context) (func() (T, error), error)) (func() (T, error), error) {
value, err := valuer(o.ctx)
if err != nil {
return nil, fmt.Errorf("cached: %w", err)
}
var mu sync.Mutex
var updated time.Time
var val T
return func() (T, error) {
mu.Lock()
defer mu.Unlock()
// refresh on first call and once the cache has expired; failures are not cached
if updated.IsZero() || o.clock.Since(updated) > o.cache {
v, err := value()
if err != nil {
return v, err
}
val = v
updated = o.clock.Now()
}
return val, nil
}, nil
}
var _ StringGetter = (*cachedPlugin)(nil)
func (o *cachedPlugin) StringGetter() (func() (string, error), error) {
return cachedGetter(o, o.value.StringGetter)
}
var _ FloatGetter = (*cachedPlugin)(nil)
func (o *cachedPlugin) FloatGetter() (func() (float64, error), error) {
return cachedGetter(o, o.value.FloatGetter)
}
var _ IntGetter = (*cachedPlugin)(nil)
func (o *cachedPlugin) IntGetter() (func() (int64, error), error) {
return cachedGetter(o, o.value.IntGetter)
}
var _ BoolGetter = (*cachedPlugin)(nil)
func (o *cachedPlugin) BoolGetter() (func() (bool, error), error) {
return cachedGetter(o, o.value.BoolGetter)
}

64
plugin/cached_test.go Normal file
View file

@ -0,0 +1,64 @@
package plugin
import (
"context"
"errors"
"testing"
"time"
"github.com/benbjohnson/clock"
"github.com/stretchr/testify/require"
)
func TestCachedHitAndExpiry(t *testing.T) {
c := clock.NewMock()
o := &cachedPlugin{ctx: context.Background(), clock: c, cache: time.Hour}
var calls int
get, err := cachedGetter(o, func(context.Context) (func() (int64, error), error) {
return func() (int64, error) {
calls++
return int64(calls), nil
}, nil
})
require.NoError(t, err)
// first call reads, subsequent calls within the window are served from cache
v, err := get()
require.NoError(t, err)
require.EqualValues(t, 1, v)
c.Add(59 * time.Minute)
v, err = get()
require.NoError(t, err)
require.EqualValues(t, 1, v)
require.Equal(t, 1, calls)
// once the cache expires the next call refreshes
c.Add(2 * time.Minute)
v, err = get()
require.NoError(t, err)
require.EqualValues(t, 2, v)
require.Equal(t, 2, calls)
}
func TestCachedDoesNotCacheErrors(t *testing.T) {
c := clock.NewMock()
o := &cachedPlugin{ctx: context.Background(), clock: c, cache: time.Hour}
var calls int
get, err := cachedGetter(o, func(context.Context) (func() (int64, error), error) {
return func() (int64, error) {
calls++
return 0, errors.New("boom")
}, nil
})
require.NoError(t, err)
// failures must not be cached, so every call retries the source
for range 3 {
_, err := get()
require.Error(t, err)
}
require.Equal(t, 3, calls)
}