evcc-io/vehicle/bmw.go
2020-07-29 13:42:09 +02:00

175 lines
4.1 KiB
Go

package vehicle
import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/util"
)
const (
bmwAuth = "https://customer.bmwgroup.com/gcdm/oauth/authenticate"
bmwAPI = "https://www.bmw-connecteddrive.com/api"
)
type bmwDynamicResponse struct {
AttributesMap struct {
ChargingLevelHv float64 `json:"chargingLevelHv,string"`
}
}
type bmwVehiclesResponse []struct {
Vin string `json:"vin"`
}
// BMW is an api.Vehicle implementation for BMW cars
type BMW struct {
*embed
*util.HTTPHelper
user, password, vin string
token string
tokenValid time.Time
chargeStateG func() (float64, error)
}
// NewBMWFromConfig creates a new vehicle
func NewBMWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
Title string
Capacity int64
User, Password, VIN string
Cache time.Duration
}{}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
log := util.NewLogger("bmw")
v := &BMW{
embed: &embed{cc.Title, cc.Capacity},
HTTPHelper: util.NewHTTPHelper(log),
user: cc.User,
password: cc.Password,
vin: cc.VIN,
}
if cc.VIN == "" {
vehicles, err := v.vehicles()
if err != nil {
return nil, fmt.Errorf("cannot get vehicles: %v", err)
}
if len(vehicles) != 1 {
return nil, fmt.Errorf("cannot find vehicle: %v", vehicles)
}
v.vin = vehicles[0].Vin
log.DEBUG.Printf("found vehicle: %v", v.vin)
}
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
return v, nil
}
func (v *BMW) login(user, password string) error {
data := url.Values{
"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, bmwAuth, strings.NewReader(data.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{
Timeout: v.HTTPHelper.Client.Timeout,
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
}
query, err := url.ParseQuery(resp.Header.Get("Location"))
if err != nil {
return err
}
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
}
func (v *BMW) 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
}
}
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err == nil {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", v.token))
}
return req, nil
}
// vehicles implements returns the list of user vehicles
func (v *BMW) vehicles() (bmwVehiclesResponse, error) {
var br bmwVehiclesResponse
uri := fmt.Sprintf("%s/me/vehicles/v2/", bmwAPI)
req, err := v.request(uri)
if err != nil {
return br, err
}
_, err = v.RequestJSON(req, &br)
return br, err
}
// chargeState implements the Vehicle.ChargeState interface
func (v *BMW) chargeState() (float64, error) {
var br bmwDynamicResponse
uri := fmt.Sprintf("%s/vehicle/dynamic/v1/%s", bmwAPI, v.vin)
req, err := v.request(uri)
if err != nil {
return 0, err
}
_, err = v.RequestJSON(req, &br)
return br.AttributesMap.ChargingLevelHv, err
}
// ChargeState implements the Vehicle.ChargeState interface
func (v *BMW) ChargeState() (float64, error) {
return v.chargeStateG()
}