Porsche: refactor and use oauth2 library (#2115)
This commit is contained in:
parent
ef68405fb8
commit
78138c91bf
6 changed files with 259 additions and 303 deletions
|
|
@ -1,27 +1,20 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/porsche"
|
||||
"github.com/thoas/go-funk"
|
||||
)
|
||||
|
||||
type porscheProvider interface {
|
||||
api.Battery
|
||||
api.ChargeState
|
||||
api.VehicleRange
|
||||
api.VehicleClimater
|
||||
api.VehicleFinishTimer
|
||||
api.VehicleOdometer
|
||||
}
|
||||
|
||||
// Porsche is an api.Vehicle implementation for Porsche cars
|
||||
type Porsche struct {
|
||||
*embed
|
||||
porscheProvider // provides the api implementations
|
||||
*porsche.Provider
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -47,23 +40,59 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
log := util.NewLogger("porsche").Redact(cc.User, cc.Password, cc.VIN)
|
||||
identity := porsche.NewIdentity(log, cc.User, cc.Password)
|
||||
identity := porsche.NewIdentity(log)
|
||||
|
||||
accessTokens, err := identity.Login()
|
||||
if err != nil {
|
||||
if err := identity.Login(cc.User, cc.Password); err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
vin, err := identity.FindVehicle(accessTokens, cc.VIN)
|
||||
api := porsche.NewAPI(log, identity.DefaultSource)
|
||||
|
||||
if cc.VIN == "" {
|
||||
vehicles, err := api.Vehicles()
|
||||
cc.VIN, err = findVehicle(funk.Map(vehicles, func(v porsche.Vehicle) string {
|
||||
return v.VIN
|
||||
}).([]string), err)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.DEBUG.Printf("found vehicle: %v", cc.VIN)
|
||||
}
|
||||
|
||||
// check if vehicle is paired
|
||||
if res, err := api.PairingStatus(cc.VIN); err == nil && res.Status != porsche.PairingComplete {
|
||||
return nil, errors.New("vehicle is not paired with the My Porsche account")
|
||||
}
|
||||
|
||||
// check if vehicle provides status:
|
||||
// some PHEVs do not provide any data
|
||||
if _, err := api.Status(cc.VIN); err != nil {
|
||||
return nil, errors.New("vehicle is not capable of providing data")
|
||||
}
|
||||
|
||||
// get eMobility capabilities
|
||||
|
||||
// Note: As of 27.10.21 the capabilities API needs to be called AFTER a
|
||||
// call to status() as it otherwise returns an HTTP 502 error.
|
||||
// The reason is unknown, even when tested with 100% identical Headers.
|
||||
// It seems to be a new backend related issue.
|
||||
if _, err := api.Status(cc.VIN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emobility := porsche.NewEmobilityAPI(log, identity.EmobilitySource)
|
||||
capabilities, err := emobility.Capabilities(cc.VIN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := porsche.NewProvider(log, identity, accessTokens, vin, cc.Cache)
|
||||
provider := porsche.NewProvider(log, api, emobility, cc.VIN, capabilities.CarModel, cc.Cache)
|
||||
|
||||
v := &Porsche{
|
||||
embed: &cc.embed,
|
||||
porscheProvider: provider,
|
||||
embed: &cc.embed,
|
||||
Provider: provider,
|
||||
}
|
||||
|
||||
return v, err
|
||||
|
|
|
|||
64
vehicle/porsche/api.go
Normal file
64
vehicle/porsche/api.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
ApiURI = "https://api.porsche.com"
|
||||
|
||||
PairingComplete = "PAIRINGCOMPLETE"
|
||||
)
|
||||
|
||||
// API is an api.Vehicle implementation for Porsche PHEV cars
|
||||
type API struct {
|
||||
*request.Helper
|
||||
}
|
||||
|
||||
// NewAPI creates a new vehicle
|
||||
func NewAPI(log *util.Logger, identity oauth2.TokenSource) *API {
|
||||
v := &API{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
|
||||
v.Client.Transport = &transport.Decorator{
|
||||
Base: &oauth2.Transport{
|
||||
Source: identity,
|
||||
Base: v.Client.Transport,
|
||||
},
|
||||
Decorator: transport.DecorateHeaders(map[string]string{
|
||||
"apikey": OAuth2Config.ClientID,
|
||||
}),
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Vehicles implements the vehicle list response
|
||||
func (v *API) Vehicles() ([]Vehicle, error) {
|
||||
var res []Vehicle
|
||||
uri := fmt.Sprintf("%s/core/api/v3/de/de_DE/vehicles", ApiURI)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// PairingStatus implements the vehicle pairing status response
|
||||
func (v *API) PairingStatus(vin string) (VehiclePairingResponse, error) {
|
||||
var res VehiclePairingResponse
|
||||
uri := fmt.Sprintf("%s/core/api/v3/de/de_DE/vehicles/%s/pairing", ApiURI, vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *API) Status(vin string) (StatusResponse, error) {
|
||||
var res StatusResponse
|
||||
uri := fmt.Sprintf("%s/vehicle-data/de/de_DE/status/%s", ApiURI, vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
55
vehicle/porsche/api_emobility.go
Normal file
55
vehicle/porsche/api_emobility.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// EmobilityAPI is the Porsche Emobility API
|
||||
type EmobilityAPI struct {
|
||||
*request.Helper
|
||||
}
|
||||
|
||||
// NewEmobilityAPI creates a new vehicle
|
||||
func NewEmobilityAPI(log *util.Logger, identity oauth2.TokenSource) *EmobilityAPI {
|
||||
v := &EmobilityAPI{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
|
||||
v.Client.Transport = &transport.Decorator{
|
||||
Base: &oauth2.Transport{
|
||||
Source: identity,
|
||||
Base: v.Client.Transport,
|
||||
},
|
||||
Decorator: transport.DecorateHeaders(map[string]string{
|
||||
"apikey": EmobilityOAuth2Config.ClientID,
|
||||
}),
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *EmobilityAPI) Capabilities(vin string) (CapabilitiesResponse, error) {
|
||||
var res CapabilitiesResponse
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/vcs/capabilities/%s", vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *EmobilityAPI) Status(vin, model string) (EmobilityResponse, error) {
|
||||
var res EmobilityResponse
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/de/de_DE/%s/%s?timezone=Europe/Berlin", model, vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
if err != nil && res.PcckErrorMessage != "" {
|
||||
err = errors.New(res.PcckErrorMessage)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -9,7 +10,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
cv "github.com/nirasan/go-oauth-pkce-code-verifier"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
|
@ -17,56 +17,75 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
ClientID = "4mPO3OE5Srjb1iaUGWsbqKBvvesya8oA"
|
||||
EmobilityClientID = "NJOxLv4QQNrpZnYQbb7mCvdiMxQWkHDq"
|
||||
OAuthURI = "https://login.porsche.com"
|
||||
)
|
||||
|
||||
type AccessTokens struct {
|
||||
Token, EmobilityToken oauth2.Token
|
||||
}
|
||||
// https://login.porsche.com/.well-known/openid-configuration
|
||||
var (
|
||||
OAuth2Config = &oauth2.Config{
|
||||
ClientID: "4mPO3OE5Srjb1iaUGWsbqKBvvesya8oA",
|
||||
RedirectURL: "https://my.porsche.com/core/de/de_DE/",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: OAuthURI + "/as/authorization.oauth2",
|
||||
TokenURL: OAuthURI + "/as/token.oauth2",
|
||||
},
|
||||
Scopes: []string{"openid"},
|
||||
}
|
||||
|
||||
EmobilityOAuth2Config = &oauth2.Config{
|
||||
ClientID: "NJOxLv4QQNrpZnYQbb7mCvdiMxQWkHDq",
|
||||
RedirectURL: "https://my.porsche.com/myservices/auth/auth.html",
|
||||
Endpoint: OAuth2Config.Endpoint,
|
||||
Scopes: OAuth2Config.Scopes,
|
||||
}
|
||||
)
|
||||
|
||||
// Identity is the Porsche Identity client
|
||||
type Identity struct {
|
||||
log *util.Logger
|
||||
*request.Helper
|
||||
user, password string
|
||||
DefaultSource, EmobilitySource oauth2.TokenSource
|
||||
}
|
||||
|
||||
// NewIdentity creates Porsche identity
|
||||
func NewIdentity(log *util.Logger, user, password string) *Identity {
|
||||
func NewIdentity(log *util.Logger) *Identity {
|
||||
v := &Identity{
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
user: user,
|
||||
password: password,
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
|
||||
jar, _ := cookiejar.New(&cookiejar.Options{
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *Identity) Login(user, password string) error {
|
||||
jar, err := cookiejar.New(&cookiejar.Options{
|
||||
PublicSuffixList: publicsuffix.List,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// track cookies and follow all (>10) redirects
|
||||
v.Client.Jar = jar
|
||||
v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
v.Client.Jar = nil
|
||||
v.Client.CheckRedirect = nil
|
||||
}()
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *Identity) Login() (AccessTokens, error) {
|
||||
var accessTokens AccessTokens
|
||||
|
||||
// get the login page to get the cookies for the subsequent requests
|
||||
resp, err := v.Client.Get("https://login.porsche.com/auth/de/de_DE")
|
||||
// get the login page
|
||||
uri := fmt.Sprintf("%s/auth/api/v1/de/de_DE/public/login", OAuthURI)
|
||||
resp, err := v.Get(uri)
|
||||
if err != nil {
|
||||
return accessTokens, err
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
query, err := url.ParseQuery(resp.Request.URL.RawQuery)
|
||||
if err != nil {
|
||||
return accessTokens, err
|
||||
return err
|
||||
}
|
||||
|
||||
sec := query.Get("sec")
|
||||
|
|
@ -79,179 +98,73 @@ func (v *Identity) Login() (AccessTokens, error) {
|
|||
"resume": []string{resume},
|
||||
"thirdPartyId": []string{thirdPartyID},
|
||||
"state": []string{state},
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
"username": []string{user},
|
||||
"password": []string{password},
|
||||
"keeploggedin": []string{"false"},
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, "https://login.porsche.com/auth/api/v1/de/de_DE/public/login", strings.NewReader(dataLoginAuth.Encode()), request.URLEncoding)
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(dataLoginAuth.Encode()), request.URLEncoding)
|
||||
if err != nil {
|
||||
return accessTokens, err
|
||||
return err
|
||||
}
|
||||
|
||||
// process the auth so the session is authenticated
|
||||
if resp, err = v.Client.Do(req); err != nil {
|
||||
return accessTokens, err
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// get the token for the generic API
|
||||
var token oauth.Token
|
||||
token, err = v.fetchToken(false)
|
||||
if err != nil {
|
||||
return accessTokens, err
|
||||
}
|
||||
accessTokens.Token = (oauth2.Token)(token)
|
||||
token, err := v.fetchToken(OAuth2Config)
|
||||
if err == nil {
|
||||
v.DefaultSource = OAuth2Config.TokenSource(context.Background(), token)
|
||||
|
||||
token, err = v.fetchToken(true)
|
||||
if err != nil {
|
||||
return accessTokens, err
|
||||
if token, err = v.fetchToken(EmobilityOAuth2Config); err == nil {
|
||||
v.EmobilitySource = EmobilityOAuth2Config.TokenSource(context.Background(), token)
|
||||
}
|
||||
}
|
||||
accessTokens.EmobilityToken = (oauth2.Token)(token)
|
||||
|
||||
return accessTokens, err
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Identity) fetchToken(emobility bool) (oauth.Token, error) {
|
||||
var pr oauth.Token
|
||||
|
||||
actualClientID := ClientID
|
||||
redirectURI := "https://my.porsche.com/core/de/de_DE/"
|
||||
|
||||
if emobility {
|
||||
actualClientID = EmobilityClientID
|
||||
redirectURI = "https://my.porsche.com/myservices/auth/auth.html"
|
||||
}
|
||||
|
||||
var CodeVerifier, _ = cv.CreateCodeVerifier()
|
||||
codeChallenge := CodeVerifier.CodeChallengeS256()
|
||||
|
||||
dataTokenAuth := url.Values{
|
||||
"redirect_uri": []string{redirectURI},
|
||||
"client_id": []string{actualClientID},
|
||||
"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)
|
||||
func (v *Identity) fetchToken(oc *oauth2.Config) (*oauth2.Token, error) {
|
||||
cv, err := cv.CreateCodeVerifier()
|
||||
if err != nil {
|
||||
return pr, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.URL.RawQuery = dataTokenAuth.Encode()
|
||||
uri := oc.AuthCodeURL("uvobn7XJs1", oauth2.AccessTypeOffline,
|
||||
oauth2.SetAuthURLParam("code_challenge", cv.CodeChallengeS256()),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
|
||||
oauth2.SetAuthURLParam("country", "de"),
|
||||
oauth2.SetAuthURLParam("locale", "de_DE"),
|
||||
)
|
||||
|
||||
resp, err := v.Client.Do(req)
|
||||
resp, err := v.Client.Get(uri)
|
||||
if err != nil {
|
||||
return pr, err
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
query, err := url.ParseQuery(resp.Request.URL.RawQuery)
|
||||
if err != nil {
|
||||
return pr, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authCode := query.Get("code")
|
||||
if authCode == "" {
|
||||
return pr, errors.New("no auth code")
|
||||
code := query.Get("code")
|
||||
if code == "" {
|
||||
return nil, errors.New("no auth code")
|
||||
}
|
||||
|
||||
codeVerifier := CodeVerifier.CodeChallengePlain()
|
||||
ctx, cancel := context.WithTimeout(
|
||||
context.WithValue(context.Background(), oauth2.HTTPClient, v.Client),
|
||||
request.Timeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
dataAPIToken := url.Values{
|
||||
"grant_type": []string{"authorization_code"},
|
||||
"client_id": []string{actualClientID},
|
||||
"redirect_uri": []string{redirectURI},
|
||||
"code": []string{authCode},
|
||||
"code_verifier": []string{codeVerifier},
|
||||
}
|
||||
token, err := oc.Exchange(ctx, code,
|
||||
oauth2.SetAuthURLParam("code_verifier", cv.CodeChallengePlain()),
|
||||
)
|
||||
|
||||
req, err = request.New(http.MethodPost, "https://login.porsche.com/as/token.oauth2", strings.NewReader(dataAPIToken.Encode()), request.URLEncoding)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &pr)
|
||||
}
|
||||
|
||||
if pr.AccessToken == "" {
|
||||
return pr, errors.New("could not obtain token")
|
||||
}
|
||||
|
||||
return pr, err
|
||||
}
|
||||
|
||||
func (v *Identity) FindVehicle(accessTokens AccessTokens, vin string) (string, error) {
|
||||
vehiclesURL := "https://api.porsche.com/core/api/v3/de/de_DE/vehicles"
|
||||
req, err := request.New(http.MethodGet, vehiclesURL, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", accessTokens.Token.AccessToken),
|
||||
"apikey": ClientID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var vehicles []VehicleResponse
|
||||
if err = v.DoJSON(req, &vehicles); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var foundVehicle VehicleResponse
|
||||
|
||||
if vin == "" && len(vehicles) == 1 {
|
||||
foundVehicle = vehicles[0]
|
||||
} else {
|
||||
for _, vehicleItem := range vehicles {
|
||||
if vehicleItem.VIN == strings.ToUpper(vin) {
|
||||
foundVehicle = vehicleItem
|
||||
}
|
||||
}
|
||||
}
|
||||
if foundVehicle.VIN == "" {
|
||||
return "", errors.New("vin not found")
|
||||
}
|
||||
|
||||
v.log.DEBUG.Printf("found vehicle: %v", foundVehicle.VIN)
|
||||
|
||||
// check if vehicle is paired
|
||||
uri := fmt.Sprintf("%s/%s/pairing", vehiclesURL, foundVehicle.VIN)
|
||||
req, err = request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", accessTokens.Token.AccessToken),
|
||||
"apikey": ClientID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var pairing VehiclePairingResponse
|
||||
if err = v.DoJSON(req, &pairing); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pairing.Status != "PAIRINGCOMPLETE" {
|
||||
return "", errors.New("vehicle is not paired with the My Porsche account")
|
||||
}
|
||||
|
||||
// now check if we get any response at all for a status request
|
||||
// there are PHEV which do not provide any data, even thought they are PHEV
|
||||
uri = fmt.Sprintf("https://api.porsche.com/vehicle-data/de/de_DE/status/%s", foundVehicle.VIN)
|
||||
req, err = request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", accessTokens.Token.AccessToken),
|
||||
"apikey": ClientID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err = v.DoBody(req); err != nil {
|
||||
return "", errors.New("vehicle is not capable of providing data")
|
||||
}
|
||||
|
||||
return foundVehicle.VIN, err
|
||||
return token, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,144 +1,39 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
)
|
||||
|
||||
// Provider is an api.Vehicle implementation for Porsche PHEV cars
|
||||
type Provider struct {
|
||||
log *util.Logger
|
||||
*request.Helper
|
||||
accessTokens AccessTokens
|
||||
identity *Identity
|
||||
carModel string
|
||||
statusG func() (interface{}, error)
|
||||
statusEmobilityG func() (interface{}, error)
|
||||
statusG func() (interface{}, error)
|
||||
emobilityG func() (interface{}, error)
|
||||
}
|
||||
|
||||
// NewProvider creates a new vehicle
|
||||
func NewProvider(log *util.Logger, identity *Identity, accessTokens AccessTokens, vin string, cache time.Duration) *Provider {
|
||||
func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, vin, model string, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
accessTokens: accessTokens,
|
||||
identity: identity,
|
||||
statusG: provider.NewCached(func() (interface{}, error) {
|
||||
return api.Status(vin)
|
||||
}, cache).InterfaceGetter(),
|
||||
|
||||
emobilityG: provider.NewCached(func() (interface{}, error) {
|
||||
return emobility.Status(vin, model)
|
||||
}, cache).InterfaceGetter(),
|
||||
}
|
||||
|
||||
impl.Client.Jar = identity.Client.Jar
|
||||
|
||||
impl.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return impl.status(vin)
|
||||
}, cache).InterfaceGetter()
|
||||
|
||||
impl.statusEmobilityG = provider.NewCached(func() (interface{}, error) {
|
||||
return impl.statusEmobility(vin)
|
||||
}, cache).InterfaceGetter()
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
func (v *Provider) request(emobility bool, uri string) (*http.Request, error) {
|
||||
defaultToken := v.accessTokens.Token
|
||||
emobilityToken := v.accessTokens.EmobilityToken
|
||||
|
||||
// if any of the two Tokens are empty or expired, login again
|
||||
if defaultToken.AccessToken == "" ||
|
||||
time.Since(defaultToken.Expiry) > 0 ||
|
||||
emobilityToken.AccessToken == "" ||
|
||||
time.Since(emobilityToken.Expiry) > 0 {
|
||||
newAccessTokens, err := v.identity.Login()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.accessTokens = newAccessTokens
|
||||
}
|
||||
|
||||
apiKey := ClientID
|
||||
apiAccessToken := v.accessTokens.Token.AccessToken
|
||||
if emobility {
|
||||
apiKey = EmobilityClientID
|
||||
apiAccessToken = v.accessTokens.EmobilityToken.AccessToken
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", apiAccessToken),
|
||||
"apikey": apiKey,
|
||||
})
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *Provider) status(vin string) (StatusResponse, error) {
|
||||
var res StatusResponse
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/vehicle-data/de/de_DE/status/%s", vin)
|
||||
req, err := v.request(false, uri)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
err = v.DoJSON(req, &res)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *Provider) statusEmobility(vin string) (EmobilityResponse, error) {
|
||||
var res EmobilityResponse
|
||||
|
||||
if v.carModel == "" {
|
||||
// Note: As of 27.10.21 the capabilities API needs to be called AFTER a
|
||||
// call to status() as it otherwise returns an HTTP 502 error.
|
||||
// The reason is unknown, even when tested with 100% identical Headers.
|
||||
// It seems to be a new backend related issue.
|
||||
|
||||
if _, err := v.status(vin); err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/vcs/capabilities/%s", vin)
|
||||
|
||||
req, err := v.request(true, uri)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var cr CapabilitiesResponse
|
||||
err = v.DoJSON(req, &cr)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
v.carModel = cr.CarModel
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/de/de_DE/%s/%s?timezone=Europe/Berlin", v.carModel, vin)
|
||||
req, err := v.request(true, uri)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
err = v.DoJSON(req, &res)
|
||||
if err != nil && res.PcckErrorMessage != "" {
|
||||
err = errors.New(res.PcckErrorMessage)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
||||
// SoC implements the api.Vehicle interface
|
||||
func (v *Provider) SoC() (float64, error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
res, err := v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return float64(res.BatteryChargeStatus.StateOfChargeInPercentage), nil
|
||||
}
|
||||
|
|
@ -155,7 +50,7 @@ var _ api.VehicleRange = (*Provider)(nil)
|
|||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Provider) Range() (int64, error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
res, err := v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return res.BatteryChargeStatus.RemainingERange.ValueInKilometers, nil
|
||||
}
|
||||
|
|
@ -172,7 +67,7 @@ var _ api.VehicleFinishTimer = (*Provider)(nil)
|
|||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *Provider) FinishTime() (time.Time, error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
res, err := v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return time.Now().Add(time.Duration(res.BatteryChargeStatus.RemainingChargeTimeUntil100PercentInMinutes) * time.Minute), err
|
||||
}
|
||||
|
|
@ -184,7 +79,7 @@ var _ api.ChargeState = (*Provider)(nil)
|
|||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Provider) Status() (api.ChargeStatus, error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
res, err := v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
switch res.BatteryChargeStatus.PlugState {
|
||||
case "DISCONNECTED":
|
||||
|
|
@ -212,7 +107,7 @@ var _ api.VehicleClimater = (*Provider)(nil)
|
|||
|
||||
// Climater implements the api.VehicleClimater interface
|
||||
func (v *Provider) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
res, err := v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
switch res.DirectClimatisation.ClimatisationState {
|
||||
case "OFF":
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package porsche
|
||||
|
||||
type VehicleResponse struct {
|
||||
type Vehicle struct {
|
||||
VIN string
|
||||
ModelDescription string
|
||||
Pictures []struct {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue