From 3160f11a02e936dbb01635a486ee79bd6e5566c3 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 12 Nov 2021 11:16:47 +0100 Subject: [PATCH] Bluelink: refactor refresh logic from api to provider (#1867) --- vehicle/bluelink/api.go | 76 +++++++++-------------------------- vehicle/bluelink/provider.go | 78 ++++++++++++++++++++++++++++++++---- vehicle/hyundai.go | 6 ++- vehicle/kia.go | 6 ++- 4 files changed, 96 insertions(+), 70 deletions(-) diff --git a/vehicle/bluelink/api.go b/vehicle/bluelink/api.go index 9916d4d3d..0c6d5a03d 100644 --- a/vehicle/bluelink/api.go +++ b/vehicle/bluelink/api.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" ) @@ -18,11 +17,9 @@ const ( ) const ( - resOK = "S" // auth fail: F - timeFormat = "20060102150405 -0700" // Note: must add timeOffset - timeOffset = " +0100" - refreshTimeout = 2 * time.Minute - statusExpiry = 5 * time.Minute + resOK = "S" // auth fail: F + timeFormat = "20060102150405 -0700" // Note: must add timeOffset + timeOffset = " +0100" ) // ErrAuthFail indicates authorization failure @@ -32,10 +29,8 @@ var ErrAuthFail = errors.New("authorization failed") // Based on https://github.com/Hacksore/bluelinky. type API struct { *request.Helper - log *util.Logger - identity *Identity - refresh bool - refreshTime time.Time + log *util.Logger + identity *Identity } // New creates a new BlueLink API @@ -116,64 +111,29 @@ type DrivingDistance struct { } } -func (v *API) Status(vid string) (StatusData, error) { - var resp StatusLatestResponse +func (v *API) Status(vid string) (StatusLatestResponse, error) { + var res StatusLatestResponse req, err := v.identity.Request(http.MethodGet, fmt.Sprintf(StatusLatestURL, vid)) if err == nil { - if err = v.DoJSON(req, &resp); err == nil && resp.RetCode != resOK { - err = fmt.Errorf("unexpected response: %s", resp.RetCode) - } - - var ts time.Time - if err == nil { - ts, err = resp.ResMsg.VehicleStatusInfo.VehicleStatus.Updated() - - // return the current value - if time.Since(ts) <= statusExpiry { - v.refresh = false - return resp.ResMsg.VehicleStatusInfo.VehicleStatus, err - } + if err = v.DoJSON(req, &res); err == nil && res.RetCode != resOK { + err = fmt.Errorf("unexpected response: %s", res.RetCode) } } - // request a refresh, irrespective of a previous error - if !v.refresh { - if err = v.refreshRequest(vid); err == nil { - err = api.ErrMustRetry - } - - return StatusData{}, err - } - - // refresh finally expired - if time.Since(v.refreshTime) > refreshTimeout { - v.refresh = false - if err == nil { - err = api.ErrTimeout - } - } else { - // wait for refresh, irrespective of a previous error - err = api.ErrMustRetry - } - - return resp.ResMsg.VehicleStatusInfo.VehicleStatus, err + return res, err } -func (v *API) refreshRequest(vid string) error { +// StatusPartial refreshes the status from the bluelink api +func (v *API) StatusPartial(vid string) (StatusResponse, error) { + var res StatusResponse + req, err := v.identity.Request(http.MethodGet, fmt.Sprintf(StatusURL, vid)) if err == nil { - v.refresh = true - v.refreshTime = time.Now() - - // run the actual update asynchronously - go func() { - var resp StatusResponse - if err := v.DoJSON(req, &resp); err == nil && resp.RetCode != resOK { - v.log.ERROR.Printf("unexpected response: %s", resp.RetCode) - } - }() + if err = v.DoJSON(req, &res); err == nil && res.RetCode != resOK { + err = fmt.Errorf("unexpected response: %s", res.RetCode) + } } - return err + return res, err } diff --git a/vehicle/bluelink/provider.go b/vehicle/bluelink/provider.go index 1760e4108..9e08aafde 100644 --- a/vehicle/bluelink/provider.go +++ b/vehicle/bluelink/provider.go @@ -7,28 +7,90 @@ import ( "github.com/evcc-io/evcc/provider" ) +const refreshTimeout = 2 * time.Minute + // Provider implements the Kia/Hyundai bluelink api. // Based on https://github.com/Hacksore/bluelinky. type Provider struct { - apiG func() (interface{}, error) + statusG func() (interface{}, error) + refreshG func() (StatusResponse, error) + expiry time.Duration + refreshTime time.Time } // New creates a new BlueLink API -func NewProvider(api *API, vid string, cache time.Duration) *Provider { +func NewProvider(api *API, vid string, expiry, cache time.Duration) *Provider { v := &Provider{ - apiG: provider.NewCached(func() (interface{}, error) { - return api.Status(vid) - }, cache).InterfaceGetter(), + refreshG: func() (StatusResponse, error) { + return api.StatusPartial(vid) + }, + expiry: expiry, } + v.statusG = provider.NewCached(func() (interface{}, error) { + return v.status( + func() (StatusLatestResponse, error) { return api.Status(vid) }, + ) + }, cache).InterfaceGetter() + return v } +// status wraps the api status call and adds status refresh +func (v *Provider) status(statusG func() (StatusLatestResponse, error)) (StatusData, error) { + res, err := statusG() + + var ts time.Time + if err == nil { + ts, err = res.ResMsg.VehicleStatusInfo.VehicleStatus.Updated() + if err != nil { + return res.ResMsg.VehicleStatusInfo.VehicleStatus, err + } + + // return the current value + if time.Since(ts) <= v.expiry { + v.refreshTime = time.Time{} + return res.ResMsg.VehicleStatusInfo.VehicleStatus, err + } + } + + // request a refresh, irrespective of a previous error + if v.refreshTime.IsZero() { + v.refreshTime = time.Now() + + // TODO async refresh + res, err := v.refreshG() + if err == nil { + if ts, err = res.ResMsg.Updated(); err == nil && time.Since(ts) <= v.expiry { + v.refreshTime = time.Time{} + return res.ResMsg, nil + } + + err = api.ErrMustRetry + } + + return StatusData{}, err + } + + // refresh finally expired + if time.Since(v.refreshTime) > refreshTimeout { + v.refreshTime = time.Time{} + if err == nil { + err = api.ErrTimeout + } + } else { + // wait for refresh, irrespective of a previous error + err = api.ErrMustRetry + } + + return StatusData{}, err +} + var _ api.Battery = (*Provider)(nil) // SoC implements the api.Battery interface func (v *Provider) SoC() (float64, error) { - res, err := v.apiG() + res, err := v.statusG() if res, ok := res.(StatusData); err == nil && ok { return float64(res.EvStatus.BatteryStatus), nil @@ -41,7 +103,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.(StatusData); err == nil && ok { remaining := res.EvStatus.RemainTime2.Atc.Value @@ -61,7 +123,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.(StatusData); err == nil && ok { if dist := res.EvStatus.DrvDistance; len(dist) == 1 { diff --git a/vehicle/hyundai.go b/vehicle/hyundai.go index cb5ca4953..fd22dc4f3 100644 --- a/vehicle/hyundai.go +++ b/vehicle/hyundai.go @@ -27,9 +27,11 @@ func NewHyundaiFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password string VIN string + Expiry time.Duration Cache time.Duration }{ - Cache: interval, + Expiry: expiry, + Cache: interval, } if err := util.DecodeOther(other, &cc); err != nil { @@ -85,7 +87,7 @@ func NewHyundaiFromConfig(other map[string]interface{}) (api.Vehicle, error) { v := &Hyundai{ embed: &cc.embed, - Provider: bluelink.NewProvider(api, vehicle.VehicleID, cc.Cache), + Provider: bluelink.NewProvider(api, vehicle.VehicleID, cc.Expiry, cc.Cache), } return v, nil diff --git a/vehicle/kia.go b/vehicle/kia.go index 1c42277e2..2e97b4689 100644 --- a/vehicle/kia.go +++ b/vehicle/kia.go @@ -27,9 +27,11 @@ func NewKiaFromConfig(other map[string]interface{}) (api.Vehicle, error) { embed `mapstructure:",squash"` User, Password string VIN string + Expiry time.Duration Cache time.Duration }{ - Cache: interval, + Expiry: expiry, + Cache: interval, } if err := util.DecodeOther(other, &cc); err != nil { @@ -85,7 +87,7 @@ func NewKiaFromConfig(other map[string]interface{}) (api.Vehicle, error) { v := &Kia{ embed: &cc.embed, - Provider: bluelink.NewProvider(api, vehicle.VehicleID, cc.Cache), + Provider: bluelink.NewProvider(api, vehicle.VehicleID, cc.Expiry, cc.Cache), } return v, nil