diff --git a/internal/vehicle/porsche.go b/internal/vehicle/porsche.go index 38991f77e..24a87c167 100644 --- a/internal/vehicle/porsche.go +++ b/internal/vehicle/porsche.go @@ -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