EEBus: apply the configured failsafe limits consistently (#33288)

This commit is contained in:
github-actions[bot] 2026-08-30 10:15:51 +02:00 • committed by GitHub
parent f193eb199a
commit 490f209e57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 69 additions and 11 deletions

View file

@ -232,6 +232,9 @@ func (c *EEBus) run() error {
if c.failsafeProductionLimit != nil {
// production limit is negative, failsafe limits are always positive
c.setProductionLimit(-*c.failsafeProductionLimit, true)
} else {
// no failsafe limit configured, release any stale Energy Guard limit
c.setProductionLimit(0, false)
}
return nil
@ -352,6 +355,24 @@ func (c *EEBus) setProductionLimit(limit float64, active bool) {
}
}
// effectiveConsumptionLimit returns the LPC limit in force: the configured failsafe
// limit while in failsafe, else the Energy Guard's. Caller holds the mutex.
func (c *EEBus) effectiveConsumptionLimit() float64 {
if c.status == StatusFailsafe {
return c.failsafeConsumptionLimit
}
return c.consumptionLimit.Value
}
// effectiveProductionLimit returns the LPP limit in force as positive watts, see
// effectiveConsumptionLimit. LPP states its limits as negative watts.
func (c *EEBus) effectiveProductionLimit() float64 {
if c.status == StatusFailsafe && c.failsafeProductionLimit != nil {
return *c.failsafeProductionLimit
}
return -c.productionLimit.Value
}
var _ api.HEMS = (*EEBus)(nil)
// CurtailedPercent implements api.HEMS, converting the active LPP production
@ -372,8 +393,7 @@ func (c *EEBus) CurtailedPercent() *int {
percent := 100
if limitActive(c.productionLimitActivated) {
// production limits are negative watts
percent = int(-c.productionLimit.Value / c.productionNominalMax * 100)
percent = int(c.effectiveProductionLimit() / c.productionNominalMax * 100)
}
return &percent
@ -390,10 +410,7 @@ func (c *EEBus) MaxConsumptionPower() *float64 {
if !limitActive(c.consumptionLimitActivated) {
return new(0.0)
}
if c.status == StatusFailsafe {
return new(c.failsafeConsumptionLimit)
}
return new(c.consumptionLimit.Value)
return new(c.effectiveConsumptionLimit())
}
// MaxProductionPower implements api.HEMS: nil until first connected,
@ -407,9 +424,5 @@ func (c *EEBus) MaxProductionPower() *float64 {
if !limitActive(c.productionLimitActivated) {
return new(0.0)
}
if c.status == StatusFailsafe {
return c.failsafeProductionLimit
}
// production limits are negative watts, the api.HEMS cap is positive
return new(-c.productionLimit.Value)
return new(c.effectiveProductionLimit())
}

View file

@ -280,6 +280,51 @@ func TestRun_LimitWithoutDuration(t *testing.T) {
assertProductionLimit(t, c, true)
}
// TestProductionLimit_Failsafe verifies both api.HEMS production getters report the
// configured failsafe limit while in failsafe, not the stale EG-supplied limit
// (#33286): the EG limit had already been released (Value=0) before the heartbeat
// was lost, which must not curtail feed-in to 0%.
func TestProductionLimit_Failsafe(t *testing.T) {
c := newTestEEBus(t)
// EG limit already released before the heartbeat is lost.
c.productionLimit = ucapi.LoadLimit{Value: 0, IsActive: false}
require.NoError(t, c.run())
require.Equal(t, StatusFailsafe, c.status)
percent := c.CurtailedPercent()
require.NotNil(t, percent)
assert.Equal(t, int(testFailsafeProduction/testProductionNominal*100), *percent)
power := c.MaxProductionPower()
require.NotNil(t, power)
assert.Equal(t, testFailsafeProduction, *power)
}
// TestProductionLimit_FailsafeUnconfigured verifies that entering failsafe without a
// configured failsafe production limit releases a stale EG limit. run() returns early
// while in failsafe, so a limit left applied here would stay applied indefinitely.
func TestProductionLimit_FailsafeUnconfigured(t *testing.T) {
c := newTestEEBus(t)
c.failsafeProductionLimit = nil
c.heartbeat.Set(struct{}{})
c.productionLimit = ucapi.LoadLimit{Value: -500, IsActive: true}
require.NoError(t, c.run())
assertProductionLimit(t, c, true)
// heartbeat lost
c.heartbeat = util.NewValue[struct{}](time.Hour)
require.NoError(t, c.run())
require.Equal(t, StatusFailsafe, c.status)
assertProductionLimit(t, c, false)
power := c.MaxProductionPower()
require.NotNil(t, power)
assert.Equal(t, 0.0, *power)
}
// TestMaxProductionPower verifies the api.HEMS export cap is a positive wattage,
// while LPP states its limits as negative watts.
func TestMaxProductionPower(t *testing.T) {