Add E3DC native implementation (#13413)

This commit is contained in:
andig 2024-04-27 10:28:49 +02:00 • committed by GitHub
parent c0bf1ec04b
commit ce97c635d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 513 additions and 13 deletions

6
go.mod
View file

@ -77,6 +77,7 @@ require (
github.com/samber/lo v1.39.0
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/smallnest/chanx v1.2.0
github.com/spali/go-rscp v0.2.0-beta4
github.com/spf13/cast v1.6.0
github.com/spf13/cobra v1.8.0
github.com/spf13/jwalterweatherman v1.1.0
@ -108,9 +109,11 @@ require (
github.com/ahmetb/go-linq/v3 v3.2.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cstockton/go-conv v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.16.0 // indirect
@ -172,8 +175,9 @@ require (
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sirupsen/logrus v1.9.3
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/teivah/onecontext v1.3.0 // indirect

10
go.sum
View file

@ -52,6 +52,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
github.com/aws/aws-sdk-go v1.51.21 h1:UrT6JC9R9PkYYXDZBV0qDKTualMr+bfK2eboTknMgbs=
github.com/aws/aws-sdk-go v1.51.21/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b h1:/2dABok/UswXOj5rjbR5bZ411ApGBq1pAEZdy5rvFrY=
github.com/azihsoyn/rijndael256 v0.0.0-20200316065338-d14eefa2b66b/go.mod h1:ef+2vMUkiKcy2Tz7HykB01KbgUnkK4gQKq4ZeR4RYVs=
github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw=
github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw=
github.com/basvdlei/gotsmart v0.0.3 h1:7hrI6btBc8dmFzzHRDkr9Xl87PvrrOT43DzbsTFqMk8=
@ -94,6 +96,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cstockton/go-conv v1.0.0 h1:zj/q/0MpQ/97XfiC9glWiohO8lhgR4TTnHYZifLTv6I=
github.com/cstockton/go-conv v1.0.0/go.mod h1:HuiHkkRgOA0IoBNPC7ysG7kNpjDYlgM7Kj62yQPxjy4=
github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk=
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -186,6 +190,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@ -606,6 +612,10 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spali/go-rscp v0.2.0-beta4 h1:ct9YZTCmTW2IMg74O16nJu0QntGF26dxY5ZejRvl280=
github.com/spali/go-rscp v0.2.0-beta4/go.mod h1:yPHx7clunJmpCLFDc60XL04/lp8p/DrrhfeBqM3J8cc=
github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127 h1:YDqvwAH/l3S4ZULmKlUYszPyLBjHq73CLuUPU+2jJeE=
github.com/spali/go-slicereader v0.0.0-20201122145524-8e262e1a5127/go.mod h1:nf5bOq6n8UugtmQiD3l0BzkE5VP4NvyngFZVkH3ZzgM=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=

301
meter/e3dc.go Normal file
View file

@ -0,0 +1,301 @@
package meter
import (
"errors"
"net"
"slices"
"strconv"
"sync"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/templates"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
"github.com/spali/go-rscp/rscp"
"github.com/spf13/cast"
)
type E3dc struct {
capacity float64
dischargeLimit uint32
usage templates.Usage // TODO check if we really want to depend on templates
conn *rscp.Client
}
func init() {
registry.Add("e3dc-rscp", NewE3dcFromConfig)
}
//go:generate go run ../cmd/tools/decorate.go -f decorateE3dc -b *E3dc -r api.Meter -t "api.BatteryCapacity,Capacity,func() float64" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryController,SetBatteryMode,func(api.BatteryMode) error"
func NewE3dcFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
Usage templates.Usage
Uri string
User string
Password string
Key string
Battery uint16 // battery id
DischargeLimit uint32
Timeout time.Duration
}{
Timeout: request.Timeout,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
host, port_, err := net.SplitHostPort(util.DefaultPort(cc.Uri, 5033))
if err != nil {
return nil, err
}
port, _ := strconv.Atoi(port_)
cfg := rscp.ClientConfig{
Address: host,
Port: uint16(port),
Username: cc.User,
Password: cc.Password,
Key: cc.Key,
ConnectionTimeout: cc.Timeout,
SendTimeout: cc.Timeout,
ReceiveTimeout: cc.Timeout,
}
return NewE3dc(cfg, cc.Usage, cc.Battery, cc.DischargeLimit)
}
var e3dcOnce sync.Once
func NewE3dc(cfg rscp.ClientConfig, usage templates.Usage, batteryId uint16, dischargeLimit uint32) (api.Meter, error) {
e3dcOnce.Do(func() {
log := util.NewLogger("e3dc")
rscp.Log.SetLevel(logrus.DebugLevel)
rscp.Log.SetOutput(log.TRACE.Writer())
})
conn, err := rscp.NewClient(cfg)
if err != nil {
return nil, err
}
m := &E3dc{
usage: usage,
conn: conn,
dischargeLimit: dischargeLimit,
}
// decorate api.BatterySoc
var (
batterySoc func() (float64, error)
batteryCapacity func() float64
batteryMode func(api.BatteryMode) error
)
if usage == templates.UsageBattery {
batterySoc = m.batterySoc
batteryCapacity = m.batteryCapacity
batteryMode = m.setBatteryMode
res, err := m.conn.Send(rscp.Message{
Tag: rscp.BAT_REQ_DATA,
DataType: rscp.Container,
Value: []rscp.Message{
{
Tag: rscp.BAT_INDEX,
DataType: rscp.UInt16,
Value: batteryId,
},
{
Tag: rscp.BAT_REQ_SPECIFICATION,
DataType: rscp.None,
},
},
})
if err != nil {
return nil, err
}
batSpec, err := rscpContains(res, rscp.BAT_SPECIFICATION)
if err != nil {
return nil, err
}
batCap, err := rscpContains(&batSpec, rscp.BAT_SPECIFIED_CAPACITY)
if err != nil {
return nil, err
}
cap, err := rscpValue(batCap, cast.ToFloat64E)
if err != nil {
return nil, err
}
m.capacity = cap / 1e3
}
return decorateE3dc(m, batteryCapacity, batterySoc, batteryMode), nil
}
func (m *E3dc) CurrentPower() (float64, error) {
switch m.usage {
case templates.UsageGrid:
res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_POWER_GRID, nil))
if err != nil {
return 0, err
}
return rscpValue(*res, cast.ToFloat64E)
case templates.UsagePV:
res, err := m.conn.SendMultiple([]rscp.Message{
*rscp.NewMessage(rscp.EMS_REQ_POWER_PV, nil),
*rscp.NewMessage(rscp.EMS_REQ_POWER_ADD, nil),
})
if err != nil {
return 0, err
}
values, err := rscpValues(res, cast.ToFloat64E)
if err != nil {
return 0, err
}
return lo.Sum(values), nil
case templates.UsageBattery:
res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_POWER_BAT, nil))
if err != nil {
return 0, err
}
pwr, err := rscpValue(*res, cast.ToFloat64E)
if err != nil {
return 0, err
}
return -pwr, nil
default:
return 0, api.ErrNotAvailable
}
}
func (m *E3dc) batteryCapacity() float64 {
return m.capacity
}
func (m *E3dc) batterySoc() (float64, error) {
res, err := m.conn.Send(*rscp.NewMessage(rscp.EMS_REQ_BAT_SOC, nil))
if err != nil {
return 0, err
}
return rscpValue(*res, cast.ToFloat64E)
}
func (m *E3dc) setBatteryMode(mode api.BatteryMode) error {
var (
res []rscp.Message
err error
)
switch mode {
case api.BatteryNormal:
res, err = m.conn.SendMultiple([]rscp.Message{
e3dcDischargeBatteryLimit(false, 0),
e3dcBatteryCharge(0),
})
case api.BatteryHold:
res, err = m.conn.SendMultiple([]rscp.Message{
e3dcDischargeBatteryLimit(true, m.dischargeLimit),
e3dcBatteryCharge(0),
})
case api.BatteryCharge:
res, err = m.conn.SendMultiple([]rscp.Message{
e3dcDischargeBatteryLimit(false, 0),
e3dcBatteryCharge(10000), // 10kWh
})
default:
return api.ErrNotAvailable
}
if err == nil {
err = rscpError(res...)
}
return err
}
func e3dcDischargeBatteryLimit(active bool, limit uint32) rscp.Message {
contents := []rscp.Message{
*rscp.NewMessage(rscp.EMS_POWER_LIMITS_USED, active),
}
if active {
contents = append(contents, *rscp.NewMessage(rscp.EMS_MAX_DISCHARGE_POWER, limit))
}
return *rscp.NewMessage(rscp.EMS_REQ_SET_POWER_SETTINGS, contents)
}
func e3dcBatteryCharge(amount uint32) rscp.Message {
return *rscp.NewMessage(rscp.EMS_REQ_START_MANUAL_CHARGE, amount)
}
func rscpError(msg ...rscp.Message) error {
var errs []error
for _, m := range msg {
if m.DataType == rscp.Error {
errs = append(errs, errors.New(rscp.RscpError(cast.ToUint32(m.Value)).String()))
}
}
return errors.Join(errs...)
}
func rscpContains(msg *rscp.Message, tag rscp.Tag) (rscp.Message, error) {
var zero rscp.Message
slice, ok := msg.Value.([]rscp.Message)
if !ok {
return zero, errors.New("not a slice looking for " + tag.String())
}
idx := slices.IndexFunc(slice, func(m rscp.Message) bool {
return m.Tag == tag
})
if idx < 0 {
return zero, errors.New("missing " + tag.String())
}
res := slice[idx]
return res, rscpError(res)
}
func rscpValue[T any](msg rscp.Message, fun func(any) (T, error)) (T, error) {
var zero T
if err := rscpError(msg); err != nil {
return zero, err
}
return fun(msg.Value)
}
func rscpValues[T any](msg []rscp.Message, fun func(any) (T, error)) ([]T, error) {
res := make([]T, 0, len(msg))
for _, m := range msg {
v, err := rscpValue(m, fun)
if err != nil {
return nil, err
}
res = append(res, v)
}
return res, nil
}

