Ford: add status refresh, range and token refresh (#860)
This commit is contained in:
parent
8562478f4a
commit
21cc2f4d90
1 changed files with 191 additions and 50 deletions
|
|
@ -12,20 +12,26 @@ import (
|
|||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/oauth"
|
||||
"github.com/andig/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
fordAuth = "https://fcis.ice.ibmcloud.com"
|
||||
fordAPI = "https://usapi.cv.ford.com"
|
||||
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
|
||||
)
|
||||
|
||||
// Ford is an api.Vehicle implementation for Ford cars
|
||||
type Ford struct {
|
||||
*embed
|
||||
*request.Helper
|
||||
log *util.Logger
|
||||
user, password, vin string
|
||||
tokens oauth.Token
|
||||
chargeStateG func() (float64, error)
|
||||
tokenSource oauth2.TokenSource
|
||||
statusG func() (interface{}, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -47,19 +53,30 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if cc.User == "" || cc.Password == "" {
|
||||
return nil, fmt.Errorf("missing credentials")
|
||||
}
|
||||
|
||||
log := util.NewLogger("ford")
|
||||
|
||||
v := &Ford{
|
||||
embed: &embed{cc.Title, cc.Capacity},
|
||||
Helper: request.NewHelper(log),
|
||||
log: log,
|
||||
user: cc.User,
|
||||
password: cc.Password,
|
||||
vin: strings.ToUpper(cc.VIN),
|
||||
}
|
||||
|
||||
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
|
||||
token, err := v.login()
|
||||
if err == nil {
|
||||
v.tokenSource = oauth.RefreshTokenSource((*oauth2.Token)(&token), v)
|
||||
}
|
||||
|
||||
v.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return v.vehicleStatus()
|
||||
}, cc.Cache).InterfaceGetter()
|
||||
|
||||
var err error
|
||||
if cc.VIN == "" {
|
||||
v.vin, err = findVehicle(v.vehicles())
|
||||
if err == nil {
|
||||
|
|
@ -70,45 +87,93 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
return v, err
|
||||
}
|
||||
|
||||
func (v *Ford) login(user, password string) error {
|
||||
// login authenticates with username/password to get new token
|
||||
func (v *Ford) login() (oauth.Token, error) {
|
||||
data := url.Values{
|
||||
"client_id": []string{"9fb503e0-715b-47e8-adfd-ad4b7770f73b"},
|
||||
"grant_type": []string{"password"},
|
||||
"username": []string{user},
|
||||
"password": []string{password},
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
}
|
||||
|
||||
uri := fordAuth + "/v1.0/endpoint/default/token"
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
var res oauth.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
var tokens oauth.Token
|
||||
if err = v.DoJSON(req, &tokens); err == nil {
|
||||
v.tokens = tokens
|
||||
}
|
||||
|
||||
return err
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (v *Ford) request(uri string) (*http.Request, error) {
|
||||
if v.tokens.AccessToken == "" || time.Until(v.tokens.Expiry) < time.Minute {
|
||||
if err := v.login(v.user, v.password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var _ oauth.TokenRefresher = (*Ford)(nil)
|
||||
|
||||
// Refresh implements the oauth.TokenRefresher interface
|
||||
func (v *Ford) Refresh(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
data := url.Values{
|
||||
"client_id": []string{"9fb503e0-715b-47e8-adfd-ad4b7770f73b"},
|
||||
"grant_type": []string{"refresh_token"},
|
||||
"refresh_token": []string{token.RefreshToken},
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Content-type": "application/json",
|
||||
"Application-Id": "71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592",
|
||||
"Auth-Token": v.tokens.AccessToken,
|
||||
})
|
||||
uri := fordAuth + "/v1.0/endpoint/default/token"
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
|
||||
var res oauth.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
res, err = v.login()
|
||||
}
|
||||
|
||||
return (*oauth2.Token)(&res), err
|
||||
}
|
||||
|
||||
// request is a helper to send API requests, sets header the Ford API expects
|
||||
func (v *Ford) request(method, uri string) (*http.Request, error) {
|
||||
token, err := v.tokenSource.Token()
|
||||
|
||||
var req *http.Request
|
||||
if err == nil {
|
||||
req, err = request.New(method, uri, nil, map[string]string{
|
||||
"Content-type": "application/json",
|
||||
"Application-Id": "71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592",
|
||||
"Auth-Token": token.AccessToken,
|
||||
})
|
||||
}
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
// vehicles implements returns the list of user vehicles
|
||||
// fordVehicleStatus holds the relevant data extracted from JSON that the server sends
|
||||
// on vehicle status request
|
||||
type fordVehicleStatus struct {
|
||||
VehicleStatus struct {
|
||||
BatteryFillLevel struct {
|
||||
Value float64
|
||||
Timestamp string
|
||||
}
|
||||
ElVehDTE struct {
|
||||
Value float64
|
||||
Timestamp string
|
||||
}
|
||||
ChargingStatus struct {
|
||||
Value string
|
||||
Timestamp string
|
||||
}
|
||||
PlugStatus struct {
|
||||
Value int
|
||||
Timestamp string
|
||||
}
|
||||
LastRefresh string
|
||||
}
|
||||
Status int
|
||||
}
|
||||
|
||||
// vehicles returns the list of user vehicles
|
||||
func (v *Ford) vehicles() ([]string, error) {
|
||||
var resp struct {
|
||||
Vehicles struct {
|
||||
|
|
@ -120,7 +185,7 @@ func (v *Ford) vehicles() ([]string, error) {
|
|||
|
||||
var vehicles []string
|
||||
|
||||
req, err := v.request("https://api.mps.ford.com/api/users/vehicles")
|
||||
req, err := v.request(http.MethodGet, fordVehicleList)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
|
@ -134,36 +199,112 @@ func (v *Ford) vehicles() ([]string, error) {
|
|||
return vehicles, err
|
||||
}
|
||||
|
||||
// chargeState implements the api.Vehicle interface
|
||||
func (v *Ford) chargeState() (float64, error) {
|
||||
var resp struct {
|
||||
VehicleStatus struct {
|
||||
Battery struct {
|
||||
BatteryStatusActual struct {
|
||||
Value int
|
||||
}
|
||||
}
|
||||
BatteryFillLevel struct {
|
||||
Value float64
|
||||
}
|
||||
}
|
||||
// vehicleStatus 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)
|
||||
|
||||
// "chargingStatus": { "value": "EvseNotDetected", ...
|
||||
// "plugStatus": { "value": 1, ...
|
||||
// "batteryChargeStatus": null, ...
|
||||
var res fordVehicleStatus
|
||||
req, err := v.request(http.MethodGet, uri)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v4/%s/status", fordAPI, v.vin)
|
||||
if err == nil {
|
||||
var lastUpdate time.Time
|
||||
lastUpdate, err = time.Parse(fordTimeFormat, res.VehicleStatus.LastRefresh)
|
||||
|
||||
req, err := v.request(uri)
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
var resp struct {
|
||||
CommandId string
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v2/%s/status", fordAPI, v.vin)
|
||||
req, err := v.request(http.MethodPut, uri)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
||||
return resp.VehicleStatus.BatteryFillLevel.Value, err
|
||||
return resp.CommandId, err
|
||||
}
|
||||
|
||||
// SoC implements the api.Vehicle interface
|
||||
var _ api.Battery = (*Ford)(nil)
|
||||
|
||||
// SoC implements the api.Battery interface
|
||||
func (v *Ford) SoC() (float64, error) {
|
||||
return v.chargeStateG()
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fordVehicleStatus); err == nil && ok {
|
||||
return float64(res.VehicleStatus.BatteryFillLevel.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Ford)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Ford) Range() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fordVehicleStatus); err == nil && ok {
|
||||
return int64(res.VehicleStatus.ElVehDTE.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Ford)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Ford) Status() (api.ChargeStatus, error) {
|
||||
status := api.StatusA // disconnected
|
||||
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fordVehicleStatus); err == nil && ok {
|
||||
if res.VehicleStatus.PlugStatus.Value == 1 {
|
||||
status = api.StatusB // connected, not charging
|
||||
}
|
||||
if res.VehicleStatus.ChargingStatus.Value == "ChargingAC" {
|
||||
status = api.StatusC // charging
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue