Nissan: improve login error handling (#1774)
This commit is contained in:
parent
717578eb28
commit
63e4eb2a6a
5 changed files with 123 additions and 87 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue