Tesla Command: implement token storage (#12021)
This commit is contained in:
parent
7e37fb4a8f
commit
5627f57ebb
6 changed files with 203 additions and 46 deletions
62
cmd/settings-get.go
Normal file
62
cmd/settings-get.go
Normal file
|
|
@ -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
cmd/settings.go
Normal file
15
cmd/settings.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
|
@ -9,8 +8,6 @@ import (
|
|||
"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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) ||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -34,45 +42,99 @@ func init() {
|
|||
|
||||
type Identity struct {
|
||||
oauth2.TokenSource
|
||||
mu sync.Mutex
|
||||
log *util.Logger
|
||||
token *oauth2.Token
|
||||
acct *account.Account
|
||||
subject string
|
||||
// acct *account.Account
|
||||
}
|
||||
|
||||
func NewIdentity(log *util.Logger, ts oauth2.TokenSource) (*Identity, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// reuse identity instance
|
||||
if instance := getInstance(claims.Subject); instance != nil {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
if !token.Valid() {
|
||||
token.Expiry = claims.ExpiresAt.Time
|
||||
}
|
||||
|
||||
v := &Identity{
|
||||
log: log,
|
||||
subject: claims.Subject,
|
||||
// acct: 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)
|
||||
}
|
||||
|
||||
if !token.Valid() {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
acct, err := account.New(token.AccessToken, userAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
err = settings.SetJson(v.settingsKey(), token)
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
return &Identity{
|
||||
TokenSource: ts,
|
||||
token: token,
|
||||
acct: acct,
|
||||
}, nil
|
||||
}
|
||||
// func (v *Identity) Account() *account.Account {
|
||||
// token, err := v.Token()
|
||||
// if err != nil {
|
||||
// v.log.ERROR.Println(err)
|
||||
// return v.acct
|
||||
// }
|
||||
|
||||
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
|
||||
// }
|
||||
|
||||
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
|
||||
// }
|
||||
|
||||
v.acct = acct
|
||||
}
|
||||
|
||||
return v.acct
|
||||
}
|
||||
// return v.acct
|
||||
// }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue