This commit is contained in:
kscholty 2024-02-28 21:30:40 +01:00 • committed by GitHub
parent c570fc5272
commit 3f457e76fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1006 additions and 0 deletions

View file

@ -0,0 +1,12 @@
template: mg
products:
- brand: MG
params:
- preset: vehicle-base
- preset: vehicle-identify
- name: vin
required: true
render: |
type: mg
{{ include "vehicle-base" . }}
{{ include "vehicle-identify" . }}

54
vehicle/mg.go Normal file
View file

@ -0,0 +1,54 @@
package vehicle
import (
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle/saic"
)
// MG is an api.Vehicle implementation for probably all SAIC cars
type MG struct {
*embed
*saic.Provider // provides the api implementations
}
func init() {
registry.Add("mg", NewMGFromConfig)
}
// NewBMWFromConfig creates a new vehicle
func NewMGFromConfig(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, api.ErrMissingCredentials
}
log := util.NewLogger("MG").Redact(cc.User, cc.Password, cc.VIN)
identity := saic.NewIdentity(log, cc.User, cc.Password)
if err := identity.Login(); err != nil {
return nil, err
}
api := saic.NewAPI(log, identity)
v := &MG{
embed: &cc.embed,
Provider: saic.NewProvider(api, cc.VIN, cc.Cache),
}
return v, nil
}

263
vehicle/saic/api.go Normal file
View file

