first commit
This commit is contained in:
882
client.go
Normal file
882
client.go
Normal file
@ -0,0 +1,882 @@
|
||||
package mysubaru
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Jeffail/gabs/v2"
|
||||
"github.com/alex-savin/go-mysubaru/config"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// credentials .
|
||||
type credentials struct {
|
||||
username string
|
||||
password string
|
||||
pin string
|
||||
deviceId string
|
||||
deviceName string
|
||||
}
|
||||
|
||||
// Client .
|
||||
type Client struct {
|
||||
baseURL string
|
||||
credentials credentials
|
||||
httpClient *resty.Client
|
||||
cookies []*http.Cookie
|
||||
country string //
|
||||
updateInterval int // 7200
|
||||
fetchInterval int // 360
|
||||
currentVin string
|
||||
listOfVins []string
|
||||
isAuthenticated bool
|
||||
isRegistered bool
|
||||
log *slog.Logger
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// Option .
|
||||
type Option func(*Client) error
|
||||
|
||||
// BaseURL allows overriding of API client baseURL for testing
|
||||
func BaseURL(baseURL string) Option {
|
||||
return func(c *Client) error {
|
||||
c.baseURL = baseURL
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Username .
|
||||
func Username(username string) Option {
|
||||
return func(c *Client) error {
|
||||
c.credentials.username = username
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Password .
|
||||
func Password(password string) Option {
|
||||
return func(c *Client) error {
|
||||
c.credentials.password = password
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// auth .
|
||||
func (c *Client) auth() []byte {
|
||||
// {
|
||||
// "success": true,
|
||||
// "errorCode": null,
|
||||
// "dataName": "sessionData",
|
||||
// "data": {
|
||||
// "sessionChanged": false,
|
||||
// "vehicleInactivated": false,
|
||||
// "account": {
|
||||
// "marketId": 1,
|
||||
// "createdDate": 1476984644000,
|
||||
// "firstName": "Tatiana",
|
||||
// "lastName": "Savin",
|
||||
// "zipCode": "07974",
|
||||
// "accountKey": 765268,
|
||||
// "lastLoginDate": 1640539132000,
|
||||
// "zipCode5": "07974"
|
||||
// },
|
||||
// "resetPassword": false,
|
||||
// "deviceId": "JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ",
|
||||
// "sessionId": "010A2C0C25A5D35AC3776DB6B7900A3B",
|
||||
// "deviceRegistered": true,
|
||||
// "passwordToken": null,
|
||||
// "vehicles": [
|
||||
// {
|
||||
// "customer": {
|
||||
// "sessionCustomer": null,
|
||||
// "email": null,
|
||||
// "firstName": null,
|
||||
// "lastName": null,
|
||||
// "zip": null,
|
||||
// "oemCustId": null,
|
||||
// "phone": null
|
||||
// },
|
||||
// "stolenVehicle": false,
|
||||
// "vehicleName": "Subaru Outback LXT",
|
||||
// "features": null,
|
||||
// "vin": "4S4BTGND8L3137058",
|
||||
// "modelYear": null,
|
||||
// "modelCode": null,
|
||||
// "engineSize": null,
|
||||
// "nickname": "Subaru Outback LXT",
|
||||
// "vehicleKey": 3832950,
|
||||
// "active": true,
|
||||
// "licensePlate": "",
|
||||
// "licensePlateState": "",
|
||||
// "email": null,
|
||||
// "firstName": null,
|
||||
// "lastName": null,
|
||||
// "subscriptionFeatures": null,
|
||||
// "accessLevel": -1,
|
||||
// "zip": null,
|
||||
// "oemCustId": "CRM-631-HQN48K",
|
||||
// "vehicleMileage": null,
|
||||
// "phone": null,
|
||||
// "userOemCustId": "CRM-631-HQN48K",
|
||||
// "subscriptionStatus": null,
|
||||
// "authorizedVehicle": false,
|
||||
// "preferredDealer": null,
|
||||
// "cachedStateCode": "NJ",
|
||||
// "subscriptionPlans": [],
|
||||
// "needMileagePrompt": false,
|
||||
// "phev": null,
|
||||
// "remoteServicePinExist": true,
|
||||
// "needEmergencyContactPrompt": false,
|
||||
// "vehicleGeoPosition": null,
|
||||
// "extDescrip": null,
|
||||
// "intDescrip": null,
|
||||
// "modelName": null,
|
||||
// "transCode": null,
|
||||
// "provisioned": true,
|
||||
// "timeZone": "America/New_York"
|
||||
// }
|
||||
// ],
|
||||
// "currentVehicleIndex": 0,
|
||||
// "handoffToken": "$2a$08$99me3RRpB00MNMSFxrG7AOg1T5BaDVacqXhbdTii0eRXoEoeFvEPy$1640540934344",
|
||||
// "enableXtime": true,
|
||||
// "termsAndConditionsAccepted": true,
|
||||
// "digitalGlobeConnectId": "0572e32b-2fcf-4bc8-abe0-1e3da8767132",
|
||||
// "digitalGlobeImageTileService": "https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132",
|
||||
// "digitalGlobeTransparentTileService": "https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132",
|
||||
// "tomtomKey": "DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs",
|
||||
// "satelliteViewEnabled": true
|
||||
// }
|
||||
// }
|
||||
params := map[string]string{
|
||||
"env": "cloudprod",
|
||||
"deviceType": "android",
|
||||
"loginUsername": c.credentials.username,
|
||||
"password": c.credentials.password,
|
||||
"deviceId": c.credentials.deviceId,
|
||||
"passwordToken": "",
|
||||
"selectedVin": "",
|
||||
"pushToken": ""}
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"]
|
||||
resp := c.execute(reqURL, POST, params, "", false)
|
||||
|
||||
c.log.Debug("AUTH HTTP OUTPUT", "body", string([]byte(resp)))
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// GET
|
||||
// https://www.mysubaru.com/profile/verifyDeviceName.json?clientId=2574212&deviceName=Alex%20Google%20Pixel%204%20XL
|
||||
// RESP: true/false
|
||||
|
||||
// POST
|
||||
// https://www.mysubaru.com/profile/editDeviceName.json?clientId=2574212&deviceName=Alex%20Google%20Pixel%204%20XL
|
||||
// clientId: 2574212
|
||||
// deviceName: Alex Google Pixel 4 XL
|
||||
// RESP: true/false
|
||||
|
||||
// GET
|
||||
// https://www.mysubaru.com/listMyDevices.json
|
||||
// {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]}
|
||||
// {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8210723,"deviceName":"Hassio Golang Integration","createdDate":"2021-12-22T01:38:43.000+0000","modifiedDate":"2021-12-22T01:38:43.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]}
|
||||
|
||||
// registerDevice .
|
||||
func (c *Client) registerDevice() bool {
|
||||
// c.httpClient.
|
||||
// SetBaseURL(WEB_API_SERVER[c.country]).
|
||||
// R().
|
||||
// SetFormData(map[string]string{
|
||||
// "username": c.credentials.username,
|
||||
// "password": c.credentials.password,
|
||||
// "deviceId": c.credentials.deviceId,
|
||||
// }).
|
||||
// Post(apiURLs["WEB_API_LOGIN"])
|
||||
params := map[string]string{
|
||||
"username": c.credentials.username,
|
||||
"password": c.credentials.password,
|
||||
"deviceId": c.credentials.deviceId}
|
||||
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"]
|
||||
c.execute(reqURL, POST, params, "", true)
|
||||
|
||||
// Authorizing device via web API
|
||||
// c.httpClient.
|
||||
// SetBaseURL(WEB_API_SERVER[c.country]).
|
||||
// R().
|
||||
// SetQueryParams(map[string]string{
|
||||
// "deviceId": c.credentials.deviceId,
|
||||
// }).
|
||||
// Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"])
|
||||
params = map[string]string{
|
||||
"deviceId": c.credentials.deviceId}
|
||||
reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"]
|
||||
c.execute(reqURL, GET, params, "", false)
|
||||
|
||||
return c.setDeviceName()
|
||||
}
|
||||
|
||||
// setDeviceName .
|
||||
func (c *Client) setDeviceName() bool {
|
||||
params := map[string]string{
|
||||
"deviceId": c.credentials.deviceId,
|
||||
"deviceName": c.credentials.deviceName}
|
||||
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"]
|
||||
c.execute(reqURL, GET, params, "", false)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// listDevices .
|
||||
func (c *Client) listDevices() {
|
||||
// Accept: application/json, text/javascript, */*; q=0.01
|
||||
// Accept-Encoding: gzip, deflate, br
|
||||
// Accept-Language: en-US,en;q=0.9,ru;q=0.8
|
||||
// Connection: keep-alive
|
||||
// Cookie: ORA_OTD_JROUTE=ozLwELf5jS-NHQ2CKZorOFfRgb8uo6lL; soa-visitor=12212021VHWnkqERZYThWe87TLUhr2Db; AMCVS_94001C8B532957140A490D4D%40AdobeOrg=1; mys-referringCodes=7~direct~; s_cc=true; AMCVS_subarucom%40AdobeOrg=1; style=null; s_pv=login.html; AMCV_subarucom%40AdobeOrg=-1124106680%7CMCIDTS%7C18988%7CMCMID%7C81535064704660726005836131001032500276%7CMCAID%7CNONE%7CMCOPTOUT-1640567559s%7CNONE%7CvVersion%7C5.2.0; AMCV_94001C8B532957140A490D4D%40AdobeOrg=-1124106680%7CMCIDTS%7C18988%7CMCMID%7C76913534164341455390435376071204508177%7CMCAID%7CNONE%7CMCOPTOUT-1640567559s%7CNONE%7CvVersion%7C5.2.0; s_sq=subarumysubarucwpprod%3D%2526c.%2526a.%2526activitymap.%2526page%253Dlogin.html%2526link%253DLog%252520In%2526region%253DloginForm%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dlogin.html%2526pidt%253D1%2526oid%253DLog%252520In%2526oidt%253D3%2526ot%253DSUBMIT; JSESSIONID=9685CFEB7888A0E6E25239D559E3B580; X-Oracle-BMC-LBS-Route=89e3283ece707e8a0ba4850e1a622122e039fd3d27da03a11a2ff120e313e9b656c62fd8a7c42ae8061a49ad6e1caf63a49d7befe4ad2a0194b0aeca
|
||||
// Host: www.mysubaru.com
|
||||
// Referer: https://www.mysubaru.com/profile/authorizedDevices.html
|
||||
// User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
|
||||
// X-Requested-With: XMLHttpRequest
|
||||
resp, err := c.httpClient.
|
||||
SetBaseURL(WEB_API_SERVER[c.country]).
|
||||
R().
|
||||
EnableTrace().
|
||||
Get(apiURLs["WEB_API_LIST_DEVICES"])
|
||||
|
||||
// Explore response object
|
||||
fmt.Println("Response Info:")
|
||||
fmt.Println(" Error :", err)
|
||||
fmt.Println(" Status Code:", resp.StatusCode())
|
||||
fmt.Println(" Status :", resp.Status())
|
||||
fmt.Println(" Proto :", resp.Proto())
|
||||
fmt.Println(" Time :", resp.Time())
|
||||
fmt.Println(" Received At:", resp.ReceivedAt())
|
||||
// fmt.Println(" Body :\n", resp)
|
||||
fmt.Println()
|
||||
|
||||
// Explore trace info
|
||||
fmt.Println("Request Trace Info:")
|
||||
ti := resp.Request.TraceInfo()
|
||||
fmt.Println(" DNSLookup :", ti.DNSLookup)
|
||||
fmt.Println(" ConnTime :", ti.ConnTime)
|
||||
fmt.Println(" TCPConnTime :", ti.TCPConnTime)
|
||||
fmt.Println(" TLSHandshake :", ti.TLSHandshake)
|
||||
fmt.Println(" ServerTime :", ti.ServerTime)
|
||||
fmt.Println(" ResponseTime :", ti.ResponseTime)
|
||||
fmt.Println(" TotalTime :", ti.TotalTime)
|
||||
fmt.Println(" IsConnReused :", ti.IsConnReused)
|
||||
fmt.Println(" IsConnWasIdle :", ti.IsConnWasIdle)
|
||||
fmt.Println(" ConnIdleTime :", ti.ConnIdleTime)
|
||||
fmt.Println(" RequestAttempt:", ti.RequestAttempt)
|
||||
fmt.Println(" RemoteAddr :", ti.RemoteAddr.String())
|
||||
|
||||
// c.log.Debug("LIST DEVICES OUTPUT", "body", string([]byte(resp.Body())))
|
||||
|
||||
// c.httpClient.SetBaseURL(WEB_API_SERVER[c.country]).SetCookies(c.cookies)
|
||||
// reqURL := apiURLs["WEB_API_LIST_DEVICES"]
|
||||
// resp := c.execute(reqURL, GET, map[string]string{}, "", false)
|
||||
|
||||
// if isResponseSuccessfull(resp) {
|
||||
// log.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp))
|
||||
// }
|
||||
}
|
||||
|
||||
// validateSession .
|
||||
func (c *Client) validateSession() bool {
|
||||
// {
|
||||
// "success": true,
|
||||
// "errorCode": null,
|
||||
// "dataName": null,
|
||||
// "data": null
|
||||
// }
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"]
|
||||
resp := c.execute(reqURL, GET, map[string]string{}, "", false)
|
||||
|
||||
if !c.isResponseSuccessfull(resp) {
|
||||
return false
|
||||
}
|
||||
// result = False
|
||||
// js_resp = await self.__open(API_VALIDATE_SESSION, GET)
|
||||
// _LOGGER.debug(pprint.pformat(js_resp))
|
||||
// if js_resp["success"]:
|
||||
// if vin != self._current_vin:
|
||||
// # API call for VIN that is not the current remote context.
|
||||
// _LOGGER.debug("Switching Subaru API vehicle context to: %s", vin)
|
||||
// if await self._select_vehicle(vin):
|
||||
// result = True
|
||||
// else:
|
||||
// result = True
|
||||
|
||||
// if result is False:
|
||||
// await self._authenticate(vin)
|
||||
// # New session cookie. Must call selectVehicle.json before any other API call.
|
||||
// if await self._select_vehicle(vin):
|
||||
// result = True
|
||||
|
||||
c.log.Debug("[DEBUG] Validate Session", "body", string([]byte(resp)))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Client) SelectVehicle(vin string) VehicleData {
|
||||
// {
|
||||
// "success": true,
|
||||
// "errorCode": null,
|
||||
// "dataName": "vehicle",
|
||||
// "data": {
|
||||
// "customer": {
|
||||
// "sessionCustomer": null,
|
||||
// "email": null,
|
||||
// "firstName": null,
|
||||
// "lastName": null,
|
||||
// "zip": null,
|
||||
// "oemCustId": null,
|
||||
// "phone": null
|
||||
// },
|
||||
// "stolenVehicle": false,
|
||||
// "vehicleName": "Subaru Outback LXT",
|
||||
// "features": [
|
||||
// "ATF_MIL",
|
||||
// "11.6MMAN",
|
||||
// "ABS_MIL",
|
||||
// "CEL_MIL",
|
||||
// "ACCS",
|
||||
// "RCC",
|
||||
// "REARBRK",
|
||||
// "TEL_MIL",
|
||||
// "VDC_MIL",
|
||||
// "TPMS_MIL",
|
||||
// "WASH_MIL",
|
||||
// "BSDRCT_MIL",
|
||||
// "OPL_MIL",
|
||||
// "EYESIGHT",
|
||||
// "RAB_MIL",
|
||||
// "SRS_MIL",
|
||||
// "ESS_MIL",
|
||||
// "RESCC",
|
||||
// "EOL_MIL",
|
||||
// "BSD",
|
||||
// "EBD_MIL",
|
||||
// "EPB_MIL",
|
||||
// "RES",
|
||||
// "RHSF",
|
||||
// "AWD_MIL",
|
||||
// "NAV_TOMTOM",
|
||||
// "ISS_MIL",
|
||||
// "RPOIA",
|
||||
// "EPAS_MIL",
|
||||
// "RPOI",
|
||||
// "AHBL_MIL",
|
||||
// "SRH_MIL",
|
||||
// "g2"
|
||||
// ],
|
||||
// "vin": "4S4BTGND8L3137058",
|
||||
// "modelYear": "2020",
|
||||
// "modelCode": "LDJ",
|
||||
// "engineSize": 2.4,
|
||||
// "nickname": "Subaru Outback LXT",
|
||||
// "vehicleKey": 3832950,
|
||||
// "active": true,
|
||||
// "licensePlate": "8KV8",
|
||||
// "licensePlateState": "NJ",
|
||||
// "email": null,
|
||||
// "firstName": null,
|
||||
// "lastName": null,
|
||||
// "subscriptionFeatures": [
|
||||
// "REMOTE",
|
||||
// "SAFETY",
|
||||
// "Retail"
|
||||
// ],
|
||||
// "accessLevel": -1,
|
||||
// "zip": null,
|
||||
// "oemCustId": "CRM-631-HQN48K",
|
||||
// "vehicleMileage": null,
|
||||
// "phone": null,
|
||||
// "userOemCustId": "CRM-631-HQN48K",
|
||||
// "subscriptionStatus": "ACTIVE",
|
||||
// "authorizedVehicle": false,
|
||||
// "preferredDealer": null,
|
||||
// "cachedStateCode": "NJ",
|
||||
// "subscriptionPlans": [],
|
||||
// "needMileagePrompt": false,
|
||||
// "phev": null,
|
||||
// "remoteServicePinExist": true,
|
||||
// "needEmergencyContactPrompt": false,
|
||||
// "vehicleGeoPosition": {
|
||||
// "latitude": 40.70019,
|
||||
// "longitude": -74.401375,
|
||||
// "speed": null,
|
||||
// "heading": null,
|
||||
// "timestamp": 1640494569000
|
||||
// },
|
||||
// "extDescrip": "Abyss Blue Pearl",
|
||||
// "intDescrip": "Gray",
|
||||
// "modelName": "Outback",
|
||||
// "transCode": "CVT",
|
||||
// "provisioned": true,
|
||||
// "timeZone": "America/New_York"
|
||||
// }
|
||||
// }
|
||||
|
||||
if vin == "" {
|
||||
vin = c.currentVin
|
||||
}
|
||||
|
||||
vinCheck(vin)
|
||||
|
||||
params := map[string]string{
|
||||
"vin": vin,
|
||||
"_": timestamp()}
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
|
||||
resp := c.execute(reqURL, GET, params, "", false)
|
||||
|
||||
var vData VehicleData
|
||||
respParsed, err := gabs.ParseJSON([]byte(resp))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
vdString := respParsed.Path("data").String()
|
||||
json.Unmarshal([]byte(vdString), &vData)
|
||||
|
||||
// resp := c.execute(reqURL, GET, params, "", false)
|
||||
// log.Debugf("SELECT VEHICLE OUTPUT >> %v\n", string([]byte(resp)))
|
||||
|
||||
// ERRORS
|
||||
// {"success":false,"errorCode":"vehicleNotInAccount","dataName":null,"data":null}
|
||||
|
||||
// """Select active vehicle for accounts with multiple VINs."""
|
||||
// params = {"vin": vin, "_": int(time.time())}
|
||||
// js_resp = await self.get(API_SELECT_VEHICLE, params=params)
|
||||
// _LOGGER.debug(pprint.pformat(js_resp))
|
||||
// if js_resp.get("success"):
|
||||
// self._current_vin = vin
|
||||
// _LOGGER.debug("Current vehicle: vin=%s", js_resp["data"]["vin"])
|
||||
// return js_resp["data"]
|
||||
// if not js_resp.get("success") and js_resp.get("errorCode") == "VEHICLESETUPERROR":
|
||||
// # Occasionally happens every few hours. Resetting the session seems to deal with it.
|
||||
// _LOGGER.warning("VEHICLESETUPERROR received. Resetting session.")
|
||||
// self.reset_session()
|
||||
// return False
|
||||
// _LOGGER.debug("Failed to switch vehicle errorCode=%s", js_resp.get("errorCode"))
|
||||
// # Something else is probably wrong with the backend server context - try resetting
|
||||
// self.reset_session()
|
||||
// raise SubaruException("Failed to switch vehicle %s - resetting session." % js_resp.get("errorCode"))
|
||||
return vData
|
||||
}
|
||||
|
||||
// New function creates a New MySubaru client
|
||||
func New(logger *slog.Logger, config *config.MySubaru) (*Client, error) {
|
||||
|
||||
credentials := credentials{
|
||||
username: config.Credentials.Username,
|
||||
password: config.Credentials.Password,
|
||||
pin: config.Credentials.PIN,
|
||||
deviceId: config.Credentials.DeviceID,
|
||||
deviceName: config.Credentials.DeviceName,
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
credentials: credentials,
|
||||
country: config.Region,
|
||||
updateInterval: 7200,
|
||||
fetchInterval: 360,
|
||||
log: logger,
|
||||
}
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient.
|
||||
SetBaseURL(MOBILE_API_SERVER[client.country]).
|
||||
SetHeaders(map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.191030.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36",
|
||||
"Origin": "file://",
|
||||
"X-Requested-With": MOBILE_APP[client.country],
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept": "*/*"})
|
||||
|
||||
client.httpClient = httpClient
|
||||
|
||||
resp := client.auth()
|
||||
|
||||
respParsed, err := gabs.ParseJSON(resp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if client.isResponseSuccessfull(resp) {
|
||||
logger.Debug("Client authentication successful", "isRegistered", respParsed.Path("data.deviceRegistered").Data().(bool))
|
||||
client.isAuthenticated = true
|
||||
client.isRegistered = respParsed.Path("data.deviceRegistered").Data().(bool)
|
||||
} else {
|
||||
error, _ := respParsed.Path("errorCode").Data().(string)
|
||||
switch {
|
||||
case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
|
||||
logger.Debug("Invalid account")
|
||||
case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
|
||||
logger.Debug("Client authentication failed")
|
||||
case error == apiErrors["ERROR_PASSWORD_WARNING"]:
|
||||
logger.Debug("Multiple Password Failures.")
|
||||
default:
|
||||
logger.Debug("Uknown error")
|
||||
}
|
||||
}
|
||||
|
||||
if !client.isRegistered {
|
||||
client.registerDevice()
|
||||
}
|
||||
|
||||
// TODO
|
||||
fmt.Printf("Parcing cars: %d\n", len(respParsed.Path("data.vehicles").Children()))
|
||||
for _, vehicle := range respParsed.Path("data.vehicles").Children() {
|
||||
fmt.Printf("CAR: %s\n", vehicle.Path("vin").Data().(string))
|
||||
client.listOfVins = append(client.listOfVins, vehicle.Path("vin").Data().(string))
|
||||
}
|
||||
client.currentVin = respParsed.Path("data.vehicles.0.vin").Data().(string)
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetVehicles .
|
||||
func (c *Client) GetVehicles() []*Vehicle {
|
||||
var vehicles []*Vehicle
|
||||
for _, vin := range c.listOfVins {
|
||||
params := map[string]string{
|
||||
"vin": vin,
|
||||
"_": timestamp()}
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
|
||||
resp := c.execute(reqURL, GET, params, "", false)
|
||||
|
||||
respParsed, err := gabs.ParseJSON([]byte(resp))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
vData := VehicleData{}
|
||||
vdString := respParsed.Path("data").String()
|
||||
|
||||
json.Unmarshal([]byte(vdString), &vData)
|
||||
|
||||
// fmt.Printf("VEHICLE STRING: %+v\n\n", vdString)
|
||||
// fmt.Printf("VEHICLE DATA: %+v\n\n", vData)
|
||||
|
||||
vehicle := &Vehicle{
|
||||
Vin: vin,
|
||||
CarName: vData.VehicleName,
|
||||
CarNickname: vData.Nickname,
|
||||
ModelName: vData.ModelName,
|
||||
ModelYear: vData.ModelYear,
|
||||
ModelCode: vData.ModelCode,
|
||||
ExtDescrip: vData.ExtDescrip,
|
||||
IntDescrip: vData.IntDescrip,
|
||||
TransCode: vData.TransCode,
|
||||
EngineSize: vData.EngineSize,
|
||||
VehicleKey: vData.VehicleKey,
|
||||
LicensePlate: vData.LicensePlate,
|
||||
LicensePlateState: vData.LicensePlateState,
|
||||
Features: vData.Features,
|
||||
SubscriptionFeatures: vData.SubscriptionFeatures,
|
||||
client: c,
|
||||
}
|
||||
vehicle.GetVehicleStatus()
|
||||
vehicle.GetVehicleCondition()
|
||||
vehicle.GetClimatePresets()
|
||||
vehicle.GetClimateUserPresets()
|
||||
|
||||
vehicles = append(vehicles, vehicle)
|
||||
}
|
||||
|
||||
return vehicles
|
||||
}
|
||||
|
||||
// GetVehicleByVIN .
|
||||
func (c *Client) GetVehicleByVIN(vin string) *Vehicle {
|
||||
var vehicle *Vehicle
|
||||
if contains(c.listOfVins, vin) {
|
||||
params := map[string]string{
|
||||
"vin": vin,
|
||||
"_": timestamp()}
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
|
||||
resp := c.execute(reqURL, GET, params, "", false)
|
||||
|
||||
respParsed, err := gabs.ParseJSON([]byte(resp))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
vData := VehicleData{}
|
||||
vdString := respParsed.Path("data").String()
|
||||
json.Unmarshal([]byte(vdString), &vData)
|
||||
|
||||
// fmt.Printf("VEHICLE DATA: %+v\n\n", vData)
|
||||
|
||||
vehicle = &Vehicle{
|
||||
Vin: vin,
|
||||
CarName: vData.VehicleName,
|
||||
CarNickname: vData.Nickname,
|
||||
ModelName: vData.ModelName,
|
||||
ModelYear: vData.ModelYear,
|
||||
ModelCode: vData.ModelCode,
|
||||
ExtDescrip: vData.ExtDescrip,
|
||||
IntDescrip: vData.IntDescrip,
|
||||
TransCode: vData.TransCode,
|
||||
EngineSize: vData.EngineSize,
|
||||
VehicleKey: vData.VehicleKey,
|
||||
LicensePlate: vData.LicensePlate,
|
||||
LicensePlateState: vData.LicensePlateState,
|
||||
Features: vData.Features,
|
||||
SubscriptionFeatures: vData.SubscriptionFeatures,
|
||||
client: c,
|
||||
}
|
||||
vehicle.GetVehicleStatus()
|
||||
vehicle.GetVehicleCondition()
|
||||
vehicle.GetClimatePresets()
|
||||
vehicle.GetClimateUserPresets()
|
||||
}
|
||||
|
||||
return vehicle
|
||||
}
|
||||
|
||||
// GetVehicleStatus .
|
||||
func (c *Client) GetVehicleStatus() {
|
||||
// {
|
||||
// "dataName": null,
|
||||
// "errorCode": null,
|
||||
// "success": true,
|
||||
// "data": {
|
||||
// "avgFuelConsumptionLitersPer100Kilometers": 12.5,
|
||||
// "avgFuelConsumptionMpg": 18.8,
|
||||
// "distanceToEmptyFuelKilometers": 563,
|
||||
// "distanceToEmptyFuelKilometers10s": 560,
|
||||
// "distanceToEmptyFuelMiles": 349.83,
|
||||
// "distanceToEmptyFuelMiles10s": 350,
|
||||
// "evDistanceToEmptyByStateKilometers": null,
|
||||
// "evDistanceToEmptyByStateMiles": null,
|
||||
// "evDistanceToEmptyKilometers": null,
|
||||
// "evDistanceToEmptyMiles": null,
|
||||
// "evStateOfChargePercent": null,
|
||||
// "eventDate": 1640494569000,
|
||||
// "eventDateStr": "2021-12-26T04:56+0000",
|
||||
// "latitude": 40.700192,
|
||||
// "longitude": -74.401377,
|
||||
// "odometerValue": 24065,
|
||||
// "odometerValueKilometers": 38721,
|
||||
// "positionHeadingDegree": "150",
|
||||
// "tirePressureFrontLeft": "2413",
|
||||
// "tirePressureFrontLeftPsi": "35",
|
||||
// "tirePressureFrontRight": "2413",
|
||||
// "tirePressureFrontRightPsi": "35",
|
||||
// "tirePressureRearLeft": "2551",
|
||||
// "tirePressureRearLeftPsi": "37",
|
||||
// "tirePressureRearRight": "2482",
|
||||
// "tirePressureRearRightPsi": "36",
|
||||
// "vehicleStateType": "IGNITION_OFF",
|
||||
// "vhsId": 923920223
|
||||
// }
|
||||
// }
|
||||
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_STATUS"]
|
||||
resp := c.execute(reqURL, GET, map[string]string{}, "", false)
|
||||
|
||||
respParsed, err := gabs.ParseJSON(resp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.log.Debug("GET VEHICLE STATUS OUTPUT", "body", respParsed)
|
||||
|
||||
success, ok := respParsed.Path("success").Data().(bool)
|
||||
// value == string, ok == false
|
||||
if !ok {
|
||||
// TODO: Work with errorCode
|
||||
panic(success)
|
||||
}
|
||||
}
|
||||
|
||||
// GetVehicleStatus .
|
||||
func (c *Client) GetClimateSettings() {
|
||||
// {
|
||||
// "success": true,
|
||||
// "errorCode": null,
|
||||
// "dataName": null,
|
||||
// "data": {
|
||||
// "climateZoneFrontTemp": "70",
|
||||
// "runTimeMinutes": "10",
|
||||
// "climateZoneFrontAirMode": "WINDOW",
|
||||
// "heatedSeatFrontLeft": "LOW_HEAT",
|
||||
// "heatedSeatFrontRight": "LOW_HEAT",
|
||||
// "heatedRearWindowActive": "true",
|
||||
// "climateZoneFrontAirVolume": "6",
|
||||
// "outerAirCirculation": "outsideAir",
|
||||
// "airConditionOn": "false",
|
||||
// "startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION"
|
||||
// }
|
||||
// }
|
||||
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_CLIMATE_SETTINGS"]
|
||||
resp := c.execute(reqURL, GET, map[string]string{}, "", false)
|
||||
|
||||
respParsed, err := gabs.ParseJSON(resp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.log.Debug("CLIMATE SETTINGS OUTPUT", "response", respParsed)
|
||||
|
||||
// ONLY FOR THAT REQUEST BECAUSE OF API SENDS BACK ESCAPING DATA IN DATA FIELD
|
||||
data, ok := respParsed.Path("data").Data().(string)
|
||||
// rawIn := json.RawMessage(in)
|
||||
// bytes, err := rawIn.MarshalJSON()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
|
||||
// value == string, ok == false
|
||||
if !ok {
|
||||
// TODO: Work with errorCode
|
||||
panic(data)
|
||||
}
|
||||
c.log.Debug("CLIMATE SETTINGS OUTPUT", "body", data)
|
||||
}
|
||||
|
||||
// func isPINRequired() {}
|
||||
// func getVehicles() {}
|
||||
// func getEVStatus() {}
|
||||
// func getRemoteOptionsStatus() {}
|
||||
// func getRemoteStartStatus() {}
|
||||
// func getSafetyStatus() {}
|
||||
// func getSubscriptionStatus() {}
|
||||
// func getAPIGen() {}
|
||||
// func getClimateData() {}
|
||||
// func saveClimateSettings() {}
|
||||
// func getVehicleName() {}
|
||||
// func fetch() {}
|
||||
|
||||
// Exec method executes a Client instance with the API URL
|
||||
func (c *Client) execute(requestUrl string, method string, params map[string]string, pollingUrl string, json bool) []byte {
|
||||
defer timeTrack("[TIMETRK] Executing Get Request")
|
||||
|
||||
// if !isNil(resp.Cookies()) {
|
||||
// log.Debugf("AUTH COOKIES OUTPUT >> %v\n", resp.Cookies())
|
||||
// c.cookies = resp.Cookies()
|
||||
// }
|
||||
|
||||
var resp *resty.Response
|
||||
// GET Requests
|
||||
if method == "GET" {
|
||||
resp, _ = c.httpClient.
|
||||
// SetBaseURL(MOBILE_API_SERVER[c.country]).
|
||||
R().
|
||||
SetQueryParams(params).
|
||||
Get(requestUrl)
|
||||
}
|
||||
|
||||
// POST Requests
|
||||
if method == "POST" {
|
||||
if json {
|
||||
// POST > JSON Body
|
||||
resp, _ = c.httpClient.R().
|
||||
SetBody(params).
|
||||
Post(requestUrl)
|
||||
} else {
|
||||
// POST > Form Data
|
||||
resp, _ = c.httpClient.R().
|
||||
SetFormData(params).
|
||||
Post(requestUrl)
|
||||
}
|
||||
}
|
||||
|
||||
respParsed, err := gabs.ParseJSON([]byte(resp.Body()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.log.Debug("HTTP OUTPUT", "body", string(resp.Body()))
|
||||
|
||||
_, ok := respParsed.Path("success").Data().(bool)
|
||||
// value == string, ok == false
|
||||
if !ok {
|
||||
// TODO: Work with errorCode
|
||||
// panic(success)
|
||||
fmt.Printf("ERROR: %+v", string([]byte(resp.Body())))
|
||||
}
|
||||
|
||||
if pollingUrl != "" {
|
||||
serviceRequestId, _ := respParsed.Path("data.serviceRequestId").Data().(string)
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
attempts := 20
|
||||
poolingLoop:
|
||||
for attempts > 0 {
|
||||
resp, _ = c.httpClient.
|
||||
SetBaseURL(MOBILE_API_SERVER[c.country]).
|
||||
R().
|
||||
SetQueryParams(map[string]string{
|
||||
"serviceRequestId": serviceRequestId,
|
||||
}).
|
||||
Get(pollingUrl)
|
||||
|
||||
c.log.Debug("POLLING HTTP OUTPUT", "body", string([]byte(resp.Body())))
|
||||
// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}}
|
||||
|
||||
respParsed, err := gabs.ParseJSON(resp.Body())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
success, ok := respParsed.Path("success").Data().(bool)
|
||||
if !ok {
|
||||
panic(success)
|
||||
}
|
||||
if success {
|
||||
status, _ := respParsed.Path("data.remoteServiceState").Data().(string)
|
||||
switch {
|
||||
case status == "finished":
|
||||
c.log.Debug("Remote service request completed successfully", "request id", serviceRequestId)
|
||||
break poolingLoop
|
||||
case status == "started":
|
||||
c.log.Debug("Subaru API reports remote service request is in progress", "request id", serviceRequestId)
|
||||
}
|
||||
} else {
|
||||
c.log.Debug("Backend session expired, please try again")
|
||||
break poolingLoop
|
||||
}
|
||||
attempts--
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// fmt.Printf("[DEBUG] HTTP OUTPUT >> %v\n", string([]byte(resp.Body())))
|
||||
|
||||
return resp.Body()
|
||||
}
|
||||
|
||||
// isResponseSuccessfull .
|
||||
func (c *Client) isResponseSuccessfull(resp []byte) bool {
|
||||
respParsed, err := gabs.ParseJSON(resp)
|
||||
if err != nil {
|
||||
c.log.Debug("error while parsing json response", "error", err)
|
||||
}
|
||||
|
||||
success, ok := respParsed.Path("success").Data().(bool)
|
||||
if !ok {
|
||||
c.log.Debug("response is not successful", "error", resp)
|
||||
}
|
||||
|
||||
// ERRORS FROM CLIENT CREATION AFTER AUTH
|
||||
// error, _ := respParsed.Path("errorCode").Data().(string)
|
||||
// switch {
|
||||
// case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
|
||||
// fmt.Println("Invalid account")
|
||||
// case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
|
||||
// {"success":false,"errorCode":"InvalidCredentials","dataName":"remoteServiceStatus","data":{"serviceRequestId":null,"success":false,"cancelled":false,"remoteServiceType":null,"remoteServiceState":null,"subState":null,"errorCode":null,"result":null,"updateTime":null,"vin":null,"errorDescription":"The credentials supplied are invalid, tries left 2"}}
|
||||
// fmt.Println("Client authentication failed")
|
||||
// case error == apiErrors["ERROR_PASSWORD_WARNING"]:
|
||||
// fmt.Println("Multiple Password Failures.")
|
||||
// default:
|
||||
// fmt.Println("Uknown error")
|
||||
// }
|
||||
|
||||
return success
|
||||
}
|
Reference in New Issue
Block a user