diff --git a/internal/charger/easee.go b/internal/charger/easee.go index cad3b6e24..728fdb61a 100644 --- a/internal/charger/easee.go +++ b/internal/charger/easee.go @@ -11,12 +11,12 @@ import ( "github.com/andig/evcc/util" "github.com/andig/evcc/util/request" "github.com/andig/evcc/util/sponsor" + "golang.org/x/oauth2" ) // Easee charger implementation type Easee struct { *request.Helper - *easee.Identity charger string site, circuit int status easee.ChargerStatus @@ -60,11 +60,17 @@ func NewEasee(user, password, charger string, cache time.Duration) (*Easee, erro cache: cache, } - var err error - if c.Identity, err = easee.NewIdentity(log, user, password); err != nil { + ts, err := easee.TokenSource(log, user, password) + if err != nil { return c, err } + // replace transport client with authenticated client + c.Client.Transport = &oauth2.Transport{ + Source: ts, + Base: c.Client.Transport, + } + // find charger if charger == "" { chargers, err := c.chargers() @@ -95,22 +101,9 @@ func NewEasee(user, password, charger string, cache time.Duration) (*Easee, erro return c, err } -// request creates JSON HTTP request with valid access token -func (c *Easee) request(method, path string, body interface{}) (*http.Request, error) { - uri := fmt.Sprintf("%s%s", easee.API, path) - - req, err := request.New(method, uri, request.MarshalJSON(body), request.JSONEncoding) - if err == nil { - var token string - token, err = c.Token() - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - } - - return req, err -} - func (c *Easee) chargers() (res []easee.Charger, err error) { - req, err := c.request(http.MethodGet, "/chargers", nil) + uri := fmt.Sprintf("%s/chargers", easee.API) + req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding) if err != nil { return nil, err } @@ -120,7 +113,8 @@ func (c *Easee) chargers() (res []easee.Charger, err error) { } func (c *Easee) chargerDetails() (res easee.Site, err error) { - req, err := c.request(http.MethodGet, fmt.Sprintf("/chargers/%s/site", c.charger), nil) + uri := fmt.Sprintf("%s/chargers/%s/site", easee.API, c.charger) + req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding) if err != nil { return res, err } @@ -134,7 +128,8 @@ func (c *Easee) state() (easee.ChargerStatus, error) { return c.status, nil } - req, err := c.request(http.MethodGet, fmt.Sprintf("/chargers/%s/state", c.charger), nil) + uri := fmt.Sprintf("%s/chargers/%s/state", easee.API, c.charger) + req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding) if err == nil { if err = c.DoJSON(req, &c.status); err == nil { c.updated = time.Now() @@ -185,7 +180,8 @@ func (c *Easee) Enable(enable bool) error { } var req *http.Request - if req, err = c.request(http.MethodPost, fmt.Sprintf("/chargers/%s/settings", c.charger), data); err == nil { + uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger) + if req, err = request.New(http.MethodGet, uri, request.MarshalJSON(data), request.JSONEncoding); err == nil { _, err = c.Do(req) c.updated = time.Time{} // clear cache } @@ -200,7 +196,8 @@ func (c *Easee) Enable(enable bool) error { } var req *http.Request - if req, err = c.request(http.MethodPost, fmt.Sprintf("/chargers/%s/commands/%s", c.charger, action), nil); err == nil { + uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action) + if req, err = request.New(http.MethodGet, uri, nil, request.JSONEncoding); err == nil { _, err = c.Do(req) c.updated = time.Time{} // clear cache } @@ -224,7 +221,8 @@ func (c *Easee) MaxCurrentMillis(current float64) error { DynamicCircuitCurrentP3: &cur, } - req, err := c.request(http.MethodPost, fmt.Sprintf("/sites/%d/circuits/%d/settings", c.site, c.circuit), data) + uri := fmt.Sprintf("%s/sites/%d/circuits/%d/settings", easee.API, c.site, c.circuit) + req, err := request.New(http.MethodGet, uri, request.MarshalJSON(data), request.JSONEncoding) if err == nil { _, err = c.Do(req) c.updated = time.Time{} // clear cache diff --git a/internal/charger/easee/identity.go b/internal/charger/easee/identity.go index d4ee13760..ed7218a2d 100644 --- a/internal/charger/easee/identity.go +++ b/internal/charger/easee/identity.go @@ -6,46 +6,41 @@ import ( "time" "github.com/andig/evcc/util" + "github.com/andig/evcc/util/oauth" "github.com/andig/evcc/util/request" + "golang.org/x/oauth2" ) // Token is the Easee Token type Token struct { - AccessToken string `json:"accessToken"` - ExpiresIn float32 `json:"expiresIn"` - TokenType string `json:"tokenType"` - RefreshToken string `json:"refreshToken"` - Valid time.Time // helper to store validity timestamp + AccessToken string `json:"accessToken"` + ExpiresIn float32 `json:"expiresIn"` + TokenType string `json:"tokenType"` + RefreshToken string `json:"refreshToken"` } -// charge mode definition -const ( - ModeOffline int = 0 - ModeDisconnected int = 1 - ModeAwaitingStart int = 2 - ModeCharging int = 3 - ModeCompleted int = 4 - ModeError int = 5 - ModeReadyToCharge int = 6 -) - -// Identity manages the /api/accounts/token and /api/accounts/refresh_token response -type Identity struct { - *request.Helper - token Token -} - -// registry manages credentials sharing between multiple chargers -var registry = make(map[string]*Identity) - -// NewIdentity creates an Easee identity -func NewIdentity(log *util.Logger, user, password string) (*Identity, error) { - // get from registry - if c, ok := registry[user]; ok { - return c, nil +func (t *Token) AsOAuth2Token() *oauth2.Token { + if t == nil { + return nil } - c := &Identity{ + return &oauth2.Token{ + AccessToken: t.AccessToken, + TokenType: t.TokenType, + RefreshToken: t.RefreshToken, + Expiry: time.Now().Add(time.Second * time.Duration(t.ExpiresIn)), + } +} + +// tokenSource is an oauth2.TokenSource +type tokenSource struct { + *request.Helper + oauth2.TokenSource +} + +// TokenSource creates an Easee token source +func TokenSource(log *util.Logger, user, password string) (oauth2.TokenSource, error) { + c := &tokenSource{ Helper: request.NewHelper(log), } @@ -57,51 +52,38 @@ func NewIdentity(log *util.Logger, user, password string) (*Identity, error) { Password: password, } - uri := fmt.Sprintf("%s%s", API, "/accounts/token") + uri := fmt.Sprintf("%s/%s", API, "accounts/token") req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) if err == nil { - err = c.DoJSON(req, &c.token) - c.token.Valid = time.Now().Add(time.Second * time.Duration(c.token.ExpiresIn)) + var token Token + if err = c.DoJSON(req, &token); err == nil { + c.TokenSource = oauth.RefreshTokenSource(token.AsOAuth2Token(), c) + } } - // add to registry - registry[user] = c - return c, err } -// Token returns the if necessary refreshed access token -func (c *Identity) Token() (string, error) { - var err error - if c.token.Valid.Before(time.Now()) { - err = c.refreshToken() - } - - return c.token.AccessToken, err -} - -func (c *Identity) refreshToken() error { +func (c *tokenSource) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { data := struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"` }{ - AccessToken: c.token.AccessToken, - RefreshToken: c.token.RefreshToken, + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, } - uri := fmt.Sprintf("%s%s", API, "/accounts/refresh_token") + uri := fmt.Sprintf("%s/%s", API, "accounts/refresh_token") req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) - var token Token + var res *oauth2.Token if err == nil { - err = c.DoJSON(req, &token) - token.Valid = time.Now().Add(time.Second * time.Duration(token.ExpiresIn)) + var refreshed Token + if err = c.DoJSON(req, &refreshed); err == nil { + res = refreshed.AsOAuth2Token() + } } - if err == nil { - c.token = token - } - - return err + return res, err } diff --git a/internal/charger/easee/message.go b/internal/charger/easee/message.go index 62831e46b..7cfa17d27 100644 --- a/internal/charger/easee/message.go +++ b/internal/charger/easee/message.go @@ -3,6 +3,17 @@ package easee // API is the Easee API endpoint const API = "https://api.easee.cloud/api" +// charge mode definition +const ( + ModeOffline int = 0 + ModeDisconnected int = 1 + ModeAwaitingStart int = 2 + ModeCharging int = 3 + ModeCompleted int = 4 + ModeError int = 5 + ModeReadyToCharge int = 6 +) + // Charger is the charger type type Charger struct { ID string `json:"id"` diff --git a/internal/vehicle/bluelink/identity.go b/internal/vehicle/bluelink/identity.go index 936a7d513..f262eeaa1 100644 --- a/internal/vehicle/bluelink/identity.go +++ b/internal/vehicle/bluelink/identity.go @@ -290,7 +290,7 @@ func (v *Identity) exchangeCode(accCode string) (oauth.Token, error) { } // Refresh implements token refresh -func (v *Identity) Refresh(token *oauth2.Token) (*oauth2.Token, error) { +func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { headers := map[string]string{ "Authorization": "Basic " + v.config.BasicToken, "Content-type": "application/x-www-form-urlencoded", diff --git a/internal/vehicle/ford.go b/internal/vehicle/ford.go index e4eb98f86..78acb5cf3 100644 --- a/internal/vehicle/ford.go +++ b/internal/vehicle/ford.go @@ -108,10 +108,8 @@ func (v *Ford) login() (oauth.Token, error) { return res, err } -var _ oauth.TokenRefresher = (*Ford)(nil) - // Refresh implements the oauth.TokenRefresher interface -func (v *Ford) Refresh(token *oauth2.Token) (*oauth2.Token, error) { +func (v *Ford) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { data := url.Values{ "client_id": []string{"9fb503e0-715b-47e8-adfd-ad4b7770f73b"}, "grant_type": []string{"refresh_token"}, diff --git a/internal/vehicle/id/refresh.go b/internal/vehicle/id/refresh.go index 73dc6799a..b51a6dfd9 100644 --- a/internal/vehicle/id/refresh.go +++ b/internal/vehicle/id/refresh.go @@ -20,7 +20,7 @@ func Refresher(log *util.Logger) oauth.TokenRefresher { } // Refresh is the oauth.TokenRefresher -func (tr *tokenRefresher) Refresh(token *oauth2.Token) (*oauth2.Token, error) { +func (tr *tokenRefresher) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { uri := "https://login.apps.emea.vwapps.io/refresh/v1" req, err := request.New(http.MethodGet, uri, nil, map[string]string{ diff --git a/internal/vehicle/nissan.go b/internal/vehicle/nissan.go index 9f2e56059..3bf0f0564 100644 --- a/internal/vehicle/nissan.go +++ b/internal/vehicle/nissan.go @@ -228,7 +228,7 @@ func (v *Nissan) authFlow() (oauth.Token, error) { return res, err } -func (v *Nissan) Refresh(token *oauth2.Token) (*oauth2.Token, error) { +func (v *Nissan) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { data := url.Values{ "client_id": []string{nissanClientID}, "client_secret": []string{nissanClientSecret}, diff --git a/internal/vehicle/vw/refresh.go b/internal/vehicle/vw/refresh.go index 7445d2093..cfa03b0a5 100644 --- a/internal/vehicle/vw/refresh.go +++ b/internal/vehicle/vw/refresh.go @@ -24,7 +24,7 @@ func refresher(log *util.Logger, clientID string) oauth.TokenRefresher { } // Refresh is the oauth.TokenRefresher -func (tr *tokenRefresher) Refresh(token *oauth2.Token) (*oauth2.Token, error) { +func (tr *tokenRefresher) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { data := url.Values(map[string][]string{ "grant_type": {"refresh_token"}, "refresh_token": {token.RefreshToken}, diff --git a/util/oauth/tokensource.go b/util/oauth/tokensource.go index c92276998..fd8bd7a06 100644 --- a/util/oauth/tokensource.go +++ b/util/oauth/tokensource.go @@ -9,7 +9,7 @@ import ( ) type TokenRefresher interface { - Refresh(token *oauth2.Token) (*oauth2.Token, error) + RefreshToken(token *oauth2.Token) (*oauth2.Token, error) } type TokenSource struct { @@ -25,7 +25,7 @@ func (ts *TokenSource) Token() (*oauth2.Token, error) { var err error if time.Until(ts.token.Expiry) < time.Minute { var token *oauth2.Token - if token, err = ts.refresher.Refresh(ts.token); err == nil { + if token, err = ts.refresher.RefreshToken(ts.token); err == nil { if token.AccessToken == "" { err = errors.New("token refresh failed to obtain access token") } else {