diff --git a/internal/vehicle/bluelink/api.go b/internal/vehicle/bluelink/api.go index a19b88c6c..4fa1760f2 100644 --- a/internal/vehicle/bluelink/api.go +++ b/internal/vehicle/bluelink/api.go @@ -6,15 +6,23 @@ import ( "net/http" "time" + "github.com/andig/evcc/api" "github.com/andig/evcc/util" "github.com/andig/evcc/util/request" ) -const resOK = "S" // auth fail: F +const ( + VehiclesURL = "/api/v1/spa/vehicles" + StatusURL = "/api/v1/spa/vehicles/%s/status" + StatusLatestURL = "/api/v1/spa/vehicles/%s/status/latest" +) const ( - VehiclesURL = "/api/v1/spa/vehicles" - StatusURL = "/api/v1/spa/vehicles/%s/status" + resOK = "S" // auth fail: F + timeFormat = "20060102150405 -0700" // Note: must add timeOffset + timeOffset = " +0100" + refreshTimeout = time.Minute + statusExpiry = 5 * time.Minute ) // ErrAuthFail indicates authorization failure @@ -24,8 +32,10 @@ var ErrAuthFail = errors.New("authorization failed") // Based on https://github.com/Hacksore/bluelinky. type API struct { *request.Helper - log *util.Logger - identity *Identity + log *util.Logger + identity *Identity + refresh bool + refreshTime time.Time } // New creates a new BlueLink API @@ -65,22 +75,39 @@ func (v *API) Vehicles() ([]Vehicle, error) { } type StatusResponse struct { - timestamp time.Time // add missing timestamp - RetCode string - ResMsg struct { - EvStatus struct { - BatteryStatus float64 - RemainTime2 struct { - Atc struct { - Value, Unit int - } - } - DrvDistance []DrivingDistance + RetCode string + ResCode string + ResMsg StatusData +} + +type StatusLatestResponse struct { + RetCode string + ResCode string + ResMsg struct { + VehicleStatusInfo struct { + VehicleStatus StatusData } - Vehicles []Vehicle } } +type StatusData struct { + Time string + EvStatus struct { + BatteryStatus float64 + RemainTime2 struct { + Atc struct { + Value, Unit int + } + } + DrvDistance []DrivingDistance + } + Vehicles []Vehicle +} + +func (d *StatusData) Updated() (time.Time, error) { + return time.Parse(timeFormat, d.Time+timeOffset) +} + type DrivingDistance struct { RangeByFuel struct { EvModeRange struct { @@ -89,15 +116,64 @@ type DrivingDistance struct { } } -func (v *API) Status(vid string) (StatusResponse, error) { - var resp StatusResponse +func (v *API) Status(vid string) (StatusData, error) { + var resp StatusLatestResponse - req, err := v.identity.Request(http.MethodGet, fmt.Sprintf(StatusURL, vid)) + 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 + } + } } - return resp, err + // 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 +} + +func (v *API) refreshRequest(vid string) error { + 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) + } + }() + } + + return err } diff --git a/internal/vehicle/bluelink/provider.go b/internal/vehicle/bluelink/provider.go index cafbdbebd..939f5f29c 100644 --- a/internal/vehicle/bluelink/provider.go +++ b/internal/vehicle/bluelink/provider.go @@ -30,8 +30,8 @@ var _ api.Battery = (*Provider)(nil) func (v *Provider) SoC() (float64, error) { res, err := v.apiG() - if res, ok := res.(StatusResponse); err == nil && ok { - return float64(res.ResMsg.EvStatus.BatteryStatus), nil + if res, ok := res.(StatusData); err == nil && ok { + return float64(res.EvStatus.BatteryStatus), nil } return 0, err @@ -43,14 +43,15 @@ var _ api.VehicleFinishTimer = (*Provider)(nil) func (v *Provider) FinishTime() (time.Time, error) { res, err := v.apiG() - if res, ok := res.(StatusResponse); err == nil && ok { - remaining := res.ResMsg.EvStatus.RemainTime2.Atc.Value + if res, ok := res.(StatusData); err == nil && ok { + remaining := res.EvStatus.RemainTime2.Atc.Value if remaining == 0 { return time.Time{}, api.ErrNotAvailable } - return res.timestamp.Add(time.Duration(remaining) * time.Minute), nil + ts, err := res.Updated() + return ts.Add(time.Duration(remaining) * time.Minute), err } return time.Time{}, err @@ -62,8 +63,8 @@ var _ api.VehicleRange = (*Provider)(nil) func (v *Provider) Range() (int64, error) { res, err := v.apiG() - if res, ok := res.(StatusResponse); err == nil && ok { - if dist := res.ResMsg.EvStatus.DrvDistance; len(dist) == 1 { + if res, ok := res.(StatusData); err == nil && ok { + if dist := res.EvStatus.DrvDistance; len(dist) == 1 { return int64(dist[0].RangeByFuel.EvModeRange.Value), nil } diff --git a/internal/vehicle/ford.go b/internal/vehicle/ford.go index c7ccff68f..aae69d822 100644 --- a/internal/vehicle/ford.go +++ b/internal/vehicle/ford.go @@ -20,7 +20,7 @@ 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 + fordStatusExpiry = 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 ) @@ -217,8 +217,8 @@ func (v *Ford) status() (res fordVehicleStatus, err error) { var lastUpdate time.Time lastUpdate, err = time.Parse(fordTimeFormat, res.VehicleStatus.LastRefresh) - if elapsed := time.Since(lastUpdate); err == nil && elapsed > fordOutdatedAfter { - v.log.DEBUG.Printf("vehicle status is outdated (age %v > %v), requesting refresh", elapsed, fordOutdatedAfter) + if elapsed := time.Since(lastUpdate); err == nil && elapsed > fordStatusExpiry { + v.log.DEBUG.Printf("vehicle status is outdated (age %v > %v), requesting refresh", elapsed, fordStatusExpiry) if err = v.refreshRequest(); err == nil { err = api.ErrMustRetry