Bluelink: refactor refresh logic from api to provider (#1867)
This commit is contained in:
parent
0abe6c2c8c
commit
3160f11a02
4 changed files with 96 additions and 70 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue