From 91f33f8569fbb4402b71bb4b1d025e694749f2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20He=C3=9F?= Date: Mon, 20 Nov 2023 22:46:35 +0100 Subject: [PATCH] Powerwall: add battery control (#10758) --- go.mod | 14 +- go.sum | 28 +-- meter/powerwall.go | 90 +++++++++- meter/powerwall_decorators.go | 162 +++++++++++++++++- .../definition/meter/tesla-powerwall.yaml | 40 +++++ 5 files changed, 296 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index 6536412a4..593f045cd 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/basgys/goxml2json v1.1.0 github.com/basvdlei/gotsmart v0.0.3 github.com/benbjohnson/clock v1.3.5 - github.com/bogosj/tesla v1.1.1 + github.com/bogosj/tesla v1.3.0 github.com/cenkalti/backoff/v4 v4.2.1 github.com/cjrd/allocate v0.0.0-20220510215731-986f24f0fb18 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 @@ -94,10 +94,10 @@ require ( gitlab.com/bboehmke/sunny v0.16.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20231030152948-74c2ba9521f1 golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.17.0 - golang.org/x/oauth2 v0.13.0 + golang.org/x/net v0.18.0 + golang.org/x/oauth2 v0.14.0 golang.org/x/sync v0.4.0 - golang.org/x/text v0.13.0 + golang.org/x/text v0.14.0 google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v3 v3.0.1 @@ -187,11 +187,11 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.14.0 // indirect + golang.org/x/crypto v0.15.0 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/term v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/term v0.14.0 // indirect golang.org/x/tools v0.14.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect diff --git a/go.sum b/go.sum index a37ee5283..65d35c588 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bogosj/tesla v1.1.1 h1:OMh870k3hb7NpoqAg3Donu/IDNEahRo/GUbBOseYqIs= -github.com/bogosj/tesla v1.1.1/go.mod h1:U0lGPUZLtWaC1q+lKHlj5Zzj3U+/El2NgzRCz6AFiJc= +github.com/bogosj/tesla v1.3.0 h1:6K/4ZMNYXe3toXUwBbBhtYcKwNFnCTKc4YyUj1Pxc2c= +github.com/bogosj/tesla v1.3.0/go.mod h1:l10JPNhF1f+61XwzHZZ2uoTINBF9Vo+PVYBtlUnVx6E= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= @@ -822,8 +822,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto/x509roots/fallback v0.0.0-20231030152948-74c2ba9521f1 h1:wQ75dCmVn5ExryuIUzbi2MC1/10fUNIL1FP918r4jx8= golang.org/x/crypto/x509roots/fallback v0.0.0-20231030152948-74c2ba9521f1/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -918,8 +918,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -929,8 +929,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1017,15 +1017,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1038,8 +1038,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/meter/powerwall.go b/meter/powerwall.go index 944f3aadf..c7090d58f 100644 --- a/meter/powerwall.go +++ b/meter/powerwall.go @@ -1,24 +1,30 @@ package meter import ( + "context" "errors" "fmt" + "math" "net/http" + "strconv" "strings" "time" "github.com/andig/go-powerwall" + "github.com/bogosj/tesla" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" + "golang.org/x/oauth2" ) // PowerWall is the tesla powerwall meter type PowerWall struct { - usage string - client *powerwall.Client - meterG func() (map[string]powerwall.MeterAggregatesData, error) + usage string + client *powerwall.Client + meterG func() (map[string]powerwall.MeterAggregatesData, error) + energySite *tesla.EnergySite } func init() { @@ -26,15 +32,22 @@ func init() { registry.Add("powerwall", NewPowerWallFromConfig) } -//go:generate go run ../cmd/tools/decorate.go -f decoratePowerWall -b *PowerWall -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 go run ../cmd/tools/decorate.go -f decoratePowerWall -b *PowerWall -r api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,Soc,func() (float64, error)" -t "api.BatteryCapacity,Capacity,func() float64" -t "api.BatteryController,SetBatteryMode,func(api.BatteryMode) error" // NewPowerWallFromConfig creates a PowerWall Powerwall Meter from generic config func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) { cc := struct { URI, Usage, User, Password string Cache time.Duration + RefreshToken string + SiteId int64 + battery `mapstructure:",squash"` }{ Cache: time.Second, + battery: battery{ + MinSoc: 20, + MaxSoc: 95, + }, } if err := util.DecodeOther(other, &cc); err != nil { @@ -57,12 +70,12 @@ func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) { cc.Usage = "solar" } - return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache) + return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache, cc.RefreshToken, cc.SiteId, cc.battery) } // NewPowerWall creates a Tesla PowerWall Meter -func NewPowerWall(uri, usage, user, password string, cache time.Duration) (api.Meter, error) { - log := util.NewLogger("powerwall").Redact(user, password) +func NewPowerWall(uri, usage, user, password string, cache time.Duration, refreshToken string, siteId int64, battery battery) (api.Meter, error) { + log := util.NewLogger("powerwall").Redact(user, password, refreshToken, strconv.FormatInt(siteId, 10)) httpClient := &http.Client{ Transport: request.NewTripper(log, powerwall.DefaultTransport()), @@ -80,6 +93,49 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration) (api.M meterG: provider.Cached(client.GetMetersAggregates, cache), } + var batteryControl bool + if refreshToken != "" || siteId != 0 { + if refreshToken == "" { + return nil, errors.New("missing refresh token") + } + batteryControl = true + } + + if batteryControl { + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log)) + + options := []tesla.ClientOption{tesla.WithToken(&oauth2.Token{ + RefreshToken: refreshToken, + Expiry: time.Now(), + })} + + cloudClient, err := tesla.NewClient(ctx, options...) + if err != nil { + return nil, err + } + + if siteId == 0 { + //auto detect energy site ID, picking first + products, err := cloudClient.Products() + if err != nil { + return nil, err + } + + for _, p := range products { + if p.EnergySiteId != 0 { + siteId = p.EnergySiteId + break + } + } + } + + energySite, err := cloudClient.EnergySite(siteId) + if err != nil { + return nil, err + } + m.energySite = energySite + } + // decorate api.MeterEnergy var totalEnergy func() (float64, error) if m.usage == "load" || m.usage == "solar" { @@ -102,7 +158,15 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration) (api.M } } - return decoratePowerWall(m, totalEnergy, batterySoc, batteryCapacity), nil + // decorate api.BatteryController + var batModeS func(api.BatteryMode) error + if batteryControl { + batModeS = battery.BatteryController(m.socG, func(limit float64) error { + return m.energySite.SetBatteryReserve(uint64(limit)) + }) + } + + return decoratePowerWall(m, totalEnergy, batterySoc, batteryCapacity, batModeS), nil } var _ api.Meter = (*PowerWall)(nil) @@ -149,3 +213,13 @@ func (m *PowerWall) batterySoc() (float64, error) { return float64(res.Percentage), err } + +// decorate soc +func (m *PowerWall) socG() (float64, error) { + ess, err := m.energySite.EnergySiteStatus() + if err != nil { + return 0, err + } + currentSoc := math.Round(ess.PercentageCharged + 0.5) // .5 ensures we round up + return currentSoc, nil +} diff --git a/meter/powerwall_decorators.go b/meter/powerwall_decorators.go index 394762417..0a3e2f836 100644 --- a/meter/powerwall_decorators.go +++ b/meter/powerwall_decorators.go @@ -6,12 +6,12 @@ import ( "github.com/evcc-io/evcc/api" ) -func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), battery func() (float64, error), batteryCapacity func() float64) api.Meter { +func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), battery func() (float64, error), batteryCapacity func() float64, batteryController func(api.BatteryMode) error) api.Meter { switch { - case battery == nil && batteryCapacity == nil && meterEnergy == nil: + case battery == nil && batteryCapacity == nil && batteryController == nil && meterEnergy == nil: return base - case battery == nil && batteryCapacity == nil && meterEnergy != nil: + case battery == nil && batteryCapacity == nil && batteryController == nil && meterEnergy != nil: return &struct { *PowerWall api.MeterEnergy @@ -22,7 +22,7 @@ func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), bat }, } - case battery != nil && batteryCapacity == nil && meterEnergy == nil: + case battery != nil && batteryCapacity == nil && batteryController == nil && meterEnergy == nil: return &struct { *PowerWall api.Battery @@ -33,7 +33,7 @@ func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), bat }, } - case battery != nil && batteryCapacity == nil && meterEnergy != nil: + case battery != nil && batteryCapacity == nil && batteryController == nil && meterEnergy != nil: return &struct { *PowerWall api.Battery @@ -48,7 +48,7 @@ func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), bat }, } - case battery == nil && batteryCapacity != nil && meterEnergy == nil: + case battery == nil && batteryCapacity != nil && batteryController == nil && meterEnergy == nil: return &struct { *PowerWall api.BatteryCapacity @@ -59,7 +59,7 @@ func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), bat }, } - case battery == nil && batteryCapacity != nil && meterEnergy != nil: + case battery == nil && batteryCapacity != nil && batteryController == nil && meterEnergy != nil: return &struct { *PowerWall api.BatteryCapacity @@ -74,7 +74,7 @@ func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), bat }, } - case battery != nil && batteryCapacity != nil && meterEnergy == nil: + case battery != nil && batteryCapacity != nil && batteryController == nil && meterEnergy == nil: return &struct { *PowerWall api.Battery @@ -89,7 +89,7 @@ func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), bat }, } - case battery != nil && batteryCapacity != nil && meterEnergy != nil: + case battery != nil && batteryCapacity != nil && batteryController == nil && meterEnergy != nil: return &struct { *PowerWall api.Battery @@ -107,6 +107,142 @@ func decoratePowerWall(base *PowerWall, meterEnergy func() (float64, error), bat meterEnergy: meterEnergy, }, } + + case battery == nil && batteryCapacity == nil && batteryController != nil && meterEnergy == nil: + return &struct { + *PowerWall + api.BatteryController + }{ + PowerWall: base, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + } + + case battery == nil && batteryCapacity == nil && batteryController != nil && meterEnergy != nil: + return &struct { + *PowerWall + api.BatteryController + api.MeterEnergy + }{ + PowerWall: base, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + MeterEnergy: &decoratePowerWallMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case battery != nil && batteryCapacity == nil && batteryController != nil && meterEnergy == nil: + return &struct { + *PowerWall + api.Battery + api.BatteryController + }{ + PowerWall: base, + Battery: &decoratePowerWallBatteryImpl{ + battery: battery, + }, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + } + + case battery != nil && batteryCapacity == nil && batteryController != nil && meterEnergy != nil: + return &struct { + *PowerWall + api.Battery + api.BatteryController + api.MeterEnergy + }{ + PowerWall: base, + Battery: &decoratePowerWallBatteryImpl{ + battery: battery, + }, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + MeterEnergy: &decoratePowerWallMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case battery == nil && batteryCapacity != nil && batteryController != nil && meterEnergy == nil: + return &struct { + *PowerWall + api.BatteryCapacity + api.BatteryController + }{ + PowerWall: base, + BatteryCapacity: &decoratePowerWallBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + } + + case battery == nil && batteryCapacity != nil && batteryController != nil && meterEnergy != nil: + return &struct { + *PowerWall + api.BatteryCapacity + api.BatteryController + api.MeterEnergy + }{ + PowerWall: base, + BatteryCapacity: &decoratePowerWallBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + MeterEnergy: &decoratePowerWallMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case battery != nil && batteryCapacity != nil && batteryController != nil && meterEnergy == nil: + return &struct { + *PowerWall + api.Battery + api.BatteryCapacity + api.BatteryController + }{ + PowerWall: base, + Battery: &decoratePowerWallBatteryImpl{ + battery: battery, + }, + BatteryCapacity: &decoratePowerWallBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + } + + case battery != nil && batteryCapacity != nil && batteryController != nil && meterEnergy != nil: + return &struct { + *PowerWall + api.Battery + api.BatteryCapacity + api.BatteryController + api.MeterEnergy + }{ + PowerWall: base, + Battery: &decoratePowerWallBatteryImpl{ + battery: battery, + }, + BatteryCapacity: &decoratePowerWallBatteryCapacityImpl{ + batteryCapacity: batteryCapacity, + }, + BatteryController: &decoratePowerWallBatteryControllerImpl{ + batteryController: batteryController, + }, + MeterEnergy: &decoratePowerWallMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } } return nil @@ -128,6 +264,14 @@ func (impl *decoratePowerWallBatteryCapacityImpl) Capacity() float64 { return impl.batteryCapacity() } +type decoratePowerWallBatteryControllerImpl struct { + batteryController func(api.BatteryMode) error +} + +func (impl *decoratePowerWallBatteryControllerImpl) SetBatteryMode(m api.BatteryMode) error { + return impl.batteryController(m) +} + type decoratePowerWallMeterEnergyImpl struct { meterEnergy func() (float64, error) } diff --git a/templates/definition/meter/tesla-powerwall.yaml b/templates/definition/meter/tesla-powerwall.yaml index ee9497c39..19ed775da 100644 --- a/templates/definition/meter/tesla-powerwall.yaml +++ b/templates/definition/meter/tesla-powerwall.yaml @@ -3,6 +3,22 @@ products: - brand: Tesla description: generic: Powerwall +requirements: + description: + de: | + Um die optionale Entladesteuerung der Battery zu nutzen wird ein `refresh` Token für die Kommunikation mit der Tesla API benötigt. + + Folgende Apps ermöglichen das Erstellen des Tokens: + - [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone) + - [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens) + - [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth) + en: | + To use the optional battery control you need to generate a `refresh` token for communicating with the Tesla API. + + The following apps allow to create the token: + - [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone) + - [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens) + - [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth) params: - name: usage choice: ["grid", "pv", "battery"] @@ -13,9 +29,33 @@ params: help: en: Password of the user "customer" de: Passwort des Benutzers "Kunde" + - name: refreshToken + help: + en: "See https://docs.evcc.io/en/docs/devices/meters#tesla-powerwall" + de: "Siehe https://docs.evcc.io/docs/devices/meters#tesla-powerwall" + - name: siteId + help: + en: optional product identifier of the energy site, use to override autodectction + de: optionale Product ID dieser Energy Site, zum Übersteuern der automatischen Erkennung + - name: minSoc + type: number + advanced: true + help: + en: the battery reserve percentage to reset to when switching the Battery back to normal operations + de: die Energyreserve auf die zurückgesetzt wird, wenn die Entladung wieder freigegeben wird + - name: maxSoc + type: number + advanced: true + help: + en: the battery reserve percentage to set to when switching the Battery to charge mode + de: die Energyreserve auf die gesetzt wird, wenn Batterieladung angefordert wird render: | type: powerwall uri: {{ .host }} usage: {{ .usage }} user: customer password: {{ .password }} # for user 'customer' + refreshToken: {{ .refreshToken }} + siteId: {{ .siteId }} + minSoc: {{ .minSoc }} + maxSoc: {{ .maxSoc }}