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

173 lines
3.9 KiB
Go

package vehicle
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/util"
)
const (
audiURL = "https://msg.audi.de/fs-car"
audiDE = "Audi/DE"
audiAuthPrefix = "AudiAuth 1"
)
type audiTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
type audiErrorResponse struct {
Error string
Description string `json:"error_description"`
}
type audiBatteryResponse struct {
Charger struct {
Status struct {
BatteryStatusData struct {
StateOfCharge struct {
Content int
}
}
}
}
}
// Audi is an api.Vehicle implementation for Audi cars
type Audi struct {
*embed
*util.HTTPHelper
user, password, vin string
token string
tokenValid time.Time
chargeStateG func() (float64, error)
}
// NewAudiFromConfig creates a new vehicle
func NewAudiFromConfig(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
}
v := &Audi{
embed: &embed{cc.Title, cc.Capacity},
HTTPHelper: util.NewHTTPHelper(util.NewLogger("audi")),
user: cc.User,
password: cc.Password,
vin: cc.VIN,
}
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
return v, nil
}
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},
}
req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode()))
if err != nil {
return err
}
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)
}
}
return err
}
v.token = tr.AccessToken
v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn) * time.Second)
return nil
}
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
}
}
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return req, err
}
v.headers(&req.Header)
req.Header.Set("Authorization", fmt.Sprintf("%s %s", audiAuthPrefix, v.token))
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
}
var br audiBatteryResponse
_, err = v.RequestJSON(req, &br)
return float64(br.Charger.Status.BatteryStatusData.StateOfCharge.Content), err
}
// ChargeState implements the Vehicle.ChargeState interface
func (v *Audi) ChargeState() (float64, error) {
return v.chargeStateG()
}