Hyundai/Kia: async soc refresh (#1015)

This commit is contained in:
andig 2021-05-14 22:30:07 +02:00 • committed by GitHub
parent 169d2ca11d
commit 0ec61aa14a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 108 additions and 31 deletions

View file

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

View file

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

View file

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