Implement identity.vwgroup.io api for Audi (#331)
This commit is contained in:
parent
f98fc8148d
commit
702f5aa100
4 changed files with 271 additions and 81 deletions
2
go.mod
2
go.mod
|
|
@ -3,6 +3,7 @@ module github.com/andig/evcc
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.5.1
|
||||
github.com/andig/evcc-config v0.0.0-20200822120120-f129cbffa9aa
|
||||
github.com/asaskevich/EventBus v0.0.0-20200428142821-4fc0642a29f3
|
||||
github.com/avast/retry-go v2.6.0+incompatible
|
||||
|
|
@ -35,6 +36,7 @@ require (
|
|||
github.com/spf13/jwalterweatherman v1.1.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
github.com/volkszaehler/mbmd v0.0.0-20200831092453-b235d6a65b21
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
||||
|
|
|
|||
11
go.sum
11
go.sum
|
|
@ -15,6 +15,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alvaroloes/enumer v1.1.2 h1:5khqHB33TZy1GWCO/lZwcroBFh7u+0j40T83VUbfAMY=
|
||||
|
|
@ -24,6 +26,8 @@ github.com/andig/evcc-config v0.0.0-20200822120120-f129cbffa9aa h1:+t8n7jFHvd/QI
|
|||
github.com/andig/evcc-config v0.0.0-20200822120120-f129cbffa9aa/go.mod h1:N0hIjIy+5E2AR1fF7Tg2IzBlblBrnFvCCaDGAaHzbWk=
|
||||
github.com/andig/gosunspec v0.0.0-20200429133549-3cf6a82fed9c h1:AMtX56iHlNYVxMID7fe9efuVtaxgtdjyMeolg7q87IE=
|
||||
github.com/andig/gosunspec v0.0.0-20200429133549-3cf6a82fed9c/go.mod h1:YkshK8WMzYn1iXAZzHUO75gIqhMSan2ctgBVtBkRIyA=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
|
|
@ -363,6 +367,8 @@ github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1u
|
|||
github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
|
|
@ -426,6 +432,7 @@ golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
|
@ -443,6 +450,7 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
|
|
@ -450,6 +458,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgN
|
|||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -534,6 +543,7 @@ google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb
|
|||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
|
|
@ -573,6 +583,7 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
|||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gosrc.io/xmpp v0.1.1 h1:iMtE9W3fx254+4E6rI34AOPJDqWvpfQR6EYaVMzhJ4s=
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ func (r *HTTPHelper) readBody(resp *http.Response, err error) ([]byte, error) {
|
|||
return []byte{}, err
|
||||
}
|
||||
|
||||
// maintain body after reading
|
||||
resp.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
||||
|
||||
if r.Log != nil {
|
||||
r.Log.TRACE.Printf("%s\n%s", resp.Request.URL.String(), string(b))
|
||||
}
|
||||
|
|
|
|||
336
vehicle/audi.go
336
vehicle/audi.go
|
|
@ -1,37 +1,53 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/util"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
const (
|
||||
audiURL = "https://msg.audi.de/fs-car"
|
||||
audiDE = "Audi/DE"
|
||||
audiAuthPrefix = "AudiAuth 1"
|
||||
vwIdentity = "https://identity.vwgroup.io"
|
||||
vwAPI = "https://msg.volkswagen.de/fs-car"
|
||||
)
|
||||
|
||||
// OIDCResponse is the well-known OIDC provider response
|
||||
// https://{oauth-provider-hostname}/.well-known/openid-configuration
|
||||
type OIDCResponse struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthURL string `json:"authorization_endpoint"`
|
||||
TokenURL string `json:"token_endpoint"`
|
||||
JWKSURL string `json:"jwks_uri"`
|
||||
UserInfoURL string `json:"userinfo_endpoint"`
|
||||
Algorithms []string `json:"id_token_signing_alg_values_supported"`
|
||||
}
|
||||
|
||||
type audiTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IDToken string `json:"id_token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type audiErrorResponse struct {
|
||||
Error string
|
||||
Description string `json:"error_description"`
|
||||
type audiVehiclesResponse struct {
|
||||
UserVehicles struct {
|
||||
Vehicle []string
|
||||
}
|
||||
}
|
||||
|
||||
type audiBatteryResponse struct {
|
||||
type audiChargerResponse struct {
|
||||
Charger struct {
|
||||
Status struct {
|
||||
BatteryStatusData struct {
|
||||
|
|
@ -48,8 +64,8 @@ type Audi struct {
|
|||
*embed
|
||||
*util.HTTPHelper
|
||||
user, password, vin string
|
||||
token string
|
||||
tokenValid time.Time
|
||||
brand, country string
|
||||
tokens audiTokenResponse
|
||||
chargeStateG func() (float64, error)
|
||||
}
|
||||
|
||||
|
|
@ -69,106 +85,264 @@ func NewAudiFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
log := util.NewLogger("audi")
|
||||
|
||||
v := &Audi{
|
||||
embed: &embed{cc.Title, cc.Capacity},
|
||||
HTTPHelper: util.NewHTTPHelper(util.NewLogger("audi")),
|
||||
HTTPHelper: util.NewHTTPHelper(log),
|
||||
user: cc.User,
|
||||
password: cc.Password,
|
||||
vin: cc.VIN,
|
||||
brand: "Audi",
|
||||
country: "DE",
|
||||
}
|
||||
|
||||
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
|
||||
|
||||
return v, nil
|
||||
}
|
||||
var err error
|
||||
jar, err := cookiejar.New(&cookiejar.Options{
|
||||
PublicSuffixList: publicsuffix.List,
|
||||
})
|
||||
|
||||
func (v *Audi) apiURL(service, part string) string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s/%s", audiURL, service, "v1", audiDE, part)
|
||||
}
|
||||
|
||||
func (v *Audi) headers(header *http.Header) {
|
||||
for k, v := range map[string]string{
|
||||
"Accept": "application/json",
|
||||
"X-App-ID": "de.audi.mmiapp",
|
||||
"X-App-Name": "MMIconnect",
|
||||
"X-App-Version": "2.8.3",
|
||||
"X-Brand": "audi",
|
||||
"X-Country-Id": "DE",
|
||||
"X-Language-Id": "de",
|
||||
"X-Platform": "google",
|
||||
"User-Agent": "okhttp/2.7.4",
|
||||
"ADRUM_1": "isModule:true",
|
||||
"ADRUM": "isAray:true",
|
||||
} {
|
||||
header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Audi) login(user, password string) error {
|
||||
uri := v.apiURL("core/auth", "token")
|
||||
|
||||
data := url.Values{
|
||||
"grant_type": []string{"password"},
|
||||
"username": []string{user},
|
||||
"password": []string{password},
|
||||
// track cookies and don't follow redirects
|
||||
v.Client = &http.Client{
|
||||
Jar: jar,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
if err == nil {
|
||||
err = v.authFlow()
|
||||
}
|
||||
|
||||
v.headers(&req.Header)
|
||||
req.Header.Set("Authorization", audiAuthPrefix)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
var tr audiTokenResponse
|
||||
if b, err := v.RequestJSON(req, &tr); err != nil {
|
||||
if len(b) > 0 {
|
||||
var er audiErrorResponse
|
||||
if err = json.Unmarshal(b, &er); err == nil {
|
||||
return errors.New(er.Description)
|
||||
}
|
||||
if err == nil && cc.VIN == "" {
|
||||
vehicles, err := v.vehicles()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get vehicles: %v", err)
|
||||
}
|
||||
return err
|
||||
|
||||
if len(vehicles) != 1 {
|
||||
return nil, fmt.Errorf("cannot find vehicle: %v", vehicles)
|
||||
}
|
||||
|
||||
v.vin = vehicles[0]
|
||||
log.DEBUG.Printf("found vehicle: %v", v.vin)
|
||||
}
|
||||
|
||||
v.token = tr.AccessToken
|
||||
v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn) * time.Second)
|
||||
|
||||
return nil
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (v *Audi) 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
|
||||
}
|
||||
func (v *Audi) redirect(resp *http.Response, err error) (*http.Response, error) {
|
||||
if err == nil {
|
||||
uri := resp.Header.Get("Location")
|
||||
resp, err = v.Client.Get(uri)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (v *Audi) request(method, uri string, data io.Reader, headers ...map[string]string) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, uri, data)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
v.headers(&req.Header)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s %s", audiAuthPrefix, v.token))
|
||||
for _, headers := range headers {
|
||||
for k, v := range headers {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// chargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Audi) chargeState() (float64, error) {
|
||||
uri := v.apiURL("bs/batterycharge", fmt.Sprintf("vehicles/%s/charger", v.vin))
|
||||
req, err := v.request(uri)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
type formVars struct {
|
||||
action string
|
||||
csrf string
|
||||
relayState string
|
||||
hmac string
|
||||
}
|
||||
|
||||
func formValues(reader io.Reader, id string) (formVars, error) {
|
||||
vars := formVars{}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(reader)
|
||||
if err == nil {
|
||||
form := doc.Find(id).First()
|
||||
if form.Length() != 1 {
|
||||
return vars, errors.New("unexpected length")
|
||||
}
|
||||
|
||||
var exists bool
|
||||
vars.action, exists = form.Attr("action")
|
||||
if !exists {
|
||||
return vars, errors.New("attribute not found")
|
||||
}
|
||||
|
||||
vars.csrf, err = attr(form, "input[name=_csrf]", "value")
|
||||
if err == nil {
|
||||
vars.relayState, err = attr(form, "input[name=relayState]", "value")
|
||||
}
|
||||
if err == nil {
|
||||
vars.hmac, err = attr(form, "input[name=hmac]", "value")
|
||||
}
|
||||
}
|
||||
|
||||
var br audiBatteryResponse
|
||||
_, err = v.RequestJSON(req, &br)
|
||||
return vars, err
|
||||
}
|
||||
|
||||
return float64(br.Charger.Status.BatteryStatusData.StateOfCharge.Content), err
|
||||
func attr(doc *goquery.Selection, path, attr string) (res string, err error) {
|
||||
sel := doc.Find(path)
|
||||
if sel.Length() != 1 {
|
||||
return "", errors.New("unexpected length")
|
||||
}
|
||||
|
||||
v, exists := sel.Attr(attr)
|
||||
if !exists {
|
||||
return "", errors.New("attribute not found")
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (v *Audi) authFlow() error {
|
||||
var err error
|
||||
var uri, body string
|
||||
var vars formVars
|
||||
var req *http.Request
|
||||
var resp *http.Response
|
||||
|
||||
uri = "https://identity.vwgroup.io/oidc/v1/authorize?" +
|
||||
"response_type=code&client_id=09b6cbec-cd19-4589-82fd-363dfa8c24da%40apps_vw-dilab_com&" +
|
||||
"redirect_uri=myaudi%3A%2F%2F%2F&scope=address%20profile%20badge%20birthdate%20birthplace%20nationalIdentifier%20nationality%20profession%20email%20vin%20phone%20nickname%20name%20picture%20mbb%20gallery%20openid&" +
|
||||
"state=7f8260b5-682f-4db8-b171-50a5189a1c08&nonce=583b9af2-7799-4c72-9cb0-e6c0f42b87b3&prompt=login&ui_locales=de-DE"
|
||||
resp, err = v.Client.Get(uri)
|
||||
if err == nil {
|
||||
uri = resp.Header.Get("Location")
|
||||
resp, err = v.Client.Get(uri)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
vars, err = formValues(resp.Body, "form#emailPasswordForm")
|
||||
}
|
||||
if err == nil {
|
||||
uri = vwIdentity + vars.action
|
||||
body := fmt.Sprintf(
|
||||
"_csrf=%s&relayState=%s&hmac=%s&email=%s",
|
||||
vars.csrf, vars.relayState, vars.hmac, url.QueryEscape(v.user),
|
||||
)
|
||||
req, err = http.NewRequest(http.MethodPost, uri, strings.NewReader(body))
|
||||
}
|
||||
if err == nil {
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err = v.Client.Do(req)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
uri = vwIdentity + resp.Header.Get("Location")
|
||||
req, err = http.NewRequest(http.MethodGet, uri, nil)
|
||||
|
||||
}
|
||||
if err == nil {
|
||||
resp, err = v.Client.Do(req)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
vars, err = formValues(resp.Body, "form#credentialsForm")
|
||||
}
|
||||
if err == nil {
|
||||
uri = vwIdentity + vars.action
|
||||
body = fmt.Sprintf(
|
||||
"_csrf=%s&relayState=%s&email=%s&hmac=%s&password=%s",
|
||||
vars.csrf,
|
||||
vars.relayState,
|
||||
url.QueryEscape(v.user),
|
||||
vars.hmac,
|
||||
url.QueryEscape(v.password),
|
||||
)
|
||||
req, err = http.NewRequest(http.MethodPost, uri, strings.NewReader(body))
|
||||
}
|
||||
if err == nil {
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err = v.Client.Do(req)
|
||||
}
|
||||
|
||||
for i := 6; i < 9; i++ {
|
||||
resp, err = v.redirect(resp, err)
|
||||
}
|
||||
|
||||
var tokens audiTokenResponse
|
||||
if err == nil {
|
||||
var code string
|
||||
if location, err := url.Parse(resp.Header.Get("Location")); err == nil {
|
||||
code = location.Query().Get("code")
|
||||
}
|
||||
|
||||
uri = "https://app-api.my.audi.com/myaudiappidk/v1/token"
|
||||
body = fmt.Sprintf(
|
||||
"client_id=%s&grant_type=%s&code=%s&redirect_uri=%s&response_type=%s",
|
||||
url.QueryEscape("09b6cbec-cd19-4589-82fd-363dfa8c24da@apps_vw-dilab_com"),
|
||||
"authorization_code",
|
||||
code,
|
||||
url.QueryEscape("myaudi:///"),
|
||||
url.QueryEscape("token id_token"),
|
||||
)
|
||||
|
||||
req, err = v.request(http.MethodPost, uri, strings.NewReader(body),
|
||||
map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
}
|
||||
if err == nil {
|
||||
_, err = v.RequestJSON(req, &tokens)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
uri = "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/token"
|
||||
body = fmt.Sprintf("grant_type=%s&token=%s&scope=%s", "id_token", tokens.IDToken, "sc2:fal")
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-App-Version": "3.14.0",
|
||||
"X-App-Name": "myAudi",
|
||||
"X-Client-Id": "77869e21-e30a-4a92-b016-48ab7d3db1d8",
|
||||
}
|
||||
|
||||
req, err = v.request(http.MethodPost, uri, strings.NewReader(body), headers)
|
||||
}
|
||||
if err == nil {
|
||||
_, err = v.RequestJSON(req, &tokens)
|
||||
v.tokens = tokens
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Audi) getJSON(uri string, res interface{}) error {
|
||||
req, err := v.request(http.MethodGet, uri, nil, map[string]string{
|
||||
"Accept": "application/json",
|
||||
"Authorization": "Bearer " + v.tokens.AccessToken,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
_, err = v.RequestJSON(req, &res)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *Audi) vehicles() ([]string, error) {
|
||||
var res audiVehiclesResponse
|
||||
uri := fmt.Sprintf("%s/usermanagement/users/v1/Audi/DE/vehicles", vwAPI)
|
||||
err := v.getJSON(uri, &res)
|
||||
return res.UserVehicles.Vehicle, err
|
||||
}
|
||||
|
||||
// chargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Audi) chargeState() (float64, error) {
|
||||
var res audiChargerResponse
|
||||
uri := fmt.Sprintf("%s/bs/batterycharge/v1/%s/%s/vehicles/%s/charger", vwAPI, v.brand, v.country, v.vin)
|
||||
err := v.getJSON(uri, &res)
|
||||
return float64(res.Charger.Status.BatteryStatusData.StateOfCharge.Content), err
|
||||
}
|
||||
|
||||
// ChargeState implements the Vehicle.ChargeState interface
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue