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:
parent
454688ac9f
commit
8008611c22
1 changed files with 138 additions and 92 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue