Tesla: unify implementations (BC) (#12277)

This commit is contained in:
andig 2024-02-17 17:51:21 +01:00 • committed by GitHub
parent 5448616912
commit 1ae96688f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 257 additions and 1060 deletions

View file

@ -52,8 +52,6 @@ func runToken(cmd *cobra.Command, args []string) {
var err error
switch strings.ToLower(vehicleConf.Type) {
case "tesla":
token, err = teslaToken()
case "tronity":
token, err = tronityToken(conf, vehicleConf)
default:

View file

@ -1,138 +0,0 @@
package cmd
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/bogosj/tesla"
"github.com/evcc-io/evcc/util/request"
"github.com/manifoldco/promptui"
"github.com/skratchdot/open-golang/open"
"golang.org/x/oauth2"
)
// copied from https://github.com/bogosj/tesla
func getUsernameAndPassword() (string, string, error) {
user, err := (&promptui.Prompt{
Label: "Username",
Pointer: promptui.PipeCursor,
Validate: func(s string) error {
if len(s) == 0 {
return errors.New("len(s) == 0")
}
return nil
},
}).Run()
if err != nil {
return "", "", err
}
password, err := (&promptui.Prompt{
Label: "Password",
Mask: '*',
Pointer: promptui.PipeCursor,
Validate: func(s string) error {
if len(s) == 0 {
return errors.New("len(s) == 0")
}
return nil
},
}).Run()
if err != nil {
return "", "", err
}
return user, password, nil
}
func codePrompt(ctx context.Context, devices []tesla.Device) (tesla.Device, string, error) {
var i int
if len(devices) > 1 {
var err error
i, _, err = (&promptui.Select{
Label: "Device",
Items: devices,
Pointer: promptui.PipeCursor,
}).Run()
if err != nil {
return tesla.Device{}, "", fmt.Errorf("select device: %w", err)
}
}
code, err := (&promptui.Prompt{
Label: "Passcode",
Pointer: promptui.PipeCursor,
Validate: func(s string) error {
if len(s) != 6 {
return errors.New("len(s) != 6")
}
return nil
},
}).Run()
if err != nil {
return tesla.Device{}, "", err
}
return devices[i], strings.TrimSpace(code), nil
}
func captchaPrompt(ctx context.Context, svg io.Reader) (string, error) {
tmpFile, err := os.CreateTemp(os.TempDir(), "evcc-*.svg")
if err != nil {
return "", fmt.Errorf("cannot create temp file: %w", err)
}
if _, err := io.Copy(tmpFile, svg); err != nil {
return "", fmt.Errorf("cannot write temp file: %w", err)
}
_ = tmpFile.Close()
if err := open.Run(tmpFile.Name()); err != nil {
return "", fmt.Errorf("cannot open captcha for display: %w", err)
}
fmt.Println("Captcha is now being opened in default application for svg files.")
captcha, err := (&promptui.Prompt{
Label: "Captcha",
Pointer: promptui.PipeCursor,
Validate: func(s string) error {
if len(s) < 4 {
return errors.New("len(s) < 4")
}
return nil
},
}).Run()
return strings.TrimSpace(captcha), err
}
func teslaToken() (*oauth2.Token, error) {
username, password, err := getUsernameAndPassword()
if err != nil {
return nil, err
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
client, err := tesla.NewClient(
ctx,
tesla.WithMFAHandler(codePrompt),
tesla.WithCaptchaHandler(captchaPrompt),
tesla.WithCredentials(username, password),
)
if err != nil {
return nil, err
}
token, err := client.Token()
if err != nil {
return nil, err
}
return token, nil
}

7
go.mod
View file

@ -17,7 +17,6 @@ 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
@ -28,7 +27,7 @@ require (
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/enbility/cemd v0.2.2
github.com/enbility/eebus-go v0.2.0
github.com/evcc-io/tesla-proxy-client v0.0.0-20240210120646-d0be579ff7b7
github.com/evcc-io/tesla-proxy-client v0.0.0-20240217133531-c8fc01cb2151
github.com/fatih/structs v1.1.0
github.com/glebarez/sqlite v1.10.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
@ -62,7 +61,6 @@ require (
github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/mabunixda/wattpilot v1.6.3
github.com/manifoldco/promptui v0.9.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.5.0
github.com/mlnoga/rct v0.1.2-0.20230731074838-03eacb926f99
@ -114,7 +112,6 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v1.5.1 // 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
@ -209,3 +206,5 @@ replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
replace github.com/spf13/viper => github.com/spf13/viper v1.18.1
replace github.com/mabunixda/wattpilot => github.com/mabunixda/wattpilot v1.6.2
// replace github.com/evcc-io/tesla-proxy-client => ../tesla-proxy-client

18
go.sum
View file

@ -74,8 +74,6 @@ 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=
@ -84,15 +82,6 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
@ -143,8 +132,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evcc-io/modbus v0.0.0-20230705160100-23e1df2c01c3 h1:QBtLqy85cy9KLRWqKxX+42bN1rWti8iU7pL7Vwf2UbE=
github.com/evcc-io/modbus v0.0.0-20230705160100-23e1df2c01c3/go.mod h1:qVX2WhsI5xyAoM6I/MV1bXSKBPdLAjp7pCvieO/S0AY=
github.com/evcc-io/tesla-proxy-client v0.0.0-20240210120646-d0be579ff7b7 h1:O2/JI5Od2x2/faJadwMFtVtJnJJxeGGzIjPExcqzD9I=
github.com/evcc-io/tesla-proxy-client v0.0.0-20240210120646-d0be579ff7b7/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I=
github.com/evcc-io/tesla-proxy-client v0.0.0-20240217133531-c8fc01cb2151 h1:nnRPTOA++dAAzY/bOJbsXquI1UgSXey4k8so2i6o96E=
github.com/evcc-io/tesla-proxy-client v0.0.0-20240217133531-c8fc01cb2151/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
@ -425,8 +414,6 @@ github.com/mabunixda/wattpilot v1.6.2 h1:xZE/elFntUJgaWTK2V7CJVQRcE+WvkT8g6dCk/o
github.com/mabunixda/wattpilot v1.6.2/go.mod h1:2tcdLRoqycO6bWYAjZMP9g1giMgw4t2MnbN3VxaCN1Q=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -842,7 +829,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -11,11 +11,14 @@ 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"
)
@ -24,7 +27,7 @@ type PowerWall struct {
usage string
client *powerwall.Client
meterG func() (map[string]powerwall.MeterAggregatesData, error)
energySite *tesla.EnergySite
energySite *teslaclient.EnergySite
}
func init() {
@ -39,7 +42,7 @@ func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
URI, Usage, User, Password string
Cache time.Duration
RefreshToken string
Tokens vehicle.Tokens
SiteId int64
battery `mapstructure:",squash"`
}{
@ -70,12 +73,15 @@ 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.RefreshToken, cc.SiteId, cc.battery)
return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache, cc.Tokens, cc.SiteId, cc.battery)
}
// NewPowerWall creates a Tesla PowerWall Meter
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)
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,
)
httpClient := &http.Client{
Transport: request.NewTripper(log, powerwall.DefaultTransport()),
@ -93,50 +99,6 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, refres
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" {
@ -161,7 +123,51 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, refres
// decorate api.BatteryController
var batModeS func(api.BatteryMode) error
if batteryControl {
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
batModeS = battery.LimitController(m.socG, func(limit float64) error {
return m.energySite.SetBatteryReserve(uint64(limit))
})

View file

@ -7,19 +7,14 @@ capabilities: ["battery-control"]
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)
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.
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)
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"]
params:
- name: usage
choice: ["grid", "pv", "battery"]
@ -30,6 +25,10 @@ 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"
@ -56,7 +55,9 @@ render: |
usage: {{ .usage }}
user: customer
password: {{ .password }} # for user 'customer'
refreshToken: {{ .refreshToken }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
siteId: {{ .siteId }}
minSoc: {{ .minSoc }}
maxSoc: {{ .maxSoc }}

View file

@ -1,61 +0,0 @@
template: tesla-command
products:
- brand: Tesla
description:
de: aktuell
en: latest
requirements:
description:
de: |
Du benötigst ein `access` und ein `refresh` Token.
Diese kannst du über [tesla.evcc.io](https://tesla.evcc.io) erstellt.
**Hinweis:** Dies ist die von Tesla offiziell unterstützte [FleetAPI](https://developer.tesla.com/docs/fleet-api).
en: |
You need an `access` and a `refresh` token.
Please generate them through [tesla.evcc.io](https://tesla.evcc.io).
**Note:** This is Tesla's officially supported [FleetAPI](https://developer.tesla.com/docs/fleet-api).
params:
- name: title
- name: icon
default: car
advanced: true
- name: accessToken
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#tesla-command"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#tesla-command"
- name: refreshToken
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#tesla-command"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#tesla-command"
- name: vin
example: W...
- name: capacity
- name: phases
advanced: true
- preset: vehicle-identify
render: |
type: tesla-command
{{- if .title }}
title: {{ .title }}
{{- end }}
{{- if .icon }}
icon: {{ .icon }}
{{- end }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
{{- if .phases }}
phases: {{ .phases }}
{{- end }}
{{- if .vin }}
vin: {{ .vin }}
{{- end }}
{{ include "vehicle-identify" . }}
features: ["coarsecurrent"]

View file

@ -1,46 +0,0 @@
template: tesla-proxy
products:
- brand: Tesla
description:
de: Fleet API Proxy
en: Fleet API Proxy
requirements:
description:
de: |
Es muss ein `access` und ein `refresh` Token für die Kommunikation mit der Tesla API erstellt werden.
Für die Steuerung des Fahrzeugs muss ferner der Virtual Key auf dem Fahrzeug installiert werden.
Siehe https://tesla.evcc.io.
en: |
You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
For vehicle control, the virtual key must be installed on the vehicle.
See https://tesla.evcc.io.
evcc: ["sponsorship"]
params:
- name: title
- name: icon
default: car
advanced: true
- name: accessToken
required: true
mask: true
- name: refreshToken
required: true
mask: true
- name: vin
example: W...
- name: capacity
- name: phases
advanced: true
- preset: vehicle-identify
render: |
type: tesla-proxy
title: {{ .title }}
icon: {{ .icon }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
phases: {{ .phases }}
vin: {{ .vin }}
{{ include "vehicle-identify" . }}
features: ["coarsecurrent"]

View file

@ -1,29 +1,18 @@
template: tesla
covers: ["tesla-command", "tesla-proxy"]
products:
- brand: Tesla
description:
de: veraltet
en: legacy
requirements:
description:
de: |
Es wird ein `access` und ein `refresh` Token für die Kommunikation mit der Tesla API erstellt werden.
Folgende Apps ermöglichen das Erstellen von den beiden 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)
**Hinweis:** Diese API ist von Tesla [nicht offiziell unterstützt](https://tesla-api.timdorr.com) und wird Stück für Stück abgeschaltet.**
Benötigt `access` und `refresh` Tokens. Diese können über [tesla.evcc.io](https://tesla.evcc.io) erstellt werden.
Die Steuerung von Fahrzeugen im Zusammenspiel mit einem Tesla Wall Connector erfolgt über einen evcc Proxy-Server und benötigt ein Sponsor Token.
Siehe https://tesla.evcc.io.
en: |
You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
The following apps allow to create these 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)
**Note:** This API is [not officially supported](https://tesla-api.timdorr.com) by Tesla and will be deprecated step by step.
Teska `access` and `refresh` tokens are required. These can be generated through [tesla.evcc.io](https://tesla.evcc.io).
Controlling vehicles in conjunction with a Tesla Wall Connector is done via an evcc proxy server and requires a sponsor token.
Siehe https://tesla.evcc.io.
evcc: ["sponsorship"]
params:
- name: title
- name: icon
@ -46,24 +35,25 @@ params:
- name: capacity
- name: phases
advanced: true
- name: control
type: bool
description:
de: Fahrzeugsteuerung aktivieren
en: Enable vehicle control
help:
de: Notwendig für Tesla Wall Connector. Der Virtual Key für evcc muss eingerichtet sein. Siehe https://tesla.evcc.io
en: Use with Tesla Wall Connector. The Virtual Key for evcc must be installed. See https://tesla.evcc.io
- preset: vehicle-identify
render: |
type: tesla
{{- if .title }}
title: {{ .title }}
{{- end }}
{{- if .icon }}
icon: {{ .icon }}
{{- end }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
{{- if .phases }}
phases: {{ .phases }}
{{- end }}
{{- if .vin }}
control: {{ .control }}
vin: {{ .vin }}
{{- end }}
{{ include "vehicle-identify" . }}
features: ["coarsecurrent"]

View file

@ -1,117 +0,0 @@
package vehicle
import (
"os"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
vc "github.com/evcc-io/evcc/vehicle/tesla-vehicle-command"
)
// TeslaCommand is an api.Vehicle implementation for Tesla cars using the official Tesla vehicle-command api.
type TeslaCommand struct {
*embed
*vc.Provider
}
func init() {
if id := os.Getenv("TESLA_CLIENT_ID"); id != "" {
vc.OAuth2Config.ClientID = id
}
if secret := os.Getenv("TESLA_CLIENT_SECRET"); secret != "" {
vc.OAuth2Config.ClientSecret = secret
}
if vc.OAuth2Config.ClientID != "" {
registry.Add("tesla-command", NewTeslaCommandFromConfig)
}
}
// const privateKeyFile = "tesla-privatekey.pem"
// NewTeslaCommandFromConfig creates a new vehicle
func NewTeslaCommandFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
Tokens Tokens
VIN string
Timeout time.Duration
Cache time.Duration
}{
Timeout: request.Timeout,
Cache: interval,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
token, err := cc.Tokens.Token()
if err != nil {
return nil, err
}
log := util.NewLogger("tesla-command").Redact(
cc.Tokens.Access, cc.Tokens.Refresh,
vc.OAuth2Config.ClientID, vc.OAuth2Config.ClientSecret,
)
identity, err := vc.NewIdentity(log, token)
if err != nil {
return nil, err
}
api := vc.NewAPI(log, identity, cc.Timeout)
// validate base url
if _, err := api.Region(); err != nil {
return nil, err
}
vehicle, err := ensureVehicleEx(
cc.VIN, api.Vehicles,
func(v *vc.Vehicle) string {
return v.Vin
},
)
if err != nil {
return nil, err
}
v := &TeslaCommand{
embed: &cc.embed,
Provider: vc.NewProvider(api, vehicle.ID, cc.Cache),
}
if v.Title_ == "" {
v.Title_ = vehicle.DisplayName
}
/*
privKey, err := protocol.LoadPrivateKey(privateKeyFile)
if err != nil {
log.WARN.Println("private key not found, commands are disabled")
return v, nil
}
vv, err := identity.Account().GetVehicle(context.Background(), vehicle.Vin, privKey, cache.New(8))
if err != nil {
return nil, err
}
cs, err := vc.NewCommandSession(vv, cc.Timeout)
if err != nil {
return nil, err
}
res := &struct {
*TeslaCommand
*vc.CommandSession
}{
TeslaCommand: v,
CommandSession: cs,
}
*/
return v, nil
}

View file

@ -1,258 +0,0 @@
package vehicle
import (
"context"
"errors"
"slices"
"time"
"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/util/transport"
vc "github.com/evcc-io/evcc/vehicle/tesla-vehicle-command"
tesla "github.com/evcc-io/tesla-proxy-client"
"golang.org/x/oauth2"
)
// TeslaProxy is an api.Vehicle implementation for Tesla cars
type TeslaProxy struct {
*embed
vehicle *tesla.Vehicle
dataG func() (*tesla.VehicleData, error)
}
func init() {
registry.Add("tesla-proxy", NewTeslaProxyFromConfig)
}
// NewTeslaProxyFromConfig creates a new vehicle
func NewTeslaProxyFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
Tokens Tokens
URI string
VIN string
Cache time.Duration
}{
URI: "https://tesla.evcc.io/api/1",
Cache: interval,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
token, err := cc.Tokens.Token()
if err != nil {
return nil, err
}
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
v := &TeslaProxy{
embed: &cc.embed,
}
// authenticated http client with logging injected to the Tesla client
log := util.NewLogger("tesla-proxy").Redact(
cc.Tokens.Access, cc.Tokens.Refresh,
vc.OAuth2Config.ClientID, vc.OAuth2Config.ClientSecret,
)
identity, err := vc.NewIdentity(log, token)
if err != nil {
return nil, err
}
hc := request.NewClient(log)
hc.Transport = &transport.Decorator{
Decorator: transport.DecorateHeaders(map[string]string{
"X-Auth-Token": sponsor.Token,
}),
Base: &oauth2.Transport{
Source: identity,
Base: hc.Transport,
},
}
options := []tesla.ClientOption{
tesla.WithClient(hc),
tesla.WithBaseURL(cc.URI),
}
client, err := tesla.NewClient(context.Background(), options...)
if err != nil {
return nil, err
}
v.vehicle, err = ensureVehicleEx(
cc.VIN, client.Vehicles,
func(v *tesla.Vehicle) string {
return v.Vin
},
)
if err != nil {
return nil, err
}
if v.Title_ == "" {
v.Title_ = v.vehicle.DisplayName
}
v.dataG = provider.Cached(func() (*tesla.VehicleData, error) {
res, err := v.vehicle.Data()
return res, v.apiError(err)
}, cc.Cache)
return v, nil
}
// apiError converts HTTP 408 error to ErrTimeout
func (v *TeslaProxy) apiError(err error) error {
if err != nil && err.Error() == "408 Request Timeout" {
err = api.ErrAsleep
}
return err
}
// Soc implements the api.Vehicle interface
func (v *TeslaProxy) Soc() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return float64(res.Response.ChargeState.UsableBatteryLevel), nil
}
var _ api.ChargeState = (*TeslaProxy)(nil)
// Status implements the api.ChargeState interface
func (v *TeslaProxy) Status() (api.ChargeStatus, error) {
status := api.StatusA // disconnected
res, err := v.dataG()
if err != nil {
return status, err
}
switch res.Response.ChargeState.ChargingState {
case "Stopped", "NoPower", "Complete":
status = api.StatusB
case "Charging":
status = api.StatusC
}
return status, nil
}
var _ api.ChargeRater = (*TeslaProxy)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (v *TeslaProxy) ChargedEnergy() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return res.Response.ChargeState.ChargeEnergyAdded, nil
}
var _ api.VehicleRange = (*TeslaProxy)(nil)
// Range implements the api.VehicleRange interface
func (v *TeslaProxy) Range() (int64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
// miles to km
return int64(kmPerMile * res.Response.ChargeState.BatteryRange), nil
}
var _ api.VehicleOdometer = (*TeslaProxy)(nil)
// Odometer implements the api.VehicleOdometer interface
func (v *TeslaProxy) Odometer() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
// miles to km
return kmPerMile * res.Response.VehicleState.Odometer, nil
}
var _ api.VehicleFinishTimer = (*TeslaProxy)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *TeslaProxy) FinishTime() (time.Time, error) {
res, err := v.dataG()
if err != nil {
return time.Time{}, err
}
return time.Now().Add(time.Duration(res.Response.ChargeState.MinutesToFullCharge) * time.Minute), nil
}
// TODO api.Climater implementation has been removed as it drains battery. Re-check at a later time.
var _ api.VehiclePosition = (*TeslaProxy)(nil)
// Position implements the api.VehiclePosition interface
func (v *TeslaProxy) Position() (float64, float64, error) {
res, err := v.dataG()
if err != nil {
return 0, 0, err
}
return res.Response.DriveState.Latitude, res.Response.DriveState.Longitude, nil
}
var _ api.SocLimiter = (*TeslaProxy)(nil)
// TargetSoc implements the api.SocLimiter interface
func (v *TeslaProxy) TargetSoc() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return float64(res.Response.ChargeState.ChargeLimitSoc), nil
}
var _ api.CurrentController = (*TeslaProxy)(nil)
// MaxCurrent implements the api.CurrentLimiter interface
func (v *TeslaProxy) MaxCurrent(current int64) error {
return v.apiError(v.vehicle.SetChargingAmps(int(current)))
}
var _ api.Resurrector = (*TeslaProxy)(nil)
// WakeUp implements the api.Resurrector interface
func (v *TeslaProxy) WakeUp() error {
_, err := v.vehicle.Wakeup()
return v.apiError(err)
}
var _ api.VehicleChargeController = (*TeslaProxy)(nil)
// StartCharge implements the api.VehicleChargeController interface
func (v *TeslaProxy) StartCharge() error {
err := v.apiError(v.vehicle.StartCharging())
if err != nil && slices.Contains([]string{"complete", "is_charging"}, err.Error()) {
return nil
}
return err
}
// StopCharge implements the api.VehicleChargeController interface
func (v *TeslaProxy) StopCharge() error {
err := v.apiError(v.vehicle.StopCharging())
// ignore sleeping vehicle
if errors.Is(err, api.ErrAsleep) {
err = nil
}
return err
}

View file

@ -1,105 +0,0 @@
package vc
import (
"fmt"
"time"
"github.com/bogosj/tesla"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
)
const (
FleetAudienceEU = "https://fleet-api.prd.eu.vn.cloud.tesla.com"
)
type API struct {
*request.Helper
identity *Identity
base string
}
func NewAPI(log *util.Logger, identity *Identity, timeout time.Duration) *API {
client := request.NewHelper(log)
client.Client.Timeout = timeout
client.Transport = &oauth2.Transport{
Source: identity,
Base: client.Transport,
}
return &API{
Helper: client,
identity: identity,
base: FleetAudienceEU,
}
}
func (v *API) Region() (Region, error) {
var res RegionResponse
err := v.GetJSON(fmt.Sprintf("%s/api/1/users/region", FleetAudienceEU), &res)
if err == nil {
v.base = res.Response.FleetApiBaseUrl
}
return res.Response, err
}
func (v *API) Vehicles() ([]*Vehicle, error) {
// ctx, cancel := context.WithTimeout(context.Background(), v.Timeout)
// defer cancel()
// b, err := v.identity.Account().Get(ctx, "api/1/vehicles")
// if err != nil {
// return nil, err
// }
// var res tesla.VehiclesResponse
// if err := json.Unmarshal(b, &res); err != nil {
// return nil, err
// }
var res tesla.VehiclesResponse
err := v.GetJSON(fmt.Sprintf("%s/api/1/vehicles", v.base), &res)
return res.Response, err
}
func (v *API) VehicleData(id int64) (*VehicleData, error) {
// ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
// defer cancel()
// b, err := v.identity.Account().Get(ctx, fmt.Sprintf("api/1/vehicles/%d/vehicle_data", id))
// if err != nil {
// return nil, err
// }
// var res tesla.VehicleData
// if err := json.Unmarshal(b, &res); err != nil {
// return nil, err
// }
var res tesla.VehicleData
err := v.GetJSON(fmt.Sprintf("%s/api/1/vehicles/%d/vehicle_data", v.base, id), &res)
return &res, err
}
func (v *API) WakeUp(id int64) (*VehicleData, error) {
// ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
// defer cancel()
// b, err := v.identity.Account().Get(ctx, fmt.Sprintf("api/1/vehicles/%d/vehicle_data", id))
// if err != nil {
// return nil, err
// }
// var res tesla.VehicleData
// if err := json.Unmarshal(b, &res); err != nil {
// return nil, err
// }
var res tesla.VehicleData
err := v.GetJSON(fmt.Sprintf("%s/api/1/vehicles/%d/wake_up", v.base, id), &res)
return &res, err
}

View file

@ -2,38 +2,55 @@ package vehicle
import (
"context"
"errors"
"slices"
"os"
"time"
"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/util/transport"
"github.com/evcc-io/evcc/vehicle/tesla"
teslaclient "github.com/evcc-io/tesla-proxy-client"
"golang.org/x/oauth2"
)
// Tesla is an api.Vehicle implementation for Tesla cars
// Tesla is an api.Vehicle implementation for Tesla cars using the official Tesla vehicle-command api.
type Tesla struct {
*embed
vehicle *tesla.Vehicle
dataG func() (*tesla.VehicleData, error)
*tesla.Provider
}
type teslaController struct {
*embed
*tesla.Provider
*tesla.Controller
}
func init() {
registry.Add("tesla", NewTeslaFromConfig)
if id := os.Getenv("TESLA_CLIENT_ID"); id != "" {
tesla.OAuth2Config.ClientID = id
}
if secret := os.Getenv("TESLA_CLIENT_SECRET"); secret != "" {
tesla.OAuth2Config.ClientSecret = secret
}
if tesla.OAuth2Config.ClientID != "" {
registry.Add("tesla", NewTeslaFromConfig)
}
}
// NewTeslaFromConfig creates a new vehicle
func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
Tokens Tokens
VIN string
Cache time.Duration
embed `mapstructure:",squash"`
Tokens Tokens
VIN string
Control bool
Timeout time.Duration
Cache time.Duration
}{
Cache: interval,
Timeout: request.Timeout,
Cache: interval,
}
if err := util.DecodeOther(other, &cc); err != nil {
@ -45,183 +62,84 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
return nil, err
}
v := &Tesla{
embed: &cc.embed,
}
log := util.NewLogger("tesla").Redact(
cc.Tokens.Access, cc.Tokens.Refresh,
tesla.OAuth2Config.ClientID, tesla.OAuth2Config.ClientSecret,
)
// authenticated http client with logging injected to the Tesla client
log := util.NewLogger("tesla").Redact(cc.Tokens.Access, cc.Tokens.Refresh)
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
client, err := tesla.NewClient(ctx, tesla.WithToken(token))
identity, err := tesla.NewIdentity(log, token)
if err != nil {
return nil, err
}
v.vehicle, err = ensureVehicleEx(
cc.VIN, client.Vehicles,
hc := request.NewClient(log)
hc.Transport = &oauth2.Transport{
Source: identity,
Base: hc.Transport,
}
tc, err := teslaclient.NewClient(context.Background(), teslaclient.WithClient(hc))
if err != nil {
return nil, err
}
// validate base url
region, err := tc.UserRegion()
if err != nil {
return nil, err
}
tc.SetBaseUrl(region.FleetApiBaseUrl)
vehicle, err := ensureVehicleEx(
cc.VIN, tc.Vehicles,
func(v *tesla.Vehicle) string {
return v.Vin
},
)
if err != nil {
return nil, err
}
if v.Title_ == "" {
v.Title_ = v.vehicle.DisplayName
v := &Tesla{
embed: &cc.embed,
Provider: tesla.NewProvider(vehicle, cc.Cache),
}
v.dataG = provider.Cached(func() (*tesla.VehicleData, error) {
res, err := v.vehicle.Data()
return res, v.apiError(err)
}, cc.Cache)
if v.Title_ == "" {
v.Title_ = vehicle.DisplayName
}
if cc.Control {
if !sponsor.IsAuthorized() {
return nil, api.ErrSponsorRequired
}
// proxy client
pc := request.NewClient(log)
pc.Transport = &transport.Decorator{
Decorator: transport.DecorateHeaders(map[string]string{
"X-Auth-Token": sponsor.Token,
}),
Base: hc.Transport,
}
tc, err := teslaclient.NewClient(context.Background(), teslaclient.WithClient(pc))
if err != nil {
return nil, err
}
tc.SetBaseUrl(tesla.ProxyBaseUrl)
vehicle, err := tc.Vehicle(vehicle.Vin)
if err != nil {
return nil, err
}
return &teslaController{
embed: v.embed,
Provider: v.Provider,
Controller: tesla.NewController(vehicle),
}, nil
}
return v, nil
}
// apiError converts HTTP 408 error to ErrTimeout
func (v *Tesla) apiError(err error) error {
if err != nil && err.Error() == "408 Request Timeout" {
err = api.ErrAsleep
}
return err
}
// Soc implements the api.Vehicle interface
func (v *Tesla) Soc() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return float64(res.Response.ChargeState.UsableBatteryLevel), nil
}
var _ api.ChargeState = (*Tesla)(nil)
// Status implements the api.ChargeState interface
func (v *Tesla) Status() (api.ChargeStatus, error) {
status := api.StatusA // disconnected
res, err := v.dataG()
if err != nil {
return status, err
}
switch res.Response.ChargeState.ChargingState {
case "Stopped", "NoPower", "Complete":
status = api.StatusB
case "Charging":
status = api.StatusC
}
return status, nil
}
var _ api.ChargeRater = (*Tesla)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (v *Tesla) ChargedEnergy() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return res.Response.ChargeState.ChargeEnergyAdded, nil
}
var _ api.VehicleRange = (*Tesla)(nil)
// Range implements the api.VehicleRange interface
func (v *Tesla) Range() (int64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
// miles to km
return int64(kmPerMile * res.Response.ChargeState.BatteryRange), nil
}
var _ api.VehicleOdometer = (*Tesla)(nil)
// Odometer implements the api.VehicleOdometer interface
func (v *Tesla) Odometer() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
// miles to km
return kmPerMile * res.Response.VehicleState.Odometer, nil
}
var _ api.VehicleFinishTimer = (*Tesla)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *Tesla) FinishTime() (time.Time, error) {
res, err := v.dataG()
if err != nil {
return time.Time{}, err
}
return time.Now().Add(time.Duration(res.Response.ChargeState.MinutesToFullCharge) * time.Minute), nil
}
// TODO api.Climater implementation has been removed as it drains battery. Re-check at a later time.
var _ api.VehiclePosition = (*Tesla)(nil)
// Position implements the api.VehiclePosition interface
func (v *Tesla) Position() (float64, float64, error) {
res, err := v.dataG()
if err != nil {
return 0, 0, err
}
return res.Response.DriveState.Latitude, res.Response.DriveState.Longitude, nil
}
var _ api.SocLimiter = (*Tesla)(nil)
// TargetSoc implements the api.SocLimiter interface
func (v *Tesla) TargetSoc() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return float64(res.Response.ChargeState.ChargeLimitSoc), nil
}
var _ api.CurrentController = (*Tesla)(nil)
// MaxCurrent implements the api.CurrentLimiter interface
func (v *Tesla) MaxCurrent(current int64) error {
return v.apiError(v.vehicle.SetChargingAmps(int(current)))
}
var _ api.Resurrector = (*Tesla)(nil)
// WakeUp implements the api.Resurrector interface
func (v *Tesla) WakeUp() error {
_, err := v.vehicle.Wakeup()
return v.apiError(err)
}
var _ api.VehicleChargeController = (*Tesla)(nil)
// StartCharge implements the api.VehicleChargeController interface
func (v *Tesla) StartCharge() error {
err := v.apiError(v.vehicle.StartCharging())
if err != nil && slices.Contains([]string{"complete", "is_charging"}, err.Error()) {
return nil
}
return err
}
// StopCharge implements the api.VehicleChargeController interface
func (v *Tesla) StopCharge() error {
err := v.apiError(v.vehicle.StopCharging())
// ignore sleeping vehicle
if errors.Is(err, api.ErrAsleep) {
err = nil
}
return err
}