@ -0,0 +1,263 @@
package saic
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/evcc-io/evcc/vehicle/saic/requests"
"golang.org/x/oauth2"
)
const (
StatRunning = iota
StatValid
StatInvalid
)
type ConcurrentRequest struct {
Status int
Result requests.ChargeStatus
}
// API is an api.Vehicle implementation for SAIC cars
type API struct {
*request.Helper
identity *Identity
request ConcurrentRequest
Logger *util.Logger
}
// NewAPI creates a new vehicle
func NewAPI(log *util.Logger, identity *Identity) *API {
v := &API{
Helper: request.NewHelper(log),
identity: identity,
Logger: log,
}
v.Client.Transport = &transport.Decorator{
Decorator: requests.Decorate,
Base: v.Client.Transport,
}
v.request.Status = StatInvalid
return v
}
/*
func (v *API) printAnswer() {
v.Logger.DEBUG.Printf("SOC:%d ", v.request.Result.ChrgMgmtData.BmsPackSOCDsp)
v.Logger.DEBUG.Printf("GUN:%d ", v.request.Result.RvsChargeStatus.ChargingGunState)
v.Logger.DEBUG.Printf("Chrg State:%d ", v.request.Result.ChrgMgmtData.BmsChrgSts)
v.Logger.DEBUG.Printf("Mileage:%d ", v.request.Result.RvsChargeStatus.Mileage)
v.Logger.DEBUG.Printf("Range:%d ", v.request.Result.RvsChargeStatus.FuelRangeElec)
}
*/
func (v *API) doRepeatedRequest(url string, event_id string) error {
var req *http.Request
answer := requests.Answer{
Data: &v.request.Result,
}
token, err := v.identity.Token()
if err != nil {
v.request.Status = StatInvalid
return err
}
req, err = requests.CreateRequest(url,
http.MethodGet,
"",
request.JSONContent,
token.AccessToken,
event_id)
if err != nil {
v.request.Status = StatInvalid
return err
}
_, err = v.DoRequest(req, &answer)
if err == nil {
v.request.Status = StatValid
} else if err != api.ErrMustRetry {
v.request.Status = StatInvalid
}
return err
}
// This is running concurrently
func (v *API) repeatRequest(url string, event_id string) {
var err error
var count = 0
v.request.Status = StatRunning
for err = api.ErrMustRetry; err == api.ErrMustRetry && count < 20; {
time.Sleep(2 * time.Second)
v.Logger.DEBUG.Printf("Starting repeated query. Count: %d\n", count)
err = v.doRepeatedRequest(url, event_id)
count++
}
v.Logger.DEBUG.Printf("Exitig repeated query. Count: %d\n", count)
//v.printAnswer()
}
func (v *API) DoRequest(req *http.Request, result *requests.Answer) (string, error) {
var body []byte
resp, err := v.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
v.Logger.DEBUG.Printf("DoRequest: %s", resp.Status)
v.identity.Login()
return "", api.ErrMustRetry
}
event_id := resp.Header.Get("event-id")
if result != nil {
body, err = requests.DecryptAnswer(resp)
if err == nil {
err = json.Unmarshal(body, result)
if err == nil && result.Code != 0 {
if result.Code == 4 {
err = api.ErrMustRetry
} else {
err = fmt.Errorf("%d: %s\n", result.Code, result.Message)
}
v.Logger.DEBUG.Printf("%d: %s\n", result.Code, result.Message)
}
} else {
if err != nil {
v.Logger.DEBUG.Printf("Decrypt: %s", err.Error())
}
}
}
return event_id, err
}
/* Vehicles implements returns the /user/vehicles api
func (v *API) Vehicles() ([]Vehicle, error) {
var res []Vehicle
uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles?apptimezone=120&appDateTime=%d", regions[v.region].CocoApiURI, time.Now().UnixMilli())
err := v.GetJSON(uri, &res)
return res, err
}
*/
func (v *API) Wakeup(vin string) error {
var req *http.Request
var err error
var token *oauth2.Token
token, err = v.identity.Token()
if err != nil {
return err
}
url := requests.BASE_URL_P + "vehicle/status?vin=" + requests.Sha256(vin)
req, err = requests.CreateRequest(url,
http.MethodGet,
"",
request.JSONContent,
token.AccessToken,
"")
if err != nil {
return err
}
v.DoRequest(req, nil)
return nil
}
// Status implements the /user/vehicles/<vin>/status api
func (v *API) Status(vin string) (requests.ChargeStatus, error) {
var req *http.Request
var res requests.ChargeStatus
var event_id string
var err error
var token *oauth2.Token
answer := requests.Answer{
Data: &res,
}
// Check if we are already running in the background
if v.request.Status == StatValid {
v.request.Status = StatInvalid
v.Logger.DEBUG.Printf("StatVaild. Returning stored value\n")
//v.printAnswer()
return v.request.Result, nil
} else if v.request.Status == StatRunning {
v.Logger.DEBUG.Printf("StatRunning. Exiting\n")
return res, api.ErrMustRetry
}
v.Logger.DEBUG.Printf("StatInvaild. Starting query\n")
token, err = v.identity.Token()
if err != nil {
return res, err
}
url := requests.BASE_URL_P + "vehicle/charging/mgmtData?vin=" + requests.Sha256(vin)
// get charging status of vehicle
req, err = requests.CreateRequest(url,
http.MethodGet,
"",
request.JSONContent,
token.AccessToken,
"")
if err != nil {
return res, err
}
event_id, err = v.DoRequest(req, &answer)
if err != nil {
v.Logger.DEBUG.Printf("Getting event id failed")
return res, err
}
if event_id == "" {
v.Logger.ERROR.Printf("Answer without event ID")
return res, api.ErrMustRetry
}
req, err = requests.CreateRequest(url,
http.MethodGet,
"",
request.JSONContent,
token.AccessToken,
event_id)
if err != nil {
v.Logger.ERROR.Printf("Could not create request %s", err.Error())
return res, err
}
_, err = v.DoRequest(req, &answer)
// Continue checking....
if err == api.ErrMustRetry {
v.request.Status = StatRunning
v.Logger.DEBUG.Printf(" No answer yet. Continue status query in background\n")
go v.repeatRequest(url, event_id)
} else if err != nil {
v.Logger.ERROR.Printf("doRequest failed with %s", err.Error())
}
return res, err
}

134
vehicle/saic/identity.go Normal file
View file

@ -0,0 +1,134 @@
package saic
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/evcc-io/evcc/vehicle/saic/requests"
"github.com/samber/lo"
"golang.org/x/oauth2"
)
type Identity struct {
*request.Helper
TokenSource oauth2.TokenSource
User string
Password string
deviceId string
}
// NewIdentity creates SAIC identity
func NewIdentity(log *util.Logger, user, password string) *Identity {
v := &Identity{
Helper: request.NewHelper(log),
User: user,
Password: requests.Sha1(password),
}
v.deviceId = lo.RandomString(64, lo.AlphanumericCharset) + "###com.saicmotor.europecar"
v.Client.Transport = &transport.Decorator{
Decorator: requests.Decorate,
Base: v.Client.Transport,
}
return v
}
func (v *Identity) Login() error {
data := url.Values{
"username": {v.User},
"password": {v.Password}, // Shold be Sha1 encoded
"grant_type": {"password"},
}
token, err := v.retrieveToken(data)
if err != nil {
return err
}
v.TokenSource = oauth2.ReuseTokenSourceWithExpiry(token, oauth.RefreshTokenSource(token, v), 15*time.Minute)
return nil
}
func (v *Identity) retrieveToken(data url.Values) (*oauth2.Token, error) {
var loginData requests.LoginData
answer := requests.Answer{
Data: &loginData,
}
data.Set("deviceId", v.deviceId)
data.Set("response_type", "code")
data.Set("scope", "all")
data.Set("deviceType", "0")
data.Set("loginType", "2")
data.Set("language", "en")
// get charging status of vehicle
req, err := requests.CreateRequest(
requests.BASE_URL_P+"oauth/token",
http.MethodPost,
data.Encode(),
request.FormContent,
"",
"")
if err != nil {
return nil, err
}
resp, err := v.Do(req)
if err != nil {
return nil, err
}
var body []byte
body, err = requests.DecryptAnswer(resp)
if err == nil && len(body) != 0 {
err = json.Unmarshal(body, &answer)
if err == nil && answer.Code != 0 {
err = fmt.Errorf("%d: %s", answer.Code, answer.Message)
}
tok := oauth2.Token{
AccessToken: loginData.Access_token,
RefreshToken: loginData.Refresh_token,
TokenType: loginData.Token_type,
}
tok.Expiry = time.Now().Add(time.Second * time.Duration(loginData.Expires_in))
return &tok, err
}
return nil, err
}
func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
data := url.Values{
"refresh_token": []string{token.RefreshToken},
"grant_type": []string{"refresh_token"},
}
token, err := v.retrieveToken(data)
if token == nil || err != nil {
// Refresh failed. Try a full login...
data.Del("refresh_token")
data.Set("username", v.User)
data.Set("password", v.Password) // Shold be Sha1 encoded
data.Set("grant_type", "password")
return v.retrieveToken(data)
}
return token, err
}
func (v *Identity) Token() (*oauth2.Token, error) {
return v.TokenSource.Token()
}

149
vehicle/saic/provider.go Normal file
View file

@ -0,0 +1,149 @@
package saic
import (
"fmt"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/provider"
"github.com/evcc-io/evcc/vehicle/saic/requests"
)
const (
Target40 = 1
Target50 = 2
Target60 = 3
Target70 = 4
Target80 = 5
Target90 = 6
Target100 = 7
)
var TargetSocVals = [...]int{0, 40, 50, 60, 70, 80, 90, 100}
// https://github.com/SAIC-iSmart-API/reverse-engineering
// Provider implements the vehicle api
type Provider struct {
status provider.Cacheable[requests.ChargeStatus]
wakeup func() error
}
// NewProvider creates a vehicle api provider
func NewProvider(api *API, vin string, cache time.Duration) *Provider {
impl := &Provider{
status: provider.ResettableCached(func() (requests.ChargeStatus, error) {
return api.Status(vin)
}, cache),
wakeup: func() error {
return api.Wakeup(vin)
},
}
return impl
}
var _ api.Battery = (*Provider)(nil)
// Soc implements the api.Vehicle interface
func (v *Provider) Soc() (float64, error) {
res, err := v.status.Get()
if err != nil {
return 0, err
}
val := res.ChrgMgmtData.BmsPackSOCDsp
if val > 1000 {
v.status.Reset()
return float64(val), fmt.Errorf("invalid raw soc value: %d: %w", val, api.ErrMustRetry)
}
return float64(val) / 10.0, nil
}
var _ api.ChargeState = (*Provider)(nil)
// Status implements the api.ChargeState interface
func (v *Provider) Status() (api.ChargeStatus, error) {
res, err := v.status.Get()
if err != nil {
return api.StatusNone, err
}
status := api.StatusA // disconnected
if res.RvsChargeStatus.ChargingGunState != 0 {
if (res.ChrgMgmtData.BmsChrgSts & 0x01) == 0 {
status = api.StatusB
} else {
status = api.StatusC
}
}
return status, nil
}
var _ api.VehicleFinishTimer = (*Provider)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *Provider) FinishTime() (time.Time, error) {
res, err := v.status.Get()
if err == nil {
ctr := res.ChrgMgmtData.ChrgngRmnngTime
return time.Now().Add(time.Duration(ctr) * time.Minute), err
}
return time.Time{}, err
}
var _ api.VehicleRange = (*Provider)(nil)
// Range implements the api.VehicleRange interface
func (v *Provider) Range() (int64, error) {
res, err := v.status.Get()
if err != nil {
return 0, err
}
val := res.RvsChargeStatus.FuelRangeElec
if val < 10 {
// Ok, 0 would be possible, but it's more likely that it's an invalid answer.
return val, fmt.Errorf("invalid raw range value: %d: %w", val, api.ErrMustRetry)
}
return val / 10, nil
}
var _ api.VehicleOdometer = (*Provider)(nil)
// Odometer implements the api.VehicleOdometer interface
func (v *Provider) Odometer() (float64, error) {
res, err := v.status.Get()
if err != nil {
return 0, err
}
return float64(res.RvsChargeStatus.Mileage) / 10.0, nil
}
var _ api.SocLimiter = (*Provider)(nil)
// TargetSoc implements the api.SocLimiter interface
func (v *Provider) TargetSoc() (float64, error) {
var result = 0
res, err := v.status.Get()
if err != nil {
return 0, err
}
index := res.ChrgMgmtData.BmsOnBdChrgTrgtSOCDspCmd
if index <= Target100 {
result = TargetSocVals[res.ChrgMgmtData.BmsOnBdChrgTrgtSOCDspCmd]
}
return float64(result), err
}
var _ api.Resurrector = (*Provider)(nil)
func (v *Provider) WakeUp() error {
return v.wakeup()
}

View file

@ -0,0 +1,9 @@
package requests
const (
CONTENT_ENCRYPTED = "1"
PARAM_AUTHENTICATION = "Basic c3dvcmQ6c3dvcmRfc2VjcmV0"
TENANT_ID = "459771"
USER_TYPE = "app"
BASE_URL_P = "https://gateway-mg-eu.soimt.com/api.app/v1/"
)

View file

@ -0,0 +1,60 @@
package requests
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
)
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS5Trimming(encrypt []byte) []byte {
padding := encrypt[len(encrypt)-1]
return encrypt[:len(encrypt)-int(padding)]
}
func Decrypt(cipherText, hexKey, hexIV string) string {
if len(cipherText) == 0 || len(hexKey) == 0 || len(hexIV) == 0 {
return ""
}
secretKey, _ := hex.DecodeString(hexKey)
ivParameter, _ := hex.DecodeString(hexIV)
block, err := aes.NewCipher(secretKey)
if err != nil {
return ""
}
decrypter := cipher.NewCBCDecrypter(block, ivParameter)
text, _ := hex.DecodeString(cipherText)
decrypted := make([]byte, len(text))
decrypter.CryptBlocks(decrypted, text)
return string(PKCS5Trimming(decrypted))
}
func Encrypt(plainText, hexKey, hexIV string) string {
if len(plainText) == 0 || len(hexKey) == 0 || len(hexIV) == 0 {
return ""
}
secretKey, _ := hex.DecodeString(hexKey)
ivParameter, _ := hex.DecodeString(hexIV)
block, err := aes.NewCipher(secretKey)
if err != nil {
return ""
}
content := PKCS5Padding([]byte(plainText), block.BlockSize())
encrypter := cipher.NewCBCEncrypter(block, ivParameter)
ciphertext := make([]byte, len(content))
encrypter.CryptBlocks(ciphertext, content)
return hex.EncodeToString(ciphertext)
}

View file

@ -0,0 +1,83 @@
package requests
import (
"strconv"
"strings"
)
func CalculateRequestVerification(
resourcePath string,
sendDate int64,
tenant,
contentType,
bodyEncrypted,
token string,
) string {
dateString := strconv.FormatInt(sendDate, 10)
str9 := resourcePath + tenant + token + USER_TYPE
a2 := Md5(str9)
str10 := dateString + CONTENT_ENCRYPTED + contentType
a3 := Md5(a2 + str10)
str11 := resourcePath + tenant + token + USER_TYPE + dateString + CONTENT_ENCRYPTED + contentType + bodyEncrypted
a5 := Md5(a3 + dateString)
if len(a5) != 0 && len(str11) != 0 {
return HmacSha256(a5, str11)
} else {
return ""
}
}
func DecryptResponse(timeStamp, contentType, cipherText string) string {
str4 := timeStamp + CONTENT_ENCRYPTED + contentType
a2 := Md5(str4)
hashedTimeStamp := Md5(timeStamp)
if len(cipherText) != 0 {
return Decrypt(cipherText, a2, hashedTimeStamp)
}
return ""
}
func CalculateResponseVerification(str, str2, str3 string) string {
str4 := str + CONTENT_ENCRYPTED + str2
a2 := Md5(str4)
str5 := str + CONTENT_ENCRYPTED + str2 + str3
a4 := Md5(a2 + str)
return HmacSha256(a4, str5)
}
func EncryptRequest(url string, time int64, tenant, token, body, contentType string) string {
sendDate := strconv.FormatInt(time, 10)
// tenant
replace := ""
if len(url) != 0 {
replace = strings.Replace(url, BASE_URL_P, "/", -1)
}
encryptedBody := ""
if len(body) != 0 {
sb3 := Md5(replace+tenant+token+USER_TYPE) + sendDate + CONTENT_ENCRYPTED + contentType
a2 := Md5(sb3)
a3 := Md5(sendDate)
if len(body) != 0 && len(a2) != 0 && len(a3) != 0 {
encryptedBody = Encrypt(body, a2, a3)
}
}
return encryptedBody
}
func DecryptRequest(url string, time int64, tenant, token, body, contentType string) string {
timeStamp := strconv.FormatInt(time, 10)
resourcePath := strings.Replace(url, BASE_URL_P, "/", -1)
if len(body) != 0 {
sb3 := Md5(resourcePath+tenant+token+USER_TYPE) + timeStamp + CONTENT_ENCRYPTED + contentType
a2 := Md5(sb3)
timeStampHash := Md5(timeStamp)
return Decrypt(body, a2, timeStampHash)
}
return ""
}

View file

@ -0,0 +1,32 @@
package requests
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
)
func Md5(value string) string {
if len(value) == 0 {
return ""
}
result := md5.Sum([]byte(value))
return hex.EncodeToString(result[:])
}
func Sha1(value string) string {
if len(value) == 0 {
return ""
}
result := sha1.Sum([]byte(value))
return hex.EncodeToString(result[:])
}
func Sha256(value string) string {
if len(value) == 0 {
return ""
}
result := sha256.Sum256([]byte(value))
return hex.EncodeToString(result[:])
}

View file

@ -0,0 +1,18 @@
package requests
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func HmacSha256(secret string, message string) string {
if len(secret) == 0 || len(message) == 0 {
return ""
}
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}

View file

