From fc6fe3dce4edc3cf3b693a7fde90d23e007c2e99 Mon Sep 17 00:00:00 2001 From: mfuchs1984 <57141790+mfuchs1984@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:30:23 +0200 Subject: [PATCH] Site: make battery discharge limit availability explicit (#32728) --- core/loadpoint.go | 4 ++-- core/loadpoint_boost_test.go | 20 ++++++++++++++------ core/site.go | 8 ++++++-- core/site/api.go | 2 +- core/site_api.go | 7 +++++-- core/site_battery_limit_test.go | 14 ++++++++++---- 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 8a2aba36d..20be0a02d 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -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() diff --git a/core/loadpoint_boost_test.go b/core/loadpoint_boost_test.go index 3a2fa09cc..df0bad18a 100644 --- a/core/loadpoint_boost_test.go +++ b/core/loadpoint_boost_test.go @@ -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) diff --git a/core/site.go b/core/site.go index e6c4bb8aa..901946704 100644 --- a/core/site.go +++ b/core/site.go @@ -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 diff --git a/core/site/api.go b/core/site/api.go index d81775ce9..f18e5c45b 100644 --- a/core/site/api.go +++ b/core/site/api.go @@ -47,7 +47,7 @@ type API interface { // GetBatterySoc() float64 - GetBatteryMaxDischargePower() float64 + GetBatteryMaxDischargePower() *float64 GetPrioritySoc() float64 SetPrioritySoc(float64) error GetBufferSoc() float64 diff --git a/core/site_api.go b/core/site_api.go index 19cee08ef..083e4c8f3 100644 --- a/core/site_api.go +++ b/core/site_api.go @@ -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. diff --git a/core/site_battery_limit_test.go b/core/site_battery_limit_test.go index 036d64ac6..5693a95df 100644 --- a/core/site_battery_limit_test.go +++ b/core/site_battery_limit_test.go @@ -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) + } }