View file

@ -0,0 +1,53 @@
package tesla
import (
"errors"
"slices"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/tesla-proxy-client"
)
const ProxyBaseUrl = "https://tesla.evcc.io"
type Controller struct {
vehicle *tesla.Vehicle
}
// NewController creates a vehicle current and charge controller
func NewController(vehicle *tesla.Vehicle) *Controller {
impl := &Controller{
vehicle: vehicle,
}
return impl
}
var _ api.CurrentController = (*Controller)(nil)
// StartCharge implements the api.VehicleChargeController interface
func (v *Controller) MaxCurrent(current int64) error {
return apiError(v.vehicle.SetChargingAmps(int(current)))
}
var _ api.VehicleChargeController = (*Controller)(nil)
// StartCharge implements the api.VehicleChargeController interface
func (v *Controller) StartCharge() error {
err := apiError(v.vehicle.StartCharging())
if err != nil && slices.Contains([]string{"complete", "is_charging"}, err.Error()) {
return nil
}
return err
}
// StopCharge implements the api.VehicleChargeController interface
func (v *Controller) StopCharge() error {
err := apiError(v.vehicle.StopCharging())
// ignore sleeping vehicle
if errors.Is(err, api.ErrAsleep) {
err = nil
}
return err
}

View file

@ -1,4 +1,4 @@
package vc
package tesla
import (
"errors"
@ -15,8 +15,7 @@ var (
)
func getInstance(subject string) *Identity {
v, _ := identities[subject]
return v
return identities[subject]
}
func addInstance(subject string, identity *Identity) {

View file

@ -1,4 +1,4 @@
package vc
package tesla
import (
"context"
@ -27,8 +27,6 @@ var OAuth2Config = &oauth2.Config{
Scopes: []string{"openid", "email", "offline_access"},
}
// const userAgent = "evcc/evcc-io"
var TESLA_CLIENT_ID, TESLA_CLIENT_SECRET string
func init() {
@ -45,10 +43,9 @@ type Identity struct {
mu sync.Mutex
log *util.Logger
subject string
// acct *account.Account
}
func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
func NewIdentity(log *util.Logger, token *oauth2.Token) (oauth2.TokenSource, error) {
// serialise instance handling
mu.Lock()
defer mu.Unlock()
@ -71,7 +68,6 @@ func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
v := &Identity{
log: log,
subject: claims.Subject,
// acct: acct,
}
// database token
@ -92,11 +88,6 @@ func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
return nil, errors.New("token expired")
}
// acct, err := account.New(token.AccessToken, userAgent)
// if err != nil {
// return nil, err
// }
v.TokenSource = oauth.RefreshTokenSource(token, v)
// add instance
@ -125,23 +116,3 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
return token, err
}
// func (v *Identity) Account() *account.Account {
// token, err := v.Token()
// if err != nil {
// v.log.ERROR.Println(err)
// return v.acct
// }
// if token.AccessToken != v.token.AccessToken {
// acct, err := account.New(token.AccessToken, userAgent)
// if err != nil {
// v.log.ERROR.Println(err)
// return v.acct
// }
// v.acct = acct
// }
// return v.acct
// }

View file

@ -1,25 +1,26 @@
package vc
package tesla
import (
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/provider"
"github.com/evcc-io/tesla-proxy-client"
)
type Provider struct {
dataG func() (*VehicleData, error)
wakeup func() (*VehicleData, error)
dataG func() (*tesla.VehicleData, error)
wakeup func() (*tesla.Vehicle, error)
}
func NewProvider(api *API, vid int64, cache time.Duration) *Provider {
func NewProvider(vehicle *tesla.Vehicle, cache time.Duration) *Provider {
impl := &Provider{
dataG: provider.Cached(func() (*VehicleData, error) {
res, err := api.VehicleData(vid)
dataG: provider.Cached(func() (*tesla.VehicleData, error) {
res, err := vehicle.Data()
return res, apiError(err)
}, cache),
wakeup: func() (*VehicleData, error) {
res, err := api.WakeUp(vid)
wakeup: func() (*tesla.Vehicle, error) {
res, err := vehicle.Wakeup()
return res, apiError(err)
},
}

View file

@ -1,4 +1,4 @@
package vc
package tesla
import (
"context"

View file

@ -1,6 +1,8 @@
package vc
package tesla
import "github.com/bogosj/tesla"
import (
tesla "github.com/evcc-io/tesla-proxy-client"
)
type (
Vehicle = tesla.Vehicle

View file

@ -7,8 +7,6 @@ import (
"golang.org/x/oauth2"
)
const kmPerMile = 1.609344
// ClientCredentials contains OAuth2 client id and secret
type ClientCredentials struct {
ID, Secret string