diff --git a/templates/definition/vehicle/mg.yaml b/templates/definition/vehicle/mg.yaml new file mode 100644 index 000000000..bfd1c31e4 --- /dev/null +++ b/templates/definition/vehicle/mg.yaml @@ -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" . }} diff --git a/vehicle/mg.go b/vehicle/mg.go new file mode 100644 index 000000000..f5eec1196 --- /dev/null +++ b/vehicle/mg.go @@ -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 +} diff --git a/vehicle/saic/api.go b/vehicle/saic/api.go new file mode 100644 index 000000000..1a8aff250 --- /dev/null +++ b/vehicle/saic/api.go @@ -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//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 +} diff --git a/vehicle/saic/identity.go b/vehicle/saic/identity.go new file mode 100644 index 000000000..c0c180a0f --- /dev/null +++ b/vehicle/saic/identity.go @@ -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() +} diff --git a/vehicle/saic/provider.go b/vehicle/saic/provider.go new file mode 100644 index 000000000..54874ff3a --- /dev/null +++ b/vehicle/saic/provider.go @@ -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() +} diff --git a/vehicle/saic/requests/api_config.go b/vehicle/saic/requests/api_config.go new file mode 100644 index 000000000..5879332b5 --- /dev/null +++ b/vehicle/saic/requests/api_config.go @@ -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/" +) diff --git a/vehicle/saic/requests/basic_encryption.go b/vehicle/saic/requests/basic_encryption.go new file mode 100644 index 000000000..04cf6cbe8 --- /dev/null +++ b/vehicle/saic/requests/basic_encryption.go @@ -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) +} diff --git a/vehicle/saic/requests/encryption.go b/vehicle/saic/requests/encryption.go new file mode 100644 index 000000000..217e2f48a --- /dev/null +++ b/vehicle/saic/requests/encryption.go @@ -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 "" +} diff --git a/vehicle/saic/requests/hashUtils.go b/vehicle/saic/requests/hashUtils.go new file mode 100644 index 000000000..78ec56e73 --- /dev/null +++ b/vehicle/saic/requests/hashUtils.go @@ -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[:]) +} diff --git a/vehicle/saic/requests/macUtils.go b/vehicle/saic/requests/macUtils.go new file mode 100644 index 000000000..a08b97cc8 --- /dev/null +++ b/vehicle/saic/requests/macUtils.go @@ -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)) +} diff --git a/vehicle/saic/requests/sendRequest.go b/vehicle/saic/requests/sendRequest.go new file mode 100644 index 000000000..b72d39361 --- /dev/null +++ b/vehicle/saic/requests/sendRequest.go @@ -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 +} diff --git a/vehicle/saic/requests/types.go b/vehicle/saic/requests/types.go new file mode 100644 index 000000000..49b40b6e1 --- /dev/null +++ b/vehicle/saic/requests/types.go @@ -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"` +}