RCT: add support for second battery (#25162)

This commit is contained in:
Maschga 2025-12-09 10:09:50 +01:00 • committed by GitHub
parent c5fc4b4f99
commit 25625e461b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 78 additions and 43 deletions

2
go.mod
View file

@ -35,7 +35,7 @@ require (
github.com/enbility/ship-go v0.6.0
github.com/enbility/spine-go v0.7.0
github.com/evcc-io/openapi-mcp v0.6.0
github.com/evcc-io/rct v0.1.2-0.20251112124403-1d380ab2264a
github.com/evcc-io/rct v0.1.2-0.20251121151844-04c32662cf6f
github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701
github.com/fatih/structs v1.1.0
github.com/getkin/kin-openapi v0.133.0

4
go.sum
View file

@ -197,8 +197,8 @@ github.com/evcc-io/ocpp-go v0.0.0-20250322092544-c0c6094051c0 h1:Qz34Pm1Wr05jjJi
github.com/evcc-io/ocpp-go v0.0.0-20250322092544-c0c6094051c0/go.mod h1:2kcukDdhui4u730VfnYVWuwzDLgw+mBRGDir/QAyBhg=
github.com/evcc-io/openapi-mcp v0.6.0 h1:cq8CWG+3gNQ9ID0ZE8MoDHZxgdJmBZ1fFyt5xAfTRfA=
github.com/evcc-io/openapi-mcp v0.6.0/go.mod h1:YyOx4zwr6xVUcchAETHERZ+75cZMIrdcjVIZFwBlE1Q=
github.com/evcc-io/rct v0.1.2-0.20251112124403-1d380ab2264a h1:kCcLOLKO2/xFrGPheeVt+otmM+85rx4x/f0dcyNybUY=
github.com/evcc-io/rct v0.1.2-0.20251112124403-1d380ab2264a/go.mod h1:n6MTBU36QOadGlxxiADu86VaT5l0eYdbmFBHF14AD/s=
github.com/evcc-io/rct v0.1.2-0.20251121151844-04c32662cf6f h1:akSiKunHdPxxxfPGvNN+zlW6PdQ59PMXy1+VIW+2gyg=
github.com/evcc-io/rct v0.1.2-0.20251121151844-04c32662cf6f/go.mod h1:n6MTBU36QOadGlxxiADu86VaT5l0eYdbmFBHF14AD/s=
github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701 h1:3JplY3KS6KMDVDNAU+3+KWmSWmoHIU34qwuIpW6SiHk=
github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=

View file

@ -38,10 +38,11 @@ func init() {
// NewRCTFromConfig creates an RCT from generic config
func NewRCTFromConfig(ctx context.Context, other map[string]any) (api.Meter, error) {
cc := struct {
batteryCapacity `mapstructure:",squash"`
batterySocLimits `mapstructure:",squash"`
Uri, Usage string
MaxChargePower int
Capacity float64
Capacity2 float64
ExternalPower bool
Cache time.Duration
}{
@ -61,11 +62,11 @@ func NewRCTFromConfig(ctx context.Context, other map[string]any) (api.Meter, err
return nil, errors.New("missing usage")
}
return NewRCT(ctx, cc.Uri, cc.Usage, cc.batterySocLimits, cc.MaxChargePower, cc.Cache, cc.ExternalPower, cc.batteryCapacity.Decorator())
return NewRCT(ctx, cc.Uri, cc.Usage, cc.batterySocLimits, cc.MaxChargePower, cc.Cache, cc.ExternalPower, cc.Capacity, cc.Capacity2)
}
// NewRCT creates an RCT meter
func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocLimits, maxchargepower int, cache time.Duration, externalPower bool, capacity func() float64) (api.Meter, error) {
func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocLimits, maxchargepower int, cache time.Duration, externalPower bool, capacity float64, capacity2 float64) (api.Meter, error) {
log := util.NewLogger("rct")
// re-use connections
@ -102,15 +103,38 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
// decorate api.Battery
var batterySoc func() (float64, error)
var batterySocLimiter func() (float64, float64)
var batteryCapacity func() float64
var batteryMode func(api.BatteryMode) error
if usage == "battery" {
batterySoc = m.batterySoc
// validate capacity configuration for dual battery setups
if capacity2 > 0 && capacity == 0 {
return nil, errors.New("missing first battery capacity")
}
batterySoc = func() (float64, error) {
soc, err := m.queryFloat(rct.BatterySoC)
if err != nil {
return 0, err
}
if capacity2 == 0 {
return soc * 100, err
}
soc2, err := m.queryFloat(rct.BatteryPlaceholder0Soc)
return (soc*capacity + soc2*capacity2) / (capacity + capacity2) * 100, err
}
batterySocLimiter = batterySocLimits.Decorator()
if capacity != 0 {
batteryCapacity = func() float64 { return capacity + capacity2 }
}
batteryMode = func(mode api.BatteryMode) error {
if mode != api.BatteryNormal {
batStatus, err := m.queryInt32(rct.BatteryBatStatus)
batStatus, err := m.queryInt32(rct.BatteryStatus2)
if err != nil {
return err
}
@ -121,43 +145,53 @@ func NewRCT(ctx context.Context, uri, usage string, batterySocLimits batterySocL
}
}
var eg errgroup.Group
switch mode {
case api.BatteryNormal:
if err := m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal}); err != nil {
return err
}
eg.Go(func() error {
return m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal})
})
if err := m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(batterySocLimits.MinSoc)/100)); err != nil {
return err
}
eg.Go(func() error {
return m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(batterySocLimits.MinSoc)/100))
})
return m.conn.Write(rct.PowerMngBatteryPowerExternW, m.floatVal(float32(0)))
eg.Go(func() error {
return m.conn.Write(rct.PowerMngBatteryPowerExternW, m.floatVal(float32(0)))
})
case api.BatteryHold:
if err := m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal}); err != nil {
return err
}
eg.Go(func() error {
return m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal})
})
return m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(batterySocLimits.MaxSoc)/100))
eg.Go(func() error {
return m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(batterySocLimits.MaxSoc)/100))
})
case api.BatteryCharge:
if err := m.conn.Write(rct.PowerMngUseGridPowerEnable, []byte{1}); err != nil {
return err
}
eg.Go(func() error {
return m.conn.Write(rct.PowerMngUseGridPowerEnable, []byte{1})
})
if err := m.conn.Write(rct.PowerMngBatteryPowerExternW, m.floatVal(float32(-maxchargepower))); err != nil {
return err
}
eg.Go(func() error {
return m.conn.Write(rct.PowerMngBatteryPowerExternW, m.floatVal(float32(-maxchargepower)))
})
return m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetExternal})
eg.Go(func() error {
return m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetExternal})
})
default:
return api.ErrNotAvailable
}
return eg.Wait()
}
}
return decorateRCT(m, totalEnergy, batterySoc, batterySocLimiter, batteryMode, capacity), nil
return decorateRCT(m, totalEnergy, batterySoc, batterySocLimiter, batteryMode, batteryCapacity), nil
}
func (m *RCT) floatVal(f float32) []byte {
@ -257,31 +291,24 @@ func (m *RCT) totalEnergy() (float64, error) {
}
}
// batterySoc implements the api.Battery interface
func (m *RCT) batterySoc() (float64, error) {
res, err := m.queryFloat(rct.BatterySoC)
return res * 100, err
}
func (m *RCT) bo() *backoff.ExponentialBackOff {
return backoff.NewExponentialBackOff(
func queryRCT[T any](id rct.Identifier, fun func(id rct.Identifier) (T, error)) (T, error) {
bo := backoff.NewExponentialBackOff(
backoff.WithInitialInterval(500*time.Millisecond),
backoff.WithMaxInterval(2*time.Second),
backoff.WithMaxElapsedTime(10*time.Second))
return backoff.RetryWithData(func() (T, error) {
return fun(id)
}, bo)
}
// queryFloat adds retry logic of recoverable errors to QueryFloat32
func (m *RCT) queryFloat(id rct.Identifier) (float64, error) {
res, err := backoff.RetryWithData(func() (float32, error) {
return m.conn.QueryFloat32(id)
}, m.bo())
res, err := queryRCT(id, m.conn.QueryFloat32)
return float64(res), err
}
// queryInt32 adds retry logic of recoverable errors to QueryInt32
func (m *RCT) queryInt32(id rct.Identifier) (int32, error) {
res, err := backoff.RetryWithData(func() (int32, error) {
return m.conn.QueryInt32(id)
}, m.bo())
return res, err
return queryRCT(id, m.conn.QueryInt32)
}

View file

@ -12,7 +12,14 @@ params:
allinone: true
- name: host
- name: capacity
advanced: true
- name: capacity2
unit: kWh
description:
de: Akkukapazität der zweiten Batterie
en: Battery capacity of the second battery
example: 50
type: float
usages: ["battery"]
# battery control
- name: minsoc
type: int
@ -42,6 +49,7 @@ render: |
usage: {{ .usage }}
cache: {{ .cache }}
capacity: {{ .capacity }} # kWh
capacity2: {{ .capacity2 }} # kWh
minsoc: {{ .minsoc }}
maxsoc: {{ .maxsoc }}
maxchargepower: {{ .maxchargepower }}