Ford: async soc refresh (#1001)
This commit is contained in:
parent
ec0831cf36
commit
79b67a30c4
2 changed files with 74 additions and 50 deletions
|
|
@ -17,12 +17,12 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
fordAuth = "https://fcis.ice.ibmcloud.com"
|
||||
fordAPI = "https://usapi.cv.ford.com"
|
||||
fordVehicleList = "https://api.mps.ford.com/api/users/vehicles"
|
||||
fordOutdatedAfter = 5 * time.Minute // if returned status value is older, evcc will init refresh
|
||||
fordMaxRefreshTrials = 20 // max trials to get status after refresh, poll interval is 1.5s, i.e. timeout = maxTrials * 1.5s
|
||||
fordTimeFormat = "01-02-2006 15:04:05" // time format used by Ford API, time is in UTC
|
||||
fordAuth = "https://fcis.ice.ibmcloud.com"
|
||||
fordAPI = "https://usapi.cv.ford.com"
|
||||
fordVehicleList = "https://api.mps.ford.com/api/users/vehicles"
|
||||
fordOutdatedAfter = 5 * time.Minute // if returned status value is older, evcc will init refresh
|
||||
fordRefreshTimeout = time.Minute // timeout to get status after refresh
|
||||
fordTimeFormat = "01-02-2006 15:04:05" // time format used by Ford API, time is in UTC
|
||||
)
|
||||
|
||||
// Ford is an api.Vehicle implementation for Ford cars
|
||||
|
|
@ -33,6 +33,8 @@ type Ford struct {
|
|||
user, password, vin string
|
||||
tokenSource oauth2.TokenSource
|
||||
statusG func() (interface{}, error)
|
||||
refreshId string
|
||||
refreshTime time.Time
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -74,7 +76,7 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
v.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return v.vehicleStatus()
|
||||
return v.status()
|
||||
}, cc.Cache).InterfaceGetter()
|
||||
|
||||
if err == nil && cc.VIN == "" {
|
||||
|
|
@ -181,13 +183,12 @@ func (v *Ford) vehicles() ([]string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
var vehicles []string
|
||||
|
||||
req, err := v.request(http.MethodGet, fordVehicleList)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
||||
var vehicles []string
|
||||
if err == nil {
|
||||
for _, v := range resp.Vehicles.Values {
|
||||
vehicles = append(vehicles, v.VIN)
|
||||
|
|
@ -197,12 +198,16 @@ func (v *Ford) vehicles() ([]string, error) {
|
|||
return vehicles, err
|
||||
}
|
||||
|
||||
// vehicleStatus performs a /status request to the Ford API and triggers a refresh if
|
||||
// status performs a /status request to the Ford API and triggers a refresh if
|
||||
// the received status is too old
|
||||
func (v *Ford) vehicleStatus() (fordVehicleStatus, error) {
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/status", fordAPI, v.vin)
|
||||
func (v *Ford) status() (res fordVehicleStatus, err error) {
|
||||
// follow up requested refresh
|
||||
if v.refreshId != "" {
|
||||
return v.refreshResult()
|
||||
}
|
||||
|
||||
var res fordVehicleStatus
|
||||
// otherwise start normal workflow
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/status", fordAPI, v.vin)
|
||||
req, err := v.request(http.MethodGet, uri)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
|
|
@ -214,43 +219,47 @@ func (v *Ford) vehicleStatus() (fordVehicleStatus, error) {
|
|||
|
||||
if elapsed := time.Since(lastUpdate); err == nil && elapsed > fordOutdatedAfter {
|
||||
v.log.DEBUG.Printf("vehicle status is outdated (age %v > %v), requesting refresh", elapsed, fordOutdatedAfter)
|
||||
res, err = v.vehicleStatusRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// vehicleStatusRefresh triggers an update and waits until refreshed data is available or request times out
|
||||
func (v *Ford) vehicleStatusRefresh() (fordVehicleStatus, error) {
|
||||
commandId, err := v.requestRefresh()
|
||||
|
||||
var res fordVehicleStatus
|
||||
if err == nil {
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/statusrefresh/%s", fordAPI, v.vin, commandId)
|
||||
|
||||
// if status attribute in JSON response is 200, update is complete, otherwise server is still
|
||||
// waiting for vehicle and the request needs to be repeated
|
||||
for counter := 0; counter < fordMaxRefreshTrials && res.Status != 200 && err == nil; counter++ {
|
||||
var req *http.Request
|
||||
if req, err = v.request(http.MethodGet, uri); err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
if err = v.refreshRequest(); err == nil {
|
||||
err = api.ErrMustRetry
|
||||
}
|
||||
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
}
|
||||
|
||||
if err == nil && res.Status != 200 {
|
||||
err = fmt.Errorf("refresh failed: status %d", res.Status)
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// requestRefresh requests Ford API to poll vehicle for updated data
|
||||
// returns commandId to track the request and get the data after server received update from vehicle
|
||||
func (v *Ford) requestRefresh() (string, error) {
|
||||
// refreshResult triggers an update if not already in progress, otherwise gets result
|
||||
func (v *Ford) refreshResult() (res fordVehicleStatus, err error) {
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/statusrefresh/%s", fordAPI, v.vin, v.refreshId)
|
||||
|
||||
var req *http.Request
|
||||
if req, err = v.request(http.MethodGet, uri); err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
// update successful and completed
|
||||
if err == nil && res.Status == 200 {
|
||||
v.refreshId = ""
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// update still in progress, keep retrying
|
||||
if time.Since(v.refreshTime) < fordRefreshTimeout {
|
||||
return res, api.ErrMustRetry
|
||||
}
|
||||
|
||||
// give up
|
||||
v.refreshId = ""
|
||||
if err == nil {
|
||||
err = api.ErrTimeout
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// refreshRequest requests status refresh tracked by commandId
|
||||
func (v *Ford) refreshRequest() error {
|
||||
var resp struct {
|
||||
CommandId string
|
||||
}
|
||||
|
|
@ -261,7 +270,16 @@ func (v *Ford) requestRefresh() (string, error) {
|
|||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
||||
return resp.CommandId, err
|
||||
if err == nil {
|
||||
v.refreshId = resp.CommandId
|
||||
v.refreshTime = time.Now()
|
||||
|
||||
if resp.CommandId == "" {
|
||||
err = errors.New("refresh failed")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Ford)(nil)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/asaskevich/EventBus"
|
||||
"github.com/benbjohnson/clock"
|
||||
|
|
@ -49,6 +51,10 @@ func (c *Cached) reset() {
|
|||
c.mux.Unlock()
|
||||
}
|
||||
|
||||
func (c *Cached) mustUpdate() bool {
|
||||
return c.clock.Since(c.updated) > c.cache || errors.Is(c.err, api.ErrMustRetry)
|
||||
}
|
||||
|
||||
// FloatGetter gets float value
|
||||
func (c *Cached) FloatGetter() func() (float64, error) {
|
||||
g, ok := c.getter.(func() (float64, error))
|
||||
|
|
@ -60,7 +66,7 @@ func (c *Cached) FloatGetter() func() (float64, error) {
|
|||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.clock.Since(c.updated) > c.cache {
|
||||
if c.mustUpdate() {
|
||||
c.val, c.err = g()
|
||||
c.updated = c.clock.Now()
|
||||
}
|
||||
|
|
@ -80,7 +86,7 @@ func (c *Cached) IntGetter() func() (int64, error) {
|
|||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.clock.Since(c.updated) > c.cache {
|
||||
if c.mustUpdate() {
|
||||
c.val, c.err = g()
|
||||
c.updated = c.clock.Now()
|
||||
}
|
||||
|
|
@ -100,7 +106,7 @@ func (c *Cached) StringGetter() func() (string, error) {
|
|||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.clock.Since(c.updated) > c.cache {
|
||||
if c.mustUpdate() {
|
||||
c.val, c.err = g()
|
||||
c.updated = c.clock.Now()
|
||||
}
|
||||
|
|
@ -120,7 +126,7 @@ func (c *Cached) BoolGetter() func() (bool, error) {
|
|||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.clock.Since(c.updated) > c.cache {
|
||||
if c.mustUpdate() {
|
||||
c.val, c.err = g()
|
||||
c.updated = c.clock.Now()
|
||||
}
|
||||
|
|
@ -140,7 +146,7 @@ func (c *Cached) DurationGetter() func() (time.Duration, error) {
|
|||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.clock.Since(c.updated) > c.cache {
|
||||
if c.mustUpdate() {
|
||||
c.val, c.err = g()
|
||||
c.updated = c.clock.Now()
|
||||
}
|
||||
|
|
@ -160,7 +166,7 @@ func (c *Cached) TimeGetter() func() (time.Time, error) {
|
|||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.clock.Since(c.updated) > c.cache {
|
||||
if c.mustUpdate() {
|
||||
c.val, c.err = g()
|
||||
c.updated = c.clock.Now()
|
||||
}
|
||||
|
|
@ -180,7 +186,7 @@ func (c *Cached) InterfaceGetter() func() (interface{}, error) {
|
|||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
|
||||
if c.clock.Since(c.updated) > c.cache {
|
||||
if c.mustUpdate() {
|
||||
c.val, c.err = g()
|
||||
c.updated = c.clock.Now()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue