Meter: make battery parameters dynamic (#31668)
This commit is contained in:
parent
579df2ad67
commit
d3ae621688
7 changed files with 296 additions and 22 deletions
|
|
@ -27,23 +27,26 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M
|
|||
pvMaxACPower `mapstructure:",squash"`
|
||||
|
||||
// battery
|
||||
batteryCapacity `mapstructure:",squash"`
|
||||
batterySocLimits `mapstructure:",squash"`
|
||||
batteryPowerLimits `mapstructure:",squash"`
|
||||
Soc *plugin.Config // optional
|
||||
LimitSoc *plugin.Config // optional
|
||||
BatteryMode *plugin.Config // optional
|
||||
}{
|
||||
batterySocLimits: batterySocLimits{
|
||||
MinSoc: 20,
|
||||
MaxSoc: 95,
|
||||
},
|
||||
}
|
||||
batteryCapacityCtx `mapstructure:",squash"`
|
||||
batterySocLimitsCtx `mapstructure:",squash"`
|
||||
batteryPowerLimitsCtx `mapstructure:",squash"`
|
||||
Soc *plugin.Config // optional
|
||||
LimitSoc *plugin.Config // optional
|
||||
BatteryMode *plugin.Config // optional
|
||||
}{}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// default soc limits (nil-preset avoids mapstructure coercing plugin config into the default's type)
|
||||
if cc.batterySocLimitsCtx.MinSoc == nil {
|
||||
cc.batterySocLimitsCtx.MinSoc = 20
|
||||
}
|
||||
if cc.batterySocLimitsCtx.MaxSoc == nil {
|
||||
cc.batterySocLimitsCtx.MaxSoc = 95
|
||||
}
|
||||
|
||||
powerG, energyG, returnG, err := cc.Energy.Configure(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -68,10 +71,25 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M
|
|||
}
|
||||
|
||||
if socG != nil {
|
||||
capacity, err := cc.batteryCapacityCtx.Decorator(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
socLimiter, err := cc.batterySocLimitsCtx.Decorator(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
powerLimiter, err := cc.batteryPowerLimitsCtx.Decorator(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
implement.Has(m, implement.Battery(socG))
|
||||
implement.May(m, implement.BatteryCapacity(cc.batteryCapacity.Decorator()))
|
||||
implement.May(m, implement.BatterySocLimiter(cc.batterySocLimits.Decorator()))
|
||||
implement.May(m, implement.BatteryPowerLimiter(cc.batteryPowerLimits.Decorator()))
|
||||
implement.May(m, implement.BatteryCapacity(capacity))
|
||||
implement.May(m, implement.BatterySocLimiter(socLimiter))
|
||||
implement.May(m, implement.BatteryPowerLimiter(powerLimiter))
|
||||
|
||||
switch {
|
||||
case cc.Soc != nil && cc.LimitSoc != nil:
|
||||
|
|
@ -80,7 +98,12 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.M
|
|||
return nil, fmt.Errorf("battery limit soc: %w", err)
|
||||
}
|
||||
|
||||
implement.Has(m, implement.BatteryController(cc.batterySocLimits.LimitController(socG, limitSocS)))
|
||||
limitController, err := cc.batterySocLimitsCtx.LimitController(ctx, socG, limitSocS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
implement.Has(m, implement.BatteryController(limitController))
|
||||
|
||||
case cc.BatteryMode != nil:
|
||||
modeS, err := cc.BatteryMode.IntSetter(ctx, "batteryMode")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
package meter
|
||||
|
||||
import "github.com/evcc-io/evcc/api"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/plugin"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
type batteryCapacity struct {
|
||||
Capacity float64
|
||||
|
|
@ -18,6 +24,64 @@ func (m *batteryCapacity) Decorator() func() float64 {
|
|||
}
|
||||
}
|
||||
|
||||
type batteryCapacityCtx struct {
|
||||
Capacity any // static kWh value or float plugin
|
||||
}
|
||||
|
||||
// var _ api.BatteryCapacity = (*batteryCapacityCtx)(nil)
|
||||
|
||||
// Decorator returns an api.BatteryCapacity decorator. Capacity may be a static
|
||||
// number or a float plugin config; nil/zero means not configured.
|
||||
func (m *batteryCapacityCtx) Decorator(ctx context.Context) (func() float64, error) {
|
||||
return resolveFloat(ctx, m.Capacity)
|
||||
}
|
||||
|
||||
// resolveFloat resolves a static number or float plugin config to a getter.
|
||||
// nil/zero static returns a nil getter (not configured).
|
||||
func resolveFloat(ctx context.Context, v any) (func() float64, error) {
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
return nil, nil
|
||||
case int:
|
||||
return staticCapacity(float64(v)), nil
|
||||
case int64:
|
||||
return staticCapacity(float64(v)), nil
|
||||
case float64:
|
||||
return staticCapacity(v), nil
|
||||
default:
|
||||
var cfg plugin.Config
|
||||
if err := util.DecodeOther(v, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
get, err := cfg.FloatGetter(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() float64 {
|
||||
f, err := get()
|
||||
if err != nil {
|
||||
return 0 // ponytail: treat plugin error as unknown value
|
||||
}
|
||||
return f
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func staticCapacity(f float64) func() float64 {
|
||||
if f == 0 {
|
||||
return nil
|
||||
}
|
||||
return func() float64 { return f }
|
||||
}
|
||||
|
||||
// floatOr0 evaluates g, returning 0 for a nil (unconfigured) getter.
|
||||
func floatOr0(g func() float64) float64 {
|
||||
if g == nil {
|
||||
return 0
|
||||
}
|
||||
return g()
|
||||
}
|
||||
|
||||
type batteryPowerLimits struct {
|
||||
MaxChargePower float64
|
||||
MaxDischargePower float64
|
||||
|
|
@ -35,6 +99,32 @@ func (m *batteryPowerLimits) Decorator() func() (float64, float64) {
|
|||
}
|
||||
}
|
||||
|
||||
type batteryPowerLimitsCtx struct {
|
||||
MaxChargePower any // static W value or float plugin
|
||||
MaxDischargePower any // static W value or float plugin
|
||||
}
|
||||
|
||||
// var _ api.BatteryPowerLimiter = (*batteryPowerLimitsCtx)(nil)
|
||||
|
||||
// Decorator returns an api.BatteryPowerLimiter decorator. Each limit may be a
|
||||
// static number or a float plugin config; either unset means not configured.
|
||||
func (m *batteryPowerLimitsCtx) Decorator(ctx context.Context) (func() (float64, float64), error) {
|
||||
charge, err := resolveFloat(ctx, m.MaxChargePower)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
discharge, err := resolveFloat(ctx, m.MaxDischargePower)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if charge == nil || discharge == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return func() (float64, float64) {
|
||||
return charge(), discharge()
|
||||
}, nil
|
||||
}
|
||||
|
||||
type batterySocLimits struct {
|
||||
MinSoc, MaxSoc float64
|
||||
}
|
||||
|
|
@ -74,3 +164,64 @@ func (m *batterySocLimits) LimitController(socG func() (float64, error), limitSo
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
type batterySocLimitsCtx struct {
|
||||
MinSoc, MaxSoc any // static % value or float plugin
|
||||
}
|
||||
|
||||
// var _ api.BatterySocLimiter = (*batterySocLimitsCtx)(nil)
|
||||
|
||||
func (m *batterySocLimitsCtx) getters(ctx context.Context) (func() float64, func() float64, error) {
|
||||
minG, err := resolveFloat(ctx, m.MinSoc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
maxG, err := resolveFloat(ctx, m.MaxSoc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return minG, maxG, nil
|
||||
}
|
||||
|
||||
// Decorator returns an api.BatterySocLimiter decorator. Each limit may be a
|
||||
// static number or a float plugin config; both unset means not configured.
|
||||
func (m *batterySocLimitsCtx) Decorator(ctx context.Context) (func() (float64, float64), error) {
|
||||
minG, maxG, err := m.getters(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if minG == nil && maxG == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return func() (float64, float64) {
|
||||
return floatOr0(minG), floatOr0(maxG)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LimitController returns an api.BatteryController decorator
|
||||
func (m *batterySocLimitsCtx) LimitController(ctx context.Context, socG func() (float64, error), limitSocS func(float64) error) (func(api.BatteryMode) error, error) {
|
||||
minG, maxG, err := m.getters(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(mode api.BatteryMode) error {
|
||||
switch mode {
|
||||
case api.BatteryNormal:
|
||||
return limitSocS(floatOr0(minG))
|
||||
|
||||
case api.BatteryHold:
|
||||
soc, err := socG()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return limitSocS(min(100, max(soc, floatOr0(minG))))
|
||||
|
||||
case api.BatteryCharge:
|
||||
return limitSocS(floatOr0(maxG))
|
||||
|
||||
// BatteryHoldCharge not implementable via limit soc
|
||||
default:
|
||||
return api.ErrNotAvailable
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,59 @@
|
|||
package meter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBatteryCapacity(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
// static value
|
||||
{
|
||||
var cc batteryCapacityCtx
|
||||
require.NoError(t, util.DecodeOther(map[string]any{"capacity": 10}, &cc))
|
||||
g, err := cc.Decorator(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, g)
|
||||
require.Equal(t, 10.0, g())
|
||||
}
|
||||
|
||||
// zero value is treated as not configured
|
||||
{
|
||||
var cc batteryCapacityCtx
|
||||
require.NoError(t, util.DecodeOther(map[string]any{"capacity": 0}, &cc))
|
||||
g, err := cc.Decorator(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, g)
|
||||
}
|
||||
|
||||
// unset is not configured
|
||||
{
|
||||
var cc batteryCapacityCtx
|
||||
g, err := cc.Decorator(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, g)
|
||||
}
|
||||
|
||||
// float plugin
|
||||
{
|
||||
var cc batteryCapacityCtx
|
||||
require.NoError(t, util.DecodeOther(map[string]any{
|
||||
"capacity": map[string]any{
|
||||
"source": "const",
|
||||
"value": "12.5",
|
||||
},
|
||||
}, &cc))
|
||||
g, err := cc.Decorator(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, g)
|
||||
require.Equal(t, 12.5, g())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatterySocLimits(t *testing.T) {
|
||||
other := map[string]any{
|
||||
"minsoc": 1,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue