Bluelink: refactor refresh logic from api to provider (#1867)

This commit is contained in:
andig 2021-11-12 11:16:47 +01:00 • committed by GitHub
parent 0abe6c2c8c
commit 3160f11a02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 70 deletions

View file

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

View file

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

View file

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

View file

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