Skip to content

Commit

Permalink
Tesla Command: implement token storage (evcc-io#12021)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Feb 3, 2024
1 parent 7e37fb4 commit 5627f57
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 44 deletions.
62 changes: 62 additions & 0 deletions cmd/settings-get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cmd

import (
"fmt"
"os"
"os/signal"
"regexp"
"syscall"
"text/tabwriter"

"github.com/evcc-io/evcc/server/db/settings"
"github.com/spf13/cobra"
)

// settingsGetCmd represents the configure command
var settingsGetCmd = &cobra.Command{
Use: "get",
Short: "Get configuration settings",
Run: runSettingsGet,
Args: cobra.MaximumNArgs(1),
}

func init() {
settingsCmd.AddCommand(settingsGetCmd)
}

func runSettingsGet(cmd *cobra.Command, args []string) {
// load config
if err := loadConfigFile(&conf); err != nil {
log.FATAL.Fatal(err)
}

// setup environment
if err := configureEnvironment(cmd, conf); err != nil {
log.FATAL.Fatal(err)
}

var re *regexp.Regexp
if len(args) > 0 {
re = regexp.MustCompile(args[0])
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
for _, s := range settings.All() {
if re != nil && !re.MatchString(s.Key) {
continue
}

fmt.Fprintf(w, "%s:\t%s\n", s.Key, s.Value)
}
w.Flush()

// catch signals
go func() {
signalC := make(chan os.Signal, 1)
signal.Notify(signalC, os.Interrupt, syscall.SIGTERM)

<-signalC // wait for signal

os.Exit(1)
}()
}
15 changes: 15 additions & 0 deletions cmd/settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cmd

import (
"github.com/spf13/cobra"
)

// settingsCmd represents the configure command
var settingsCmd = &cobra.Command{
Use: "settings",
Short: "Manage configuration settings",
}

func init() {
rootCmd.AddCommand(settingsCmd)
}
13 changes: 13 additions & 0 deletions server/db/settings/setting.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package settings

import (
"cmp"
"encoding/json"
"errors"
"slices"
Expand Down Expand Up @@ -43,6 +44,18 @@ func Persist() error {
return db.Instance.Save(settings).Error
}

func All() []setting {
mu.RLock()
defer mu.RUnlock()

res := slices.Clone(settings)
slices.SortFunc(res, func(i, j setting) int {
return cmp.Compare(i.Key, j.Key)
})

return res
}

func SetString(key string, val string) {
mu.Lock()
defer mu.Unlock()
Expand Down
16 changes: 1 addition & 15 deletions vehicle/tesla-command.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
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"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
)

// TeslaCommand is an api.Vehicle implementation for Tesla cars using the official Tesla vehicle-command api.
Expand Down Expand Up @@ -55,23 +52,12 @@ func NewTeslaCommandFromConfig(other map[string]interface{}) (api.Vehicle, error
return nil, err
}

if t := cc.Tokens.Access; t != "" {
var claims jwt.RegisteredClaims
if _, _, err := jwt.NewParser().ParseUnverified(t, &claims); err != nil {
return nil, err
}
token.Expiry = claims.ExpiresAt.Time
}

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

ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
ts := vc.OAuth2Config.TokenSource(ctx, token)

identity, err := vc.NewIdentity(log, ts)
identity, err := vc.NewIdentity(log, token)
if err != nil {
return nil, err
}
Expand Down
19 changes: 19 additions & 0 deletions vehicle/tesla-vehicle-command/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,30 @@ package vc
import (
"errors"
"strings"
"sync"

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

var (
mu sync.Mutex
identities = make(map[string]*Identity)
)

func getInstance(subject string) *Identity {
mu.Lock()
defer mu.Unlock()
v, _ := identities[subject]
return v
}

func addInstance(subject string, identity *Identity) {
mu.Lock()
defer mu.Unlock()
identities[subject] = identity
}

// apiError converts HTTP 408 error to ErrTimeout
func apiError(err error) error {
if err != nil && (errors.Is(err, inet.ErrVehicleNotAwake) ||
Expand Down
120 changes: 91 additions & 29 deletions vehicle/tesla-vehicle-command/identity.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package vc

import (
"context"
"errors"
"fmt"
"sync"

"github.com/evcc-io/evcc/server/db/settings"
"github.com/evcc-io/evcc/util"
"github.com/teslamotors/vehicle-command/pkg/account"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/oauth2"
)

Expand All @@ -19,7 +27,7 @@ var OAuth2Config = &oauth2.Config{
Scopes: []string{"openid", "email", "offline_access"},
}

const userAgent = "evcc/evcc-io"
// const userAgent = "evcc/evcc-io"

var TESLA_CLIENT_ID, TESLA_CLIENT_SECRET string

Expand All @@ -34,45 +42,99 @@ func init() {

type Identity struct {
oauth2.TokenSource
log *util.Logger
token *oauth2.Token
acct *account.Account
mu sync.Mutex
log *util.Logger
subject string
// acct *account.Account
}

func NewIdentity(log *util.Logger, ts oauth2.TokenSource) (*Identity, error) {
token, err := ts.Token()
if err != nil {
func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
// serialise instance handling
mu.Lock()
defer mu.Unlock()

// determine tesla identity
var claims jwt.RegisteredClaims
if _, _, err := jwt.NewParser().ParseUnverified(token.AccessToken, &claims); err != nil {
return nil, err
}

acct, err := account.New(token.AccessToken, userAgent)
if err != nil {
return nil, err
// reuse identity instance
if instance := getInstance(claims.Subject); instance != nil {
return instance, nil
}

return &Identity{
TokenSource: ts,
token: token,
acct: acct,
}, nil
}
if !token.Valid() {
token.Expiry = claims.ExpiresAt.Time
}

func (v *Identity) Account() *account.Account {
token, err := v.Token()
if err != nil {
v.log.ERROR.Println(err)
return v.acct
v := &Identity{
log: log,
subject: claims.Subject,
// acct: 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
// database token
if !token.Valid() {
if err := settings.Json(v.settingsKey(), &token); err != nil {
return nil, fmt.Errorf("missing token setting for %s: %w", claims.Subject, err)
}

v.acct = acct
if !token.Valid() {
return nil, errors.New("token expired")
}
}

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

v.TokenSource = oauth.RefreshTokenSource(token, v)

// add instance
addInstance(claims.Subject, v)

return v, nil
}

func (v *Identity) settingsKey() string {
return fmt.Sprintf("tesla-command.%s", v.subject)
}

func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
v.mu.Lock()
defer v.mu.Unlock()

ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(v.log))
ts := OAuth2Config.TokenSource(ctx, token)

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

err = settings.SetJson(v.settingsKey(), token)

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

0 comments on commit 5627f57

Please sign in to comment.