Curtailment: report curtailed percent instead of bool (#32010)
This commit is contained in:
parent
435ed72a56
commit
608aa36629
19 changed files with 178 additions and 56 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -264,18 +265,29 @@ func (c *EEBus) Dim(dim bool) error {
|
|||
|
||||
var _ api.Curtailer = (*EEBus)(nil)
|
||||
|
||||
// Curtailed implements the api.Curtailer interface
|
||||
func (c *EEBus) Curtailed() (bool, error) {
|
||||
// CurtailedPercent implements the api.Curtailer interface
|
||||
func (c *EEBus) CurtailedPercent() (int, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
limit, err := eebusReadValue(c.eg.EgLPPInterface, c.egLppEntity, eebus.LPPLimit, c.eg.EgLPPInterface.ProductionLimit)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Check if limit is active and has a valid power value (valid is zero or negative)
|
||||
return limit.IsActive && limit.Value <= 0, nil
|
||||
// production limits are negative watts, a positive value is invalid
|
||||
if !limit.IsActive || limit.Value > 0 {
|
||||
return 100, nil
|
||||
}
|
||||
|
||||
// without a nominal reference the limit cannot be expressed as a percent
|
||||
nominal, err := c.eg.EgLPPInterface.ProductionNominalMax(c.egLppEntity)
|
||||
if err != nil || nominal <= 0 {
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
// round, the watt conversion does not reproduce the written percent exactly
|
||||
return int(math.Round(-limit.Value / nominal * 100)), nil
|
||||
}
|
||||
|
||||
// SetCurtailPercent implements the api.Curtailer interface
|
||||
|
|
|
|||
|
|
@ -189,31 +189,63 @@ func TestLPP_SetCurtailPercent_Gating(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// Curtailed reports an active production limit. Per LPP-TS-001 valid values are ≤ 0,
|
||||
// so a positive value is not treated as curtailed.
|
||||
func TestLPP_Curtailed(t *testing.T) {
|
||||
// CurtailedPercent expresses an active production limit as percent of nominal.
|
||||
// Per LPP-TS-001 valid values are ≤ 0, so a positive value is not treated as curtailed.
|
||||
func TestLPP_CurtailedPercent(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
limit ucapi.LoadLimit
|
||||
want bool
|
||||
want int
|
||||
}{
|
||||
{"active_negative", ucapi.LoadLimit{IsActive: true, Value: -2000}, true},
|
||||
{"active_zero", ucapi.LoadLimit{IsActive: true, Value: 0}, true},
|
||||
{"active_positive_invalid", ucapi.LoadLimit{IsActive: true, Value: 100}, false},
|
||||
{"inactive", ucapi.LoadLimit{IsActive: false, Value: -2000}, false},
|
||||
{"active_negative", ucapi.LoadLimit{IsActive: true, Value: -2000}, 40},
|
||||
{"active_zero", ucapi.LoadLimit{IsActive: true, Value: 0}, 0},
|
||||
{"active_positive_invalid", ucapi.LoadLimit{IsActive: true, Value: 100}, 100},
|
||||
{"inactive", ucapi.LoadLimit{IsActive: false, Value: -2000}, 100},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c, _, lpp, entity := newEGMeter(t)
|
||||
lpp.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.LPPLimit).Return(true)
|
||||
lpp.EXPECT().ProductionLimit(entity).Return(tc.limit, nil)
|
||||
if tc.want != 100 {
|
||||
lpp.EXPECT().ProductionNominalMax(entity).Return(5000.0, nil)
|
||||
}
|
||||
|
||||
got, err := c.Curtailed()
|
||||
got, err := c.CurtailedPercent()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The watt conversion must reproduce the written percent, else the site would
|
||||
// rewrite the same limit on every update.
|
||||
func TestLPP_CurtailedPercent_RoundTrip(t *testing.T) {
|
||||
const nominal = 4600.0
|
||||
|
||||
for percent := range 101 {
|
||||
c, _, lpp, entity := newEGMeter(t)
|
||||
lpp.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.LPPLimit).Return(true)
|
||||
lpp.EXPECT().ProductionLimit(entity).
|
||||
Return(ucapi.LoadLimit{IsActive: true, Value: -float64(percent) / 100 * nominal}, nil)
|
||||
lpp.EXPECT().ProductionNominalMax(entity).Return(nominal, nil)
|
||||
|
||||
got, err := c.CurtailedPercent()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, percent, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Without a nominal reference the watt limit cannot be expressed as a percent.
|
||||
func TestLPP_CurtailedPercent_NoNominal(t *testing.T) {
|
||||
c, _, lpp, entity := newEGMeter(t)
|
||||
lpp.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.LPPLimit).Return(true)
|
||||
lpp.EXPECT().ProductionLimit(entity).Return(ucapi.LoadLimit{IsActive: true, Value: -2000}, nil)
|
||||
lpp.EXPECT().ProductionNominalMax(entity).Return(0.0, api.ErrNotAvailable)
|
||||
|
||||
_, err := c.CurtailedPercent()
|
||||
assert.ErrorIs(t, err, api.ErrNotAvailable)
|
||||
}
|
||||
|
||||
// TestLPC_LPP_NonCoverage records the Controllable-System and connection/heartbeat
|
||||
// abstract test cases that belong to eebus-go and the evcc HEMS/charger, not the meter.
|
||||
func TestLPC_LPP_NonCoverage(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ type Curtailer struct {
|
|||
|
||||
func (cc *Curtailer) Configure(ctx context.Context) (
|
||||
func(int64) error,
|
||||
func() (bool, error),
|
||||
func() (int64, error),
|
||||
error,
|
||||
) {
|
||||
curtailS, err := cc.Curtail.IntSetter(ctx, "curtail")
|
||||
|
|
@ -62,7 +62,7 @@ func (cc *Curtailer) Configure(ctx context.Context) (
|
|||
return nil, nil, fmt.Errorf("curtail: %w", err)
|
||||
}
|
||||
|
||||
curtailedG, err := cc.Curtailed.BoolGetter(ctx)
|
||||
curtailedG, err := cc.Curtailed.IntGetter(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("curtailed: %w", err)
|
||||
}
|
||||
|
|
@ -81,8 +81,12 @@ func (cc *Curtailer) Implement(ctx context.Context, i implement.Caps) error {
|
|||
}
|
||||
|
||||
if curtailS != nil {
|
||||
getPercent := func() (int, error) {
|
||||
percent, err := curtailedG()
|
||||
return int(percent), err
|
||||
}
|
||||
setPercent := func(percent int) error { return curtailS(int64(percent)) }
|
||||
implement.May(i, implement.Curtailer(curtailedG, setPercent))
|
||||
implement.May(i, implement.Curtailer(getPercent, setPercent))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -141,9 +141,10 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
|
|||
return m.conn.Write(rct.BufVControlPowerReduction, floatVal(float64(percent)/100))
|
||||
}
|
||||
|
||||
curtailed := func() (bool, error) {
|
||||
curtailed := func() (int, error) {
|
||||
r, err := m.queryFloat(rct.BufVControlPowerReduction)
|
||||
return r != 1, err
|
||||
// round, the float32 round-trip does not reproduce the written percent exactly
|
||||
return int(math.Round(r * 100)), err
|
||||
}
|
||||
|
||||
implement.Has(m, implement.Curtailer(curtailed, curtail))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue