From 2f86d769fc970c5f5d183c8459d3f1e8cce51033 Mon Sep 17 00:00:00 2001 From: Alex Savin Date: Sun, 1 Jun 2025 15:30:08 -0400 Subject: [PATCH] More changes --- client.go | 811 +++++++++++++++++++++++---------------------- example/example.go | 2 +- vehicle.go | 117 ++----- 3 files changed, 448 insertions(+), 482 deletions(-) diff --git a/client.go b/client.go index 8b2ff9c..e512bee 100644 --- a/client.go +++ b/client.go @@ -27,6 +27,370 @@ type Client struct { sync.RWMutex } +// 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() + + if r, ok := client.parseResponse(resp); ok { + 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 sd.DeviceRegistered && sd.RegisteredDevicePermanent { + client.logger.Debug("client authentication successful") + client.isAuthenticated = true + client.isRegistered = true + } 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 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") + // TODO: Work on providing error + return nil, nil + } + 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) + + if r, ok := c.parseResponse(resp); ok { + 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) + + if r, ok := c.parseResponse(resp); ok { + 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", "data", r.Data) + + 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 + + // for { + // if attempt >= attempts { + // break + // } + // resp := c.execute(pollingUrl, GET, map[string]string{"serviceRequestId": sr.ServiceRequestID}, pollingUrl, false) + // var r Response + // err = json.Unmarshal(resp, &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", "data", r.Data) + + // 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 + // case sr.RemoteServiceState == "started": + // c.logger.Debug("Subaru API reports remote service request is in progress", "request id", sr.ServiceRequestID) + // } + // attempt++ + // } + + 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", "data", r.Data) + + 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 +// } + // auth . func (c *Client) auth() []byte { params := map[string]string{ @@ -44,6 +408,59 @@ func (c *Client) auth() []byte { return resp } +// parseResponse . +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, true +} + +// 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", "validateSession", "body", resp) + + var r Response + err := json.Unmarshal(resp, &r) + if err != nil { + c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error()) + } + + if r.Success { + return true + } + 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 +} + // GET // https://www.mysubaru.com/profile/verifyDeviceName.json?clientId=2574212&deviceName=Alex%20Google%20Pixel%204%20XL // RESP: true/false @@ -158,397 +575,3 @@ func (c *Client) listDevices() { // 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", "validateSession", "body", resp) - - var r Response - err := json.Unmarshal(resp, &r) - if err != nil { - c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error()) - } - - if r.Success { - return true - } - 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 -} - -// 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 sd.DeviceRegistered && sd.RegisteredDevicePermanent { - client.logger.Debug("client authentication successful") - client.isAuthenticated = true - client.isRegistered = true - } 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 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", "data", r.Data) - - 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", "data", r.Data) - - 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 -// } diff --git a/example/example.go b/example/example.go index f120e90..c792bc2 100644 --- a/example/example.go +++ b/example/example.go @@ -43,7 +43,7 @@ func main() { // subaru.EngineStart() fmt.Printf("SUBARU #1 (Vehicle Status):\n") - subaru.GetVehicleStatus() + // subaru.GetVehicleStatus() // fmt.Printf("SUBARU #1 (Vehicle Condition):\n") // subaru.GetVehicleCondition() // fmt.Printf("SUBARU #1: %+v\n", subaru) diff --git a/vehicle.go b/vehicle.go index d53b088..455b9a4 100644 --- a/vehicle.go +++ b/vehicle.go @@ -88,6 +88,10 @@ type Vehicle struct { // ClimateProfile . type ClimateProfile struct { Name string `json:"name,omitempty"` + VehicleType string `json:"vehicleType,omitempty"` // vehicleType [ gas | phev ] + PresetType string `json:"presetType,omitempty"` // presetType [ subaruPreset | userPreset ] + CanEdit bool `json:"canEdit,string,omitempty"` // canEdit [ false | true ] + Disabled bool `json:"disabled,string,omitempty"` // disabled [ false | true ] RunTimeMinutes int `json:"runTimeMinutes,string"` // runTimeMinutes [ 5 | 10 ] ClimateZoneFrontTemp int `json:"climateZoneFrontTemp,string"` // climateZoneFrontTemp: [ for _ in range(60, 85 + 1)] // climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1) ] ClimateZoneFrontAirMode string `json:"climateZoneFrontAirMode"` // climateZoneFrontAirMode: [ WINDOW | FEET_WINDOW | FACE | FEET | FEET_FACE_BALANCED | AUTO ] @@ -98,20 +102,14 @@ type ClimateProfile struct { HeatedSeatFrontLeft string `json:"heatedSeatFrontLeft"` // heatedSeatFrontLeft: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ] HeatedSeatFrontRight string `json:"heatedSeatFrontRight"` // heatedSeatFrontRight: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ] StartConfiguration string `json:"startConfiguration"` // startConfiguration [ START_ENGINE_ALLOW_KEY_IN_IGNITION (gas) | START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION (phev) ] - CanEdit bool `json:"canEdit,string,omitempty"` // canEdit [ false | true ] - Disabled bool `json:"disabled,string,omitempty"` // disabled [ false | true ] - VehicleType string `json:"vehicleType,omitempty"` // vehicleType [ gas | phev ] - PresetType string `json:"presetType,omitempty"` // presetType [ subaruPreset | userPreset ] } -// func (cp *ClimateProfile) New() {} - // GeoLocation . type GeoLocation struct { Latitude float64 // 40.700184 Longitude float64 // -74.401375 - Speed float64 // 0.00 Heading int // 189 + Speed float64 // 0.00 Updated time.Time } @@ -268,6 +266,7 @@ func (v *Vehicle) Unlock() { func (v *Vehicle) EngineStart() { if v.getRemoteOptionsStatus() { v.selectVehicle() + // TODO: Get Quick Climate Preset from the Currect Car params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -579,15 +578,9 @@ func (v *Vehicle) GetVehicleStatus() { resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) // v.client.logger.Debug("http request output", "request", "GetVehicleStatus", "body", resp) - var r Response - err := json.Unmarshal(resp, &r) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) - } - - if r.Success { + if r, ok := v.client.parseResponse(resp); ok { var vs VehicleStatus - err = json.Unmarshal(r.Data, &vs) + err := json.Unmarshal(r.Data, &vs) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) } @@ -614,6 +607,15 @@ func (v *Vehicle) GetVehicleStatus() { typeOfS := val.Type() for i := 0; i < val.NumField(); i++ { + if val.Field(i).Interface() == "NOT_EQUIPPED" || + val.Field(i).Interface() == "UNKNOWN" || + val.Field(i).Interface() == "None" || + val.Field(i).Interface() == "16383" || + val.Field(i).Interface() == "65535" || + val.Field(i).Interface() == "-64" || + val.Field(i).Interface() == nil { + continue + } fmt.Printf("Field: %s, Value: %v, Type: %v\n", typeOfS.Field(i).Name, val.Field(i).Interface(), val.Field(i).Type()) } // v := reflect.ValueOf(vSta) @@ -733,52 +735,6 @@ func (v *Vehicle) GetVehicleStatus() { // fmt.Printf("PARTS: %+v\n", parts) v.Updated = time.Now() } - - // vData := VehicleData{} - // vdString := respParsed.Path("data").String() - // json.Unmarshal([]byte(vdString), &vData) - - // { "data": { - // "vhsId": 1038682433, - - // "odometerValue": 24999, - // "odometerValueKilometers": 40223, - - // "eventDate": 1642538410000, - // "eventDateStr": "2022-01-18T20:40+0000", - - // "latitude": 40.70018, - // "longitude": -74.40139, - // "positionHeadingDegree": "155", - - // "tirePressureFrontLeft": "2275", - // "tirePressureFrontRight": "2206", - // "tirePressureRearLeft": "2344", - // "tirePressureRearRight": "2275", - - // "tirePressureFrontLeftPsi": "33", - // "tirePressureFrontRightPsi": "32", - // "tirePressureRearLeftPsi": "34", - // "tirePressureRearRightPsi": "33", - - // "distanceToEmptyFuelMiles": 149.75, - // "distanceToEmptyFuelKilometers": 241, - - // "avgFuelConsumptionMpg": 18.5, - // "avgFuelConsumptionLitersPer100Kilometers": 12.7, - - // "evStateOfChargePercent": null, - // "evDistanceToEmptyMiles": null, - // "evDistanceToEmptyKilometers": null, - // "evDistanceToEmptyByStateMiles": null, - // "evDistanceToEmptyByStateKilometers": null, - - // "vehicleStateType": "IGNITION_OFF", - - // "distanceToEmptyFuelMiles10s": 150, - // "distanceToEmptyFuelKilometers10s": 240 - // } - // } } // GetVehicleCondition . @@ -788,15 +744,9 @@ func (v *Vehicle) GetVehicleCondition() { resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) // v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp) - var r Response - err := json.Unmarshal(resp, &r) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) - } - - if r.Success { + if r, ok := v.client.parseResponse(resp); ok { var sr ServiceRequest - err = json.Unmarshal(r.Data, &sr) + err := json.Unmarshal(r.Data, &sr) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) } @@ -813,6 +763,15 @@ func (v *Vehicle) GetVehicleCondition() { typeOfS := val.Type() for i := 0; i < val.NumField(); i++ { + if val.Field(i).Interface() == "NOT_EQUIPPED" || + val.Field(i).Interface() == "UNKNOWN" || + val.Field(i).Interface() == "None" || + val.Field(i).Interface() == "16383" || + val.Field(i).Interface() == "65535" || + val.Field(i).Interface() == "-64" || + val.Field(i).Interface() == nil { + continue + } fmt.Printf("Field: %s, Value: %v, Type: %v\n", typeOfS.Field(i).Name, val.Field(i).Interface(), val.Field(i).Type()) } @@ -952,36 +911,20 @@ func (v *Vehicle) GetVehicleHealth() { resp := v.client.execute(reqURL, GET, params, "", false) // v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "body", resp) - var r Response - err := json.Unmarshal(resp, &r) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error()) - } - - if r.Success { + if r, ok := v.client.parseResponse(resp); ok { var vh VehicleHealth - err = json.Unmarshal(r.Data, &vh) + err := json.Unmarshal(r.Data, &vh) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error()) } v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "vehicle health", vh) - // TODO: Loop over all the Vehicle Health Items - } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } -// // GetClimateSettings . -// func (v *Vehicle) GetClimateSettings() { -// v.selectVehicle() -// reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_CLIMATE_SETTINGS"] -// v.client.execute(reqURL, GET, map[string]string{}, "", false) -// // TODO -// } - // GetFeaturesList . func (v *Vehicle) GetFeaturesList() { for i, f := range v.Features {