Add MG (#12003)
This commit is contained in:
parent
c570fc5272
commit
3f457e76fd
12 changed files with 1006 additions and 0 deletions
12
templates/definition/vehicle/mg.yaml
Normal file
12
templates/definition/vehicle/mg.yaml
Normal 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
54
vehicle/mg.go
Normal 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
263
vehicle/saic/api.go
Normal 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
134
vehicle/saic/identity.go
Normal 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
149
vehicle/saic/provider.go
Normal 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()
|
||||
}
|
||||
9
vehicle/saic/requests/api_config.go
Normal file
9
vehicle/saic/requests/api_config.go
Normal 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/"
|
||||
)
|
||||
60
vehicle/saic/requests/basic_encryption.go
Normal file
60
vehicle/saic/requests/basic_encryption.go
Normal 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)
|
||||
}
|
||||
83
vehicle/saic/requests/encryption.go
Normal file
83
vehicle/saic/requests/encryption.go
Normal 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 ""
|
||||
}
|
||||
32
vehicle/saic/requests/hashUtils.go
Normal file
32
vehicle/saic/requests/hashUtils.go
Normal 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[:])
|
||||
}
|
||||
18
vehicle/saic/requests/macUtils.go
Normal file
18
vehicle/saic/requests/macUtils.go
Normal 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))
|
||||
}
|
||||
94
vehicle/saic/requests/sendRequest.go
Normal file
94
vehicle/saic/requests/sendRequest.go
Normal 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
|
||||
}
|
||||
98
vehicle/saic/requests/types.go
Normal file
98
vehicle/saic/requests/types.go
Normal 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"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue