Skip to content

Commit

Permalink
Add FordConnect api (#14069)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed May 26, 2024
1 parent 52a2a59 commit 19ffe52
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 5 deletions.
2 changes: 2 additions & 0 deletions cmd/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func runToken(cmd *cobra.Command, args []string) {
switch typ {
case "mercedes":
token, err = mercedesToken()
case "ford", "ford-connect":
token, err = fordConnectToken(vehicleConf)
case "tronity":
token, err = tronityToken(conf, vehicleConf)
case "citroen", "ds", "opel", "peugeot":
Expand Down
55 changes: 55 additions & 0 deletions cmd/token_ford-connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"context"

"github.com/AlecAivazis/survey/v2"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/vehicle"
"github.com/evcc-io/evcc/vehicle/ford/connect"
"golang.org/x/oauth2"
)

func fordConnectToken(conf config.Named) (*oauth2.Token, error) {
var cc struct {
Credentials vehicle.ClientCredentials
Tokens vehicle.Tokens
}

if err := util.DecodeOther(conf.Other, &cc); err != nil {
return nil, err
}

if cc.Credentials.ID == "" {
if err := survey.AskOne(&survey.Input{
Message: "Please enter your client id:",
}, &cc.Credentials.ID, survey.WithValidator(survey.Required)); err != nil {
return nil, err
}
}

if cc.Credentials.Secret == "" {
if err := survey.AskOne(&survey.Input{
Message: "Please enter your client secret:",
}, &cc.Credentials.Secret, survey.WithValidator(survey.Required)); err != nil {
return nil, err
}
}

var code string
if err := survey.AskOne(&survey.Input{
Message: "Please enter your authorization code:",
}, &code, survey.WithValidator(survey.Required)); err != nil {
return nil, err
}

cv := oauth2.GenerateVerifier()
oc := connect.Oauth2Config(cc.Credentials.ID, cc.Credentials.Secret)

client := request.NewClient(util.NewLogger("ford-connect"))
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client)

return oc.Exchange(ctx, code, oauth2.VerifierOption(cv))
}
53 changes: 53 additions & 0 deletions templates/definition/vehicle/ford-connect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
template: ford-connect
products:
- brand: Ford
params:
- name: title
- name: icon
default: car
advanced: true
- name: clientid
description:
generic: FordConnect API Client ID
help:
de: Einrichtung unter https://developer.ford.com
en: Setup at https://developer.ford.com
required: true
- name: clientsecret
description:
generic: FordConnect API Client Secret
help:
de: Einrichtung unter https://developer.ford.com
en: Setup at https://developer.ford.com
required: true
- name: accessToken
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#ford-connect"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#ford-connect"
- name: refreshToken
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#ford-connect"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#ford-connect"
- name: vin
example: WF0FXX...
- name: capacity
default: 10
- name: phases
advanced: true
- preset: vehicle-identify
render: |
type: ford-connect
credentials:
id: {{ .clientid }}
secret: {{ .clientsecret }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
vin: {{ .vin }}
capacity: {{ .capacity }}
phases: {{ .phases }}
{{ include "vehicle-identify" . }}
1 change: 1 addition & 0 deletions templates/definition/vehicle/ford.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
template: ford
deprecated: true
products:
- brand: Ford
params:
Expand Down
6 changes: 1 addition & 5 deletions templates/definition/vehicle/tronity.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,7 @@ render: |
credentials:
id: {{ .clientid }}
secret: {{ .clientsecret }}
vin: {{ .vin }}
capacity: {{ .capacity }}
{{- if .phases }}
phases: {{ .phases }}
{{- end }}
{{- if .vin }}
vin: {{ .vin }}
{{- end }}
{{ include "vehicle-identify" . }}
82 changes: 82 additions & 0 deletions vehicle/ford-connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package vehicle

import (
"time"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle/ford/connect"
)

// https://developer.ford.com/apis/fordconnect

// FordConnect is an api.Vehicle implementation for Ford cars
type FordConnect struct {
*embed
*connect.Provider
}

func init() {
registry.Add("ford-connect", NewFordConnectFromConfig)
}

// NewFordConnectFromConfig creates a new vehicle
func NewFordConnectFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
Credentials ClientCredentials
Tokens Tokens
VIN string
Cache time.Duration
}{
Cache: interval,
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

v := &FordConnect{
embed: &cc.embed,
}

if err := cc.Credentials.Error(); err != nil {
return nil, err
}

token, err := cc.Tokens.Token()
if err != nil {
return nil, err
}

log := util.NewLogger("ford").Redact(cc.VIN)
identity := connect.NewIdentity(log, cc.Credentials.ID, cc.Credentials.Secret, token)

api := connect.NewAPI(log, identity)

var vinErr error
vehicle, err := ensureVehicleEx(cc.VIN, api.Vehicles, func(v connect.Vehicle) string {
if vinErr != nil {
return ""
}
vin, err := api.VIN(v.VehicleID)
if err != nil {
vinErr = err
}
return vin
})
if err == nil {
err = vinErr
}
if err != nil {
return nil, err
}

if v.Title_ == "" {
v.Title_ = vehicle.NickName
}

v.Provider = connect.NewProvider(api, vehicle.VehicleID, cc.Cache)

return v, err
}
71 changes: 71 additions & 0 deletions vehicle/ford/connect/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package connect

import (
"fmt"

"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.mps.ford.com/api/fordconnect"

// 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{
Base: &oauth2.Transport{
Source: ts,
Base: v.Client.Transport,
},
Decorator: transport.DecorateHeaders(map[string]string{
"Application-Id": ApplicationID,
}),
}

return v
}

// Vehicles returns the list of user vehicles
func (v *API) Vehicles() ([]Vehicle, error) {
var res VehiclesResponse

uri := fmt.Sprintf("%s/v3/vehicles", ApiURI)
err := v.GetJSON(uri, &res)

return res.Vehicles, err
}

// VIN returns the vehicle's vIN
func (v *API) VIN(id string) (string, error) {
var res struct {
VIN string
}

uri := fmt.Sprintf("%s/v3/vehicles/%s/vin", ApiURI, id)
err := v.GetJSON(uri, &res)

return res.VIN, err
}

func (v *API) Status(vin string) (Vehicle, error) {
var res InformationResponse

uri := fmt.Sprintf("%s/v3/vehicles/%s", ApiURI, vin)
err := v.GetJSON(uri, &res)

if err == nil && res.Status != StatusSuccess {
err = fmt.Errorf("status %s", res.Status)
}

return res.Vehicle, err
}
39 changes: 39 additions & 0 deletions vehicle/ford/connect/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package connect

import (
"context"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
)

const (
ApplicationID = "AFDC085B-377A-4351-B23E-5E1D35FB3700"
baseURL = "https://dah2vb2cprod.b2clogin.com/914d88b1-3523-4bf6-9be4-1b96b4f6f919/oauth2/v2.0/token?p=B2C_1A_signup_signin_common"
)

func Oauth2Config(id, secret string) *oauth2.Config {
return &oauth2.Config{
ClientID: id,
ClientSecret: secret,
Endpoint: oauth2.Endpoint{
AuthURL: baseURL,
TokenURL: baseURL,
},
RedirectURL: "https://localhost:3000",
Scopes: []string{
oidc.ScopeOpenID,
oidc.ScopeOfflineAccess,
},
}
}

// NewIdentity creates FordConnect token source
func NewIdentity(log *util.Logger, id, secret string, token *oauth2.Token) oauth2.TokenSource {
oc := Oauth2Config(id, secret)
client := request.NewClient(log)
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client)
return oc.TokenSource(ctx, token)
}
Loading

0 comments on commit 19ffe52

Please sign in to comment.