diff --git a/internal/vehicle/ford.go b/internal/vehicle/ford.go index b6a577129..c9380801c 100644 --- a/internal/vehicle/ford.go +++ b/internal/vehicle/ford.go @@ -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 }