RCT: add battery control (#18178)
This commit is contained in:
parent
b7a9abb643
commit
ae0a656e6d
5 changed files with 183 additions and 15 deletions
4
go.mod
4
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
84
meter/rct.go
84
meter/rct.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue