Add Porsche api support (#118)
This commit is contained in:
parent
0ba100bf44
commit
08cee90ea8
6 changed files with 263 additions and 1 deletions
|
|
@ -233,6 +233,7 @@ Available vehicle implementations are:
|
|||
- `nissan`: Nissan (Leaf)
|
||||
- `tesla`: Tesla (any model)
|
||||
- `renault`: Renault (Zoe, Kangoo ZE)
|
||||
- `porsche`: Porsche (Taycan)
|
||||
- `default`: default vehicle implementation using configurable [plugins](#plugins) for integrating any type of vehicle
|
||||
|
||||
## Plugins
|
||||
|
|
|
|||
|
|
@ -192,6 +192,14 @@ vehicles:
|
|||
# region: de_DE # gigya region
|
||||
# vin: WREN...
|
||||
# cache: 5m
|
||||
# - name: porsche
|
||||
# type: porsche
|
||||
# title: Taycan
|
||||
# capacity: 83 # kWh
|
||||
# user: # user
|
||||
# password: # password
|
||||
# vin: WP...
|
||||
# cache: 5m
|
||||
|
||||
loadpoints:
|
||||
- name: main # name for logging
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -23,6 +23,7 @@ require (
|
|||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mitchellh/mapstructure v1.3.0
|
||||
github.com/mjibson/esc v0.2.0
|
||||
github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/jwalterweatherman v1.1.0
|
||||
|
|
@ -31,7 +32,7 @@ require (
|
|||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/volkszaehler/mbmd v0.0.0-20200505114042-9732bf1ad846
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 // indirect
|
||||
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c // indirect
|
||||
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 // indirect
|
||||
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8
|
||||
)
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -158,6 +158,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da h1:qiPWuGGr+1GQE6s9NPSK8iggR/6x/V+0snIoOPYsBgc=
|
||||
github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da/go.mod h1:DvuJJ/w1Y59rG8UTDxsMk5U+UJXJwuvUgbiJSm9yhX8=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 h1:/I3lTljEEDNYLho3/FUB7iD/oc2cEFgVmbHzV+O0PtU=
|
||||
github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1/go.mod h1:eD5JxqMiuNYyFNmyY9rkJ/slN8y59oEu4Ei7F8OoKWQ=
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ func NewFromConfig(log *util.Logger, typ string, other map[string]interface{}) a
|
|||
c = NewNissanFromConfig(log, other)
|
||||
case "renault", "zoe":
|
||||
c = NewRenaultFromConfig(log, other)
|
||||
case "porsche", "taycan":
|
||||
c = NewPorscheFromConfig(log, other)
|
||||
default:
|
||||
log.FATAL.Fatalf("invalid vehicle type '%s'", typ)
|
||||
}
|
||||
|
|
|
|||
248
vehicle/porsche.go
Normal file
248
vehicle/porsche.go
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/util"
|
||||
cv "github.com/nirasan/go-oauth-pkce-code-verifier"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type porscheTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IdToken string `json:"id_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type porscheVehicleResponse struct {
|
||||
CarControlData struct {
|
||||
BatteryLevel struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
Mileage struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Porsche is an api.Vehicle implementation for Porsche cars
|
||||
type Porsche struct {
|
||||
*embed
|
||||
*util.HTTPHelper
|
||||
user, password, vin string
|
||||
token string
|
||||
tokenValid time.Time
|
||||
chargeStateG provider.FloatGetter
|
||||
}
|
||||
|
||||
// NewPorscheFromConfig creates a new vehicle
|
||||
func NewPorscheFromConfig(log *util.Logger, other map[string]interface{}) api.Vehicle {
|
||||
cc := struct {
|
||||
Title string
|
||||
Capacity int64
|
||||
User, Password, VIN string
|
||||
Cache time.Duration
|
||||
}{}
|
||||
util.DecodeOther(log, other, &cc)
|
||||
|
||||
v := &Porsche{
|
||||
embed: &embed{cc.Title, cc.Capacity},
|
||||
HTTPHelper: util.NewHTTPHelper(util.NewLogger("porsche")),
|
||||
user: cc.User,
|
||||
password: cc.Password,
|
||||
vin: cc.VIN,
|
||||
}
|
||||
|
||||
v.chargeStateG = provider.NewCached(log, v.chargeState, cc.Cache).FloatGetter()
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// login with a my Porsche account
|
||||
// looks like the backend is using a PingFederate Server with OAuth2
|
||||
func (v *Porsche) login(user, password string) error {
|
||||
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// the flow is using Oauth2 and >10 redirects
|
||||
client := &http.Client{
|
||||
Jar: jar,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// get the login page to get the cookies for the subsequent requests
|
||||
reqLogin, err := http.NewRequest(http.MethodGet, porscheLogin, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
respLogin, err := client.Do(reqLogin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryLogin, err := url.ParseQuery(respLogin.Request.URL.RawQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sec := queryLogin.Get("sec")
|
||||
resume := queryLogin.Get("resume")
|
||||
state := queryLogin.Get("state")
|
||||
thirdPartyId := queryLogin.Get("thirdPartyId")
|
||||
|
||||
dataLoginAuth := url.Values{
|
||||
"sec": []string{sec},
|
||||
"resume": []string{resume},
|
||||
"thirdPartyId": []string{thirdPartyId},
|
||||
"state": []string{state},
|
||||
"username": []string{user},
|
||||
"password": []string{password},
|
||||
"keeploggedin": []string{"false"},
|
||||
}
|
||||
|
||||
reqLoginAuth, err := http.NewRequest(http.MethodPost, porscheLoginAuth, strings.NewReader(dataLoginAuth.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqLoginAuth.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// process the auth so the session is authenticated
|
||||
_, err = client.Do(reqLoginAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
reqAPIAuth, err := http.NewRequest(http.MethodGet, porscheAPIAuth, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqAPIAuth.URL.RawQuery = dataAuth.Encode()
|
||||
|
||||
respAPIAuth, err := client.Do(reqAPIAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryAPIAuth, err := url.ParseQuery(respAPIAuth.Request.URL.RawQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authCode := queryAPIAuth.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},
|
||||
}
|
||||
|
||||
reqAPIToken, err := http.NewRequest(http.MethodPost, porscheAPIToken, strings.NewReader(dataAPIToken.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqAPIToken.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
respAPIToken, err := client.Do(reqAPIToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, _ := ioutil.ReadAll(respAPIToken.Body)
|
||||
|
||||
var pr porscheTokenResponse
|
||||
err = json.Unmarshal(b, &pr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pr.AccessToken == "" || pr.ExpiresIn == 0 {
|
||||
return errors.New("could not obtain token")
|
||||
}
|
||||
|
||||
v.token = pr.AccessToken
|
||||
v.tokenValid = time.Now().Add(time.Duration(pr.ExpiresIn) * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Porsche) request(uri string) (*http.Request, error) {
|
||||
if v.token == "" || time.Since(v.tokenValid) > 0 {
|
||||
if err := v.login(v.user, v.password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
if err == nil {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", v.token))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// chargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Porsche) chargeState() (float64, error) {
|
||||
uri := fmt.Sprintf("%s/vehicles/%s", porscheAPI, v.vin)
|
||||
req, err := v.request(uri)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var pr porscheVehicleResponse
|
||||
_, err = v.RequestJSON(req, &pr)
|
||||
|
||||
return pr.CarControlData.BatteryLevel.Value, err
|
||||
}
|
||||
|
||||
// ChargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Porsche) ChargeState() (float64, error) {
|
||||
return v.chargeStateG()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue