Huawei: clamp battery charge/discharge power to inverter maximum (#30776)
This commit is contained in:
parent
3aac67bdd0
commit
eadcd31257
3 changed files with 225 additions and 18 deletions
99
plugin/cached.go
Normal file
99
plugin/cached.go
Normal 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
64
plugin/cached_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -313,15 +313,37 @@ render: |
|
||||||
address: 47100 # Forcible charge/discharge
|
address: 47100 # Forcible charge/discharge
|
||||||
type: writesingle
|
type: writesingle
|
||||||
encoding: uint16
|
encoding: uint16
|
||||||
- source: const
|
# register 47077 is range-checked: a maxdischargepower above the
|
||||||
value: {{ .maxdischargepower }}
|
# inverter rated max (37048) is rejected, so clamp before writing
|
||||||
set:
|
- source: go
|
||||||
source: modbus
|
in:
|
||||||
{{- include "modbus" . | indent 16 }}
|
- name: rated
|
||||||
register:
|
type: int
|
||||||
address: 47077 # Max. Discharge Power
|
config:
|
||||||
type: writemultiple
|
source: cached
|
||||||
encoding: uint32
|
cache: 1h # rated max is static
|
||||||
|
value:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 22 }}
|
||||||
|
register:
|
||||||
|
address: 37048 # [ESS] Maximum discharge power (RO)
|
||||||
|
type: holding
|
||||||
|
decode: uint32
|
||||||
|
out:
|
||||||
|
- name: power
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 20 }}
|
||||||
|
register:
|
||||||
|
address: 47077 # Max. Discharge Power
|
||||||
|
type: writemultiple
|
||||||
|
encoding: uint32
|
||||||
|
script: |
|
||||||
|
limit := {{ .maxdischargepower }}
|
||||||
|
out := rated
|
||||||
|
if limit < rated { out = limit }
|
||||||
|
out
|
||||||
{{- if eq .forceaccharging "false" }}
|
{{- if eq .forceaccharging "false" }}
|
||||||
- source: const
|
- source: const
|
||||||
value: 0 # Disable
|
value: 0 # Disable
|
||||||
|
|
@ -379,15 +401,37 @@ render: |
|
||||||
address: 47087 # Charge from grid
|
address: 47087 # Charge from grid
|
||||||
type: writesingle
|
type: writesingle
|
||||||
encoding: uint16
|
encoding: uint16
|
||||||
- source: const
|
# clamp maxchargepower to the inverter rated max (37046) for parity
|
||||||
value: {{ .maxchargepower }} # W
|
# with discharge, avoiding a stale out-of-range value
|
||||||
set:
|
- source: go
|
||||||
source: modbus
|
in:
|
||||||
{{- include "modbus" . | indent 16 }}
|
- name: rated
|
||||||
register:
|
type: int
|
||||||
address: 47247 # Forcible charge power
|
config:
|
||||||
type: writemultiple
|
source: cached
|
||||||
encoding: uint32
|
cache: 1h # rated max is static
|
||||||
|
value:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 22 }}
|
||||||
|
register:
|
||||||
|
address: 37046 # [ESS] Maximum charge power (RO)
|
||||||
|
type: holding
|
||||||
|
decode: uint32
|
||||||
|
out:
|
||||||
|
- name: power
|
||||||
|
type: int
|
||||||
|
config:
|
||||||
|
source: modbus
|
||||||
|
{{- include "modbus" . | indent 20 }}
|
||||||
|
register:
|
||||||
|
address: 47247 # Forcible charge power
|
||||||
|
type: writemultiple
|
||||||
|
encoding: uint32
|
||||||
|
script: |
|
||||||
|
limit := {{ .maxchargepower }}
|
||||||
|
out := rated
|
||||||
|
if limit < rated { out = limit }
|
||||||
|
out
|
||||||
- source: const
|
- source: const
|
||||||
value: 1 # Minute
|
value: 1 # Minute
|
||||||
set:
|
set:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue