Skip to content

Commit

Permalink
Add Tesla using official vehicle command library (evcc-io#10802)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Jan 24, 2024
1 parent 19ea3fb commit fd33a0d
Show file tree
Hide file tree
Showing 13 changed files with 570 additions and 3 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ endif
VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
BUILD_DATE := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
BUILD_TAGS := -tags=release
LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -s -w
TESLA_CLIENT_ID := ${TESLA_CLIENT_ID}
LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -X github.com/evcc-io/evcc/vehicle/tesla-vehicle-command.OAuth2Config.ClientID=$(TESLA_CLIENT_ID) -s -w
BUILD_ARGS := -trimpath -ldflags='$(LD_FLAGS)'

# docker
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
github.com/teslamotors/vehicle-command v0.0.2
github.com/traefik/yaegi v0.15.1
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20231215091549-af16b1f597b9
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1r
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/JuulLabs-OSS/cbgo v0.0.1 h1:A5JdglvFot1J9qYR0POZ4qInttpsVPN9lqatjaPp2ro=
github.com/JuulLabs-OSS/cbgo v0.0.1/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
Expand Down Expand Up @@ -164,6 +166,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633 h1:ZrzoZQz1CF33SPHLkjRpnVuZwr9cO1lTEc4Js7SgBos=
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk=
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 h1:zga7zaRE8HCbWjcXMDlfvmQtH0/kMVLo7cQ48dy6kWg=
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1/go.mod h1:PumS+5d59wmAGsZo6IfRpVNaJUq+6xjC4Utt/k8GO6Q=
github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2DMosGsXzlMe2E9qXgXCVkRLCoRX+5amxI=
Expand Down Expand Up @@ -439,6 +443,8 @@ github.com/mergermarket/go-pkcs7 v0.0.0-20170926155232-153b18ea13c9/go.mod h1:GH
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
Expand Down Expand Up @@ -576,6 +582,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99 h1:JtoVdxWJ3tgyqtnPq3r4hJ9aULcIDDnPXBWxZsdmqWU=
github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko=
github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
Expand Down Expand Up @@ -671,6 +679,8 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/teivah/onecontext v1.3.0 h1:tbikMhAlo6VhAuEGCvhc8HlTnpX4xTNPTOseWuhO1J0=
github.com/teivah/onecontext v1.3.0/go.mod h1:hoW1nmdPVK/0jrvGtcx8sCKYs2PiS4z0zzfdeuEVyb0=
github.com/teslamotors/vehicle-command v0.0.2 h1:NOoI7d5OVq+Yeaom7iv31sbUMPDhJ/2QRKT4HKHfTa8=
github.com/teslamotors/vehicle-command v0.0.2/go.mod h1:SydThpjTNvhUXBy1VyiD/A9scQSYcp+E6iElHa36Gk0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4=
github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0=
Expand Down
56 changes: 56 additions & 0 deletions templates/definition/vehicle/tesla-command.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
template: tesla-command
products:
- brand: Tesla
description:
generic: Vehicle-Command API
requirements:
description:
de: |
Es wird ein `access` und ein `refresh` Token für die Kommunikation mit der Tesla API erstellt werden.
Die Tokens werden unter https://tesla.evcc.io erstellt.
en: |
You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
Tokens are generated using https://tesla.evcc.io.
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"]
119 changes: 119 additions & 0 deletions vehicle/tesla-command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package vehicle

import (
"context"
"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"
"golang.org/x/oauth2"
)

// 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: 10 * time.Second,
Cache: interval,
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

if err := cc.Tokens.Error(); err != nil {
return nil, err
}

log := util.NewLogger("tesla-command").Redact(vc.OAuth2Config.ClientID, vc.OAuth2Config.ClientSecret)

client := request.NewClient(log)

ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client)
ts := vc.OAuth2Config.TokenSource(ctx, &oauth2.Token{
AccessToken: cc.Tokens.Access,
RefreshToken: cc.Tokens.Refresh,
Expiry: time.Now(),
})

identity, err := vc.NewIdentity(log, ts)
if err != nil {
return nil, err
}

api := vc.NewAPI(log, identity, cc.Timeout)

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
}
74 changes: 74 additions & 0 deletions vehicle/tesla-vehicle-command/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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
}

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,
}
}

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", FleetAudienceEU), &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", FleetAudienceEU, id), &res)

return &res, err
}
18 changes: 18 additions & 0 deletions vehicle/tesla-vehicle-command/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package vc

import (
"errors"
"strings"

"github.com/evcc-io/evcc/api"
"github.com/teslamotors/vehicle-command/pkg/connector/inet"
)

// ApiError converts HTTP 408 error to ErrTimeout
func ApiError(err error) error {
if err != nil && (errors.Is(err, inet.ErrVehicleNotAwake) ||
strings.HasSuffix(err.Error(), "408 Request Timeout") || strings.HasSuffix(err.Error(), "408 (Request Timeout)")) {
err = api.ErrAsleep
}
return err
}
Loading

0 comments on commit fd33a0d

Please sign in to comment.