diff --git a/cmd/token.go b/cmd/token.go index 18ca3adef..e794c3752 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -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: diff --git a/cmd/token_tesla.go b/cmd/token_tesla.go deleted file mode 100644 index aef926572..000000000 --- a/cmd/token_tesla.go +++ /dev/null @@ -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 -} diff --git a/go.mod b/go.mod index eb12609ec..47559dac4 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 841fcbb60..904335ca7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/meter/powerwall.go b/meter/powerwall.go index 9eb52076a..25c0c7176 100644 --- a/meter/powerwall.go +++ b/meter/powerwall.go @@ -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)) }) diff --git a/templates/definition/meter/tesla-powerwall.yaml b/templates/definition/meter/tesla-powerwall.yaml index 6c33b672b..3eff15688 100644 --- a/templates/definition/meter/tesla-powerwall.yaml +++ b/templates/definition/meter/tesla-powerwall.yaml @@ -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 }} diff --git a/templates/definition/vehicle/tesla-command.yaml b/templates/definition/vehicle/tesla-command.yaml deleted file mode 100644 index 9b7275c55..000000000 --- a/templates/definition/vehicle/tesla-command.yaml +++ /dev/null @@ -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"] diff --git a/templates/definition/vehicle/tesla-proxy.yaml b/templates/definition/vehicle/tesla-proxy.yaml deleted file mode 100644 index 822cfc830..000000000 --- a/templates/definition/vehicle/tesla-proxy.yaml +++ /dev/null @@ -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"] diff --git a/templates/definition/vehicle/tesla.yaml b/templates/definition/vehicle/tesla.yaml index 07c3713bd..c5669203f 100644 --- a/templates/definition/vehicle/tesla.yaml +++ b/templates/definition/vehicle/tesla.yaml @@ -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"] diff --git a/vehicle/tesla-command.go b/vehicle/tesla-command.go deleted file mode 100644 index b550bdb9a..000000000 --- a/vehicle/tesla-command.go +++ /dev/null @@ -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 -} diff --git a/vehicle/tesla-proxy.go b/vehicle/tesla-proxy.go deleted file mode 100644 index 3ab62860b..000000000 --- a/vehicle/tesla-proxy.go +++ /dev/null @@ -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 -} diff --git a/vehicle/tesla-vehicle-command/api.go b/vehicle/tesla-vehicle-command/api.go deleted file mode 100644 index 73f6b698b..000000000 --- a/vehicle/tesla-vehicle-command/api.go +++ /dev/null @@ -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 -} diff --git a/vehicle/tesla.go b/vehicle/tesla.go index 3089ab293..66a1967d3 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -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 -} diff --git a/vehicle/tesla/controller.go b/vehicle/tesla/controller.go new file mode 100644 index 000000000..bc7188ff3 --- /dev/null +++ b/vehicle/tesla/controller.go @@ -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 +} diff --git a/vehicle/tesla-vehicle-command/helper.go b/vehicle/tesla/helper.go similarity index 92% rename from vehicle/tesla-vehicle-command/helper.go rename to vehicle/tesla/helper.go index 9b1875af6..1cdc3aa6e 100644 --- a/vehicle/tesla-vehicle-command/helper.go +++ b/vehicle/tesla/helper.go @@ -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) { diff --git a/vehicle/tesla-vehicle-command/identity.go b/vehicle/tesla/identity.go similarity index 78% rename from vehicle/tesla-vehicle-command/identity.go rename to vehicle/tesla/identity.go index 8878e2623..bc3295b15 100644 --- a/vehicle/tesla-vehicle-command/identity.go +++ b/vehicle/tesla/identity.go @@ -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 -// } diff --git a/vehicle/tesla-vehicle-command/provider.go b/vehicle/tesla/provider.go similarity index 89% rename from vehicle/tesla-vehicle-command/provider.go rename to vehicle/tesla/provider.go index d3e5cd11f..651a7c5c6 100644 --- a/vehicle/tesla-vehicle-command/provider.go +++ b/vehicle/tesla/provider.go @@ -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) }, } diff --git a/vehicle/tesla-vehicle-command/session.go b/vehicle/tesla/session.go similarity index 99% rename from vehicle/tesla-vehicle-command/session.go rename to vehicle/tesla/session.go index 4554f4234..8d13de8cc 100644 --- a/vehicle/tesla-vehicle-command/session.go +++ b/vehicle/tesla/session.go @@ -1,4 +1,4 @@ -package vc +package tesla import ( "context" diff --git a/vehicle/tesla-vehicle-command/types.go b/vehicle/tesla/types.go similarity index 79% rename from vehicle/tesla-vehicle-command/types.go rename to vehicle/tesla/types.go index 226d58428..aeb703c35 100644 --- a/vehicle/tesla-vehicle-command/types.go +++ b/vehicle/tesla/types.go @@ -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 diff --git a/vehicle/types.go b/vehicle/types.go index aef067ae1..c30b58bf2 100644 --- a/vehicle/types.go +++ b/vehicle/types.go @@ -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