From 25625e461b1b91e930eea71232662ffb61176414 Mon Sep 17 00:00:00 2001 From: Maschga <88616799+Maschga@users.noreply.github.com> Date: Tue, 9 Dec 2025 10:09:50 +0100 Subject: [PATCH] RCT: add support for second battery (#25162) --- go.mod | 2 +- go.sum | 4 +- meter/rct.go | 105 ++++++++++++++-------- templates/definition/meter/rct-power.yaml | 10 ++- 4 files changed, 78 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index 65cdf55fc..4ffc80e4e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e130879f5..ff089dd66 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/meter/rct.go b/meter/rct.go index d1908af07..5d900a37c 100644 --- a/meter/rct.go +++ b/meter/rct.go @@ -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) } diff --git a/templates/definition/meter/rct-power.yaml b/templates/definition/meter/rct-power.yaml index 2ea69f123..4d294018c 100644 --- a/templates/definition/meter/rct-power.yaml +++ b/templates/definition/meter/rct-power.yaml @@ -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 }}