From 63e4eb2a6a44c0050fd931c208c4805a2d6485dc Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 24 Oct 2021 15:02:38 +0200 Subject: [PATCH] Nissan: improve login error handling (#1774) --- vehicle/nissan.go | 6 ++- vehicle/nissan/api.go | 66 ++++-------------------------- vehicle/nissan/identity.go | 37 ++++++++++------- vehicle/nissan/provider.go | 82 +++++++++++++++++++++++++++++++++----- vehicle/nissan/types.go | 19 +++++++++ 5 files changed, 123 insertions(+), 87 deletions(-) diff --git a/vehicle/nissan.go b/vehicle/nissan.go index 01739a914..30739b127 100644 --- a/vehicle/nissan.go +++ b/vehicle/nissan.go @@ -33,9 +33,11 @@ func NewNissanFromConfig(other map[string]interface{}) (api.Vehicle, error) { cc := struct { embed `mapstructure:",squash"` User, Password, VIN string + Expiry time.Duration Cache time.Duration }{ - Cache: interval, + Expiry: expiry, + Cache: interval, } if err := util.DecodeOther(other, &cc); err != nil { @@ -63,7 +65,7 @@ func NewNissanFromConfig(other map[string]interface{}) (api.Vehicle, error) { } } - v.Provider = nissan.NewProvider(api, cc.Cache) + v.Provider = nissan.NewProvider(api, cc.Expiry, cc.Cache) return v, err } diff --git a/vehicle/nissan/api.go b/vehicle/nissan/api.go index abee3dacc..7d38d1f48 100644 --- a/vehicle/nissan/api.go +++ b/vehicle/nissan/api.go @@ -1,12 +1,10 @@ package nissan import ( - "errors" "fmt" "net/http" "time" - "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "golang.org/x/oauth2" @@ -14,14 +12,11 @@ import ( const ( refreshTimeout = 5 * time.Minute - statusExpiry = time.Minute ) type API struct { *request.Helper - VIN string - refreshID string - refreshTime time.Time + VIN string } func NewAPI(log *util.Logger, identity oauth2.TokenSource, vin string) *API { @@ -66,60 +61,23 @@ func (v *API) Vehicles() ([]string, error) { const timeFormat = "2006-01-02T15:04:05Z" // Battery provides battery api response -func (v *API) Battery() (Response, error) { - // request battery status +func (v *API) BatteryStatus() (Response, error) { uri := fmt.Sprintf("%s/v1/cars/%s/battery-status", CarAdapterBaseURL, v.VIN) var res Response err := v.GetJSON(uri, &res) - var ts time.Time - if err == nil { - ts, err = time.Parse(timeFormat, res.Data.Attributes.LastUpdateTime) - - // return the current value - if time.Since(ts) <= statusExpiry { - v.refreshID = "" - return res, err - } - } - - // request a refresh, irrespective of a previous error - if v.refreshID == "" { - if err = v.refreshRequest(); err == nil { - err = api.ErrMustRetry - } - - return res, err - } - - // refresh finally expired - if time.Since(v.refreshTime) > refreshTimeout { - v.refreshID = "" - if err == nil { - err = api.ErrTimeout - } - } else { - if len(res.Errors) > 0 { - // extract error code - e := res.Errors[0] - err = fmt.Errorf("%s: %s", e.Code, e.Detail) - } else { - // wait for refresh, irrespective of a previous error - err = api.ErrMustRetry - } - } - return res, err } -// refreshRequest requests battery status refresh -func (v *API) refreshRequest() error { +// RefreshRequest requests battery status refresh +func (v *API) RefreshRequest(typ string) (Response, error) { + var res Response uri := fmt.Sprintf("%s/v1/cars/%s/actions/refresh-battery-status", CarAdapterBaseURL, v.VIN) data := Request{ Data: Payload{ - Type: "RefreshBatteryStatus", + Type: typ, }, } @@ -127,21 +85,11 @@ func (v *API) refreshRequest() error { "Content-Type": "application/vnd.api+json", }) - var res Response if err == nil { err = v.DoJSON(req, &res) } - if err == nil { - v.refreshID = res.Data.ID - v.refreshTime = time.Now() - - if v.refreshID == "" { - err = errors.New("refresh failed") - } - } - - return err + return res, err } type Action string diff --git a/vehicle/nissan/identity.go b/vehicle/nissan/identity.go index 8bedb3fa6..eb38a4f24 100644 --- a/vehicle/nissan/identity.go +++ b/vehicle/nissan/identity.go @@ -6,7 +6,7 @@ import ( "net/url" "strings" - "github.com/avast/retry-go/v3" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/oauth" "github.com/evcc-io/evcc/util/request" @@ -55,22 +55,29 @@ func (v *Identity) Login(user, password string) error { } // https://github.com/Tobiaswk/dartnissanconnect/commit/7d28dd5461aaed3e46b5be0c9fd58887e1e0cd0b - err = retry.Do(func() error { - req, err = request.New(http.MethodPost, uri, request.MarshalJSON(res), map[string]string{ - "Accept-Api-Version": APIVersion, - "X-Username": "anonymous", - "X-Password": "anonymous", - "Content-type": "application/json", - "Accept": "application/json", - }) - - if err == nil { - err = v.DoJSON(req, &nToken) - realm = strings.Trim(nToken.Realm, "/") + if err == nil { + err = api.ErrNotAvailable // not nil + for attempt := 1; attempt <= 10 && err != nil; attempt++ { + req, err = request.New(http.MethodPost, uri, request.MarshalJSON(res), map[string]string{ + "Accept-Api-Version": APIVersion, + "X-Username": "anonymous", + "X-Password": "anonymous", + "Content-type": "application/json", + "Accept": "application/json", + }) + if err == nil { + if err = v.DoJSON(req, &nToken); err != nil && !nToken.SessionExpired() { + break + } + } } - return err - }, retry.Attempts(10), retry.LastErrorOnly(true)) + if errT := nToken.Error(); err != nil && errT != nil { + err = errT + } + + realm = strings.Trim(nToken.Realm, "/") + } } if err == nil { diff --git a/vehicle/nissan/provider.go b/vehicle/nissan/provider.go index 4a256f77d..fbe5079ed 100644 --- a/vehicle/nissan/provider.go +++ b/vehicle/nissan/provider.go @@ -1,6 +1,8 @@ package nissan import ( + "errors" + "fmt" "time" "github.com/evcc-io/evcc/api" @@ -9,28 +11,86 @@ import ( // Provider is a kamereon provider type Provider struct { - apiG func() (interface{}, error) - action func(value Action) error + statusG func() (interface{}, error) + action func(value Action) error + expiry time.Duration + refreshID string + refreshTime time.Time } // NewProvider returns a kamereon provider -func NewProvider(api *API, cache time.Duration) *Provider { - return &Provider{ - apiG: provider.NewCached(func() (interface{}, error) { - return api.Battery() - }, cache).InterfaceGetter(), +func NewProvider(api *API, expiry, cache time.Duration) *Provider { + impl := &Provider{ action: func(value Action) error { _, err := api.ChargingAction(value) return err }, + expiry: expiry, } + + impl.statusG = provider.NewCached(func() (interface{}, error) { + return impl.status(api.BatteryStatus, api.RefreshRequest) + }, cache).InterfaceGetter() + + return impl +} + +func (v *Provider) status(battery func() (Response, error), refresh func(string) (Response, error)) (Response, error) { + res, err := battery() + + var ts time.Time + if err == nil { + ts, err = time.Parse(timeFormat, res.Data.Attributes.LastUpdateTime) + + // return the current value + if time.Since(ts) <= v.expiry { + v.refreshID = "" + return res, err + } + } + + // request a refresh, irrespective of a previous error + if v.refreshID == "" { + var refreshRes Response + if refreshRes, err = refresh("RefreshBatteryStatus"); err == nil { + err = api.ErrMustRetry + + v.refreshID = refreshRes.Data.ID + v.refreshTime = time.Now() + + if v.refreshID == "" { + err = errors.New("refresh failed") + } + } + + return res, err + } + + // refresh finally expired + if time.Since(v.refreshTime) > refreshTimeout { + v.refreshID = "" + if err == nil { + err = api.ErrTimeout + } + } else { + if len(res.Errors) > 0 { + // extract error code + e := res.Errors[0] + err = fmt.Errorf("%s: %s", e.Code, e.Detail) + } else { + // wait for refresh, irrespective of a previous error + err = api.ErrMustRetry + } + } + + return res, err } var _ api.Battery = (*Provider)(nil) // SoC implements the api.Vehicle interface func (v *Provider) SoC() (float64, error) { - res, err := v.apiG() + res, err := v.statusG() if res, ok := res.(Response); err == nil && ok { return float64(res.Data.Attributes.BatteryLevel), nil @@ -45,7 +105,7 @@ var _ api.ChargeState = (*Provider)(nil) func (v *Provider) Status() (api.ChargeStatus, error) { status := api.StatusA // disconnected - res, err := v.apiG() + res, err := v.statusG() if res, ok := res.(Response); err == nil && ok { if res.Data.Attributes.PlugStatus > 0 { status = api.StatusB @@ -62,7 +122,7 @@ var _ api.VehicleRange = (*Provider)(nil) // Range implements the api.VehicleRange interface func (v *Provider) Range() (int64, error) { - res, err := v.apiG() + res, err := v.statusG() if res, ok := res.(Response); err == nil && ok { return int64(res.Data.Attributes.RangeHvacOff), nil @@ -75,7 +135,7 @@ var _ api.VehicleFinishTimer = (*Provider)(nil) // FinishTime implements the api.VehicleFinishTimer interface func (v *Provider) FinishTime() (time.Time, error) { - res, err := v.apiG() + res, err := v.statusG() if res, ok := res.(Response); err == nil && ok { timestamp, err := time.Parse(time.RFC3339, res.Data.Attributes.Timestamp) diff --git a/vehicle/nissan/types.go b/vehicle/nissan/types.go index 30cf2559a..837f44021 100644 --- a/vehicle/nissan/types.go +++ b/vehicle/nissan/types.go @@ -1,5 +1,10 @@ package nissan +import ( + "fmt" + "strings" +) + // api constants const ( APIVersion = "protocol=1.0,resource=2.1" @@ -37,6 +42,20 @@ type Token struct { TokenID string `json:"tokenId"` SuccessURL string `json:"successUrl"` Realm string `json:"realm"` + Code int `json:"code"` // error response + Reason string `json:"reason"` // error response + Message string `json:"message"` // error response +} + +func (t *Token) SessionExpired() bool { + return strings.EqualFold(t.Message, "Session has timed out") +} + +func (t *Token) Error() error { + if t.Code == 0 { + return nil + } + return fmt.Errorf("%s: %s", t.Reason, t.Message) } type Vehicles struct {