Site: make battery discharge limit availability explicit (#32728)

This commit is contained in:
mfuchs1984 2026-08-11 18:30:23 +02:00 • committed by GitHub
parent 8525e215a9
commit fc6fe3dce4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 38 additions and 17 deletions

View file

@ -1605,9 +1605,9 @@ func (lp *Loadpoint) boostPower(batteryPower float64) float64 {
}
}
if maxDischargePower := lp.site.GetBatteryMaxDischargePower(); maxDischargePower > 0 {
if maxDischargePower := lp.site.GetBatteryMaxDischargePower(); maxDischargePower != nil {
// limit delta to what the battery can still provide
delta = min(delta, max(0, maxDischargePower-batteryPower))
delta = min(delta, max(0, *maxDischargePower-batteryPower))
}
res := max(0, batteryPower) + delta + lp.site.GetResidualPower()

View file

@ -11,7 +11,7 @@ import (
type mockSite struct {
site.API
maxDischargePower float64
maxDischargePower *float64
residualPower float64
optimized int
}
@ -20,7 +20,7 @@ func (m *mockSite) Optimize() {
m.optimized++
}
func (m *mockSite) GetBatteryMaxDischargePower() float64 {
func (m *mockSite) GetBatteryMaxDischargePower() *float64 {
return m.maxDischargePower
}
@ -40,15 +40,22 @@ func TestBoostPower(t *testing.T) {
s := &mockSite{}
lp.site = s
// No max discharge power limit
s.maxDischargePower = 0
// No max discharge power limit (nil)
s.maxDischargePower = nil
// EffectiveMaxPower will be 230 * 16 * 3 = 11040
res := lp.boostPower(0)
assert.Equal(t, 11040.0, res)
assert.Equal(t, boostContinue, lp.batteryBoost)
// Discharge power limit is 0W (battery empty)
s.maxDischargePower = new(float64)
lp.batteryBoost = boostStart
res = lp.boostPower(0)
assert.Equal(t, 0.0, res)
// With max discharge power limit
s.maxDischargePower = 5000
limit5000 := 5000.0
s.maxDischargePower = &limit5000
lp.batteryBoost = boostStart
res = lp.boostPower(0)
assert.Equal(t, 5000.0, res)
@ -89,7 +96,8 @@ func TestBoostPower(t *testing.T) {
// limit is 50W (less than the standard 790W delta)
// without raw negative power, delta would be restricted to 50W
// with raw negative power (-2000W), headroom is 2050W, so delta is allowed to be 790W
s.maxDischargePower = 50
limit50 := 50.0
s.maxDischargePower = &limit50
s.residualPower = 0 // base delta = 100 + 690 = 790
lp.batteryBoost = boostContinue
res = lp.boostPower(-2000)

View file

@ -111,7 +111,7 @@ type Site struct {
excessDCPower float64 // PV excess DC charge power (hybrid only)
auxPower float64 // Aux power
battery types.BatteryState // Battery cached and published state
batteryMaxDischargePower float64 // Max discharge power of all battery meters
batteryMaxDischargePower *float64 // Max discharge power of all battery meters
batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
batteryModeExternal api.BatteryMode // Battery mode (external, runtime only, not persisted)
batteryModeExternalTimer time.Time // Battery mode timer for external control
@ -765,7 +765,11 @@ func (site *Site) updateBatteryMeters() {
// written from the meter goroutine, read via GetBatteryMaxDischargePower
site.Lock()
site.batteryMaxDischargePower = max(0, maxDischargePower)
if maxDischargePower >= 0 {
site.batteryMaxDischargePower = &maxDischargePower
} else {
site.batteryMaxDischargePower = nil
}
site.Unlock()
// retain the last known soc when every battery read failed this cycle, so a

View file

@ -47,7 +47,7 @@ type API interface {
//
GetBatterySoc() float64
GetBatteryMaxDischargePower() float64
GetBatteryMaxDischargePower() *float64
GetPrioritySoc() float64
SetPrioritySoc(float64) error
GetBufferSoc() float64

View file

@ -164,10 +164,13 @@ func (site *Site) GetBatterySoc() float64 {
}
// GetBatteryMaxDischargePower returns the current battery max discharge power
func (site *Site) GetBatteryMaxDischargePower() float64 {
func (site *Site) GetBatteryMaxDischargePower() *float64 {
site.RLock()
defer site.RUnlock()
return site.batteryMaxDischargePower
if site.batteryMaxDischargePower == nil {
return nil
}
return new(*site.batteryMaxDischargePower)
}
// Loadpoints returns the loadpoints as api interfaces.

View file

@ -49,7 +49,7 @@ func TestBatteryMaxDischargePowerAggregation(t *testing.T) {
}
site.updateBatteryMeters()
assert.Equal(t, 0.0, site.GetBatteryMaxDischargePower())
assert.Nil(t, site.GetBatteryMaxDischargePower())
// both batteries with limit
m3 := &mockBatteryPowerLimiter{Meter: &mockMeter{}, discharge: 3000}
@ -59,7 +59,9 @@ func TestBatteryMaxDischargePowerAggregation(t *testing.T) {
}
site.updateBatteryMeters()
assert.Equal(t, 5000.0, site.GetBatteryMaxDischargePower())
if res := site.GetBatteryMaxDischargePower(); assert.NotNil(t, res) {
assert.Equal(t, 5000.0, *res)
}
}
type mockBatterySocLimiter struct {
@ -100,7 +102,9 @@ func TestBatteryMaxDischargePowerWithMinSoc(t *testing.T) {
site.updateBatteryMeters()
// Only m2 should contribute
assert.Equal(t, 3000.0, site.GetBatteryMaxDischargePower())
if res := site.GetBatteryMaxDischargePower(); assert.NotNil(t, res) {
assert.Equal(t, 3000.0, *res)
}
// Both empty
m3 := &mockLimiter{mockBatterySocLimiter: mockBatterySocLimiter{Meter: &mockMeter{}, soc: 15, min: 20}, discharge: 3000}
@ -110,5 +114,7 @@ func TestBatteryMaxDischargePowerWithMinSoc(t *testing.T) {
}
site.updateBatteryMeters()
assert.Equal(t, 0.0, site.GetBatteryMaxDischargePower())
if res := site.GetBatteryMaxDischargePower(); assert.NotNil(t, res) {
assert.Equal(t, 0.0, *res)
}
}