From 7c9374f34af175a00b99d5ad2b25f96011f52668 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 16 Apr 2020 17:03:24 +0200 Subject: [PATCH] Fix BMW api (#39) Refactor for token validity --- vehicle/audi.go | 31 +++++--------- vehicle/bmw.go | 100 ++++++++++++++++++++------------------------- vehicle/vehicle.go | 4 -- 3 files changed, 56 insertions(+), 79 deletions(-) diff --git a/vehicle/audi.go b/vehicle/audi.go index 9149f0829..866f59973 100644 --- a/vehicle/audi.go +++ b/vehicle/audi.go @@ -31,23 +31,15 @@ type audiErrorResponse struct { } type audiBatteryResponse struct { - Charger audiBrCharger -} - -type audiBrCharger struct { - Status audiBrStatus -} - -type audiBrStatus struct { - BatteryStatusData audiBrStatusData -} - -type audiBrStatusData struct { - StateOfCharge audiBrStateOfCharge -} - -type audiBrStateOfCharge struct { - Content int + Charger struct { + Status struct { + BatteryStatusData struct { + StateOfCharge struct { + Content int + } + } + } + } } // Audi is an api.Vehicle implementation for Audi cars @@ -135,14 +127,13 @@ func (v *Audi) login(user, password string) error { } v.token = tr.AccessToken - v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn)*time.Second - tokenValidMargin) + v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn) * time.Second) return nil } func (v *Audi) request(uri string) (*http.Request, error) { - // token invalid or expired - if v.token == "" || time.Now().After(v.tokenValid) { + if v.token == "" || time.Since(v.tokenValid) > 0 { if err := v.login(v.user, v.password); err != nil { return nil, err } diff --git a/vehicle/bmw.go b/vehicle/bmw.go index f4d4747b9..5c27b7a32 100644 --- a/vehicle/bmw.go +++ b/vehicle/bmw.go @@ -1,10 +1,11 @@ package vehicle import ( - "encoding/base64" + "errors" "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -13,26 +14,14 @@ import ( ) const ( - bmwURL = "https://b2vapi.bmwgroup.com/webapi" + bmwAuth = "https://customer.bmwgroup.com/gcdm/oauth/authenticate" + bmwAPI = "https://www.bmw-connecteddrive.com/api" ) -type bmwTokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - Scope string `json:"scope"` -} - -type bmwStatusResponse struct { - VehicleStatus bmwVehicleStatus `json:"vehicleStatus"` -} - -type bmwVehicleStatus struct { - ConnectionStatus string `json:"connectionStatus"` - ChargingStatus string `json:"chargingStatus"` - ChargingLevelHv int `json:"chargingLevelHv"` - ChargingTimeRemaining int `json:"chargingTimeRemaining"` +type bmwDynamicResponse struct { + AttributesMap struct { + ChargingLevelHv float64 `json:"chargingLevelHv,string"` + } } // BMW is an api.Vehicle implementation for BMW cars @@ -40,7 +29,7 @@ type BMW struct { *embed *api.HTTPHelper user, password, vin string - token, refreshToken string + token string tokenValid time.Time chargeStateG provider.FloatGetter } @@ -57,7 +46,7 @@ func NewBMWFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle v := &BMW{ embed: &embed{cc.Title, cc.Capacity}, - HTTPHelper: api.NewHTTPHelper(api.NewLogger("bmwi")), + HTTPHelper: api.NewHTTPHelper(api.NewLogger("bmw ")), user: cc.User, password: cc.Password, vin: cc.VIN, @@ -68,76 +57,77 @@ func NewBMWFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle return v } -func (v *BMW) apiURL(service string) string { - return fmt.Sprintf("%s/%s", bmwURL, service) -} - -func (v *BMW) authHeader() string { - token := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", v.user, v.password))) - return fmt.Sprintf("Basic %s", token) -} - func (v *BMW) login(user, password string) error { - uri := v.apiURL("oauth/token") - data := url.Values{ - "grant_type": []string{"password"}, - "username": []string{user}, - "password": []string{password}, - "scope": []string{"remote_services vehicle_data"}, + "username": []string{user}, + "password": []string{password}, + "client_id": []string{"dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"}, + "redirect_uri": []string{"https://www.bmw-connecteddrive.com/app/default/static/external-dispatch.html"}, + "response_type": []string{"token"}, + "scope": []string{"authenticate_user fupo"}, + "state": []string{"eyJtYXJrZXQiOiJkZSIsImxhbmd1YWdlIjoiZGUiLCJkZXN0aW5hdGlvbiI6ImxhbmRpbmdQYWdlIn0"}, + "locale": []string{"DE-de"}, } - req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode())) + req, err := http.NewRequest(http.MethodPost, bmwAuth, strings.NewReader(data.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, // don't follow redirects + } + + resp, err := client.Do(req) if err != nil { return err } - req.Header.Set("Authorization", v.authHeader()) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - var tr bmwTokenResponse - if _, err = v.RequestJSON(req, &tr); err != nil { + query, err := url.ParseQuery(resp.Header.Get("Location")) + if err != nil { return err } - v.token = tr.AccessToken - v.refreshToken = tr.RefreshToken - v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn)*time.Second - tokenValidMargin) + token := query.Get("access_token") + expires, err := strconv.Atoi(query.Get("expires_in")) + if err != nil || token == "" || expires == 0 { + return errors.New("could not obtain token") + } + + v.token = token + v.tokenValid = time.Now().Add(time.Duration(expires) * time.Second) return nil } -// @TODO implement refresh_token func (v *BMW) request(uri string) (*http.Request, error) { - // token invalid or expired - if v.token == "" || time.Now().After(v.tokenValid) { + 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 { - return req, err + if err == nil { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", v.token)) } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", v.token)) - return req, nil } // chargeState implements the Vehicle.ChargeState interface func (v *BMW) chargeState() (float64, error) { - uri := v.apiURL(fmt.Sprintf("v1/user/vehicles/%s/status", v.vin)) + uri := fmt.Sprintf("%s/vehicle/dynamic/v1/%s", bmwAPI, v.vin) req, err := v.request(uri) if err != nil { return 0, err } - var br bmwStatusResponse + var br bmwDynamicResponse _, err = v.RequestJSON(req, &br) - return float64(br.VehicleStatus.ChargingLevelHv), err + return br.AttributesMap.ChargingLevelHv, err } // ChargeState implements the Vehicle.ChargeState interface diff --git a/vehicle/vehicle.go b/vehicle/vehicle.go index cc1d58c4a..f3e338389 100644 --- a/vehicle/vehicle.go +++ b/vehicle/vehicle.go @@ -7,10 +7,6 @@ import ( "github.com/andig/evcc/provider" ) -const ( - tokenValidMargin = 10 * time.Second // safety margin for api token validity -) - type embed struct { title string capacity int64