From 628a12ee45ffd7f35f5994c3f6e878710e6e7cd7 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 24 Nov 2025 11:03:54 +0100 Subject: [PATCH] Ford: remove legacy integration (BC) (#25516) --- templates/definition/vehicle/ford.yaml | 21 --- vehicle/ford.go | 73 -------- vehicle/ford/api.go | 73 -------- vehicle/ford/autonomic/api.go | 87 ---------- vehicle/ford/autonomic/identity.go | 78 --------- vehicle/ford/autonomic/types.go | 32 ---- vehicle/ford/identity.go | 229 ------------------------- vehicle/ford/provider.go | 100 ----------- vehicle/ford/types.go | 9 - 9 files changed, 702 deletions(-) delete mode 100644 templates/definition/vehicle/ford.yaml delete mode 100644 vehicle/ford.go delete mode 100644 vehicle/ford/api.go delete mode 100644 vehicle/ford/autonomic/api.go delete mode 100644 vehicle/ford/autonomic/identity.go delete mode 100644 vehicle/ford/autonomic/types.go delete mode 100644 vehicle/ford/identity.go delete mode 100644 vehicle/ford/provider.go delete mode 100644 vehicle/ford/types.go diff --git a/templates/definition/vehicle/ford.yaml b/templates/definition/vehicle/ford.yaml deleted file mode 100644 index f2978464e..000000000 --- a/templates/definition/vehicle/ford.yaml +++ /dev/null @@ -1,21 +0,0 @@ -template: ford -deprecated: true -products: - - brand: Ford -requirements: - description: - de: "Hinweis: Ford hat kürzlich den API-Zugriff für seine Benutzer deaktiviert." - en: "Note: Ford has recently disabled API-access for their users." -params: - - preset: vehicle-base - - name: vin - example: WF0FXX... - - name: domain - type: choice - choice: ["com", "de"] - default: com - required: true -render: | - type: ford - {{ include "vehicle-base" . }} - domain: {{ .domain }} diff --git a/vehicle/ford.go b/vehicle/ford.go deleted file mode 100644 index f774a898d..000000000 --- a/vehicle/ford.go +++ /dev/null @@ -1,73 +0,0 @@ -package vehicle - -import ( - "fmt" - "time" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/vehicle/ford" - "github.com/evcc-io/evcc/vehicle/ford/autonomic" -) - -// https://github.com/d4v3y0rk/ffpass-module -// https://github.com/ianjwhite99/connected-car-node-sdk -// https://github.com/TA2k/ioBroker.ford - -// Ford is an api.Vehicle implementation for Ford cars -type Ford struct { - *embed - *ford.Provider -} - -func init() { - registry.Add("ford", NewFordFromConfig) -} - -// NewFordFromConfig creates a new vehicle -func NewFordFromConfig(other map[string]any) (api.Vehicle, error) { - cc := struct { - embed `mapstructure:",squash"` - User, Password, VIN string - Domain string - Cache time.Duration - }{ - Domain: "com", - Cache: interval, - } - - if err := util.DecodeOther(other, &cc); err != nil { - return nil, err - } - - if cc.User == "" || cc.Password == "" { - return nil, api.ErrMissingCredentials - } - - v := &Ford{ - embed: &cc.embed, - } - - log := util.NewLogger("ford").Redact(cc.User, cc.Password, cc.VIN) - identity := ford.NewIdentity(log, cc.User, cc.Password, cc.Domain) - - err := identity.Login() - if err != nil { - return nil, fmt.Errorf("login failed: %w", err) - } - - api := ford.NewAPI(log, identity) - - cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) - if err != nil { - return nil, err - } - - autoIdentity, err := autonomic.NewIdentity(log, identity) - if err == nil { - api := autonomic.NewAPI(log, autoIdentity) - v.Provider = ford.NewProvider(api, cc.VIN, cc.Cache) - } - - return v, err -} diff --git a/vehicle/ford/api.go b/vehicle/ford/api.go deleted file mode 100644 index 606a7ca6f..000000000 --- a/vehicle/ford/api.go +++ /dev/null @@ -1,73 +0,0 @@ -package ford - -import ( - "fmt" - "net/http" - - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/request" - "github.com/evcc-io/evcc/util/transport" - "golang.org/x/oauth2" -) - -const ApiURI = "https://usapi.cv.ford.com" - -// API is the Ford api client -type API struct { - *request.Helper -} - -// NewAPI creates a new api client -func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API { - v := &API{ - Helper: request.NewHelper(log), - } - - v.Client.Transport = &transport.Decorator{ - Decorator: func(req *http.Request) error { - token, err := ts.Token() - if err != nil { - return err - } - - for k, v := range map[string]string{ - "Content-type": request.JSONContent, - "User-Agent": "FordPass/5 CFNetwork/1333.0.4 Darwin/21.5.0", - "locale": "de-DE", - "Application-Id": ApplicationID, - "Auth-Token": token.AccessToken, - "CountryCode": "DEU", - } { - req.Header.Set(k, v) - } - - return nil - }, - Base: v.Client.Transport, - } - - return v -} - -// Vehicles returns the list of user vehicles -func (v *API) Vehicles() ([]string, error) { - var res []string - - data := map[string]string{ - "dashboardRefreshRequest": "All", - } - - uri := fmt.Sprintf("%s/api/expdashboard/v1/details", TokenURI) - - req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) - if err == nil { - var resp VehiclesResponse - if err = v.DoJSON(req, &resp); err == nil { - for _, v := range resp.UserVehicles.VehicleDetails { - res = append(res, v.VIN) - } - } - } - - return res, err -} diff --git a/vehicle/ford/autonomic/api.go b/vehicle/ford/autonomic/api.go deleted file mode 100644 index 7cc9fff1a..000000000 --- a/vehicle/ford/autonomic/api.go +++ /dev/null @@ -1,87 +0,0 @@ -package autonomic - -import ( - "fmt" - "net/http" - "strings" - - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/request" - "github.com/evcc-io/evcc/util/transport" - "golang.org/x/oauth2" -) - -const ApiURI = "https://api.autonomic.ai/v1beta/telemetry/sources/fordpass" - -var ( - ApplicationID = "667D773E-1BDC-4139-8AD0-2B16474E8DC7" - Dyna = "MT_3_30_2352378557_3-0_" + "uuidv4()" + "_0_789_87" -) - -// API is the Ford api client -type API struct { - *request.Helper -} - -// NewAPI creates a new api client -func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API { - v := &API{ - Helper: request.NewHelper(log), - } - - v.Client.Transport = &transport.Decorator{ - Decorator: func(req *http.Request) error { - token, err := ts.Token() - if err != nil { - return err - } - - for k, v := range map[string]string{ - "Content-type": request.JSONContent, - "Application-Id": ApplicationID, - "x-dynatrace": Dyna, - "Authorization": "Bearer " + token.AccessToken, - } { - req.Header.Set(k, v) - } - - return nil - }, - Base: v.Client.Transport, - } - - return v -} - -// Status retrieves a metrics result using :query -func (v *API) Status(vin string) (MetricsResponse, error) { - var res MetricsResponse - - uri := fmt.Sprintf("%s/vehicles/%s:query", ApiURI, vin) - req, err := request.New(http.MethodPost, uri, strings.NewReader("{}"), request.JSONEncoding) - if err == nil { - err = v.DoJSON(req, &res) - } - - return res, err -} - -// Refresh executes the refresh command -func (v *API) Refresh(vin string) (any, error) { - var res map[string]any - - data := map[string]any{ - "properties": struct{}{}, - "tags": struct{}{}, - "type": "statusRefresh", - "wakeUp": true, - } - - uri := fmt.Sprintf("%s/vehicles/%s/command", ApiURI, vin) - req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) - if err == nil { - err = v.DoJSON(req, &res) - } - - return res, err -} diff --git a/vehicle/ford/autonomic/identity.go b/vehicle/ford/autonomic/identity.go deleted file mode 100644 index ebfa69b60..000000000 --- a/vehicle/ford/autonomic/identity.go +++ /dev/null @@ -1,78 +0,0 @@ -package autonomic - -import ( - "net/http" - "net/url" - "strings" - - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/oauth" - "github.com/evcc-io/evcc/util/request" - "golang.org/x/oauth2" -) - -// https://accounts.autonomic.ai/v1/auth/oidc/.well-known/openid-configuration/ -var OAuth2Config = &oauth2.Config{ - Endpoint: oauth2.Endpoint{ - AuthURL: "https://accounts.autonomic.ai/v1/auth/oidc/authorize", - TokenURL: "https://accounts.autonomic.ai/v1/auth/oidc/token", - }, - Scopes: []string{"openid"}, -} - -type Identity struct { - *request.Helper - ts oauth2.TokenSource - oauth2.TokenSource -} - -// NewIdentity creates autonomic token source -func NewIdentity(log *util.Logger, ts oauth2.TokenSource) (*Identity, error) { - v := &Identity{ - Helper: request.NewHelper(log), - ts: ts, - } - - token, err := ts.Token() - if err != nil { - return nil, err - } - - auto, err := v.exchange(token) - if err != nil { - return nil, err - } - - v.TokenSource = oauth.RefreshTokenSource(auto, v) - - return v, nil -} - -// exchange authenticates with username/password to get new aws credentials -func (v *Identity) exchange(token *oauth2.Token) (*oauth2.Token, error) { - data := url.Values{ - "subject_token": {token.AccessToken}, - "subject_issuer": {"fordpass"}, - "client_id": {"fordpass-prod"}, - "grant_type": {"urn:ietf:params:oauth:grant-type:token-exchange"}, - "subject_token_type": {"urn:ietf:params:oauth:token-type:jwt"}, - } - - var res *oauth2.Token - req, err := request.New(http.MethodPost, OAuth2Config.Endpoint.TokenURL, strings.NewReader(data.Encode()), request.URLEncoding) - if err == nil { - err = v.DoJSON(req, &res) - } - - return util.TokenWithExpiry(res), err -} - -// RefreshToken implements oauth.TokenRefresher -func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) { - token, err := v.ts.Token() - if err != nil { - return nil, err - } - - return v.exchange(token) -} diff --git a/vehicle/ford/autonomic/types.go b/vehicle/ford/autonomic/types.go deleted file mode 100644 index db69030d7..000000000 --- a/vehicle/ford/autonomic/types.go +++ /dev/null @@ -1,32 +0,0 @@ -package autonomic - -import "time" - -type IntValue struct { - UpdateTime time.Time - Value int -} -type FloatValue struct { - UpdateTime time.Time - Value float64 -} -type StringValue struct { - UpdateTime time.Time - Value string -} - -type MetricsResponse struct { - Metrics struct { - Position struct { - Value struct { - Location struct { - Lat, Lon float64 - } - } - } - Odometer FloatValue - XevPlugChargerStatus StringValue - XevBatteryRange FloatValue - XevBatteryStateOfCharge FloatValue - } -} diff --git a/vehicle/ford/identity.go b/vehicle/ford/identity.go deleted file mode 100644 index b01b37b5d..000000000 --- a/vehicle/ford/identity.go +++ /dev/null @@ -1,229 +0,0 @@ -package ford - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/cookiejar" - "net/url" - "regexp" - "strings" - - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/oauth" - "github.com/evcc-io/evcc/util/request" - "github.com/samber/lo" - "golang.org/x/oauth2" -) - -const ( - TokenURI = "https://api.mps.ford.com" - LoginUriTmpl = "https://login.ford.%s/4566605f-43a7-400a-946e-89cc9fdb0bd7/B2C_1A_SignInSignUp_de-DE" - ClientID = "09852200-05fd-41f6-8c21-d36d3497dc64" - ApplicationID = "1E8C7794-FF5F-49BC-9596-A1E0C86C5B19" -) - -var loginHeaders = map[string]string{ - "Accept": "*/*", - "Accept-Language": "en-us", - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1", -} - -func OAuth2Config(domain string) *oauth2.Config { - uri := loginUri(domain) - return &oauth2.Config{ - ClientID: ClientID, - Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf("%s/oauth2/v2.0/authorize", uri), - TokenURL: fmt.Sprintf("%s/oauth2/v2.0/token", uri), - }, - RedirectURL: "fordapp://userauthorized", - Scopes: []string{ClientID, "openid"}, - } -} - -func loginUri(domain string) string { - return fmt.Sprintf(LoginUriTmpl, domain) -} - -type Settings struct { - TransId string `json:"transId"` - Csrf string `json:"csrf"` -} - -type Identity struct { - *request.Helper - user, password, domain string - oauth2.TokenSource -} - -// NewIdentity creates Ford identity -func NewIdentity(log *util.Logger, user, password, domain string) *Identity { - return &Identity{ - Helper: request.NewHelper(log), - user: user, - password: password, - domain: domain, - } -} - -// Login authenticates with username/password to get new aws credentials -func (v *Identity) Login() error { - token, err := v.login() - if err == nil { - v.TokenSource = oauth.RefreshTokenSource(token, v) - } - return err -} - -// login authenticates with username/password to get new token -func (v *Identity) login() (*oauth2.Token, error) { - oc := OAuth2Config(v.domain) - - cv := oauth2.GenerateVerifier() - state := lo.RandomString(16, lo.AlphanumericCharset) - uri := oc.AuthCodeURL(state, oauth2.S256ChallengeOption(cv), - oauth2.SetAuthURLParam("max_age", "3600"), - oauth2.SetAuthURLParam("ui_locales", "de-DE"), - oauth2.SetAuthURLParam("language_code", "de-DE"), - oauth2.SetAuthURLParam("country_code", "DE"), - oauth2.SetAuthURLParam("ford_application_id", ApplicationID), - ) - - v.Jar, _ = cookiejar.New(nil) - defer func() { v.Jar = nil }() - - var body []byte - req, err := request.New(http.MethodGet, uri, nil, loginHeaders) - if err == nil { - body, err = v.DoBody(req) - } - - if err != nil { - return nil, err - } - - match := regexp.MustCompile(`var SETTINGS = (\{[^;]*\});`).FindSubmatch(body) - if len(match) < 2 { - return nil, errors.New("missing settings variable") - } - - var settings Settings - - if err := json.Unmarshal(match[1], &settings); err != nil { - return nil, err - } - - data := url.Values{ - "request_type": {"RESPONSE"}, - "signInName": {v.user}, - "password": {v.password}, - } - - v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if req.URL.Scheme != "https" { - return http.ErrUseLastResponse - } - return nil - } - defer func() { v.Client.CheckRedirect = nil }() - - uri2 := fmt.Sprintf("%s/SelfAsserted?tx=%s&p=B2C_1A_SignInSignUp_de-DE", loginUri(v.domain), settings.TransId) - req, err = request.New(http.MethodPost, uri2, strings.NewReader(data.Encode()), request.URLEncoding, map[string]string{ - "Accept": "*/*", - "Accept-Language": "en-us", - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1", - "Origin": "https://login.ford.com", - "Referer": uri, - "X-Csrf-Token": settings.Csrf, - }) - - if err == nil { - var resp *http.Response - if resp, err = v.Do(req); err == nil { - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, errors.New("self-assert failed") - } - } - } - - uri3 := fmt.Sprintf("%s/api/CombinedSigninAndSignup/confirmed?rememberMe=false&csrf_token=%s&tx=%s&p=B2C_1A_SignInSignUp_de-DE", loginUri(v.domain), settings.Csrf, settings.TransId) - req, err = request.New(http.MethodGet, uri3, nil, request.URLEncoding, map[string]string{ - "Origin": "https://login.ford.com", - "Referer": uri, - "X-Csrf-Token": settings.Csrf, - }) - - var loc *url.URL - if err == nil { - var resp *http.Response - if resp, err = v.Do(req); err == nil { - defer resp.Body.Close() - loc, err = url.Parse(resp.Header.Get("location")) - } - } - - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout( - context.WithValue(context.Background(), oauth2.HTTPClient, v.Client), - request.Timeout, - ) - defer cancel() - - code := loc.Query().Get("code") - if code == "" { - return nil, errors.New("could not obtain auth code- check user and password") - } - - tok, err := oc.Exchange(ctx, code, oauth2.VerifierOption(cv)) - - // exchange code for api token - var token oauth2.Token - if err == nil { - data := map[string]string{ - "idpToken": tok.AccessToken, - } - - uri := fmt.Sprintf("%s/api/token/v2/cat-with-b2c-access-token", TokenURI) - - var req *http.Request - req, err = request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{ - "Content-type": request.JSONContent, - "Application-Id": ApplicationID, - }) - - if err == nil { - err = v.DoJSON(req, &token) - } - } - - return util.TokenWithExpiry(&token), err -} - -// RefreshToken implements oauth.TokenRefresher -func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { - data := map[string]string{ - "refresh_token": token.RefreshToken, - } - - uri := fmt.Sprintf("%s/api/token/v2/cat-with-refresh-token", TokenURI) - req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{ - "Content-type": request.JSONContent, - "Application-Id": ApplicationID, - }) - - var res *oauth2.Token - err := v.DoJSON(req, &res) - if err != nil { - res, err = v.login() - } - - return util.TokenWithExpiry(res), err -} diff --git a/vehicle/ford/provider.go b/vehicle/ford/provider.go deleted file mode 100644 index 1bf4dc876..000000000 --- a/vehicle/ford/provider.go +++ /dev/null @@ -1,100 +0,0 @@ -package ford - -import ( - "fmt" - "time" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/vehicle/ford/autonomic" -) - -type Provider struct { - statusG func() (autonomic.MetricsResponse, error) - refreshG func() error -} - -func NewProvider(api *autonomic.API, vin string, cache time.Duration) *Provider { - impl := &Provider{ - statusG: util.Cached(func() (autonomic.MetricsResponse, error) { - return api.Status(vin) - }, cache), - refreshG: func() error { - _, err := api.Refresh(vin) - return err - }, - } - - return impl -} - -var _ api.Battery = (*Provider)(nil) - -// Soc implements the api.Battery interface -func (v *Provider) Soc() (float64, error) { - res, err := v.statusG() - if err == nil { - return res.Metrics.XevBatteryStateOfCharge.Value, nil - } - - return 0, err -} - -var _ api.VehicleRange = (*Provider)(nil) - -// Range implements the api.VehicleRange interface -func (v *Provider) Range() (int64, error) { - res, err := v.statusG() - if err == nil { - return int64(res.Metrics.XevBatteryRange.Value), nil - } - - return 0, err -} - -var _ api.ChargeState = (*Provider)(nil) - -// Status implements the api.ChargeState interface -func (v *Provider) Status() (api.ChargeStatus, error) { - status := api.StatusNone - - res, err := v.statusG() - if err == nil { - switch res.Metrics.XevPlugChargerStatus.Value { - case "DISCONNECTED": - status = api.StatusA // disconnected - case "CONNECTED": - status = api.StatusB // connected, not charging - case "CHARGING", "CHARGINGAC": - status = api.StatusC // charging - default: - err = fmt.Errorf("unknown charge status: %s", res.Metrics.XevPlugChargerStatus.Value) - } - } - - return status, err -} - -var _ api.VehicleOdometer = (*Provider)(nil) - -// Odometer implements the api.VehicleOdometer interface -func (v *Provider) Odometer() (float64, error) { - res, err := v.statusG() - return res.Metrics.Odometer.Value, err -} - -var _ api.VehiclePosition = (*Provider)(nil) - -// Position implements the api.VehiclePosition interface -func (v *Provider) Position() (float64, float64, error) { - res, err := v.statusG() - loc := res.Metrics.Position.Value.Location - return loc.Lat, loc.Lon, err -} - -var _ api.Resurrector = (*Provider)(nil) - -// WakeUp implements the api.Resurrector interface -func (v *Provider) WakeUp() error { - return v.refreshG() -} diff --git a/vehicle/ford/types.go b/vehicle/ford/types.go deleted file mode 100644 index 3d4f80abb..000000000 --- a/vehicle/ford/types.go +++ /dev/null @@ -1,9 +0,0 @@ -package ford - -type VehiclesResponse struct { - UserVehicles struct { - VehicleDetails []struct { - VIN string - } - } -}