Curtailment: distribute limits to all devices by percent (#31390)
This commit is contained in:
parent
949daf4670
commit
eea55d282d
22 changed files with 147 additions and 131 deletions
|
|
@ -41,9 +41,10 @@ type EEBus struct {
|
|||
failsafeConsumptionLimit float64
|
||||
|
||||
smartgridProductionId uint
|
||||
productionLimit ucapi.LoadLimit
|
||||
productionLimit ucapi.LoadLimit // feed-in limit (NOT production despite its name)
|
||||
productionLimitActivated time.Time
|
||||
failsafeProductionLimit *float64
|
||||
failsafeProductionLimit *float64 // feed-in limit (NOT production despite its name)
|
||||
productionNominalMax float64
|
||||
|
||||
heartbeat *util.Value[struct{}]
|
||||
interval time.Duration
|
||||
|
|
@ -113,6 +114,7 @@ func NewEEBus(ctx context.Context, ski string, limits Limits, passthrough func(b
|
|||
failsafeDuration: limits.FailsafeDurationMinimum,
|
||||
failsafeConsumptionLimit: limits.FailsafeConsumptionActivePowerLimit,
|
||||
failsafeProductionLimit: limits.FailsafeProductionActivePowerLimit,
|
||||
productionNominalMax: limits.ProductionNominalMax,
|
||||
}
|
||||
|
||||
// simulate a received heartbeat
|
||||
|
|
@ -308,11 +310,24 @@ func (c *EEBus) Dimmed() *bool {
|
|||
return new(!c.consumptionLimitActivated.IsZero())
|
||||
}
|
||||
|
||||
// Curtailed implements api.HEMS, derived from productionLimitActivated.
|
||||
func (c *EEBus) Curtailed() *bool {
|
||||
// CurtailedPercent implements api.HEMS, converting the active LPP production
|
||||
// limit to an allowed production percent via the configured nominal production power.
|
||||
func (c *EEBus) CurtailedPercent() *int {
|
||||
c.mux.RLock()
|
||||
defer c.mux.RUnlock()
|
||||
return new(!c.productionLimitActivated.IsZero())
|
||||
|
||||
// without a nominal reference the W limit cannot be expressed as a percent
|
||||
if c.productionNominalMax <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
percent := 100
|
||||
if !c.productionLimitActivated.IsZero() {
|
||||
// production limits are negative watts
|
||||
percent = int(-c.productionLimit.Value / c.productionNominalMax * 100)
|
||||
}
|
||||
|
||||
return &percent
|
||||
}
|
||||
|
||||
// MaxConsumptionPower implements api.HEMS, returning the consumption cap
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const (
|
|||
testFailsafeConsumption = 4200.0
|
||||
testFailsafeProduction = 1000.0
|
||||
testFailsafeDuration = 2 * time.Hour
|
||||
testProductionNominal = 2000.0
|
||||
)
|
||||
|
||||
// stubSite implements site.API for testing — only GetGridPower is exercised;
|
||||
|
|
@ -43,6 +44,7 @@ func newTestEEBus(t *testing.T) *EEBus {
|
|||
failsafeConsumptionLimit: testFailsafeConsumption,
|
||||
failsafeProductionLimit: &failsafeProduction,
|
||||
failsafeDuration: testFailsafeDuration,
|
||||
productionNominalMax: testProductionNominal,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +58,9 @@ func assertConsumptionLimit(t *testing.T, c *EEBus, limit float64) {
|
|||
// assertProductionLimit checks the HEMS production state through the api.HEMS surface.
|
||||
func assertProductionLimit(t *testing.T, c *EEBus, active bool) {
|
||||
t.Helper()
|
||||
assert.Equal(t, new(active), c.Curtailed())
|
||||
percent := c.CurtailedPercent()
|
||||
require.NotNil(t, percent)
|
||||
assert.Equal(t, active, *percent < 100)
|
||||
}
|
||||
|
||||
// TestRun_HeartbeatLost_EntersFailsafe verifies the LPC-911/LPP-911 transition:
|
||||
|
|
|
|||
|
|
@ -78,15 +78,16 @@ func NewFnn(site site.API, maxDimPower, maxCurtailPower float64, w3G, s1G, s2G,
|
|||
}
|
||||
|
||||
return &Fnn{
|
||||
log: util.NewLogger("fnn"),
|
||||
site: site,
|
||||
maxDimPower: maxDimPower,
|
||||
maxCurtailPower: maxCurtailPower,
|
||||
s1: s1G,
|
||||
s2: s2G,
|
||||
w3: w3G,
|
||||
w4: w4G,
|
||||
interval: interval,
|
||||
log: util.NewLogger("fnn"),
|
||||
site: site,
|
||||
maxDimPower: maxDimPower,
|
||||
maxCurtailPower: maxCurtailPower,
|
||||
s1: s1G,
|
||||
s2: s2G,
|
||||
w3: w3G,
|
||||
w4: w4G,
|
||||
productionPercent: 100,
|
||||
interval: interval,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -106,8 +107,8 @@ type Fnn struct {
|
|||
smartgridConsumptionID uint
|
||||
smartgridProductionID uint
|
||||
|
||||
consumptionLimit float64
|
||||
productionLimit *float64
|
||||
consumptionLimit float64
|
||||
productionPercent int // allowed feed-in percent (0..100), 100 = uncurtailed
|
||||
|
||||
interval time.Duration
|
||||
}
|
||||
|
|
@ -144,12 +145,12 @@ func (c *Fnn) runCurtail() error {
|
|||
}
|
||||
|
||||
rules := []struct {
|
||||
get func() (bool, error)
|
||||
frac float64
|
||||
get func() (bool, error)
|
||||
percent int
|
||||
}{
|
||||
{get: c.w3, frac: 0.0},
|
||||
{get: c.s2, frac: 0.3},
|
||||
{get: c.s1, frac: 0.6},
|
||||
{get: c.w3, percent: 0},
|
||||
{get: c.s2, percent: 30},
|
||||
{get: c.s1, percent: 60},
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
|
|
@ -163,12 +164,12 @@ func (c *Fnn) runCurtail() error {
|
|||
}
|
||||
|
||||
if active {
|
||||
return c.setProductionLimit(rule.frac)
|
||||
return c.setProductionLimit(rule.percent)
|
||||
}
|
||||
}
|
||||
|
||||
// 100%
|
||||
return c.setProductionLimit(1.0)
|
||||
return c.setProductionLimit(100)
|
||||
}
|
||||
|
||||
// runDim evaluates the dimming rule and applies the dim limit.
|
||||
|
|
@ -192,20 +193,16 @@ func (c *Fnn) runDim() error {
|
|||
}
|
||||
|
||||
// setProductionLimit applies the curtailment limit.
|
||||
func (c *Fnn) setProductionLimit(frac float64) error {
|
||||
func (c *Fnn) setProductionLimit(percent int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
active := frac < 1.0
|
||||
|
||||
c.productionLimit = nil
|
||||
if active {
|
||||
c.productionLimit = new(c.maxCurtailPower * frac)
|
||||
}
|
||||
active := percent < 100
|
||||
c.productionPercent = percent
|
||||
|
||||
limit := 0.0
|
||||
if c.productionLimit != nil {
|
||||
limit = *c.productionLimit
|
||||
if active {
|
||||
limit = float64(percent) / 100 * c.maxCurtailPower
|
||||
}
|
||||
|
||||
if err := smartgrid.UpdateSession(&c.smartgridProductionID, smartgrid.Curtail, c.site.GetGridPower(), limit, active); err != nil {
|
||||
|
|
@ -243,15 +240,17 @@ func (c *Fnn) Dimmed() *bool {
|
|||
return new(c.consumptionLimit > 0)
|
||||
}
|
||||
|
||||
// Curtailed implements api.HEMS.
|
||||
func (c *Fnn) Curtailed() *bool {
|
||||
// CurtailedPercent implements api.HEMS, returning the allowed production percent.
|
||||
func (c *Fnn) CurtailedPercent() *int {
|
||||
if c.w3 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return new(c.productionLimit != nil)
|
||||
|
||||
percent := c.productionPercent
|
||||
return &percent
|
||||
}
|
||||
|
||||
// MaxConsumptionPower implements api.HEMS.
|
||||
|
|
@ -265,9 +264,9 @@ func (c *Fnn) MaxConsumptionPower() float64 {
|
|||
func (c *Fnn) MaxProductionPower() *float64 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.productionLimit == nil {
|
||||
if c.productionPercent >= 100 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return new(*c.productionLimit)
|
||||
return new(float64(c.productionPercent) / 100 * c.maxCurtailPower)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ func TestFnn(t *testing.T) {
|
|||
return true, nil
|
||||
}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, fnn.Curtailed())
|
||||
require.Nil(t, fnn.CurtailedPercent())
|
||||
// require.NoError(t, fnn.runDim())
|
||||
// require.Equal(t, new(true), fnn.Dimmed())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,9 +149,9 @@ func (c *Relay) Dimmed() *bool {
|
|||
return new(c.limit != nil)
|
||||
}
|
||||
|
||||
// Curtailed implements api.HEMS. Relay does not curtail production and
|
||||
// CurtailedPercent implements api.HEMS. Relay does not curtail production and
|
||||
// hence makes no statement.
|
||||
func (c *Relay) Curtailed() *bool {
|
||||
func (c *Relay) CurtailedPercent() *int {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue