Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PSA: change authentication from user/password to token #13612

Merged
merged 24 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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