Add Smart Hello (#10945)
This commit is contained in:
parent
6c19952f24
commit
08b2cb61ca
10 changed files with 784 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
12
templates/definition/vehicle/smart-hello.yaml
Normal file
12
templates/definition/vehicle/smart-hello.yaml
Normal 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" . }}
|
||||
|
|
@ -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
61
vehicle/smart-hello.go
Normal 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
196
vehicle/smart/hello/api.go
Normal 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
|
||||
}
|
||||
10
vehicle/smart/hello/const.go
Normal file
10
vehicle/smart/hello/const.go
Normal 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"
|
||||
)
|
||||
69
vehicle/smart/hello/helper.go
Normal file
69
vehicle/smart/hello/helper.go
Normal 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)
|
||||
}
|
||||
264
vehicle/smart/hello/identity.go
Normal file
264
vehicle/smart/hello/identity.go
Normal 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
|
||||
}
|
||||
64
vehicle/smart/hello/provider.go
Normal file
64
vehicle/smart/hello/provider.go
Normal 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
|
||||
}
|
||||
104
vehicle/smart/hello/types.go
Normal file
104
vehicle/smart/hello/types.go
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue