RCT: add battery control (#18178)

This commit is contained in:
andig 2025-02-02 11:24:03 +01:00 • committed by GitHub
parent b7a9abb643
commit ae0a656e6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 183 additions and 15 deletions

4
go.mod
View file

@ -67,7 +67,7 @@ require (
github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543
github.com/mabunixda/wattpilot v1.8.4
github.com/mitchellh/go-homedir v1.1.0
github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f
github.com/mlnoga/rct v0.1.2-0.20250123194852-f6c64dfa2f45
github.com/muka/go-bluetooth v0.0.0-20240701044517-04c4f09c514e
github.com/mxschmitt/golang-combinations v1.2.0
github.com/nicksnyder/go-i18n/v2 v2.4.1
@ -210,4 +210,6 @@ replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20241027151
replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20241230132027-815870498cc3
replace github.com/mlnoga/rct => github.com/Maschga/rct v0.1.2-0.20250123202746-3bacbc13d633
replace github.com/hasura/go-graphql-client => github.com/andig/go-graphql-client v0.0.0-20250123081454-14ac4443ffd9

4
go.sum
View file

@ -8,6 +8,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Maschga/rct v0.1.2-0.20250123202746-3bacbc13d633 h1:WH5hu3UDfAMfSz8s8AazSBM5Ts2UfQbpH8FDyNbyg0o=
github.com/Maschga/rct v0.1.2-0.20250123202746-3bacbc13d633/go.mod h1:0lfd2mmBnBzIvuzYtdhG+2371u+cUfIxsYErm4P9KRI=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
@ -450,8 +452,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f h1:63hbvjkIpccli0EA60fqvzokvU1fH1ZPiNzlF/vUmvM=
github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f/go.mod h1:0lfd2mmBnBzIvuzYtdhG+2371u+cUfIxsYErm4P9KRI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View file

@ -1,8 +1,10 @@
package meter
import (
"encoding/binary"
"errors"
"fmt"
"math"
"strings"
"sync"
"time"
@ -52,14 +54,15 @@ func init() {
registry.Add("rct", NewRCTFromConfig)
}
//go:generate decorate -f decorateRCT -b *RCT -r api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64"
//go:generate decorate -f decorateRCT -b *RCT -r api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryController,SetBatteryMode,func(api.BatteryMode) error" -t "api.BatteryCapacity,Capacity,func() float64"
// NewRCTFromConfig creates an RCT from generic config
func NewRCTFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
capacity `mapstructure:",squash"`
Uri, Usage string
Cache time.Duration
capacity `mapstructure:",squash"`
Uri, Usage string
MinSoc, MaxSoc int
Cache time.Duration
}{
Cache: time.Second,
}
@ -72,13 +75,13 @@ func NewRCTFromConfig(other map[string]interface{}) (api.Meter, error) {
return nil, errors.New("missing usage")
}
return NewRCT(cc.Uri, cc.Usage, cc.Cache, cc.capacity.Decorator())
return NewRCT(cc.Uri, cc.Usage, cc.MinSoc, cc.MaxSoc, cc.Cache, cc.capacity.Decorator())
}
var rctMu sync.Mutex
// NewRCT creates an RCT meter
func NewRCT(uri, usage string, cache time.Duration, capacity func() float64) (api.Meter, error) {
func NewRCT(uri, usage string, minSoc, maxSoc int, cache time.Duration, capacity func() float64) (api.Meter, error) {
rctMu.Lock()
defer rctMu.Unlock()
@ -105,11 +108,62 @@ func NewRCT(uri, usage string, cache time.Duration, capacity func() float64) (ap
// decorate api.BatterySoc
var batterySoc func() (float64, error)
var batteryMode func(api.BatteryMode) error
if usage == "battery" {
batterySoc = m.batterySoc
batteryMode = func(mode api.BatteryMode) error {
if mode != api.BatteryNormal {
batStatus, err := m.queryInt32(rct.BatteryBatStatus)
if err != nil {
return err
}
// see https://github.com/weltenwort/home-assistant-rct-power-integration/issues/264#issuecomment-2124811644
if batStatus != 0 {
return errors.New("invalid battery operating mode")
}
}
switch mode {
case api.BatteryNormal:
if err := m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetInternal}); err != nil {
return err
}
if err := m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(minSoc)/100)); err != nil {
return err
}
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
}
return m.conn.Write(rct.BatterySoCTargetMin, m.floatVal(float32(maxSoc)/100))
case api.BatteryCharge:
if err := m.conn.Write(rct.PowerMngBatteryPowerExternW, m.floatVal(float32(-10_000))); err != nil {
return err
}
return m.conn.Write(rct.PowerMngSocStrategy, []byte{rct.SOCTargetExternal})
default:
return api.ErrNotAvailable
}
}
}
return decorateRCT(m, totalEnergy, batterySoc, capacity), nil
return decorateRCT(m, totalEnergy, batterySoc, batteryMode, capacity), nil
}
func (m *RCT) floatVal(f float32) []byte {
data := make([]byte, 4)
binary.BigEndian.PutUint32(data, math.Float32bits(f))
return data
}
// CurrentPower implements the api.Meter interface
@ -187,3 +241,19 @@ func (m *RCT) queryFloat(id rct.Identifier) (float64, error) {
return float64(res), err
}
// queryInt32 adds retry logic of recoverable errors to QueryInt32
func (m *RCT) queryInt32(id rct.Identifier) (int32, error) {
m.bo.Reset()
res, err := backoff.RetryWithData(func() (int32, error) {
res, err := m.conn.QueryInt32(id)
if err != nil && !errors.As(err, new(rct.RecoverableError)) {
err = backoff.Permanent(err)
}
return res, err
}, m.bo)
return res, err
}

View file

@ -6,7 +6,7 @@ import (
"github.com/evcc-io/evcc/api"
)
func decorateRCT(base *RCT, meterEnergy func() (float64, error), battery func() (float64, error), batteryCapacity func() float64) api.Meter {
func decorateRCT(base *RCT, meterEnergy func() (float64, error), battery func() (float64, error), batteryController func(api.BatteryMode) error, batteryCapacity func() float64) api.Meter {
switch {
case battery == nil && meterEnergy == nil:
return base
@ -22,7 +22,7 @@ func decorateRCT(base *RCT, meterEnergy func() (float64, error), battery func()
},
}
case battery != nil && batteryCapacity == nil && meterEnergy == nil:
case battery != nil && batteryCapacity == nil && batteryController == nil && meterEnergy == nil:
return &struct {
*RCT
api.Battery
@ -33,7 +33,7 @@ func decorateRCT(base *RCT, meterEnergy func() (float64, error), battery func()
},
}
case battery != nil && batteryCapacity == nil && meterEnergy != nil:
case battery != nil && batteryCapacity == nil && batteryController == nil && meterEnergy != nil:
return &struct {
*RCT
api.Battery
@ -48,7 +48,41 @@ func decorateRCT(base *RCT, meterEnergy func() (float64, error), battery func()
},
}
case battery != nil && batteryCapacity != nil && meterEnergy == nil:
case battery != nil && batteryCapacity == nil && batteryController != nil && meterEnergy == nil:
return &struct {
*RCT
api.Battery
api.BatteryController
}{
RCT: base,
Battery: &decorateRCTBatteryImpl{
battery: battery,
},
BatteryController: &decorateRCTBatteryControllerImpl{
batteryController: batteryController,
},
}
case battery != nil && batteryCapacity == nil && batteryController != nil && meterEnergy != nil:
return &struct {
*RCT
api.Battery
api.BatteryController
api.MeterEnergy
}{
RCT: base,
Battery: &decorateRCTBatteryImpl{
battery: battery,
},
BatteryController: &decorateRCTBatteryControllerImpl{
batteryController: batteryController,
},
MeterEnergy: &decorateRCTMeterEnergyImpl{
meterEnergy: meterEnergy,
},
}
case battery != nil && batteryCapacity != nil && batteryController == nil && meterEnergy == nil:
return &struct {
*RCT
api.Battery
@ -63,7 +97,7 @@ func decorateRCT(base *RCT, meterEnergy func() (float64, error), battery func()
},
}
case battery != nil && batteryCapacity != nil && meterEnergy != nil:
case battery != nil && batteryCapacity != nil && batteryController == nil && meterEnergy != nil:
return &struct {
*RCT
api.Battery
@ -81,6 +115,48 @@ func decorateRCT(base *RCT, meterEnergy func() (float64, error), battery func()
meterEnergy: meterEnergy,
},
}
case battery != nil && batteryCapacity != nil && batteryController != nil && meterEnergy == nil:
return &struct {
*RCT
api.Battery
api.BatteryCapacity
api.BatteryController
}{
RCT: base,
Battery: &decorateRCTBatteryImpl{
battery: battery,
},
BatteryCapacity: &decorateRCTBatteryCapacityImpl{
batteryCapacity: batteryCapacity,
},
BatteryController: &decorateRCTBatteryControllerImpl{
batteryController: batteryController,
},
}
case battery != nil && batteryCapacity != nil && batteryController != nil && meterEnergy != nil:
return &struct {
*RCT
api.Battery
api.BatteryCapacity
api.BatteryController
api.MeterEnergy
}{
RCT: base,
Battery: &decorateRCTBatteryImpl{
battery: battery,
},
BatteryCapacity: &decorateRCTBatteryCapacityImpl{
batteryCapacity: batteryCapacity,
},
BatteryController: &decorateRCTBatteryControllerImpl{
batteryController: batteryController,
},
MeterEnergy: &decorateRCTMeterEnergyImpl{
meterEnergy: meterEnergy,
},
}
}
return nil
@ -102,6 +178,14 @@ func (impl *decorateRCTBatteryCapacityImpl) Capacity() float64 {
return impl.batteryCapacity()
}
type decorateRCTBatteryControllerImpl struct {
batteryController func(api.BatteryMode) error
}
func (impl *decorateRCTBatteryControllerImpl) SetBatteryMode(p0 api.BatteryMode) error {
return impl.batteryController(p0)
}
type decorateRCTMeterEnergyImpl struct {
meterEnergy func() (float64, error)
}

View file

@ -3,6 +3,7 @@ products:
- brand: RCT
description:
generic: Power
capabilities: ["battery-control"]
params:
- name: usage
choice: ["grid", "pv", "battery"]
@ -10,9 +11,20 @@ params:
- name: host
- name: capacity
advanced: true
# battery control
- name: minsoc
type: int
default: 7
advanced: true
- name: maxsoc
type: int
default: 97
advanced: true
render: |
type: rct
uri: {{ .host }}
usage: {{ .usage }}
cache: 2s
capacity: {{ .capacity }} # kWh
minsoc: {{ .minsoc }}
maxsoc: {{ .maxsoc }}