RCT: fix batteryhold grid-charging and BatteryNormal nil pointer panic (#31294)
This commit is contained in:
parent
84bd0717ad
commit
ab31938daa
2 changed files with 83 additions and 9 deletions
54
meter/rct.go
54
meter/rct.go
|
|
@ -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
38
meter/rct_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue