package mysubaru import ( "encoding/json" "fmt" "io" "log/slog" "sync" "time" "git.savin.nyc/alex/mysubaru/config" "resty.dev/v3" ) // Client . type Client struct { credentials config.Credentials httpClient *resty.Client country string // USA | CA updateInterval int // 7200 fetchInterval int // 360 currentVin string listOfVins []string isAuthenticated bool isRegistered bool logger *slog.Logger sync.RWMutex } // auth . func (c *Client) auth() []byte { 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.logger.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.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)) // } } // 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) c.logger.Debug("http request output", "request", "GetVehicleStatus", "body", resp) var r Response err := json.Unmarshal(resp, &r) if err != nil { c.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) } if !r.Success { return false } return true // 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 } // New function creates a New MySubaru 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 resp := client.auth() var r Response err := json.Unmarshal(resp, &r) if err != nil { client.logger.Error("error while parsing json", "request", "auth", "error", err.Error()) } if r.Success { var sd SessionData err := json.Unmarshal(r.Data, &sd) if err != nil { client.logger.Error("error while parsing json", "request", "auth", "error", err.Error()) } client.logger.Debug("unmarshaled json data", "request", "auth", "type", "sessionData", "body", sd) if client.isRegistered { client.logger.Debug("Client authentication successful", "isRegistered", sd.DeviceRegistered) client.isAuthenticated = true client.isRegistered = sd.DeviceRegistered } else { client.registerDevice() } client.logger.Debug("parcing cars assigned to the account", "quantity", len(sd.Vehicles)) if len(sd.Vehicles) > 0 { for _, vehicle := range sd.Vehicles { client.logger.Debug("parsing car", "vin", vehicle.Vin) client.listOfVins = append(client.listOfVins, vehicle.Vin) } client.currentVin = client.listOfVins[0] } else { client.logger.Error("there no cars assigned to the account") return nil, err } } else { // 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") // } client.logger.Error("request was not successfull", "request", "auth", "error", err.Error()) return nil, err } return client, nil } // SelectVehicle . func (c *Client) SelectVehicle(vin string) VehicleData { // API > json > dataName > vehicle 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) c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp) var r Response err := json.Unmarshal(resp, &r) if err != nil { c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error()) } if r.Success { var vd VehicleData err = json.Unmarshal(r.Data, &vd) if err != nil { c.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) } c.logger.Debug("http request output", "request", "GetVehicleStatus", "body", resp) return vd } else { return VehicleData{} } // resp := c.execute(reqURL, GET, params, "", false) // logger.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")) } // GetVehicles . func (c *Client) GetVehicles() []*Vehicle { var vehicles []*Vehicle for _, vin := range c.listOfVins { vehicle := c.GetVehicleByVIN(vin) 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) c.logger.Debug("http request output", "request", "GetVehicleByVIN", "body", resp) var r Response err := json.Unmarshal(resp, &r) if err != nil { c.logger.Error("error while parsing json", "request", "GetVehicleByVIN", "error", err.Error()) } if r.Success { var vd VehicleData err = json.Unmarshal(r.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.GetVehicleStatus() vehicle.GetVehicleCondition() vehicle.GetVehicleHealth() vehicle.GetClimatePresets() vehicle.GetClimateUserPresets() vehicle.GetClimateQuickPresets() return vehicle } } c.logger.Error("error while parsing json", "request", "GetVehicleByVIN") return &Vehicle{} } // 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, j bool) []byte { defer timeTrack("[TIMETRK] Executing Get Request") var resp *resty.Response // GET Requests if method == "GET" { resp, _ = c.httpClient. R(). SetQueryParams(params). Get(requestUrl) } // POST Requests if method == "POST" { if j { // POST > JSON Body resp, _ = c.httpClient. R(). SetBody(params). Post(requestUrl) } else { // POST > Form Data resp, _ = c.httpClient. R(). SetFormData(params). Post(requestUrl) } } resBytes, err := io.ReadAll(resp.Body) if err != nil { c.logger.Error("error while getting body", "error", err.Error()) } var r Response err = json.Unmarshal(resBytes, &r) if err != nil { c.logger.Error("error while parsing json", "request", "execute", "method", method, "url", requestUrl, "error", err.Error()) } c.logger.Debug("parsed http request output", "request", "HTTP POLLING", "body", r) if r.Success { var sr ServiceRequest err := json.Unmarshal(r.Data, &sr) if err != nil { c.logger.Error("error while parsing json", "request", "HTTP POLLING", "error", err.Error()) } if pollingUrl != "" { 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": sr.ServiceRequestID, }). Get(pollingUrl) resBytes, _ := io.ReadAll(resp.Body) c.logger.Debug("POLLING HTTP OUTPUT", "body", string(resBytes)) // {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}} var r Response err := json.Unmarshal(resBytes, &r) if err != nil { c.logger.Error("error while parsing json", "request", "HTTP POLLING", "error", err.Error()) } c.logger.Debug("parsed loop http request output", "request", "HTTP POLLING", "body", r) if r.Success { var sr ServiceRequest err := json.Unmarshal(r.Data, &sr) if err != nil { c.logger.Error("error while parsing json", "request", "HTTP POLLING", "error", err.Error()) } switch { case sr.RemoteServiceState == "finished": c.logger.Debug("Remote service request completed successfully", "request id", sr.ServiceRequestID) break poolingLoop case sr.RemoteServiceState == "started": c.logger.Debug("Subaru API reports remote service request is in progress", "request id", sr.ServiceRequestID) } } else { c.logger.Debug("Backend session expired, please try again") break poolingLoop } attempts-- time.Sleep(3 * time.Second) } } } else { c.logger.Error("request is not successfull", "request", "execute", "method", method, "url", requestUrl, "error", err.Error()) } return resBytes } // // isResponseSuccessfull . // func (c *Client) isResponseSuccessfull(resp []byte) bool { // respParsed, err := gabs.ParseJSON(resp) // if err != nil { // c.logger.Debug("error while parsing json response", "error", err) // } // success, ok := respParsed.Path("success").Data().(bool) // if !ok { // c.logger.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 // }