PSA: refactor authentication (#1319)
This commit is contained in:
parent
dea21f7628
commit
d2b376fceb
5 changed files with 100 additions and 55 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
49
vehicle/psa/identity.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue