Update API usage

- Use Emobility API calls to get proper SoC and Range values as the default API call isn’t always up to date
- This required to use a second auth token, that’s why the auth flow had to be adjusted
This commit is contained in:
Andreas Linde 2021-03-21 11:45:25 +01:00
parent 454688ac9f
commit 8008611c22
No known key found for this signature in database
GPG key ID: 96178502495847D9

View file

@ -18,13 +18,8 @@ import (
)
const (
porscheLogin = "https://login.porsche.com/auth/de/de_DE"
porscheLoginAuth = "https://login.porsche.com/auth/api/v1/de/de_DE/public/login"
porscheAPIClientID = "TZ4Vf5wnKeipJxvatJ60lPHYEzqZ4WNp"
porscheAPIRedirectURI = "https://my-static02.porsche.com/static/cms/auth.html"
porscheAPIAuth = "https://login.porsche.com/as/authorization.oauth2"
porscheAPIToken = "https://login.porsche.com/as/token.oauth2"
porscheAPI = "https://connect-portal.porsche.com/core/api/v3/de/de_DE"
porscheAPIClientID = "4mPO3OE5Srjb1iaUGWsbqKBvvesya8oA"
porscheEmobilityAPIClientID = "gZLSI7ThXFB4d2ld9t8Cx2DBRvGr1zN2"
)
type porscheTokenResponse struct {
@ -34,25 +29,39 @@ type porscheTokenResponse struct {
ExpiresIn int `json:"expires_in"`
}
type porscheVehicleResponse struct {
CarControlData struct {
BatteryLevel struct {
Unit string
Value float64
type porscheEmobilityResponse struct {
BatteryChargeStatus struct {
ChargeRate struct {
Unit string
Value float64
ValueInKmPerHour int64
}
Mileage struct {
Unit string
Value float64
}
RemainingRanges struct {
ElectricalRange struct {
Distance struct {
Unit string
Value float64
}
}
ChargingInDCMode bool
ChargingMode string
ChargingPower float64
ChargingReason string
ChargingState string
ChargingTargetDateTime string
ExternalPowerSupplyState string
PlugState string
RemainingChargeTimeUntil100PercentInMinutes int64
StateOfChargeInPercentage int64
RemainingERange struct {
OriginalUnit string
OriginalValue int64
Unit string
Value int64
ValueInKilometers int64
}
}
ChargingStatus string
DirectCharge struct {
Disabled bool
IsActive bool
}
DirectClimatisation struct {
ClimatisationState string
}
}
// Porsche is an api.Vehicle implementation for Porsche cars
@ -62,6 +71,8 @@ type Porsche struct {
user, password, vin string
token string
tokenValid time.Time
emobiltyToken string
emobilityTokenValid time.Time
chargerG func() (interface{}, error)
}
@ -112,6 +123,79 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
return v, err
}
func (v *Porsche) fetchToken(client *http.Client, emobility bool) (porscheTokenResponse, error) {
var pr porscheTokenResponse
clientID := porscheAPIClientID
redirectURI := "https://my.porsche.com/core/de/de_DE/"
if emobility {
clientID = porscheEmobilityAPIClientID
redirectURI = "https://connect-portal.porsche.com/myservices/auth/auth.html"
}
var CodeVerifier, _ = cv.CreateCodeVerifier()
codeChallenge := CodeVerifier.CodeChallengeS256()
dataTokenAuth := url.Values{
"redirect_uri": []string{redirectURI},
"client_id": []string{clientID},
"response_type": []string{"code"},
"state": []string{"uvobn7XJs1"},
"scope": []string{"openid"},
"access_type": []string{"offline"},
"country": []string{"de"},
"locale": []string{"de_DE"},
"code_challenge": []string{codeChallenge},
"code_challenge_method": []string{"S256"},
}
req, err := http.NewRequest(http.MethodGet, "https://login.porsche.com/as/authorization.oauth2", nil)
if err != nil {
return pr, err
}
req.URL.RawQuery = dataTokenAuth.Encode()
resp, err := client.Do(req)
if err != nil {
return pr, err
}
resp.Body.Close()
query, err := url.ParseQuery(resp.Request.URL.RawQuery)
if err != nil {
return pr, err
}
authCode := query.Get("code")
codeVerifier := CodeVerifier.CodeChallengePlain()
dataAPIToken := url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{clientID},
"redirect_uri": []string{redirectURI},
"code": []string{authCode},
"code_verifier": []string{codeVerifier},
}
req, err = request.New(http.MethodPost, "https://login.porsche.com/as/token.oauth2", strings.NewReader(dataAPIToken.Encode()), request.URLEncoding)
if err == nil {
resp, err = client.Do(req)
if err == nil {
err = request.DecodeJSON(resp, &pr)
}
}
if pr.AccessToken == "" || pr.ExpiresIn == 0 {
return pr, errors.New("could not obtain token")
}
return pr, err
}
// login with a my Porsche account
// looks like the backend is using a PingFederate Server with OAuth2
func (v *Porsche) authFlow() error {
@ -120,17 +204,17 @@ func (v *Porsche) authFlow() error {
return err
}
// the flow is using Oauth2 and >10 redirects
// track cookies and follow all (>10) redirects
client := &http.Client{
Jar: jar,
Timeout: v.Helper.Client.Timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return nil // allow >10 redirects
return nil
},
}
// get the login page to get the cookies for the subsequent requests
resp, err := client.Get(porscheLogin)
resp, err := client.Get("https://login.porsche.com/auth/de/de_DE")
if err != nil {
return err
}
@ -156,7 +240,7 @@ func (v *Porsche) authFlow() error {
"keeploggedin": []string{"false"},
}
req, err := request.New(http.MethodPost, porscheLoginAuth, strings.NewReader(dataLoginAuth.Encode()), request.URLEncoding)
req, err := request.New(http.MethodPost, "https://login.porsche.com/auth/api/v1/de/de_DE/public/login", strings.NewReader(dataLoginAuth.Encode()), request.URLEncoding)
if err != nil {
return err
}
@ -167,78 +251,39 @@ func (v *Porsche) authFlow() error {
}
resp.Body.Close()
var CodeVerifier, _ = cv.CreateCodeVerifier()
codeChallenge := CodeVerifier.CodeChallengeS256()
dataAuth := url.Values{
"scope": []string{"openid"},
"response_type": []string{"code"},
"access_type": []string{"offline"},
"prompt": []string{"none"},
"client_id": []string{porscheAPIClientID},
"redirect_uri": []string{porscheAPIRedirectURI},
"code_challenge": []string{codeChallenge},
"code_challenge_method": []string{"S256"},
}
req, err = http.NewRequest(http.MethodGet, porscheAPIAuth, nil)
if err == nil {
req.URL.RawQuery = dataAuth.Encode()
}
resp, err = client.Do(req)
if err != nil {
return err
}
resp.Body.Close()
query, err = url.ParseQuery(resp.Request.URL.RawQuery)
if err != nil {
return err
}
authCode := query.Get("code")
codeVerifier := CodeVerifier.CodeChallengePlain()
dataAPIToken := url.Values{
"grant_type": []string{"authorization_code"},
"client_id": []string{porscheAPIClientID},
"redirect_uri": []string{porscheAPIRedirectURI},
"code": []string{authCode},
"prompt": []string{"none"},
"code_verifier": []string{codeVerifier},
}
req, err = request.New(http.MethodPost, porscheAPIToken, strings.NewReader(dataAPIToken.Encode()), request.URLEncoding)
// get the token for the generic API
var pr porscheTokenResponse
if err == nil {
resp, err = client.Do(req)
if err == nil {
err = request.DecodeJSON(resp, &pr)
}
}
if pr.AccessToken == "" || pr.ExpiresIn == 0 {
return errors.New("could not obtain token")
if pr, err = v.fetchToken(client, false); err != nil {
return err
}
v.token = pr.AccessToken
v.tokenValid = time.Now().Add(time.Duration(pr.ExpiresIn) * time.Second)
if pr, err = v.fetchToken(client, true); err != nil {
return err
}
v.emobiltyToken = pr.AccessToken
v.emobilityTokenValid = time.Now().Add(time.Duration(pr.ExpiresIn) * time.Second)
return err
}
func (v *Porsche) request(uri string) (*http.Request, error) {
if v.token == "" || time.Since(v.tokenValid) > 0 {
func (v *Porsche) request(uri string, emobility bool) (*http.Request, error) {
if v.token == "" || time.Since(v.tokenValid) > 0 || v.emobiltyToken == "" || time.Since(v.emobilityTokenValid) > 0 {
if err := v.authFlow(); err != nil {
return nil, err
}
}
token := v.token
if emobility {
token = v.emobiltyToken
}
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", v.token),
"Authorization": fmt.Sprintf("Bearer %s", token),
})
return req, err
@ -246,7 +291,7 @@ func (v *Porsche) request(uri string) (*http.Request, error) {
func (v *Porsche) vehicles() (res []string, err error) {
uri := "https://connect-portal.porsche.com/core/api/v3/de/de_DE/vehicles"
req, err := v.request(uri)
req, err := v.request(uri, false)
var vehicles []struct {
VIN string
@ -265,13 +310,14 @@ func (v *Porsche) vehicles() (res []string, err error) {
// chargeState implements the api.Vehicle interface
func (v *Porsche) chargeState() (interface{}, error) {
uri := fmt.Sprintf("%s/vehicles/%s", porscheAPI, v.vin)
req, err := v.request(uri)
uri := fmt.Sprintf("https://api.porsche.com/service-vehicle/de/de_DE/e-mobility/J1/%s?timezone=Europe/Berlin", v.vin)
req, err := v.request(uri, true)
if err != nil {
return 0, err
}
var pr porscheVehicleResponse
req.Header.Set("apikey", porscheEmobilityAPIClientID)
var pr porscheEmobilityResponse
err = v.DoJSON(req, &pr)
return pr, err
@ -280,8 +326,8 @@ func (v *Porsche) chargeState() (interface{}, error) {
// SoC implements the api.Vehicle interface
func (v *Porsche) SoC() (float64, error) {
res, err := v.chargerG()
if res, ok := res.(porscheVehicleResponse); err == nil && ok {
return res.CarControlData.BatteryLevel.Value, nil
if res, ok := res.(porscheEmobilityResponse); err == nil && ok {
return float64(res.BatteryChargeStatus.StateOfChargeInPercentage), nil
}
return 0, err
@ -290,8 +336,8 @@ func (v *Porsche) SoC() (float64, error) {
// Range implements the api.VehicleRange interface
func (v *Porsche) Range() (int64, error) {
res, err := v.chargerG()
if res, ok := res.(porscheVehicleResponse); err == nil && ok {
return int64(res.CarControlData.RemainingRanges.ElectricalRange.Distance.Value), nil
if res, ok := res.(porscheEmobilityResponse); err == nil && ok {
return int64(res.BatteryChargeStatus.RemainingERange.ValueInKilometers), nil
}
return 0, err