Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tesla using official vehicle command library #10802

Merged
merged 22 commits into from
Jan 24, 2024
Prev Previous commit
Next Next commit
wip
  • Loading branch information
andig committed Dec 22, 2023
commit bfd4d29094db990da873af9f458bb34040408aa7
54 changes: 20 additions & 34 deletions vehicle/tesla-vehicle-command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,36 @@ package vehicle

import (
"context"
"os"
"strings"
"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"
vc "github.com/evcc-io/evcc/vehicle/tesla-vehicle-command"
"github.com/teslamotors/vehicle-command/pkg/account"
"golang.org/x/oauth2"
)

// TeslaVC is an api.Vehicle implementation for Tesla cars.
// It uses the official Tesla vehicle-command api.
type TeslaVC struct {
*embed
dataG func() (*tesla.VehicleData, error)
dataG func() (*vc.VehicleData, error)
}

func init() {
registry.Add("tesla-vehicle-command", NewTeslaVCFromConfig)
}

// https://auth.tesla.com/oauth2/v3/.well-known/openid-configuration

// OAuth2Config is the OAuth2 configuration for authenticating with the Tesla API.
var OAuth2Config = &oauth2.Config{
ClientID: os.Getenv("TESLA_CLIENT_ID"),
RedirectURL: "https://auth.tesla.com/void/callback",
Endpoint: oauth2.Endpoint{
AuthURL: "https://auth.tesla.com/en_us/oauth2/v3/authorize",
TokenURL: "https://auth.tesla.com/oauth2/v3/token",
AuthStyle: oauth2.AuthStyleInParams,
},
Scopes: []string{"openid", "email", "offline_access"},
}

// NewTeslaVCFromConfig creates a new vehicle
func NewTeslaVCFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
Tokens Tokens
VIN string
Cache time.Duration
embed `mapstructure:",squash"`
ClientID string
Tokens Tokens
VIN string
Cache time.Duration
}{
Cache: interval,
}
Expand All @@ -59,25 +44,26 @@ func NewTeslaVCFromConfig(other map[string]interface{}) (api.Vehicle, error) {
return nil, err
}

client := request.NewClient(util.NewLogger("tesla-vc"))
if cc.ClientID != "" {
vc.OAuth2Config.ClientID = cc.ClientID
}

log := util.NewLogger("tesla-vc")
client := request.NewClient(log)

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

token, err := ts.Token()
if err != nil {
return nil, err
}

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

api := vc.NewAPI(account)
api := vc.NewAPI(identity)

v := &TeslaVC{
embed: &cc.embed,
Expand All @@ -101,7 +87,7 @@ func NewTeslaVCFromConfig(other map[string]interface{}) (api.Vehicle, error) {

vehicle, err := ensureVehicleEx(
cc.VIN, api.Vehicles,
func(v *tesla.Vehicle) string {
func(v *vc.Vehicle) string {
return v.Vin
},
)
Expand All @@ -113,7 +99,7 @@ func NewTeslaVCFromConfig(other map[string]interface{}) (api.Vehicle, error) {
v.Title_ = vehicle.DisplayName
}

v.dataG = provider.Cached(func() (*tesla.VehicleData, error) {
v.dataG = provider.Cached(func() (*vc.VehicleData, error) {
res, err := api.VehicleData(vehicle.ID)
return res, v.apiError(err)
}, cc.Cache)
Expand All @@ -123,7 +109,7 @@ func NewTeslaVCFromConfig(other map[string]interface{}) (api.Vehicle, error) {

// apiError converts HTTP 408 error to ErrTimeout
func (v *TeslaVC) apiError(err error) error {
if err != nil && err.Error() == "408 Request Timeout" {
if err != nil && strings.HasSuffix(err.Error(), "408 Request Timeout") {
err = api.ErrAsleep
}
return err
Expand Down
15 changes: 7 additions & 8 deletions vehicle/tesla-vehicle-command/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,23 @@ import (

"github.com/bogosj/tesla"
"github.com/evcc-io/evcc/util/request"
"github.com/teslamotors/vehicle-command/pkg/account"
)

type API struct {
user *account.Account
identity *Identity
}

func NewAPI(user *account.Account) *API {
func NewAPI(identity *Identity) *API {
return &API{
user: user,
identity: identity,
}
}

func (v *API) Vehicles() ([]*tesla.Vehicle, error) {
func (v *API) Vehicles() ([]*Vehicle, error) {
ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
defer cancel()

b, err := v.user.Get(ctx, "api/1/vehicles")
b, err := v.identity.Account().Get(ctx, "api/1/vehicles")
if err != nil {
return nil, err
}
Expand All @@ -37,11 +36,11 @@ func (v *API) Vehicles() ([]*tesla.Vehicle, error) {
return res.Response, nil
}

func (v *API) VehicleData(id int64) (*tesla.VehicleData, error) {
func (v *API) VehicleData(id int64) (*VehicleData, error) {
ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
defer cancel()

b, err := v.user.Get(ctx, fmt.Sprintf("api/1/vehicles/%d/vehicle_data", id))
b, err := v.identity.Account().Get(ctx, fmt.Sprintf("api/1/vehicles/%d/vehicle_data", id))
if err != nil {
return nil, err
}
Expand Down
68 changes: 68 additions & 0 deletions vehicle/tesla-vehicle-command/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package vc

import (
"os"

"github.com/evcc-io/evcc/util"
"github.com/teslamotors/vehicle-command/pkg/account"
"golang.org/x/oauth2"
)

// https://auth.tesla.com/oauth2/v3/.well-known/openid-configuration

// OAuth2Config is the OAuth2 configuration for authenticating with the Tesla API.
var OAuth2Config = &oauth2.Config{
ClientID: os.Getenv("TESLA_CLIENT_ID"),
RedirectURL: "https://auth.tesla.com/void/callback",
Endpoint: oauth2.Endpoint{
AuthURL: "https://auth.tesla.com/en_us/oauth2/v3/authorize",
TokenURL: "https://auth.tesla.com/oauth2/v3/token",
AuthStyle: oauth2.AuthStyleInParams,
},
Scopes: []string{"openid", "email", "offline_access"},
}

type Identity struct {
log *util.Logger
ts oauth2.TokenSource
token *oauth2.Token
acct *account.Account
}

func NewIdentity(log *util.Logger, ts oauth2.TokenSource) (*Identity, error) {
token, err := ts.Token()
if err != nil {
return nil, err
}

acct, err := account.New(token.AccessToken)
if err != nil {
return nil, err
}

return &Identity{
ts: ts,
token: token,
acct: acct,
}, nil
}

func (v *Identity) Account() *account.Account {
token, err := v.ts.Token()
if err != nil {
v.log.ERROR.Println(err)
return v.acct
}

if token.AccessToken != v.token.AccessToken {
acct, err := account.New(token.AccessToken)
if err != nil {
v.log.ERROR.Println(err)
return v.acct
}

v.acct = acct
}

return v.acct
}
8 changes: 8 additions & 0 deletions vehicle/tesla-vehicle-command/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package vc

import "github.com/bogosj/tesla"

type (
Vehicle = tesla.Vehicle
VehicleData = tesla.VehicleData
)