Curtailment: distribute limits to all devices by percent (#31390)

This commit is contained in:
andig 2026-07-02 20:43:12 +02:00 • committed by GitHub
parent 949daf4670
commit eea55d282d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 147 additions and 131 deletions

View file

@ -278,18 +278,9 @@ func (c *EEBus) Curtailed() (bool, error) {
return limit.IsActive && limit.Value <= 0, nil
}
// Curtail implements the api.Curtailer interface
func (c *EEBus) Curtail(curtail bool) error {
// Sets or removes the production power limit
// TODO: change api.Curtailer to make limit configurable
// For now, we use a fixed safe limit of 0W
limit := 0.0
var value float64
if curtail {
value = limit
}
// SetCurtailPercent implements the api.Curtailer interface
func (c *EEBus) SetCurtailPercent(percent int) error {
curtail := percent < 100
c.mu.Lock()
entity := c.egLppEntity
@ -299,6 +290,15 @@ func (c *EEBus) Curtail(curtail bool) error {
return api.ErrNotAvailable
}
// derive a proportional feed-in limit from the producer's nominal power
// (limits are negative watts); fall back to a safe 0W limit if unavailable
var value float64
if curtail {
if nominal, err := c.eg.EgLPPInterface.ProductionNominalMax(entity); err == nil && nominal > 0 {
value = -float64(percent) / 100 * nominal
}
}
return eebus.Await(func(cb func(model.ResultDataType)) (*model.MsgCounterType, error) {
return c.eg.EgLPPInterface.WriteProductionLimit(entity, ucapi.LoadLimit{Value: value, IsActive: curtail}, cb)
})

View file

@ -62,7 +62,7 @@ func (c *EEBus) maUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface) {
c.mu.Lock()
defer c.mu.Unlock()
// use most specific selector
// prefer the shallowest (device-level) entity
if c.maEntity == nil || len(entity.Address().Entity) < len(c.maEntity.Address().Entity) {
c.maEntity = entity
}
@ -76,7 +76,7 @@ func (c *EEBus) egLpcUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface)
c.mu.Lock()
defer c.mu.Unlock()
// use most specific selector
// prefer the shallowest (device-level) entity
if c.egLpcEntity == nil || len(entity.Address().Entity) < len(c.egLpcEntity.Address().Entity) {
c.egLpcEntity = entity
}
@ -90,7 +90,7 @@ func (c *EEBus) egLppUseCaseSupportUpdate(entity spineapi.EntityRemoteInterface)
c.mu.Lock()
defer c.mu.Unlock()
// use most specific selector
// prefer the shallowest (device-level) entity
if c.egLppEntity == nil || len(entity.Address().Entity) < len(c.egLppEntity.Address().Entity) {
c.egLppEntity = entity
}

View file

@ -53,11 +53,11 @@ type Curtailer struct {
}
func (cc *Curtailer) Configure(ctx context.Context) (
func(bool) error,
func(int64) error,
func() (bool, error),
error,
) {
curtailS, err := cc.Curtail.BoolSetter(ctx, "curtail")
curtailS, err := cc.Curtail.IntSetter(ctx, "curtail")
if err != nil {
return nil, nil, fmt.Errorf("curtail: %w", err)
}
@ -80,7 +80,10 @@ func (cc *Curtailer) Implement(ctx context.Context, i implement.Caps) error {
return err
}
implement.May(i, implement.Curtailer(curtailS, curtailedG))
if curtailS != nil {
setPercent := func(percent int) error { return curtailS(int64(percent)) }
implement.May(i, implement.Curtailer(curtailedG, setPercent))
}
return nil
}

View file

@ -107,12 +107,8 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
}
if usage == "pv" {
curtail := func(b bool) error {
var r float64
if !b {
r = 1.0
}
return m.conn.Write(rct.BufVControlPowerReduction, floatVal(r))
curtail := func(percent int) error {
return m.conn.Write(rct.BufVControlPowerReduction, floatVal(float64(percent)/100))
}
curtailed := func() (bool, error) {
@ -120,7 +116,7 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
return r != 1, err
}
implement.Has(m, implement.Curtailer(curtail, curtailed))
implement.Has(m, implement.Curtailer(curtailed, curtail))
implement.May(m, implement.MaxACPowerGetter(maxACPower))
}