From 08b2cb61cae85eda1ae1a3559d5c0e18d1f7c591 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 21 Dec 2023 16:44:54 +0100 Subject: [PATCH] Add Smart Hello (#10945) --- meter/fritzdect/fritzdect.go | 4 +- templates/definition/vehicle/smart-hello.yaml | 12 + vehicle/niu.go | 14 +- vehicle/smart-hello.go | 61 ++++ vehicle/smart/hello/api.go | 196 +++++++++++++ vehicle/smart/hello/const.go | 10 + vehicle/smart/hello/helper.go | 69 +++++ vehicle/smart/hello/identity.go | 264 ++++++++++++++++++ vehicle/smart/hello/provider.go | 64 +++++ vehicle/smart/hello/types.go | 104 +++++++ 10 files changed, 784 insertions(+), 14 deletions(-) create mode 100644 templates/definition/vehicle/smart-hello.yaml create mode 100644 vehicle/smart-hello.go create mode 100644 vehicle/smart/hello/api.go create mode 100644 vehicle/smart/hello/const.go create mode 100644 vehicle/smart/hello/helper.go create mode 100644 vehicle/smart/hello/identity.go create mode 100644 vehicle/smart/hello/provider.go create mode 100644 vehicle/smart/hello/types.go diff --git a/meter/fritzdect/fritzdect.go b/meter/fritzdect/fritzdect.go index 05ce38904..cda3df7b0 100644 --- a/meter/fritzdect/fritzdect.go +++ b/meter/fritzdect/fritzdect.go @@ -182,9 +182,7 @@ func createChallengeResponse(challenge, pass string) (string, error) { } hash := md5.New() - if _, err = hash.Write([]byte(utf16le)); err != nil { - return "", err - } + hash.Write([]byte(utf16le)) md5hash := hex.EncodeToString(hash.Sum(nil)) return challenge + "-" + md5hash, nil diff --git a/templates/definition/vehicle/smart-hello.yaml b/templates/definition/vehicle/smart-hello.yaml new file mode 100644 index 000000000..892fe3f97 --- /dev/null +++ b/templates/definition/vehicle/smart-hello.yaml @@ -0,0 +1,12 @@ +template: smart-hello +products: + - brand: Smart + description: + generic: "#1" +params: + - preset: vehicle-base + - preset: vehicle-identify +render: | + type: smart-hello + {{ include "vehicle-base" . }} + {{ include "vehicle-identify" . }} diff --git a/vehicle/niu.go b/vehicle/niu.go index 9fb535910..8d6c9a236 100644 --- a/vehicle/niu.go +++ b/vehicle/niu.go @@ -69,10 +69,9 @@ func NewNiuFromConfig(other map[string]interface{}) (api.Vehicle, error) { // login implements the Niu oauth2 api func (v *Niu) login() error { - md5hash, err := md5Hash(v.password) - if err != nil { - return err - } + hash := md5.New() + hash.Write([]byte(v.password)) + md5hash := hex.EncodeToString(hash.Sum(nil)) data := url.Values{ "account": []string{v.user}, @@ -97,13 +96,6 @@ func (v *Niu) login() error { return err } -// md5Hash creates a MD5 hash based on a string -func md5Hash(text string) (string, error) { - hasher := md5.New() - _, err := hasher.Write([]byte(text)) - return hex.EncodeToString(hasher.Sum(nil)), err -} - // request implements the Niu web request func (v *Niu) request(uri string) (*http.Request, error) { if v.token.AccessToken == "" || time.Until(v.token.Expiry) < time.Minute { diff --git a/vehicle/smart-hello.go b/vehicle/smart-hello.go new file mode 100644 index 000000000..0c0b53dde --- /dev/null +++ b/vehicle/smart-hello.go @@ -0,0 +1,61 @@ +package vehicle + +import ( + "fmt" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/vehicle/smart/hello" +) + +// SmartHello is an api.Vehicle implementation for Smart Hello cars +type SmartHello struct { + *embed + *hello.Provider +} + +func init() { + registry.Add("smart-hello", NewSmartHelloFromConfig) +} + +// NewSmartHelloFromConfig creates a new vehicle +func NewSmartHelloFromConfig(other map[string]interface{}) (api.Vehicle, error) { + cc := struct { + embed `mapstructure:",squash"` + User, Password string + 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, api.ErrMissingCredentials + } + + log := util.NewLogger("smart-hello").Redact(cc.User, cc.Password, cc.VIN) + + v := &SmartHello{ + embed: &cc.embed, + } + + identity, err := hello.NewIdentity(log, cc.User, cc.Password) + if err != nil { + return v, fmt.Errorf("login failed: %w", err) + } + + api := hello.NewAPI(log, identity) + + cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) + + if err == nil { + v.Provider = hello.NewProvider(log, api, cc.VIN, cc.Cache) + } + + return v, err +} diff --git a/vehicle/smart/hello/api.go b/vehicle/smart/hello/api.go new file mode 100644 index 000000000..a76e16816 --- /dev/null +++ b/vehicle/smart/hello/api.go @@ -0,0 +1,196 @@ +package hello + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" + "github.com/samber/lo" +) + +// https://github.com/TA2k/ioBroker.smart-eq + +type API struct { + *request.Helper + identity *Identity +} + +func NewAPI(log *util.Logger, identity *Identity) *API { + v := &API{ + Helper: request.NewHelper(log), + identity: identity, + } + + v.Client.Transport = &transport.Decorator{ + Base: &transport.Decorator{ + Base: v.Client.Transport, + + // decorate token + Decorator: func(req *http.Request) error { + token, err := identity.Token() + if err == nil { + req.Header.Set("authorization", token.AccessToken) + } + return err + }, + }, + + // decorate headers + Decorator: transport.DecorateHeaders(map[string]string{ + "accept": "application/json;responseformat=3", + "content-type": "application/json; charset=utf-8", + "x-operator-code": operatorCode, + "x-api-signature-version": "1.0", + "x-app-id": appID, + "x-device-identifier": v.identity.DeviceID(), + }), + } + + return v +} + +func (v *API) request(method, path string, params url.Values, body io.Reader) (*http.Request, error) { + if body != nil { + b, err := io.ReadAll(body) + if err != nil { + return nil, err + } + // read from buffer + body = bytes.NewReader(b) + } + + nonce, ts, sign, err := createSignature(method, path, params, body) + if err != nil { + return nil, err + } + + if body != nil { + // rewind body + body.(*bytes.Reader).Seek(0, io.SeekStart) + } + + uri := fmt.Sprintf("%s/%s?%s", ApiURI, strings.TrimPrefix(path, "/"), params.Encode()) + req, err := request.New(method, uri, body, map[string]string{ + "x-api-signature-nonce": nonce, + "x-signature": sign, + "x-timestamp": ts, + }) + + return req, err +} + +func (v *API) Vehicles() ([]string, error) { + var res struct { + Code ResponseCode + Message string + Error Error + Data struct { + List []Vehicle + } + } + + userID, err := v.identity.UserID() + if err != nil { + return nil, err + } + + params := url.Values{ + "needSharedCar": {"1"}, + "userId": {userID}, + } + + path := "/device-platform/user/vehicle/secure" + req, err := v.request(http.MethodGet, path, params, nil) + if err != nil { + return nil, err + } + + err = v.DoJSON(req, &res) + if err := responseError(err, res.Code, res.Message, res.Error); err != nil { + return nil, err + } + + vehicles := lo.Map(res.Data.List, func(v Vehicle, _ int) string { + return v.VIN + }) + + return vehicles, err +} + +func (v *API) UpdateSession(vin string) error { + token, err := v.identity.Token() + if err != nil { + return err + } + + params := url.Values{ + "identity_type": {"smart"}, + } + + data := map[string]string{ + "vin": vin, + "sessionToken": token.AccessToken, + "language": "", + } + + path := "/device-platform/user/session/update" + req, err := v.request(http.MethodPost, path, params, request.MarshalJSON(data)) + if err != nil { + return err + } + + var res struct { + Code ResponseCode + Message string + Error Error + } + + err = v.DoJSON(req, &res) + return responseError(err, res.Code, res.Message, res.Error) +} + +func (v *API) Status(vin string) (VehicleStatus, error) { + if _, err := v.identity.UpdateSession(vin); err != nil { + // if err := v.UpdateSession(vin); err != nil { + return VehicleStatus{}, fmt.Errorf("update session failed: %w", err) + } + + var res struct { + Code ResponseCode + Message string + Error Error + Data struct { + VehicleStatus VehicleStatus + } + } + + userID, err := v.identity.UserID() + if err != nil { + return VehicleStatus{}, err + } + + params := url.Values{ + "latest": {"true"}, + "target": {""}, + "userId": {userID}, + } + + path := "/remote-control/vehicle/status/" + vin + req, err := v.request(http.MethodGet, path, params, nil) + if err != nil { + return VehicleStatus{}, err + } + + err = v.DoJSON(req, &res) + if err := responseError(err, res.Code, res.Message, res.Error); err != nil { + return VehicleStatus{}, err + } + + return res.Data.VehicleStatus, err +} diff --git a/vehicle/smart/hello/const.go b/vehicle/smart/hello/const.go new file mode 100644 index 000000000..9ba093543 --- /dev/null +++ b/vehicle/smart/hello/const.go @@ -0,0 +1,10 @@ +package hello + +const ( + ApiURI = "https://api.ecloudeu.com" + ApiKey = "3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a" + + appID = "SmartAPPEU" + operatorCode = "SMART" + userAgent = "Mozilla/5.0 (Linux; Android 9; ANE-LX1 Build/HUAWEIANE-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/118.0.0.0 Mobile Safari/537.36" +) diff --git a/vehicle/smart/hello/helper.go b/vehicle/smart/hello/helper.go new file mode 100644 index 000000000..c9ee01b18 --- /dev/null +++ b/vehicle/smart/hello/helper.go @@ -0,0 +1,69 @@ +package hello + +import ( + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strconv" + "time" + + "github.com/samber/lo" +) + +func createSignature(method, path string, params url.Values, post any) (string, string, string, error) { + nonce := lo.RandomString(16, lo.AlphanumericCharset) + ts := strconv.FormatInt(time.Now().UnixMilli(), 10) + + md5Hash := "1B2M2Y8AsgTpgAmY7PhCfg==" + if post != nil { + bytes, err := json.Marshal(post) + if err != nil { + return "", "", "", err + } + + hash := md5.New() + hash.Write(bytes) + md5Hash = base64.StdEncoding.EncodeToString(hash.Sum(nil)) + } + + payload := fmt.Sprintf(`application/json;responseformat=3 +x-api-signature-nonce:%s +x-api-signature-version:1.0 + +%s +%s +%s +%s +%s`, nonce, params.Encode(), md5Hash, ts, method, path) + + secret, err := base64.StdEncoding.DecodeString("NzRlNzQ2OWFmZjUwNDJiYmJlZDdiYmIxYjM2YzE1ZTk=") + if err != nil { + return "", "", "", err + } + + mac := hmac.New(sha1.New, secret) + mac.Write([]byte(payload)) + sign := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + + return nonce, ts, sign, nil +} + +func responseError(err error, code ResponseCode, msg string, errS Error) error { + var body error + + if code != 0 && code != ResponseOK { + body = fmt.Errorf("%d: %s", code, msg) + } else if errS.Code != 0 && errS.Code != ResponseOK { + body = fmt.Errorf("%d: %s", errS.Code, errS.Message) + } + + if err == nil { + return body + } + + return fmt.Errorf("%w: %s", err, body) +} diff --git a/vehicle/smart/hello/identity.go b/vehicle/smart/hello/identity.go new file mode 100644 index 000000000..854c0edef --- /dev/null +++ b/vehicle/smart/hello/identity.go @@ -0,0 +1,264 @@ +package hello + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/oauth" + "github.com/evcc-io/evcc/util/request" + "github.com/samber/lo" + "golang.org/x/oauth2" +) + +type Identity struct { + *request.Helper + oauth2.TokenSource + user, password string + userID, deviceID string +} + +func NewIdentity(log *util.Logger, user, password string) (*Identity, error) { + v := &Identity{ + Helper: request.NewHelper(log), + user: user, + password: password, + deviceID: lo.RandomString(16, lo.AlphanumericCharset), + } + + v.TokenSource = oauth.RefreshTokenSource(nil, v) + + _, err := v.Token() + + return v, err +} + +func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { + token, err := v.login() + if err != nil { + return nil, err + } + + appToken, userID, err := v.appToken(token) + if err != nil { + return nil, err + } + + if err == nil { + v.userID = userID + } + + return appToken, err +} + +func (v *Identity) DeviceID() string { + return v.deviceID +} + +func (v *Identity) UserID() (string, error) { + var err error + if v.userID == "" { + err = errors.New("missing user id") + } + return v.userID, err +} + +func (v *Identity) login() (*oauth2.Token, error) { + uri := "https://awsapi.future.smart.com/login-app/api/v1/authorize?uiLocales=de-DE&uiLocales=de-DE" + req, _ := request.New(http.MethodGet, uri, nil, map[string]string{ + "user-agent": userAgent, + "x-requested-with": "com.smart.hellosmart", + }) + + resp, err := v.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + u := resp.Request.URL + + data := url.Values{ + "loginID": {v.user}, + "password": {v.password}, + "sessionExpiration": {"2592000"}, + "targetEnv": {"jssdk"}, + "include": {"profile,data,emails,subscriptions,preferences"}, + "includeUserInfo": {"true"}, + "loginMode": {"standard"}, + "lang": {"de"}, + "APIKey": {ApiKey}, + "source": {"showScreenSet"}, + "sdk": {"js_latest"}, + "authMode": {"cookie"}, + "pageURL": {"https://app.id.smart.com/login?gig_ui_locales=de-DE"}, + "sdkBuild": {"15482"}, + "format": {"json"}, + } + + uri = "https://auth.smart.com/accounts.login" + req, _ = request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), map[string]string{ + "user-agent": userAgent, + "x-requested-with": "com.smart.hellosmart", + "content-type": request.FormContent, + "cookie": "gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4", + }) + + var login struct { + ErrorCode int + ErrorDetails, ErrorMessage string + SessionInfo struct { + LoginToken string `json:"login_token"` + ExpiresIn int `json:"expires_in,string"` + } + UserInfo struct { + UID string + FirstName, LastName, NickName string + } + } + + if err := v.DoJSON(req, &login); err != nil { + return nil, err + } + if login.ErrorCode != 0 { + return nil, fmt.Errorf("%s: %s", login.ErrorMessage, login.ErrorDetails) + } + defer resp.Body.Close() + + var param request.InterceptResult + v.Client.CheckRedirect, param = request.InterceptRedirect("access_token", true) + + uri = fmt.Sprintf("https://auth.smart.com/oidc/op/v1.0/%s/authorize/continue?context=%s&login_token=%s", ApiKey, u.Query().Get("context"), login.SessionInfo.LoginToken) + req, _ = request.New(http.MethodGet, uri, nil, map[string]string{ + "user-agent": userAgent, + "x-requested-with": "com.smart.hellosmart", + "content-type": request.FormContent, + "cookie": "gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4;glt_" + ApiKey + "=" + login.SessionInfo.LoginToken, + }) + + resp, err = v.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if _, err := param(); err != nil { + return nil, err + } + + u, err = url.Parse(resp.Header.Get("location")) + if err != nil { + return nil, err + } + + token := oauth2.Token{ + AccessToken: u.Query().Get("access_token"), + RefreshToken: u.Query().Get("refresh_token"), + } + + return &token, nil +} + +func (v *Identity) appToken(token *oauth2.Token) (*oauth2.Token, string, error) { + params := url.Values{ + "identity_type": {"smart"}, + } + + data := map[string]string{ + "accessToken": token.AccessToken, + } + + path := "/auth/account/session/secure" + nonce, ts, sign, err := createSignature(http.MethodPost, path, params, data) + if err != nil { + return nil, "", err + } + + uri := fmt.Sprintf("%s/%s?%s", ApiURI, strings.TrimPrefix(path, "/"), params.Encode()) + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{ + "Accept": "application/json;responseformat=3", + "Content-Type": "application/json; charset=utf-8", + "X-Api-Signature-Version": "1.0", + "X-Api-Signature-Nonce": nonce, + "X-App-Id": appID, + "X-Device-Identifier": v.deviceID, + "X-Operator-Code": operatorCode, + "X-Signature": sign, + "X-Timestamp": ts, + }) + + var res struct { + Code ResponseCode + Message string + Data AppToken + } + + if err := v.DoJSON(req, &res); err != nil { + return nil, "", err + } else if res.Code != ResponseOK { + return nil, "", fmt.Errorf("%d: %s", res.Code, res.Message) + } + + tok := oauth2.Token{ + AccessToken: res.Data.AccessToken, + RefreshToken: res.Data.RefreshToken, + Expiry: time.Now().Add(time.Duration(res.Data.ExpiresIn) * time.Second), + } + + return &tok, res.Data.UserId, nil +} + +func (v *Identity) UpdateSession(vin string) (string, error) { + token, err := v.Token() + if err != nil { + return "", err + } + + params := url.Values{ + "identity_type": []string{"smart"}, + } + + data := map[string]string{ + "vin": vin, + "sessionToken": token.AccessToken, + "language": "", + } + + path := "/device-platform/user/session/update" + nonce, ts, sign, err := createSignature(http.MethodPost, path, params, data) + if err != nil { + return "", err + } + + uri := fmt.Sprintf("%s/%s?%s", ApiURI, strings.TrimPrefix(path, "/"), params.Encode()) + req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{ + "Accept": "application/json;responseformat=3", + "Content-Type": "application/json; charset=utf-8", + "X-Api-Signature-Version": "1.0", + "X-Api-Signature-Nonce": nonce, + "Authorization": token.AccessToken, + "X-App-Id": appID, + "X-Device-Identifier": v.deviceID, + "X-Operator-Code": operatorCode, + "X-Signature": sign, + "X-Timestamp": ts, + }) + + var res struct { + Code ResponseCode + Message string + Data AppToken + } + + if err := v.DoJSON(req, &res); err != nil { + return "", err + } else if res.Code != ResponseOK { + return "", fmt.Errorf("%d: %s", res.Code, res.Message) + } + + return "", nil +} diff --git a/vehicle/smart/hello/provider.go b/vehicle/smart/hello/provider.go new file mode 100644 index 000000000..35969ec4e --- /dev/null +++ b/vehicle/smart/hello/provider.go @@ -0,0 +1,64 @@ +package hello + +import ( + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/provider" + "github.com/evcc-io/evcc/util" +) + +// https://github.com/TA2k/ioBroker.smart-eq + +type Provider struct { + statusG func() (VehicleStatus, error) +} + +func NewProvider(log *util.Logger, api *API, vin string, cache time.Duration) *Provider { + v := &Provider{ + statusG: provider.Cached(func() (VehicleStatus, error) { + return api.Status(vin) + }, cache), + } + + return v +} + +// Soc implements the api.Vehicle interface +func (v *Provider) Soc() (float64, error) { + res, err := v.statusG() + return float64(res.AdditionalVehicleStatus.ElectricVehicleStatus.ChargeLevel), err +} + +var _ api.VehicleRange = (*Provider)(nil) + +// Range implements the api.VehicleRange interface +func (v *Provider) Range() (int64, error) { + res, err := v.statusG() + return int64(res.AdditionalVehicleStatus.ElectricVehicleStatus.DistanceToEmptyOnBatteryOnly), err +} + +var _ api.VehiclePosition = (*Provider)(nil) + +// Position implements the api.VehiclePosition interface +func (v *Provider) Position() (float64, float64, error) { + res, err := v.statusG() + const div = 3600000.0 + return float64(res.BasicVehicleStatus.Position.Latitude) / div, float64(res.BasicVehicleStatus.Position.Longitude) / div, err +} + +var _ api.VehicleOdometer = (*Provider)(nil) + +// Odometer implements the Provider.VehicleOdometer interface +func (v *Provider) Odometer() (float64, error) { + res, err := v.statusG() + return res.AdditionalVehicleStatus.MaintenanceStatus.Odometer, err +} + +var _ api.VehicleClimater = (*Provider)(nil) + +// Climater implements the api.VehicleClimater interface +func (v *Provider) Climater() (bool, error) { + res, err := v.statusG() + return res.AdditionalVehicleStatus.ClimateStatus.PreClimateActive || res.AdditionalVehicleStatus.ClimateStatus.Defrost, err +} diff --git a/vehicle/smart/hello/types.go b/vehicle/smart/hello/types.go new file mode 100644 index 000000000..3656c2ad4 --- /dev/null +++ b/vehicle/smart/hello/types.go @@ -0,0 +1,104 @@ +package hello + +import ( + "strconv" + "strings" +) + +const ResponseOK = 1000 + +type ResponseCode int + +func (rc *ResponseCode) UnmarshalJSON(data []byte) error { + i, err := strconv.Atoi(strings.Trim(string(data), `"`)) + if err == nil { + *rc = ResponseCode(i) + } + return err +} + +type Error struct { + Code ResponseCode + Message string +} + +type AppToken struct { + ExpiresIn int + AccessToken string + UserId string + RefreshToken string +} + +type Vehicle struct { + VIN string +} + +type VehicleStatus struct { + BasicVehicleStatus struct { + UsageMode int `json:"usageMode,string"` // "0", + EngineStatus string `json:"engineStatus"` // "engine_off", + Position struct { + Altitude int `json:"altitude,string"` // "117", + PosCanBeTrusted bool `json:"posCanBeTrusted,string"` // "true", + Latitude int `json:"latitude,string"` // "18...", + CarLocatorStatUploadEn bool `json:"carLocatorStatUploadEn,string"` // "true", + Longitude int `json:"longitude,string"` // "28..." + } + DistanceToEmpty int `json:"distanceToEmpty,string"` // "0", + CarMode int `json:"carMode,string"` // "0", + Speed float64 `json:"speed,string"` // "0.0", + SpeedValidity bool `json:"speedValidity,string"` // "true", + Direction int `json:"direction,string"` // "277" + } + UpdateTime int64 `json:"updateTime,string"` // "1703072512182", + AdditionalVehicleStatus struct { + MaintenanceStatus struct { + TyreTempWarningPassengerRear int `json:"tyreTempWarningPassengerRear,string"` // "0", + DaysToService int `json:"daysToService,string"` // "455", + EngineHrsToService int `json:"engineHrsToService,string"` // "500", + Odometer float64 `json:"odometer,string"` // "7854.000", + BrakeFluidLevelStatus int `json:"brakeFluidLevelStatus,string"` // "3", + MainBatteryStatus struct { + StateOfCharge int `json:"stateOfCharge,string"` // "1", + ChargeLevel float64 `json:"chargeLevel,string"` // "0.0", + EnergyLevel int `json:"energyLevel,string"` // "0", + StateOfHealth int `json:"stateOfHealth,string"` // "0", + PowerLevel int `json:"powerLevel,string"` // "0", + Voltage float64 `json:"voltage,string"` // "5.000" + } + } + ElectricVehicleStatus struct { + DisChargeUAct float64 `json:"disChargeUAct,string"` // "0.0", + DisChargeSts int `json:"disChargeSts,string"` // "0", + WptFineAlignt int `json:"wptFineAlignt,string"` // "0", + ChargeLidAcStatus int `json:"chargeLidAcStatus,string"` // "2", + DistanceToEmptyOnBatteryOnly int `json:"distanceToEmptyOnBatteryOnly,string"` // "233", + DistanceToEmptyOnBattery100Soc int `json:"distanceToEmptyOnBattery100Soc,string"` // "330", + ChargeSts int `json:"chargeSts,string"` // "0", + AverPowerConsumption float64 `json:"averPowerConsumption,string"` // "-85.5", + ChargerState int `json:"chargerState,string"` // "0", + TimeToTargetDisCharged int `json:"timeToTargetDisCharged,string"` // "2047", + DistanceToEmptyOnBattery20Soc int `json:"distanceToEmptyOnBattery20Soc,string"` // "66", + DisChargeConnectStatus int `json:"disChargeConnectStatus,string"` // "0", + ChargeLidDcAcStatus int `json:"chargeLidDcAcStatus,string"` // "2", + DcChargeSts int `json:"dcChargeSts,string"` // "0", + PtReady int `json:"ptReady,string"` // "0", + ChargeLevel int `json:"chargeLevel,string"` // "76", + StatusOfChargerConnection int `json:"statusOfChargerConnection,string"` // "0", + DcDcActvd int `json:"dcDcActvd,string"` // "0", + IndPowerConsumption int `json:"indPowerConsumption,string"` // "1000", + DcDcConnectStatus int `json:"dcDcConnectStatus,string"` // "0", + DisChargeIAct float64 `json:"disChargeIAct,string"` // "0.0", + DcChargeIAct float64 `json:"dcChargeIAct,string"` // "0.0", + ChargeUAct float64 `json:"chargeUAct,string"` // "0.0", + BookChargeSts int `json:"bookChargeSts,string"` // "0", + ChargeIAct float64 `json:"chargeIAct,string"` // "0.000", + TimeToFullyCharged int `json:"timeToFullyCharged,string"` // "2047" + } + ChargeHvSts int `json:"chargeHvSts,string"` // "1", + ClimateStatus struct { + PreClimateActive bool `json:"preClimateActive"` // false, + Defrost bool `json:"defrost,string"` // "false", + } + } +}