PSA: refactor authentication (#1319)

This commit is contained in:
andig 2021-08-10 10:11:12 +02:00 • committed by GitHub
parent dea21f7628
commit d2b376fceb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 100 additions and 55 deletions

View file

@ -199,6 +199,7 @@ Available charger implementations are:
- `custom`: default charger implementation using configurable [plugins](#plugins) for integrating any type of charger
Smart-Home outlet charger implementations:
- `fritzdect`: Fritz!DECT 200/210 outlets
- `shelly`: Shelly outlets
- `tasmota`: Tasmota outlets
@ -261,7 +262,7 @@ Available vehicle remote interface implementations are:
- `audi`: Audi (eTron, Q55)
- `bmw`: BMW (i3)
- `carwings`: Nissan (Leaf pre 2019)
- `citroen`, `opel`, `peugeot`: Follow this [tutorial](https://github.com/flobz/psa_car_controller) to obtain client credentials for PSA.
- `citroen`, `ds`, `opel`, `peugeot`: All PSA brands
- `fiat`: Fiat (500e)
- `ford`: Ford (Kuga, Mustang)
- `kia`: Kia (Soul and other Bluelink models)

View file

@ -50,14 +50,14 @@ func NewNissanFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("nissan")
identity := nissan.NewIdentity(log)
err := identity.Login(cc.User, cc.Password)
if err != nil {
if err := identity.Login(cc.User, cc.Password); err != nil {
return v, fmt.Errorf("login failed: %w", err)
}
api := nissan.NewAPI(log, identity, strings.ToUpper(cc.VIN))
if err == nil && cc.VIN == "" {
var err error
if cc.VIN == "" {
api.VIN, err = findVehicle(api.Vehicles())
if err == nil {
log.DEBUG.Printf("found vehicle: %v", api.VIN)

View file

@ -11,10 +11,11 @@ import (
"github.com/andig/evcc/vehicle/psa"
)
// https://github.com/flobz/psa_car_controller
// https://github.com/TA2k/ioBroker.psa
func init() {
registry.Add("citroen", NewCitroenFromConfig)
registry.Add("ds", NewDSFromConfig)
registry.Add("opel", NewOpelFromConfig)
registry.Add("peugeot", NewPeugeotFromConfig)
}
@ -22,19 +23,37 @@ func init() {
// NewCitroenFromConfig creates a new vehicle
func NewCitroenFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("citroen")
return newPSA(log, "citroen.com", "clientsB2CCitroen", other)
return newPSA(log,
"citroen.com", "clientsB2CCitroen",
"5364defc-80e6-447b-bec6-4af8d1542cae", "iE0cD8bB0yJ0dS6rO3nN1hI2wU7uA5xR4gP7lD6vM0oH0nS8dN",
other)
}
// NewDSFromConfig creates a new vehicle
func NewDSFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("ds")
return newPSA(log,
"driveds.com", "clientsB2CDS",
"cbf74ee7-a303-4c3d-aba3-29f5994e2dfa", "X6bE6yQ3tH1cG5oA6aW4fS6hK0cR0aK5yN2wE4hP8vL8oW5gU3",
other)
}
// NewOpelFromConfig creates a new vehicle
func NewOpelFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("opel")
return newPSA(log, "opel.com", "clientsB2COpel", other)
return newPSA(log,
"opel.com", "clientsB2COpel",
"07364655-93cb-4194-8158-6b035ac2c24c", "F2kK7lC5kF5qN7tM0wT8kE3cW1dP0wC5pI6vC0sQ5iP5cN8cJ8",
other)
}
// NewPeugeotFromConfig creates a new vehicle
func NewPeugeotFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("peugeot")
return newPSA(log, "peugeot.com", "clientsB2CPeugeot", other)
return newPSA(log,
"peugeot.com", "clientsB2CPeugeot",
"1eebc2d5-5df3-459b-a624-20abfcf82530", "T5tP7iS0cO8sC0lA2iE2aR7gK6uE5rF3lJ8pC3nO1pR7tL8vU1",
other)
}
// PSA is an api.Vehicle implementation for PSA cars
@ -44,13 +63,17 @@ type PSA struct {
}
// newPSA creates a new vehicle
func newPSA(log *util.Logger, brand, realm string, other map[string]interface{}) (api.Vehicle, error) {
func newPSA(log *util.Logger, brand, realm, id, secret string, other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
ClientID, ClientSecret string
User, Password, VIN string
Cache time.Duration
embed `mapstructure:",squash"`
Credentials ClientCredentials
User, Password, VIN string
Cache time.Duration
}{
Credentials: ClientCredentials{
ID: id,
Secret: secret,
},
Cache: interval,
}
@ -58,20 +81,18 @@ func newPSA(log *util.Logger, brand, realm string, other map[string]interface{})
return nil, err
}
if cc.ClientID == "" || cc.ClientSecret == "" {
return nil, errors.New("missing client id and/or secret (see https://github.com/flobz/psa_car_controller)")
}
v := &PSA{
embed: &cc.embed,
}
api := psa.NewAPI(log, brand, realm, cc.ClientID, cc.ClientSecret)
err := api.Login(cc.User, cc.Password)
if err != nil {
identity := psa.NewIdentity(log, brand, cc.Credentials.ID, cc.Credentials.Secret)
if err := identity.Login(cc.User, cc.Password); err != nil {
return v, fmt.Errorf("login failed: %w", err)
}
api := psa.NewAPI(log, identity, realm, cc.Credentials.ID)
vehicles, err := api.Vehicles()
if err != nil {
return nil, err

View file

@ -1,7 +1,6 @@
package psa
import (
"context"
"fmt"
"net/http"
"net/url"
@ -12,7 +11,6 @@ import (
"golang.org/x/oauth2"
)
// https://github.com/flobz/psa_car_controller
// https://developer.groupe-psa.io/webapi/b2c/api-reference/specification
// BaseURL is the API base url
@ -21,51 +19,27 @@ const BaseURL = "https://api.groupe-psa.com/connectedcar/v4"
// API is an api.Vehicle implementation for PSA cars
type API struct {
*request.Helper
brand, realm string
id, secret string // client
realm string
id string
}
// NewAPI creates a new vehicle
func NewAPI(log *util.Logger, brand, realm, id, secret string) *API {
func NewAPI(log *util.Logger, identity oauth2.TokenSource, realm, id string) *API {
v := &API{
Helper: request.NewHelper(log),
brand: brand,
realm: realm,
id: id,
secret: secret,
}
// replace client transport with authenticated transport
v.Client.Transport = &oauth2.Transport{
Source: identity,
Base: v.Client.Transport,
}
return v
}
// Login performs the login
func (v *API) Login(user, password string) error {
config := oauth2.Config{
ClientID: v.id,
ClientSecret: v.secret,
Endpoint: oauth2.Endpoint{
AuthURL: "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize",
TokenURL: fmt.Sprintf("https://idpcvs.%s/am/oauth2/access_token", v.brand),
AuthStyle: oauth2.AuthStyleInHeader,
},
Scopes: []string{"openid profile"},
}
ctx := context.WithValue(
context.Background(),
oauth2.HTTPClient,
v.Client,
)
// replace client with authenticated oauth client
token, err := config.PasswordCredentialsToken(ctx, user, password)
if err == nil {
v.Client = config.Client(ctx, token)
}
return err
}
// Vehicle is a single vehicle
type Vehicle struct {
ID string `json:"id"`

49
vehicle/psa/identity.go Normal file
View file

@ -0,0 +1,49 @@
package psa
import (
"context"
"fmt"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/request"
"golang.org/x/oauth2"
)
type Identity struct {
*request.Helper
oc *oauth2.Config
oauth2.TokenSource
}
// NewIdentity creates PSA identity
func NewIdentity(log *util.Logger, brand, id, secret string) *Identity {
return &Identity{
Helper: request.NewHelper(log),
oc: &oauth2.Config{
ClientID: id,
ClientSecret: secret,
Endpoint: oauth2.Endpoint{
AuthURL: "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize",
TokenURL: fmt.Sprintf("https://idpcvs.%s/am/oauth2/access_token", brand),
AuthStyle: oauth2.AuthStyleInHeader,
},
Scopes: []string{"openid profile"},
},
}
}
func (v *Identity) Login(user, password string) error {
ctx := context.WithValue(
context.Background(),
oauth2.HTTPClient,
v.Client,
)
// replace client with authenticated oauth client
token, err := v.oc.PasswordCredentialsToken(ctx, user, password)
if err == nil {
v.TokenSource = v.oc.TokenSource(ctx, token)
}
return err
}