diff --git a/vehicle/smart/hello/identity.go b/vehicle/smart/hello/identity.go index 33e7740a6..8c699c1b3 100644 --- a/vehicle/smart/hello/identity.go +++ b/vehicle/smart/hello/identity.go @@ -6,8 +6,10 @@ import ( "net/http" "net/url" "strings" + "sync" "time" + "github.com/evcc-io/evcc/server/db/settings" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" @@ -15,41 +17,84 @@ import ( "golang.org/x/oauth2" ) +// savedState holds the identity data persisted across evcc restarts. +type savedState struct { + Token oauth2.Token `json:"token"` + UserID string `json:"userId"` + DeviceID string `json:"deviceId"` +} + type Identity struct { *request.Helper oauth2.TokenSource + log *util.Logger user, password string userID, deviceID string + subject string + mu sync.Mutex } func NewIdentity(log *util.Logger, user, password string) (*Identity, error) { v := &Identity{ Helper: request.NewHelper(log), + log: log, user: user, password: password, - deviceID: lo.RandomString(16, lo.AlphanumericCharset), + subject: "smart-hello." + user, } - v.TokenSource = oauth2.ReuseTokenSource(nil, oauth.BootstrapTokenSource(v.refreshToken)) + var state savedState + if err := settings.Json(v.subject, &state); err != nil { + if !errors.Is(err, settings.ErrNotFound) { + v.log.WARN.Printf("load state: %v", err) + } + // no usable persisted state — generate a fresh device ID (sent in login headers) + state.DeviceID = lo.RandomString(16, lo.AlphanumericCharset) + } - _, err := v.Token() + // deviceID must be set before any login — it is sent in request headers. + v.deviceID = state.DeviceID + v.userID = state.UserID - return v, err + var token *oauth2.Token + if state.Token.Valid() { + token = &state.Token + } else { + var err error + token, err = v.refreshToken(nil) + if err != nil { + return nil, err + } + } + + v.TokenSource = oauth.RefreshTokenSource(token, v.refreshToken) + return v, nil } -func (v *Identity) refreshToken() (*oauth2.Token, error) { - token, err := v.login() +func (v *Identity) refreshToken(_ *oauth2.Token) (*oauth2.Token, error) { + v.mu.Lock() + defer v.mu.Unlock() + + tok, err := v.login() if err != nil { return nil, err } - appToken, userID, err := v.appToken(token) + appToken, userID, err := v.appToken(tok) if err != nil { return nil, err } v.userID = userID + if err := settings.SetJson(v.subject, savedState{ + Token: *appToken, + UserID: userID, + DeviceID: v.deviceID, + }); err != nil { + return nil, err + } + return appToken, nil }