Powerwall: revert unification with tesla identity

This commit is contained in:
andig 2024-02-19 07:57:08 +01:00
parent 4d0c46ef13
commit a74098382a
4 changed files with 67 additions and 71 deletions

1
go.mod
View file

@ -17,6 +17,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.3.1
github.com/cenkalti/backoff/v4 v4.2.1
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/containrrr/shoutrrr v0.8.0

2
go.sum
View file

@ -74,6 +74,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.3.1 h1:2iTieXh8jqG8FGWs63aQfRWbD7toLUm6fQ7677eevIE=
github.com/bogosj/tesla v1.3.1/go.mod h1:JvkjZAswd288BeRYd37fKqyYKLcDj3E5oY/OuLzndaI=
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=

View file

@ -11,14 +11,11 @@ import (
"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"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/evcc-io/evcc/vehicle"
"github.com/evcc-io/evcc/vehicle/tesla"
teslaclient "github.com/evcc-io/tesla-proxy-client"
"golang.org/x/oauth2"
)
@ -27,7 +24,7 @@ type PowerWall struct {
usage string
client *powerwall.Client
meterG func() (map[string]powerwall.MeterAggregatesData, error)
energySite *teslaclient.EnergySite
energySite *tesla.EnergySite
}
func init() {
@ -42,7 +39,7 @@ func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
URI, Usage, User, Password string
Cache time.Duration
Tokens vehicle.Tokens
RefreshToken string
SiteId int64
battery `mapstructure:",squash"`
}{
@ -73,15 +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, cc.Tokens, cc.SiteId, cc.battery)
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, tokens vehicle.Tokens, siteId int64, battery battery) (api.Meter, error) {
log := util.NewLogger("powerwall").Redact(
user, password,
tokens.Access, tokens.Refresh,
)
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)
httpClient := &http.Client{
Transport: request.NewTripper(log, powerwall.DefaultTransport()),
@ -99,6 +93,50 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, tokens
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
}
}
}
log.Redact(strconv.FormatInt(siteId, 10))
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" {
@ -123,51 +161,7 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, tokens
// decorate api.BatteryController
var batModeS func(api.BatteryMode) error
token, err := tokens.Token()
if err == nil {
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
identity, err := tesla.NewIdentity(log, token)
if err != nil {
return nil, err
}
hc := request.NewClient(log)
hc.Transport = &oauth2.Transport{
Source: identity,
Base: hc.Transport,
}
cloudClient, err := teslaclient.NewClient(context.Background(), teslaclient.WithClient(hc))
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
}
}
}
log.Redact(strconv.FormatInt(siteId, 10))
energySite, err := cloudClient.EnergySite(siteId)
if err != nil {
return nil, err
}
m.energySite = energySite
if batteryControl {
batModeS = battery.LimitController(m.socG, func(limit float64) error {
return m.energySite.SetBatteryReserve(uint64(limit))
})

View file

@ -7,14 +7,19 @@ capabilities: ["battery-control"]
requirements:
description:
de: |
Benötigt `access` und `refresh` Tokens. Diese können über [tesla.evcc.io](https://tesla.evcc.io) erstellt werden.
Die optionale Batteriesteuerung erfolgt über einen evcc Proxy-Server und erfordert ein Sponsor Token.
Siehe https://tesla.evcc.io.
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: |
Teska `access` and `refresh` tokens are required. These can be generated through [tesla.evcc.io](https://tesla.evcc.io).
Optional battery control is done via an evcc proxy server and requires a sponsor token.
Siehe https://tesla.evcc.io.
evcc: ["sponsorship"]
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"]
@ -25,10 +30,6 @@ params:
help:
en: Password of the user "customer". By default this is the last 5 characters of password stated on the Tesla Gateway.
de: Passwort des Benutzers "Kunde". Default sind die letzten 5 Zeichen des auf dem Tesla Gateway genannten Passworts.
- name: accessToken
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: refreshToken
help:
en: "See https://docs.evcc.io/en/docs/devices/meters#tesla-powerwall"
@ -55,9 +56,7 @@ render: |
usage: {{ .usage }}
user: customer
password: {{ .password }} # for user 'customer'
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
refreshToken: {{ .refreshToken }}
siteId: {{ .siteId }}
minSoc: {{ .minSoc }}
maxSoc: {{ .maxSoc }}