137
meter/e3dc_decorators.go Normal file
View file

@ -0,0 +1,137 @@
package meter
// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
import (
"github.com/evcc-io/evcc/api"
)
func decorateE3dc(base *E3dc, batteryCapacity func() float64, battery func() (float64, error), batteryController func(api.BatteryMode) error) api.Meter {
switch {
case battery == nil && batteryCapacity == nil && batteryController == nil:
return base
case battery == nil && batteryCapacity != nil && batteryController == nil:
return &struct {
*E3dc
api.BatteryCapacity
}{
E3dc: base,
BatteryCapacity: &decorateE3dcBatteryCapacityImpl{
batteryCapacity: batteryCapacity,
},
}
case battery != nil && batteryCapacity == nil && batteryController == nil:
return &struct {
*E3dc
api.Battery
}{
E3dc: base,
Battery: &decorateE3dcBatteryImpl{
battery: battery,
},
}
case battery != nil && batteryCapacity != nil && batteryController == nil:
return &struct {
*E3dc
api.Battery
api.BatteryCapacity
}{
E3dc: base,
Battery: &decorateE3dcBatteryImpl{
battery: battery,
},
BatteryCapacity: &decorateE3dcBatteryCapacityImpl{
batteryCapacity: batteryCapacity,
},
}
case battery == nil && batteryCapacity == nil && batteryController != nil:
return &struct {
*E3dc
api.BatteryController
}{
E3dc: base,
BatteryController: &decorateE3dcBatteryControllerImpl{
batteryController: batteryController,
},
}
case battery == nil && batteryCapacity != nil && batteryController != nil:
return &struct {
*E3dc
api.BatteryCapacity
api.BatteryController
}{
E3dc: base,
BatteryCapacity: &decorateE3dcBatteryCapacityImpl{
batteryCapacity: batteryCapacity,
},
BatteryController: &decorateE3dcBatteryControllerImpl{
batteryController: batteryController,
},
}
case battery != nil && batteryCapacity == nil && batteryController != nil:
return &struct {
*E3dc
api.Battery
api.BatteryController
}{
E3dc: base,
Battery: &decorateE3dcBatteryImpl{
battery: battery,
},
BatteryController: &decorateE3dcBatteryControllerImpl{
batteryController: batteryController,
},
}
case battery != nil && batteryCapacity != nil && batteryController != nil:
return &struct {
*E3dc
api.Battery
api.BatteryCapacity
api.BatteryController
}{
E3dc: base,
Battery: &decorateE3dcBatteryImpl{
battery: battery,
},
BatteryCapacity: &decorateE3dcBatteryCapacityImpl{
batteryCapacity: batteryCapacity,
},
BatteryController: &decorateE3dcBatteryControllerImpl{
batteryController: batteryController,
},
}
}
return nil
}
type decorateE3dcBatteryImpl struct {
battery func() (float64, error)
}
func (impl *decorateE3dcBatteryImpl) Soc() (float64, error) {
return impl.battery()
}
type decorateE3dcBatteryCapacityImpl struct {
batteryCapacity func() float64
}
func (impl *decorateE3dcBatteryCapacityImpl) Capacity() float64 {
return impl.batteryCapacity()
}
type decorateE3dcBatteryControllerImpl struct {
batteryController func(api.BatteryMode) error
}
func (impl *decorateE3dcBatteryControllerImpl) SetBatteryMode(p0 api.BatteryMode) error {
return impl.batteryController(p0)
}

