Fix BMW api (#39)

Refactor for token validity
This commit is contained in:
andig 2020-04-16 17:03:24 +02:00 • committed by GitHub
parent 09eeb8f8c3
commit 7c9374f34a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 56 additions and 79 deletions

View file

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

View file

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

View file

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