diff --git a/go.mod b/go.mod index 47559dac46..cc9fea744c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 904335ca7f..599b693355 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/meter/powerwall.go b/meter/powerwall.go index 25c0c71761..9eb52076a2 100644 --- a/meter/powerwall.go +++ b/meter/powerwall.go @@ -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,49 +93,23 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, tokens meterG: provider.Cached(client.GetMetersAggregates, cache), } - // decorate api.MeterEnergy - var totalEnergy func() (float64, error) - if m.usage == "load" || m.usage == "solar" { - totalEnergy = m.totalEnergy - } - - // decorate api.BatterySoc - var batterySoc func() (float64, error) - var batteryCapacity func() float64 - if usage == "battery" { - batterySoc = m.batterySoc - - res, err := m.client.GetSystemStatus() - if err != nil { - return nil, err - } - - batteryCapacity = func() float64 { - return res.NominalFullPackEnergy / 1e3 + var batteryControl bool + if refreshToken != "" || siteId != 0 { + if refreshToken == "" { + return nil, errors.New("missing refresh token") } + batteryControl = true } - // decorate api.BatteryController - var batModeS func(api.BatteryMode) error + if batteryControl { + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log)) - token, err := tokens.Token() - if err == nil { - if !sponsor.IsAuthorized() { - return nil, api.ErrSponsorRequired - } + options := []tesla.ClientOption{tesla.WithToken(&oauth2.Token{ + RefreshToken: refreshToken, + Expiry: time.Now(), + })} - 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)) + cloudClient, err := tesla.NewClient(ctx, options...) if err != nil { return nil, err } @@ -167,7 +135,33 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, tokens return nil, err } m.energySite = energySite + } + + // decorate api.MeterEnergy + var totalEnergy func() (float64, error) + if m.usage == "load" || m.usage == "solar" { + totalEnergy = m.totalEnergy + } + + // decorate api.BatterySoc + var batterySoc func() (float64, error) + var batteryCapacity func() float64 + if usage == "battery" { + batterySoc = m.batterySoc + res, err := m.client.GetSystemStatus() + if err != nil { + return nil, err + } + + batteryCapacity = func() float64 { + return res.NominalFullPackEnergy / 1e3 + } + } + + // decorate api.BatteryController + var batModeS func(api.BatteryMode) error + if batteryControl { batModeS = battery.LimitController(m.socG, func(limit float64) error { return m.energySite.SetBatteryReserve(uint64(limit)) }) diff --git a/templates/definition/meter/tesla-powerwall.yaml b/templates/definition/meter/tesla-powerwall.yaml index 3eff15688c..6c33b672ba 100644 --- a/templates/definition/meter/tesla-powerwall.yaml +++ b/templates/definition/meter/tesla-powerwall.yaml @@ -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 }}