View file

@ -1,4 +1,5 @@
template: e3dc
deprecated: true
products:
- brand: E3/DC
params:

View file

@ -0,0 +1,34 @@
template: e3dc-rscp
products:
- brand: E3/DC
params:
- name: usage
choice: ["grid", "pv", "battery"]
allinone: true
- name: host
- name: port
default: 5033
- name: user
- name: password
- name: key
- name: battery
type: number
advanced: true
- name: dischargelimit
description:
de: Entladelimit in W
en: Discharge limit in W
help:
de: Limitiert die Entladeleistung im 'Halten' Batteriemodus
en: Limits discharge power in 'Hold' battery mode
type: number
advanced: true
render: |
type: e3dc-rscp
usage: {{ .usage }}
uri: {{ .host }}:{{ .port }}
user: {{ .user }}
password: {{ .password }}
key: {{ .key }}
battery: {{ .battery }}
dischargelimit: {{ .dischargelimit }}

View file

@ -9,17 +9,6 @@ import (
"dario.cat/mergo"
)
type Usage int
//go:generate enumer -type Usage -trimprefix Usage -transform=lower
const (
UsageGrid Usage = iota
UsagePV
UsageBattery
UsageCharge
UsageAux
)
const (
ParamUsage = "usage"
ParamModbus = "modbus"

12
util/templates/usage.go Normal file
View file

@ -0,0 +1,12 @@
package templates
type Usage int
//go:generate enumer -type Usage -trimprefix Usage -transform=lower -text
const (
UsageGrid Usage = iota
UsagePV
UsageBattery
UsageCharge
UsageAux
)

View file

@ -1,4 +1,4 @@
// Code generated by "enumer -type Usage -trimprefix Usage -transform=lower"; DO NOT EDIT.
// Code generated by "enumer -type Usage -trimprefix Usage -transform=lower -text"; DO NOT EDIT.
package templates
@ -88,3 +88,15 @@ func (i Usage) IsAUsage() bool {
}
return false
}
// MarshalText implements the encoding.TextMarshaler interface for Usage
func (i Usage) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for Usage
func (i *Usage) UnmarshalText(text []byte) error {
var err error
*i, err = UsageString(string(text))
return err
}