RCT: fix batteryhold grid-charging and BatteryNormal nil pointer panic (#31294)

This commit is contained in:
Maschga 2026-07-07 10:29:16 +02:00 • committed by GitHub
parent 84bd0717ad
commit ab31938daa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 83 additions and 9 deletions

View file

@ -24,7 +24,37 @@ type RCT struct {
conn *rct.Connection // connection with the RCT device
usage string // grid, pv, battery
externalPower bool // whether to query external power
rSocStrategy *uint8 // remembers overwritten soc strategy value
mu sync.Mutex
rSocStrategy *uint8 // remembers overwritten soc strategy value
}
// takeRSocStrategy atomically returns and clears the saved soc strategy, or nil
// if none is saved (e.g. BatteryNormal applied twice in a row, #31471).
func (m *RCT) takeRSocStrategy() *uint8 {
m.mu.Lock()
defer m.mu.Unlock()
s := m.rSocStrategy
m.rSocStrategy = nil
return s
}
// setRSocStrategyIfAbsent saves strategy unless a value is already saved.
func (m *RCT) setRSocStrategyIfAbsent(strategy uint8) {
m.mu.Lock()
defer m.mu.Unlock()
if m.rSocStrategy == nil {
m.rSocStrategy = &strategy
}
}
// hasRSocStrategy reports whether a soc strategy is currently saved
func (m *RCT) hasRSocStrategy() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.rSocStrategy != nil
}
var (
@ -161,12 +191,12 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
}
// read soc strategy to reset afterwards
if m.rSocStrategy == nil {
if !m.hasRSocStrategy() {
strategy, err := m.queryUint8(rct.PowerMngSocStrategy)
if err != nil {
return err
}
m.rSocStrategy = &strategy
m.setRSocStrategyIfAbsent(strategy)
}
}
@ -174,11 +204,11 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
switch mode {
case api.BatteryNormal:
eg.Go(func() error {
err := m.conn.Write(rct.PowerMngSocStrategy, []byte{*m.rSocStrategy})
m.rSocStrategy = nil
return err
})
if strategy := m.takeRSocStrategy(); strategy != nil {
eg.Go(func() error {
return m.conn.Write(rct.PowerMngSocStrategy, []byte{*strategy})
})
}
eg.Go(func() error {
return m.conn.Write(rct.BatterySoCTargetMin, floatVal(batterySocLimits.MinSoc/100))
@ -202,7 +232,13 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
})
eg.Go(func() error {
return m.conn.Write(rct.BatterySoCTargetMin, floatVal(batterySocLimits.MaxSoc/100))
// hold at current SoC (not MaxSoc): otherwise the gap between current
// and target SoC lets the inverter charge from the grid (#31280)
soc, err := m.queryFloat(rct.BatterySoC)
if err != nil {
return err
}
return m.conn.Write(rct.BatterySoCTargetMin, floatVal(soc))
})
case api.BatteryCharge:

38
meter/rct_test.go Normal file
View file

@ -0,0 +1,38 @@
package meter
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestRCTTakeRSocStrategy verifies that a repeated BatteryNormal application
// (no intervening non-normal mode) is a no-op instead of a nil dereference (#31471).
func TestRCTTakeRSocStrategy(t *testing.T) {
m := &RCT{}
// nothing saved yet
assert.Nil(t, m.takeRSocStrategy())
m.setRSocStrategyIfAbsent(4)
strategy := m.takeRSocStrategy()
require.NotNil(t, strategy)
assert.Equal(t, uint8(4), *strategy)
// second take without an intervening set must not repeat the value or panic
assert.Nil(t, m.takeRSocStrategy())
}
// TestRCTSetRSocStrategyIfAbsent verifies the saved value is never overwritten
// while still present, matching the original guard's intent.
func TestRCTSetRSocStrategyIfAbsent(t *testing.T) {
m := &RCT{}
m.setRSocStrategyIfAbsent(4)
m.setRSocStrategyIfAbsent(7)
strategy := m.takeRSocStrategy()
require.NotNil(t, strategy)
assert.Equal(t, uint8(4), *strategy)
}