From 702f5aa100cebae0fc2d4294abe42ed57b72ebe5 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 11 Sep 2020 15:03:14 +0200 Subject: [PATCH] Implement identity.vwgroup.io api for Audi (#331) --- go.mod | 2 + go.sum | 11 ++ util/http.go | 3 + vehicle/audi.go | 336 ++++++++++++++++++++++++++++++++++++------------ 4 files changed, 271 insertions(+), 81 deletions(-) diff --git a/go.mod b/go.mod index d66c3e09d..021a2a59b 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 9fee21fbe..4f6c1f925 100644 --- a/go.sum +++ b/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= diff --git a/util/http.go b/util/http.go index f63dcce88..3d28b5bd1 100644 --- a/util/http.go +++ b/util/http.go @@ -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)) } diff --git a/vehicle/audi.go b/vehicle/audi.go index 07ffef8c8..7cafbe653 100644 --- a/vehicle/audi.go +++ b/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