Add Smart Hello (#10945)

This commit is contained in:
andig 2023-12-21 16:44:54 +01:00 • committed by GitHub
parent 6c19952f24
commit 08b2cb61ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 784 additions and 14 deletions

View file

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

View file

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

View file

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

61
vehicle/smart-hello.go Normal file
View file

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

196
vehicle/smart/hello/api.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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