Nissan: improve login error handling (#1774)

This commit is contained in:
andig 2021-10-24 15:02:38 +02:00 • committed by GitHub
parent 717578eb28
commit 63e4eb2a6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 87 deletions

View file

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

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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 {