Add Fiat 500e (#1229)
This commit is contained in:
parent
699bee4dec
commit
14cfc97d22
8 changed files with 428 additions and 12 deletions
1
go.mod
1
go.mod
|
|
@ -8,6 +8,7 @@ require (
|
|||
github.com/andig/gosunspec v0.0.0-20210511114617-aa30cf9b7a3f // indirect
|
||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
|
||||
github.com/avast/retry-go v3.0.0+incompatible
|
||||
github.com/aws/aws-sdk-go v1.40.3
|
||||
github.com/benbjohnson/clock v1.1.0
|
||||
github.com/bogosj/tesla v1.0.1-0.20210712193118-0e5b7bb5e924
|
||||
github.com/containrrr/shoutrrr v0.4.4
|
||||
|
|
|
|||
6
go.sum
6
go.sum
|
|
@ -71,6 +71,8 @@ github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7h
|
|||
github.com/avast/retry-go v2.6.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/aws/aws-sdk-go v1.40.3 h1:NzjcLRsb+C9L1dVPajdNbdzkuPBi0pQJWiQW0eYJGo8=
|
||||
github.com/aws/aws-sdk-go v1.40.3/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
|
|
@ -383,6 +385,10 @@ github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbg
|
|||
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
|
||||
github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w=
|
||||
github.com/jinzhu/copier v0.3.2/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs=
|
||||
github.com/joeshaw/carwings v0.0.0-20210616193757-7fbc6f7c3c73 h1:0GC7kKnW8vFHZB1txMdeiaVx1X65V9x8sWi5xoxUCfc=
|
||||
github.com/joeshaw/carwings v0.0.0-20210616193757-7fbc6f7c3c73/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs=
|
||||
|
|
|
|||
13
util/helper.go
Normal file
13
util/helper.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package util
|
||||
|
||||
import "math/rand"
|
||||
|
||||
// RandomString creates random string of N integers
|
||||
func RandomString(n int) string {
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
s := make([]rune, n)
|
||||
for i := range s {
|
||||
s[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
168
vehicle/fiat.go
Normal file
168
vehicle/fiat.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/request"
|
||||
"github.com/andig/evcc/vehicle/fiat"
|
||||
)
|
||||
|
||||
// https://github.com/TA2k/ioBroker.fiat
|
||||
|
||||
// Fiat is an api.Vehicle implementation for Fiat cars
|
||||
type Fiat struct {
|
||||
*embed
|
||||
*request.Helper
|
||||
vin string
|
||||
identity *fiat.Identity
|
||||
statusG func() (interface{}, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("fiat", NewFiatFromConfig)
|
||||
}
|
||||
|
||||
// NewFiatFromConfig creates a new vehicle
|
||||
func NewFiatFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
||||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
User, Password, VIN string
|
||||
Cache time.Duration
|
||||
}{
|
||||
Cache: interval,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.User == "" || cc.Password == "" {
|
||||
return nil, errors.New("missing credentials")
|
||||
}
|
||||
|
||||
log := util.NewLogger("fiat")
|
||||
|
||||
v := &Fiat{
|
||||
embed: &cc.embed,
|
||||
Helper: request.NewHelper(log),
|
||||
vin: strings.ToUpper(cc.VIN),
|
||||
identity: fiat.NewIdentity(log, cc.User, cc.Password),
|
||||
}
|
||||
|
||||
err := v.identity.Login()
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
if cc.VIN == "" {
|
||||
v.vin, err = findVehicle(v.vehicles())
|
||||
if err == nil {
|
||||
log.DEBUG.Printf("found vehicle: %v", v.vin)
|
||||
}
|
||||
}
|
||||
|
||||
v.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return v.status()
|
||||
}, cc.Cache).InterfaceGetter()
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (v *Fiat) request(method, uri string, body io.ReadSeeker) (*http.Request, error) {
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Clientapp-Version": "1.0",
|
||||
"ClientrequestId": util.RandomString(16),
|
||||
"X-Api-Key": fiat.XApiKey,
|
||||
"X-Originator-Type": "web",
|
||||
}
|
||||
|
||||
req, err := request.New(method, uri, body, headers)
|
||||
if err == nil {
|
||||
err = v.identity.Sign(req, body)
|
||||
}
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
func (v *Fiat) vehicles() ([]string, error) {
|
||||
var res fiat.Vehicles
|
||||
|
||||
uri := fmt.Sprintf("%s/v4/accounts/%s/vehicles?stage=ALL", fiat.ApiURI, v.identity.UID())
|
||||
|
||||
req, err := v.request(http.MethodGet, uri, nil)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
var vehicles []string
|
||||
if err == nil {
|
||||
for _, v := range res.Vehicles {
|
||||
vehicles = append(vehicles, v.VIN)
|
||||
}
|
||||
}
|
||||
|
||||
return vehicles, err
|
||||
}
|
||||
|
||||
func (v *Fiat) status() (interface{}, error) {
|
||||
var res fiat.Status
|
||||
|
||||
uri := fmt.Sprintf("%s/v2/accounts/%s/vehicles/%s/status", fiat.ApiURI, v.identity.UID(), v.vin)
|
||||
|
||||
req, err := v.request(http.MethodGet, uri, nil)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// SoC implements the api.Vehicle interface
|
||||
func (v *Fiat) SoC() (float64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fiat.Status); err == nil && ok {
|
||||
return float64(res.EvInfo.Battery.StateOfCharge), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Fiat)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Fiat) Range() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fiat.Status); err == nil && ok {
|
||||
return int64(res.EvInfo.Battery.DistanceToEmpty.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Fiat)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Fiat) Status() (api.ChargeStatus, error) {
|
||||
status := api.StatusA // disconnected
|
||||
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fiat.Status); err == nil && ok {
|
||||
if res.EvInfo.Battery.PlugInStatus {
|
||||
status = api.StatusB // connected, not charging
|
||||
}
|
||||
if res.EvInfo.Battery.ChargingStatus == "CHARGING" {
|
||||
status = api.StatusC // charging
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
202
vehicle/fiat/identity.go
Normal file
202
vehicle/fiat/identity.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
package fiat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/request"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go/service/cognitoidentity"
|
||||
)
|
||||
|
||||
const Region = "eu-west-1"
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
user, password string
|
||||
uid string
|
||||
creds *cognitoidentity.Credentials
|
||||
}
|
||||
|
||||
// NewIdentity creates Fiat identity
|
||||
func NewIdentity(log *util.Logger, user, password string) *Identity {
|
||||
return &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
user: user,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates with username/password to get new aws credentials
|
||||
func (v *Identity) Login() error {
|
||||
v.Client.Jar, _ = cookiejar.New(nil)
|
||||
|
||||
uri := fmt.Sprintf("%s/accounts.webSdkBootstrap", LoginURI)
|
||||
|
||||
data := url.Values(map[string][]string{
|
||||
"APIKey": {ApiKey},
|
||||
"pageURL": {"https://myuconnect.fiat.com/de/de/vehicle-services"},
|
||||
"sdk": {"js_latest"},
|
||||
"sdkBuild": {"12234"},
|
||||
"format": {"json"},
|
||||
})
|
||||
|
||||
headers := map[string]string{
|
||||
"Accept": "*/*",
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, headers)
|
||||
if err == nil {
|
||||
req.URL.RawQuery = data.Encode()
|
||||
_, err = v.Do(req)
|
||||
}
|
||||
|
||||
var res struct {
|
||||
ErrorCode int
|
||||
UID string
|
||||
StatusReason string
|
||||
SessionInfo struct {
|
||||
LoginToken string `json:"login_token"`
|
||||
ExpiresIn string `json:"expires_in"`
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
uri = fmt.Sprintf("%s/accounts.login", LoginURI)
|
||||
|
||||
data := url.Values(map[string][]string{
|
||||
"loginID": {v.user},
|
||||
"password": {v.password},
|
||||
"sessionExpiration": {"7776000"},
|
||||
"APIKey": {ApiKey},
|
||||
"pageURL": {"https://myuconnect.fiat.com/de/de/login"},
|
||||
"sdk": {"js_latest"},
|
||||
"sdkBuild": {"12234"},
|
||||
"format": {"json"},
|
||||
"targetEnv": {"jssdk"},
|
||||
"include": {"profile,data,emails"}, // subscriptions,preferences
|
||||
"includeUserInfo": {"true"},
|
||||
"loginMode": {"standard"},
|
||||
"lang": {"de0de"},
|
||||
"source": {"showScreenSet"},
|
||||
"authMode": {"cookie"},
|
||||
})
|
||||
|
||||
headers := map[string]string{
|
||||
"Accept": "*/*",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
if req, err = request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), headers); err == nil {
|
||||
if err = v.DoJSON(req, &res); err == nil {
|
||||
v.uid = res.UID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var token struct {
|
||||
ErrorCode int `json:"errorCode"`
|
||||
StatusReason string
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
uri = fmt.Sprintf("%s/accounts.getJWT", LoginURI)
|
||||
|
||||
data := url.Values(map[string][]string{
|
||||
"fields": {"profile.firstName,profile.lastName,profile.email,country,locale,data.disclaimerCodeGSDP"}, // data.GSDPisVerified
|
||||
"APIKey": {ApiKey},
|
||||
"pageURL": {"https://myuconnect.fiat.com/de/de/dashboard"},
|
||||
"sdk": {"js_latest"},
|
||||
"sdkBuild": {"12234"},
|
||||
"format": {"json"},
|
||||
"login_token": {res.SessionInfo.LoginToken},
|
||||
"authMode": {"cookie"},
|
||||
})
|
||||
|
||||
headers := map[string]string{
|
||||
"Accept": "*/*",
|
||||
}
|
||||
|
||||
if req, err = request.New(http.MethodGet, uri, nil, headers); err == nil {
|
||||
req.URL.RawQuery = data.Encode()
|
||||
err = v.DoJSON(req, &token)
|
||||
}
|
||||
}
|
||||
|
||||
var identity struct {
|
||||
Token, IdentityID string
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
data := struct {
|
||||
GigyaToken string `json:"gigya_token"`
|
||||
}{
|
||||
GigyaToken: token.IDToken,
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"X-Clientapp-Version": "1.0",
|
||||
"ClientRequestId": util.RandomString(16),
|
||||
"X-Api-Key": XApiKey,
|
||||
"X-Originator-Type": "web",
|
||||
}
|
||||
|
||||
if req, err = request.New(http.MethodPost, TokenURI, request.MarshalJSON(data), headers); err == nil {
|
||||
err = v.DoJSON(req, &identity)
|
||||
}
|
||||
}
|
||||
|
||||
var credRes *cognitoidentity.GetCredentialsForIdentityOutput
|
||||
|
||||
if err == nil {
|
||||
session := session.Must(session.NewSession(&aws.Config{Region: aws.String(Region)}))
|
||||
svc := cognitoidentity.New(session)
|
||||
|
||||
credRes, err = svc.GetCredentialsForIdentity(&cognitoidentity.GetCredentialsForIdentityInput{
|
||||
IdentityId: &identity.IdentityID,
|
||||
Logins: map[string]*string{
|
||||
"cognito-identity.amazonaws.com": &identity.Token,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
v.creds = credRes.Credentials
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UID returns the logged in users uid
|
||||
func (v *Identity) UID() string {
|
||||
return v.uid
|
||||
}
|
||||
|
||||
// Sign signs an AWS request using identity's credentials
|
||||
func (v *Identity) Sign(req *http.Request, body io.ReadSeeker) error {
|
||||
// refresh credentials
|
||||
if v.creds.Expiration.After(time.Now().Add(-time.Minute)) {
|
||||
if err := v.Login(); err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sign request
|
||||
signer := v4.NewSigner(credentials.NewStaticCredentials(
|
||||
*v.creds.AccessKeyId, *v.creds.SecretKey, *v.creds.SessionToken,
|
||||
))
|
||||
_, err := signer.Sign(req, body, "execute-api", Region, time.Now())
|
||||
|
||||
return err
|
||||
}
|
||||
36
vehicle/fiat/types.go
Normal file
36
vehicle/fiat/types.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package fiat
|
||||
|
||||
const (
|
||||
LoginURI = "https://loginmyuconnect.fiat.com"
|
||||
ApiURI = "https://channels.sdpr-01.fcagcv.com"
|
||||
TokenURI = "https://authz.sdpr-01.fcagcv.com/v2/cognito/identity/token"
|
||||
ApiKey = "3_mOx_J2dRgjXYCdyhchv3b5lhi54eBcdCTX4BI8MORqmZCoQWhA0mV2PTlptLGUQI"
|
||||
XApiKey = "qLYupk65UU1tw2Ih1cJhs4izijgRDbir2UFHA3Je"
|
||||
)
|
||||
|
||||
type Vehicle struct {
|
||||
VIN string
|
||||
}
|
||||
|
||||
type Vehicles struct {
|
||||
Vehicles []Vehicle
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
EvInfo struct {
|
||||
Battery struct {
|
||||
ChargingLevel string // LEVEL_2
|
||||
ChargingStatus string // CHARGING
|
||||
DistanceToEmpty struct {
|
||||
Value int
|
||||
Unit string
|
||||
}
|
||||
PlugInStatus bool // true
|
||||
StateOfCharge int // 75
|
||||
TimeToFullyChargeL1 int // 0
|
||||
TimeToFullyChargeL2 int // 540
|
||||
TotalRange int // 17
|
||||
}
|
||||
Timestamp int64
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,6 @@ import (
|
|||
"math/rand"
|
||||
)
|
||||
|
||||
// RandomString creates random string of N integers
|
||||
func RandomString(n int) string {
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
s := make([]rune, n)
|
||||
for i := range s {
|
||||
s[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// ChallengeVerifier returns a challenge/verifier base64-encoded code combination
|
||||
func ChallengeVerifier() (string, string, error) {
|
||||
bytes := make([]byte, 32)
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ func (v *Identity) login(uri, user, password string) (url.Values, error) {
|
|||
|
||||
// add nonce and state
|
||||
query := url.Values{
|
||||
"nonce": []string{RandomString(43)},
|
||||
"state": []string{RandomString(43)},
|
||||
"nonce": []string{util.RandomString(43)},
|
||||
"state": []string{util.RandomString(43)},
|
||||
}
|
||||
uri += "&" + query.Encode()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue