Files
mysubaru/client.go
Alex Savin 07e3005e9c
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 24s
Remove session validation checks from vehicle selection and contact methods
2025-07-08 16:42:39 -04:00

664 lines
30 KiB
Go

package mysubaru
import (
"encoding/json"
"errors"
"io"
"log/slog"
"regexp"
"slices"
"sync"
"time"
"git.savin.nyc/alex/mysubaru/config"
"resty.dev/v3"
)
// Client represents a MySubaru API client that interacts with the MySubaru API.
type Client struct {
credentials config.Credentials
httpClient *resty.Client
country string // USA | CA
contactMethods dataMap // List of contact methods for 2FA
currentVin string
listOfVins []string
isAuthenticated bool
isRegistered bool
isAlive bool
updateInterval int // 7200
fetchInterval int // 360
logger *slog.Logger
sync.RWMutex
}
// New function creates a New MySubaru API client
func New(config *config.Config) (*Client, error) {
client := &Client{
credentials: config.MySubaru.Credentials,
country: config.MySubaru.Region,
updateInterval: 7200,
fetchInterval: 360,
logger: config.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
if ok, err := client.auth(); !ok {
client.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
return nil, errors.New("error while executing auth request: " + err.Error())
}
return client, nil
}
// auth authenticates the client with the MySubaru API using the provided credentials.
func (c *Client) auth() (bool, error) {
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, err := c.execute(POST, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
return false, errors.New("error while executing auth request: " + err.Error())
}
c.logger.Debug("http request output", "request", "auth", "body", resp)
var sd SessionData
err = json.Unmarshal(resp.Data, &sd)
if err != nil {
c.logger.Error("error while parsing json", "request", "auth", "error", err.Error())
}
// client.logger.Debug("unmarshaled json data", "request", "auth", "type", "sessionData", "body", sd)
if !sd.DeviceRegistered {
err := c.getContactMethods()
if err != nil {
c.logger.Error("error while getting contact methods", "request", "auth", "error", err.Error())
return false, errors.New("error while getting contact methods: " + err.Error())
}
c.logger.Error("device is not registered", "request", "auth", "deviceId", c.credentials.DeviceID)
return false, errors.New("device is not registered: " + c.credentials.DeviceID)
}
if sd.DeviceRegistered && sd.RegisteredDevicePermanent {
c.isAuthenticated = true
c.isRegistered = true
c.isAlive = true
}
c.logger.Debug("MySubaru API client authenticated")
if len(sd.Vehicles) > 0 {
for _, vehicle := range sd.Vehicles {
c.listOfVins = append(c.listOfVins, vehicle.Vin)
}
c.currentVin = c.listOfVins[0]
} else {
c.logger.Error("there are no vehicles associated with the account", "request", "auth", "error", "no vehicles found")
return false, err
}
return true, nil
}
// SelectVehicle selects a vehicle by its VIN. If no VIN is provided, it uses the current VIN.
func (c *Client) SelectVehicle(vin string) (*VehicleData, error) {
if vin == "" {
vin = c.currentVin
}
vinCheck(vin)
params := map[string]string{
"vin": vin,
"_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp, err := c.execute(GET, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing SelectVehicle request", "request", "SelectVehicle", "error", err.Error())
return nil, errors.New("error while executing SelectVehicle request: " + err.Error())
}
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
var vd VehicleData
err = json.Unmarshal(resp.Data, &vd)
if err != nil {
c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error())
return nil, errors.New("error while parsing json while vehicle selection")
}
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
return &vd, nil
}
// GetVehicles retrieves a list of vehicles associated with the client's account.
func (c *Client) GetVehicles() ([]*Vehicle, error) {
var vehicles []*Vehicle
for _, vin := range c.listOfVins {
vehicle, err := c.GetVehicleByVin(vin)
if err != nil {
c.logger.Error("cannot get vehicle data", "request", "GetVehicles", "error", err.Error())
return nil, errors.New("cannot get vehicle data: " + err.Error())
}
vehicles = append(vehicles, vehicle)
}
return vehicles, nil
}
// GetVehicleByVin retrieves a vehicle by its VIN from the client's list of vehicles.
func (c *Client) GetVehicleByVin(vin string) (*Vehicle, error) {
var vehicle *Vehicle
if slices.Contains(c.listOfVins, vin) {
params := map[string]string{
"vin": vin,
"_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp, err := c.execute(GET, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing GetVehicleByVin request", "request", "GetVehicleByVin", "error", err.Error())
return nil, errors.New("error while executing GetVehicleByVin request: " + err.Error())
}
// c.logger.Debug("http request output", "request", "GetVehicleByVin", "body", resp)
var vd VehicleData
err = json.Unmarshal(resp.Data, &vd)
if err != nil {
c.logger.Error("error while parsing json", "request", "GetVehicleByVin", "error", err.Error())
}
// c.logger.Debug("http request output", "request", "GetVehicleByVin", "body", resp)
vehicle = &Vehicle{
Vin: vin,
CarName: vd.VehicleName,
CarNickname: vd.Nickname,
ModelName: vd.ModelName,
ModelYear: vd.ModelYear,
ModelCode: vd.ModelCode,
ExtDescrip: vd.ExtDescrip,
IntDescrip: vd.IntDescrip,
TransCode: vd.TransCode,
EngineSize: vd.EngineSize,
VehicleKey: vd.VehicleKey,
LicensePlate: vd.LicensePlate,
LicensePlateState: vd.LicensePlateState,
Features: vd.Features,
SubscriptionFeatures: vd.SubscriptionFeatures,
client: c,
}
vehicle.Doors = make(map[string]Door)
vehicle.Windows = make(map[string]Window)
vehicle.Tires = make(map[string]Tire)
vehicle.ClimateProfiles = make(map[string]ClimateProfile)
vehicle.Troubles = make(map[string]Trouble)
if vehicle.isEV() {
vehicle.EV = true
} else {
vehicle.EV = false
}
vehicle.GetVehicleStatus()
vehicle.GetVehicleCondition()
vehicle.GetVehicleHealth()
vehicle.GetClimatePresets()
vehicle.GetClimateUserPresets()
vehicle.GetClimateQuickPresets()
return vehicle, nil
}
c.logger.Error("vin code is not in the list of the available vin codes", "request", "GetVehicleByVIN")
return nil, errors.New("vin code is not in the list of the available vin codes")
}
// RefreshVehicles refreshes the list of vehicles associated with the client's account.
// {"success":true,"dataName":"sessionData","data":{"sessionChanged":true,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751835464000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"0E154A99FA3D014D866840123E5E666A","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":{"firstName":"Tatiana","lastName":"Savin","title":"","suffix":"","email":"tanya@savin.nyc","address":"29a Marion Ave","address2":"","city":"New Providence","state":"NJ","zip":"07974-1906","cellularPhone":"","workPhone":"","homePhone":"4013050505","countryCode":"USA","relationshipType":null,"gender":"","dealerCode":null,"oemCustId":"CRM-41PLM-5TYE","createMysAccount":null,"sourceSystemCode":"mys","vehicles":[{"vin":"4S4BSETC4H3265676","siebelVehicleRelationship":"Previous Owner","primary":false,"oemCustId":"1-8K7OBOJ","status":""},{"vin":"4S4BSETC4H3265676","siebelVehicleRelationship":"Previous TM Subscriber","primary":false,"oemCustId":"1-8JY3UVS","status":"Inactive"},{"vin":"4S4BTGND8L3137058","siebelVehicleRelationship":"Previous TM Subscriber","primary":false,"oemCustId":"CRM-44UFUA14-V","status":"Draft"},{"vin":"4S4BTGPD0P3199198","siebelVehicleRelationship":"TM Subscriber","primary":true,"oemCustId":"CRM-41PLM-5TYE","status":"Active"}],"phone":"","zip5Digits":"07974","primaryPersonalCountry":"USA"},"email":"","firstName":"Tatiana","lastName":"Savin","zip":"07974-1906","oemCustId":"CRM-41PLM-5TYE","phone":""},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"4S4BTGPD0P3199198","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":"tanya@savin.nyc","firstName":"Tatiana","lastName":"Savin","subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":"07974-1906","oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":"","timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false,"vehicleBranded":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","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","currentVehicleIndex":0,"handoffToken":"$2a$08$EvONydxMhrhVgFsv4OgX3O8QaOu3naCFYex2Crqhl27cPQwJYXera$1751837730624","satelliteViewEnabled":true,"registeredDevicePermanent":true}}
func (c *Client) RefreshVehicles() error {
params := map[string]string{}
reqURL := MOBILE_API_VERSION + apiURLs["API_REFRESH_VEHICLES"]
resp, err := c.execute(GET, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing RefreshVehicles request", "request", "RefreshVehicles", "error", err.Error())
return errors.New("error while executing RefreshVehicles request: " + err.Error())
}
c.logger.Debug("http request output", "request", "RefreshVehicles", "body", resp)
// var vd VehicleData
// err = json.Unmarshal(resp.Data, &vd)
// if err != nil {
// c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error())
// return errors.New("error while parsing json while vehicle selection")
// }
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
return nil
}
// RequestAuthCode requests an authentication code for two-factor authentication (2FA).
// (?!^).(?=.*@)
// (?!^): This is a negative lookbehind assertion. It ensures that the matched character is not at the beginning of the string.
// .: This matches any single character (except newline, by default).
// (?=.*@): This is a positive lookahead assertion. It ensures that the matched character is followed by any characters (.*) and then an "@" symbol. This targets the username part of the email address.
func (c *Client) RequestAuthCode(email string) error {
email, err := emailMasking(email)
if err != nil {
c.logger.Error("error while hiding email", "request", "RequestAuthCode", "error", err.Error())
return errors.New("error while hiding email: " + err.Error())
}
if !containsValueInStruct(c.contactMethods, email) {
c.logger.Error("email is not in the list of contact methods", "request", "RequestAuthCode", "email", email)
return errors.New("email is not in the list of contact methods: " + email)
}
params := map[string]string{
"contactMethod": email,
"languagePreference": "EN"}
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_SEND_VERIFICATION"]
resp, err := c.execute(POST, reqUrl, params, false)
if err != nil {
c.logger.Error("error while executing RequestAuthCode request", "request", "RequestAuthCode", "error", err.Error())
return errors.New("error while executing RequestAuthCode request: " + err.Error())
}
c.logger.Debug("http request output", "request", "RequestAuthCode", "body", resp)
return nil
}
// SubmitAuthCode submits the authentication code received from the RequestAuthCode method.
func (c *Client) SubmitAuthCode(code string, permanent bool) error {
regex := regexp.MustCompile(`^\d{6}$`)
if !regex.MatchString(code) {
c.logger.Error("invalid verification code format", "request", "SubmitAuthCode", "code", code)
return errors.New("invalid verification code format, must be 6 digits")
}
params := map[string]string{
"deviceId": c.credentials.DeviceID,
"deviceName": c.credentials.DeviceName,
"verificationCode": code}
if permanent {
params["rememberDevice"] = "on"
}
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_AUTH_VERIFY"]
resp, err := c.execute(POST, reqUrl, params, false)
if err != nil {
c.logger.Error("error while executing SubmitAuthCode request", "request", "SubmitAuthCode", "error", err.Error())
return errors.New("error while executing SubmitAuthCode request: " + err.Error())
}
c.logger.Debug("http request output", "request", "SubmitAuthCode", "body", resp)
// Device registration does not always immediately take effect
time.Sleep(time.Second * 3)
// Reauthenticate after submitting the code
if ok, err := c.auth(); !ok {
c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
return errors.New("error while executing auth request: " + err.Error())
}
return nil
}
// getContactMethods retrieves the available contact methods for two-factor authentication (2FA).
// {"success":true,"dataName":"dataMap","data":{"userName":"a**x@savin.nyc","email":"t***a@savin.nyc"}}
func (c *Client) getContactMethods() error {
// // Validate session before executing the request
// if !c.validateSession() {
// c.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
// return errors.New(APP_ERRORS["SESSION_EXPIRED"])
// }
params := map[string]string{}
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_CONTACT"]
resp, err := c.execute(POST, reqUrl, params, false)
if err != nil {
c.logger.Error("error while executing getContactMethods request", "request", "getContactMethods", "error", err.Error())
return errors.New("error while executing getContactMethods request: " + err.Error())
}
c.logger.Debug("http request output", "request", "getContactMethods", "body", resp)
var dm dataMap
err = json.Unmarshal(resp.Data, &dm)
if err != nil {
c.logger.Error("error while parsing json", "request", "getContactMethods", "error", err.Error())
return errors.New("error while parsing json while getting contact methods: " + err.Error())
}
c.contactMethods = dm
c.logger.Debug("contact methods successfully retrieved", "request", "getContactMethods", "methods", dm)
return nil
}
// IsAlive checks if the Client instance is alive
func (c *Client) IsAlive() bool {
return c.isAlive
}
// execute executes an HTTP request based on the method, URL, and parameters provided.
func (c *Client) execute(method string, url string, params map[string]string, j bool) (*Response, error) {
c.Lock()
// defer timeTrack("[TIMETRK] Executing HTTP Request")
var resp *resty.Response
var err error
// c.logger.Debug("executing http request", "method", method, "url", url, "params", params)
// GET Requests
if method == GET {
resp, err = c.httpClient.
R().
SetQueryParams(params).
Get(url)
if err != nil {
c.logger.Error("error while executing GET request", "request", "execute", "method", method, "url", url, "error", err.Error())
return nil, err
}
c.logger.Debug("executed GET request", "method", method, "url", url, "params", params)
}
// POST Requests
if method == POST {
if j { // POST > JSON Body
resp, err = c.httpClient.
R().
SetBody(params).
Post(url)
if err != nil {
c.logger.Error("error while executing POST request", "request", "execute", "method", method, "url", url, "error", err.Error())
return nil, err
}
} else { // POST > Form Data
resp, err = c.httpClient.
R().
SetFormData(params).
Post(url)
if err != nil {
c.logger.Error("error while executing POST request", "request", "execute", "method", method, "url", url, "error", err.Error())
return nil, err
}
}
c.logger.Debug("executed POST request", "method", method, "url", url, "params", params)
}
if resp.IsSuccess() {
resBytes, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Error("error while getting body", "error", err.Error())
}
c.logger.Debug("parsed http request output", "data", string(resBytes))
c.httpClient.SetCookies(resp.Cookies())
if r, ok := c.parseResponse(resBytes); ok {
c.isAlive = true
c.Unlock()
return &r, nil
} else {
if r.DataName == "errorResponse" {
var er ErrorResponse
err := json.Unmarshal(r.Data, &er)
if err != nil {
c.logger.Error("error while parsing json", "request", "errorResponse", "error", err.Error())
}
if _, ok := API_ERRORS[er.ErrorLabel]; ok {
c.logger.Error("request got an error", "request", "execute", "method", method, "url", url, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
}
c.logger.Error("request got an unknown error", "request", "execute", "method", method, "url", url, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
c.Unlock()
return nil, errors.New("request is not successfull, HTTP code: " + resp.Status())
}
c.logger.Error("request is not successfull", "request", "execute", "method", method, "url", url, "error", err.Error())
}
}
c.isAlive = false
c.Unlock()
return nil, errors.New("request is not successfull, HTTP code: " + resp.Status())
}
// parseResponse parses the JSON response from the MySubaru API into a Response struct.
func (c *Client) parseResponse(b []byte) (Response, bool) {
var r Response
err := json.Unmarshal(b, &r)
if err != nil {
c.logger.Error("error while parsing json", "error", err.Error())
return r, false
}
return r, true
}
// ValidateSession checks if the current session is valid by making a request to the vehicle status API.
func (c *Client) validateSession() bool {
reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"]
resp, err := c.execute(GET, reqURL, map[string]string{}, false)
if err != nil {
c.logger.Error("error while executing validateSession request", "request", "validateSession", "error", err.Error())
return false
}
c.logger.Debug("http request output", "request", "validateSession", "body", resp)
if resp.Success {
_, err := c.SelectVehicle(c.currentVin)
if err != nil {
c.logger.Error("error while selecting vehicle", "request", "validateSession", "error", err.Error())
return false
}
}
if !resp.Success {
_, err := c.auth()
if err != nil {
c.logger.Error("error while re-authenticating", "request", "validateSession", "error", err.Error())
return false
}
_, err = c.SelectVehicle(c.currentVin)
if err != nil {
c.logger.Error("error while selecting vehicle", "request", "validateSession", "error", err.Error())
return false
}
}
return true
}
// isPINRequired .
// Return if a vehicle with an active remote service subscription exists.
// func (v *Vehicle) isPINRequired() bool {
// return v.getRemoteOptionsStatus()
// }
// func isPINRequired() {}
// func getEVStatus() {}
// func getRemoteOptionsStatus() {}
// func getRemoteStartStatus() {}
// func getSafetyStatus() {}
// func getSubscriptionStatus() {}
// validateSession .
// TODO: add session validation process and add it to the proper functions
// func (c *Client) validateSession() bool {
// reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"]
// resp := c.execute(reqURL, GET, map[string]string{}, "", false)
// // c.logger.Debug("http request output", "request", "validateSession", "body", resp)
// if r, ok := c.parseResponse(resp); ok {
// c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error())
// return true
// } else {
// resp := c.auth()
// return true
// }
// return false
// }
// 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"]
// resp, _ := c.execute(POST, reqURL, 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.Duration())
// 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)
// // c.logger.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) {
// // logger.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp))
// // }
// }
// c.logger.Debug("parsed http request output", "data", r.Data)
// ERROR
// {"httpCode":500,"errorCode":"error","errorMessage":"org.springframework.web.HttpMediaTypeNotSupportedException - Content type 'application/x-www-form-urlencoded' not supported"}
// {"success":true,"errorCode":null,"dataName":"remoteServiceStatus","data":{"serviceRequestId":"4S4BTGND8L3137058_1640203129607_19_@NGTP","success":false,"cancelled":false,"remoteServiceType":"unlock","remoteServiceState":"started","subState":null,"errorCode":null,"result":null,"updateTime":null,"vin":"4S4BTGND8L3137058"}}
// API_REMOTE_SVC_STATUS
// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}}
// if js_resp["errorCode"] == sc.ERROR_SOA_403:
// raise RemoteServiceFailure("Backend session expired, please try again")
// if js_resp["data"]["remoteServiceState"] == "finished":
// if js_resp["data"]["success"]:
// _LOGGER.info("Remote service request completed successfully: %s", req_id)
// return True, js_resp
// _LOGGER.error(
// "Remote service request completed but failed: %s Error: %s",
// req_id,
// js_resp["data"]["errorCode"],
// )
// raise RemoteServiceFailure(
// "Remote service request completed but failed: %s" % js_resp["data"]["errorCode"]
// )
// if js_resp["data"].get("remoteServiceState") == "started":
// _LOGGER.info(
// "Subaru API reports remote service request is in progress: %s",
// req_id,
// )
// attempts_left -= 1
// await asyncio.sleep(2)
// TODO: Work on errors
// error, _ := respParsed.Path("errorCode").Data().(string)
// switch {
// case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
// client.logger.Debug("Invalid account")
// case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
// client.logger.Debug("Client authentication failed")
// case error == apiErrors["ERROR_PASSWORD_WARNING"]:
// client.logger.Debug("Multiple Password Failures.")
// default:
// client.logger.Debug("Uknown error")
// }