@ -0,0 +1,94 @@
package requests
import (
"bytes"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
func Decorate(req *http.Request) error {
req.Header.Set("tenant-id", TENANT_ID)
req.Header.Set("user-type", USER_TYPE)
req.Header.Set("app-content-encrypted", CONTENT_ENCRYPTED)
req.Header.Set("Authorization", PARAM_AUTHENTICATION)
return nil
}
func CreateRequest(
endpoint string,
httpMethod string,
request string,
contentType string,
token string,
eventId string,
) (*http.Request, error) {
appSendDate := time.Now().UnixMilli()
if len(request) != 0 {
request = EncryptRequest(
endpoint,
appSendDate,
TENANT_ID,
token,
request,
contentType)
}
bodyReader := bytes.NewReader([]byte(request))
req, err := http.NewRequest(httpMethod, endpoint, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("app-send-date", strconv.FormatInt(appSendDate, 10))
req.Header.Set("original-content-type", contentType)
if len(token) != 0 {
req.Header.Set("blade-auth", token)
}
if len(eventId) != 0 {
req.Header.Set("event-id", eventId)
}
replace := strings.Replace(endpoint, BASE_URL_P, "/", -1)
req.Header.Set("app-verification-string",
CalculateRequestVerification(
replace,
appSendDate,
TENANT_ID,
contentType,
request,
token))
return req, nil
}
func DecryptAnswer(resp *http.Response) ([]byte, error) {
var body string
var bodyRaw []byte
var err error
if resp.StatusCode == http.StatusOK {
bodyRaw, err = io.ReadAll(resp.Body)
if err != nil {
return bodyRaw, err
}
if resp.Header.Get("app-content-encrypted") == "1" {
body = DecryptResponse(
resp.Header.Get("app-send-date"),
resp.Header.Get("original-content-type"),
string(bodyRaw))
} else {
body = string(bodyRaw)
}
} else {
err = fmt.Errorf(resp.Status)
}
return []byte(body), err
}

View file

@ -0,0 +1,98 @@
package requests
type ChargeStatus struct {
RvsChargeStatus struct {
MileageSinceLastCharge int
TotalBatteryCapacity int
WorkingVoltage int
ChargingDuration int
ChargingType int
LastChargeEndingPower int
FuelRangeElec int64 // Value / 10 = Range
RealtimePower int
WorkingCurrent int
ChargingGunState int // Gun connected
MileageOfDay int
StartTime int64
EndTime int64
PowerUsageOfDay int
PowerUsageSinceLastCharge int
Mileage uint // Odometer
}
ChrgMgmtData struct {
BmsChrgOtptCrntReqV int
CcuOnbdChrgrPlugOn int
BmsPTCHeatResp int
BmsChrgSts int
ChrgngAddedElecRngV int
BmsPackSOCDsp int // SOC in per mille
CcuOffBdChrgrPlugOn int
BmsPackCrnt int
ImcuChrgngEstdElecRng int
ChrgngSpdngTimeV int
BmsReserStMintueDspCmd int
BmsDsChrgSpRsn int
ImcuChrgngEstdElecRngV int
BmsPackVol int
BmsPackCrntV int
ImcuVehElecRngV int
BmsReserSpHourDspCmd int
BmsAdpPubChrgSttnDspCmd int
BmsReserSpMintueDspCmd int
BmsAltngChrgCrntDspCmd int
DisChrgngRmnngTimeV int
ImcuDschrgngEstdElecRngV int
BmsChrgSpRsn int
DisChrgngRmnngTime int
ChrgngAddedElecRng int
ClstrElecRngToEPT int // Range
BmsPTCHeatReqDspCmd int
CcuEleccLckCtrlDspCmd int
BmsChrgCtrlDspCmd int
BmsOnBdChrgTrgtSOCDspCmd int
OnBdChrgrAltrCrntInptCrnt int
BmsEstdElecRng int
ChrgngDoorOpenCnd int
BmsReserCtrlDspCmd int
BmsChrgOtptCrntReq int
ChrgngDoorPosSts int
BmsReserStHourDspCmd int
ImcuVehElecRng int // Range
ChrgngSpdngTime int
ChrgngRmnngTimeV int
ImcuDschrgngEstdElecRng int
ChrgngRmnngTime int64 // Charging remaining time
OnBdChrgrAltrCrntInptVol int
}
}
type LoginData struct {
Tenant_id string
LanguageType string
User_name string
Avatar string
Token_type string `json:"token_type,omitempty"`
Client_id string
Access_token string `json:"access_token"`
Role_name string
Refresh_token string `json:"refresh_token,omitempty"`
License string
Post_id string
User_id string
Role_id string
Scope string
Oauth_id string
Detail struct {
LanguageType string
}
Dept_id string
Expires_in int64 `json:"expires_in,omitempty"`
Aaccount string
Jti string
}
type Answer struct {
Code int `json:"code"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message"`
}