Skip to content

Commit

Permalink
PSA: change authentication from user/password to token (#13612)
Browse files Browse the repository at this point in the history
  • Loading branch information
hurzhurz authored May 1, 2024
1 parent fd653ec commit db88086
Show file tree
Hide file tree
Showing 10 changed files with 378 additions and 95 deletions.
10 changes: 9 additions & 1 deletion cmd/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,19 @@ func runToken(cmd *cobra.Command, args []string) {
var token *oauth2.Token
var err error

switch strings.ToLower(vehicleConf.Type) {
typ := strings.ToLower(vehicleConf.Type)
if typ == "template" {
typ = strings.ToLower(vehicleConf.Other["template"].(string))
}

switch typ {
case "mercedes":
token, err = mercedesToken()
case "tronity":
token, err = tronityToken(conf, vehicleConf)
case "citroen", "ds", "opel", "peugeot":
token, err = psaToken(typ)

default:
log.FATAL.Fatalf("vehicle type '%s' does not support token authentication", vehicleConf.Type)
}
Expand Down
44 changes: 44 additions & 0 deletions cmd/token_psa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package cmd

import (
"context"
"fmt"
"strings"

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

func psaToken(brand string) (*oauth2.Token, error) {
var country string
prompt_country := &survey.Input{
Message: "Please enter your country code:",
}
if err := survey.AskOne(prompt_country, &country, survey.WithValidator(survey.Required)); err != nil {
return nil, err
}

cv := oauth2.GenerateVerifier()
oc := psa.Oauth2Config(brand, strings.ToLower(country))

authorize_url := oc.AuthCodeURL("", oauth2.S256ChallengeOption(cv))

fmt.Println("Please visit: ", authorize_url)
fmt.Println("And grab the authorization code like described here: https://github.com/flobz/psa_car_controller/discussions/779")

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

client := request.NewClient(util.NewLogger(brand))
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client)

return oc.Exchange(ctx, code, oauth2.VerifierOption(cv))
}
38 changes: 36 additions & 2 deletions templates/definition/vehicle/citroen.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
template: citroen
products:
- brand: Citroën
requirements:
description:
de: |
Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden.
en: |
Citroën `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]".
params:
- preset: vehicle-base
- name: title
- name: icon
default: car
advanced: true
- name: user
required: true
- name: password
deprecated: true
- name: tokens
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#citroen"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#citroen"
- name: vin
example: V...
- name: capacity
- name: phases
advanced: true
- name: password
deprecated: true
- preset: vehicle-identify
render: |
type: citroen
{{ include "vehicle-base" . }}
title: {{ .title }}
icon: {{ .icon }}
user: {{ .user }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
phases: {{ .phases }}
vin: {{ .vin }}
{{ include "vehicle-identify" . }}
38 changes: 36 additions & 2 deletions templates/definition/vehicle/ds.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
template: ds
products:
- brand: DS
requirements:
description:
de: |
Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden.
en: |
DS `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]".
params:
- preset: vehicle-base
- name: title
- name: icon
default: car
advanced: true
- name: user
required: true
- name: password
deprecated: true
- name: tokens
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#ds"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#ds"
- name: vin
example: V...
- name: capacity
- name: phases
advanced: true
- name: password
deprecated: true
- preset: vehicle-identify
render: |
type: ds
{{ include "vehicle-base" . }}
title: {{ .title }}
icon: {{ .icon }}
user: {{ .user }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
phases: {{ .phases }}
vin: {{ .vin }}
{{ include "vehicle-identify" . }}
40 changes: 36 additions & 4 deletions templates/definition/vehicle/opel.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
template: opel
products:
- brand: Opel
requirements:
description:
de: |
Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden.
en: |
Opel `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]".
params:
- preset: vehicle-base
- preset: vehicle-identify
- name: title
- name: icon
default: car
advanced: true
- name: user
required: true
- name: password
deprecated: true
- name: tokens
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#opel"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#opel"
- name: vin
example: WP0...
example: V...
- name: capacity
- name: phases
advanced: true
- name: password
deprecated: true
- preset: vehicle-identify
render: |
type: opel
{{ include "vehicle-base" . }}
title: {{ .title }}
icon: {{ .icon }}
user: {{ .user }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
phases: {{ .phases }}
vin: {{ .vin }}
{{ include "vehicle-identify" . }}
38 changes: 36 additions & 2 deletions templates/definition/vehicle/peugeot.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
template: peugeot
products:
- brand: Peugeot
requirements:
description:
de: |
Benötigt `access` und `refresh` Tokens. Diese können über den Befehl "evcc token [name]" generiert werden.
en: |
Peugeot `access` and `refresh` tokens are required. These can be generated with command "evcc token [name]".
params:
- preset: vehicle-base
- name: title
- name: icon
default: car
advanced: true
- name: user
required: true
- name: password
deprecated: true
- name: tokens
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#peugeot"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#peugeot"
- name: vin
example: V...
- name: capacity
- name: phases
advanced: true
- name: password
deprecated: true
- preset: vehicle-identify
render: |
type: peugeot
{{ include "vehicle-base" . }}
title: {{ .title }}
icon: {{ .icon }}
user: {{ .user }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
phases: {{ .phases }}
vin: {{ .vin }}
{{ include "vehicle-identify" . }}
91 changes: 35 additions & 56 deletions vehicle/psa.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package vehicle

import (
"fmt"
"strings"
"time"

"github.com/evcc-io/evcc/api"
Expand All @@ -12,46 +12,18 @@ import (
// https://github.com/TA2k/ioBroker.psa

func init() {
registry.Add("citroen", NewCitroenFromConfig)
registry.Add("ds", NewDSFromConfig)
registry.Add("opel", NewOpelFromConfig)
registry.Add("peugeot", NewPeugeotFromConfig)
}

// NewCitroenFromConfig creates a new vehicle
func NewCitroenFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("citroen")
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",
"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",
"1eebc2d5-5df3-459b-a624-20abfcf82530", "T5tP7iS0cO8sC0lA2iE2aR7gK6uE5rF3lJ8pC3nO1pR7tL8vU1",
other)
registry.Add("citroen", func(other map[string]any) (api.Vehicle, error) {
return newPSA("citroen", "clientsB2CCitroen", other)
})
registry.Add("ds", func(other map[string]any) (api.Vehicle, error) {
return newPSA("ds", "clientsB2CDS", other)
})
registry.Add("opel", func(other map[string]any) (api.Vehicle, error) {
return newPSA("opel", "clientsB2COpel", other)
})
registry.Add("peugeot", func(other map[string]any) (api.Vehicle, error) {
return newPSA("peugeot", "clientsB2CPeugeot", other)
})
}

// PSA is an api.Vehicle implementation for PSA cars
Expand All @@ -61,40 +33,47 @@ type PSA struct {
}

// newPSA creates a new vehicle
func newPSA(log *util.Logger, brand, realm, id, secret string, other map[string]interface{}) (api.Vehicle, error) {
func newPSA(brand, realm string, other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
Credentials ClientCredentials
User, Password, VIN string
Cache time.Duration
embed `mapstructure:",squash"`
VIN string
User string
Password string `mapstructure:"password"`
Country string
Tokens Tokens
Cache time.Duration
}{
Credentials: ClientCredentials{
ID: id,
Secret: secret,
},
Cache: interval,
}

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

if cc.User == "" || cc.Password == "" {
if cc.User == "" {
return nil, api.ErrMissingCredentials
}

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

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

log.Redact(cc.User, cc.Password, cc.VIN)
identity := psa.NewIdentity(log, brand, cc.Credentials.ID, cc.Credentials.Secret)
log := util.NewLogger(brand)
log.Redact(cc.User, cc.Tokens.Access, cc.Tokens.Refresh)

if err := identity.Login(cc.User, cc.Password); err != nil {
return v, fmt.Errorf("login failed: %w", err)
oc := psa.Oauth2Config(brand, strings.ToLower(cc.Country))
identity, err := psa.NewIdentity(log, brand, cc.User, oc, token)
if err != nil {
return nil, err
}

api := psa.NewAPI(log, identity, realm, cc.Credentials.ID)
// TODO still needed?
api := psa.NewAPI(log, identity, realm, oc.ClientID)

vehicle, err := ensureVehicleEx(
cc.VIN, api.Vehicles,
Expand Down
Loading

0 comments on commit db88086

Please sign in to comment.