Implement identity.vwgroup.io api for Audi (#331)

This commit is contained in:
andig 2020-09-11 15:03:14 +02:00
parent f98fc8148d
commit 702f5aa100
4 changed files with 271 additions and 81 deletions

2
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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))
}

View file

@ -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