package mysubaru import ( "encoding/json" "errors" "io" "log/slog" "slices" "sync" "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 updateInterval int // 7200 fetchInterval int // 360 currentVin string listOfVins []string isAuthenticated bool isRegistered bool isAlive bool 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 resp, err := client.auth() if err != nil { client.logger.Error("error while executing auth request", "request", "auth", "error", err.Error()) return nil, errors.New("error while executing auth request: " + err.Error()) } var sd SessionData err = json.Unmarshal(resp.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 sd.DeviceRegistered && sd.RegisteredDevicePermanent { // client.logger.Debug("client authentication successful") client.isAuthenticated = true client.isRegistered = true } // TODO: Work on registerDevice() // } else { // // client.logger.Debug("client authentication successful, but devices is not registered") // client.registerDevice() // } // client.logger.Debug("parsing 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 are no cars assigned to the account") return nil, err } return client, 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") } // func isPINRequired() {} // func getVehicles() {} // func getEVStatus() {} // func getRemoteOptionsStatus() {} // func getRemoteStartStatus() {} // func getSafetyStatus() {} // func getSubscriptionStatus() {} // 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)) 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()) } // auth authenticates the client with the MySubaru API using the provided credentials. func (c *Client) auth() (*Response, 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) c.logger.Debug("AUTH HTTP OUTPUT", "body", resp) if err != nil { c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error()) return nil, errors.New("error while executing auth request: " + err.Error()) } c.logger.Debug("AUTH HTTP OUTPUT", "body", resp) return resp, nil } // 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_VEHICLE_STATUS"] 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) return true } // 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") // }