Skip to content

Commit

Permalink
Add E3DC native implementation (#13413)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed Apr 27, 2024
1 parent c0bf1ec commit ce97c63
Show file tree
Hide file tree
Showing 9 changed files with 513 additions and 13 deletions.
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
301 changes: 301 additions & 0 deletions meter/e3dc.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit ce97c63

Please sign in to comment.