diff --git a/client.go b/client.go index 708e782..05b8acd 100644 --- a/client.go +++ b/client.go @@ -2,11 +2,11 @@ package mysubaru import ( "encoding/json" + "errors" "io" "log/slog" "slices" "sync" - "time" "git.savin.nyc/alex/mysubaru/config" "resty.dev/v3" @@ -23,6 +23,7 @@ type Client struct { listOfVins []string isAuthenticated bool isRegistered bool + isAlive bool logger *slog.Logger sync.RWMutex } @@ -52,56 +53,40 @@ func New(config *config.Config) (*Client, error) { 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) + 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 - } else { - // client.logger.Debug("client authentication successful, but devices is not registered") - client.registerDevice() - } + 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 no cars assigned to the account") - return nil, err + // 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 { - // 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 + client.logger.Error("there no cars assigned to the account") + return nil, err } return client, nil } // SelectVehicle . -func (c *Client) SelectVehicle(vin string) VehicleData { +func (c *Client) SelectVehicle(vin string) (*VehicleData, error) { if vin == "" { vin = c.currentVin } @@ -112,93 +97,93 @@ func (c *Client) SelectVehicle(vin string) VehicleData { "vin": vin, "_": timestamp()} reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] - resp := c.execute(reqURL, GET, params, "", false) + // TODO: Add error handling + resp, _ := c.execute(GET, reqURL, 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", "SelectVehicle", "error", err.Error()) - } - // c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp) - return vd - } else { - return VehicleData{} + 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 . func (c *Client) GetVehicles() []*Vehicle { var vehicles []*Vehicle for _, vin := range c.listOfVins { - vehicle := c.GetVehicleByVIN(vin) + vehicle, err := c.GetVehicleByVIN(vin) + if err != nil { + c.logger.Error("cannot get vehile data", "request", "SelectVehicle", "error", err.Error()) + } vehicles = append(vehicles, vehicle) } return vehicles } // GetVehicleByVIN . -func (c *Client) GetVehicleByVIN(vin string) *Vehicle { +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 := c.execute(reqURL, GET, params, "", false) + // TODO: Add error handling + resp, _ := c.execute(GET, reqURL, 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.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 + 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("error while parsing json", "request", "GetVehicleByVIN") - return &Vehicle{} + 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() {} @@ -211,8 +196,12 @@ func (c *Client) GetVehicleByVIN(vin string) *Vehicle { // func getClimateData() {} // func saveClimateSettings() {} +func (c *Client) IsAlive() bool { + return c.isAlive +} + // 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, attempts ...int) []byte { +func (c *Client) execute(method string, url string, params map[string]string, j bool) (*Response, error) { // defer timeTrack("[TIMETRK] Executing HTTP Request") var resp *resty.Response @@ -221,7 +210,7 @@ func (c *Client) execute(requestUrl string, method string, params map[string]str resp, _ = c.httpClient. R(). SetQueryParams(params). - Get(requestUrl) + Get(url) } // POST Requests @@ -230,70 +219,47 @@ func (c *Client) execute(requestUrl string, method string, params map[string]str resp, _ = c.httpClient. R(). SetBody(params). - Post(requestUrl) + Post(url) } else { // POST > Form Data resp, _ = c.httpClient. R(). SetFormData(params). - Post(requestUrl) + Post(url) } } - 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.logger.Debug("parsed http request output", "data", r.Data) + 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)) - // dataName field has the list of the states [ remoteServiceStatus | errorResponse ] - if r.DataName == "remoteServiceStatus" { - var sr ServiceRequest - err := json.Unmarshal(r.Data, &sr) - if err != nil { - c.logger.Error("error while parsing json", "request", "remoteServiceStatus", "error", err.Error()) - } - - if pollingUrl != "" { - switch { - case sr.RemoteServiceState == "finished": - // Finished RemoteServiceState Service Request does not include Service Request ID - c.logger.Debug("Remote service request completed successfully") - case sr.RemoteServiceState == "started": - time.Sleep(5 * time.Second) - c.logger.Debug("Subaru API reports remote service request (started) is in progress", "id", sr.ServiceRequestID) - c.execute(pollingUrl, GET, map[string]string{"serviceRequestId": sr.ServiceRequestID}, pollingUrl, false) - case sr.RemoteServiceState == "stopping": - time.Sleep(5 * time.Second) - c.logger.Debug("Subaru API reports remote service request (stopping) is in progress", "id", sr.ServiceRequestID) - c.execute(pollingUrl, GET, map[string]string{"serviceRequestId": sr.ServiceRequestID}, pollingUrl, false) - default: - time.Sleep(5 * time.Second) - c.logger.Debug("Subaru API reports remote service request (stopping) is in progress") - c.execute(pollingUrl, GET, map[string]string{"serviceRequestId": sr.ServiceRequestID}, pollingUrl, false, 1) + if r, ok := c.parseResponse(resBytes); ok { + c.isAlive = true + 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) + 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()) } - } 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", requestUrl, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription) - } - c.logger.Error("request got an unknown error", "request", "execute", "method", method, "url", requestUrl, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription) - } - c.logger.Error("request is not successfull", "request", "execute", "method", method, "url", requestUrl, "error", err.Error()) } - return resBytes + c.isAlive = false + return nil, errors.New("request is not successfull, HTTP code: " + resp.Status()) } // auth . -func (c *Client) auth() []byte { +func (c *Client) auth() *Response { params := map[string]string{ "env": "cloudprod", "deviceType": "android", @@ -304,7 +270,8 @@ func (c *Client) auth() []byte { "selectedVin": "", "pushToken": ""} reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"] - resp := c.execute(reqURL, POST, params, "", false) + // TODO: Add error handling + resp, _ := c.execute(POST, reqURL, params, false) // c.logger.Debug("AUTH HTTP OUTPUT", "body", string([]byte(resp))) return resp } @@ -315,29 +282,23 @@ func (c *Client) parseResponse(b []byte) (Response, bool) { 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 . +// TODO: add session validation process and add it to the proper functions // 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) +// // c.logger.Debug("http request output", "request", "validateSession", "body", resp) -// var r Response -// err := json.Unmarshal(resp, &r) -// if err != nil { +// if r, ok := c.parseResponse(resp); ok { // c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error()) -// } - -// if r.Success { +// return true +// } else { +// resp := c.auth() // return true // } // return false @@ -358,50 +319,50 @@ func (c *Client) parseResponse(b []byte) (Response, bool) { // {"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) +// // 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) +// // 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() -} +// 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) +// // 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 -} +// return true +// } // // listDevices . // func (c *Client) listDevices() { @@ -457,3 +418,45 @@ func (c *Client) setDeviceName() bool { // // 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") +// } diff --git a/consts.go b/consts.go index 59a75b2..246cfd8 100644 --- a/consts.go +++ b/consts.go @@ -102,6 +102,8 @@ var API_ERRORS = map[string]string{ "passwordWarning": "ERROR_PASSWORD_WARNING", "accountLocked": "ERROR_ACCOUNT_LOCKED", "noVehiclesOnAccount": "ERROR_NO_VEHICLES", + "noVehiclesAvailable": "ERROR_NO_VEHICLE_AVAILABLE", + "VEHICLESETUPERROR": "ERROR_VEHICLE_SETUP_ERROR", // Vehicle Select "accountNotFound": "ERROR_NO_ACCOUNT", "tooManyAttempts": "ERROR_TOO_MANY_ATTEMPTS", "vehicleNotInAccount": "ERROR_VEHICLE_NOT_IN_ACCOUNT", @@ -112,6 +114,10 @@ var API_ERRORS = map[string]string{ "SXM40017": "ERROR_G1_PIN_LOCKED", } +var APP_ERRORS = map[string]string{ + "SUBSCRIBTION_REQUIRED": "active STARLINK Security Plus subscription required", +} + // TODO: Get back and add error wrapper // var apiErrors = map[string]string{ // "ERROR_SOA_403": "403-soa-unableToParseResponseBody", // G2 Error Codes diff --git a/go.mod b/go.mod index 06e3aba..1c0c1fa 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/neilotoole/slogt v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.41.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect diff --git a/mysubaru.go b/mysubaru.go index a6baa58..6bb8106 100644 --- a/mysubaru.go +++ b/mysubaru.go @@ -25,6 +25,27 @@ type Response struct { // Unmarshal . // func (r *Response) Unmarshal(b []byte) {} +// Request . +type Request struct { + Vin string `json:"vin"` // + Pin string `json:"pin"` // + Delay int `json:"delay,string,omitempty"` // + ForceKeyInCar *bool `json:"forceKeyInCar,string,omitempty"` // + UnlockDoorType *string `json:"unlockDoorType,omitempty"` // [ ALL_DOORS_CMD | FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD ] + Horn *string `json:"horn,omitempty"` // + ClimateSettings *string `json:"climateSettings,omitempty"` // + ClimateZoneFrontTemp *string `json:"climateZoneFrontTemp,omitempty"` // + ClimateZoneFrontAirMode *string `json:"climateZoneFrontAirMode,omitempty"` // + ClimateZoneFrontAirVolume *string `json:"climateZoneFrontAirVolume,omitempty"` // + HeatedSeatFrontLeft *string `json:"heatedSeatFrontLeft,omitempty"` // + HeatedSeatFrontRight *string `json:"heatedSeatFrontRight,omitempty"` // + HeatedRearWindowActive *string `json:"heatedRearWindowActive,omitempty"` // + OuterAirCirculation *string `json:"outerAirCirculation,omitempty"` // + AirConditionOn *string `json:"airConditionOn,omitempty"` // + RunTimeMinutes *string `json:"runTimeMinutes,omitempty"` // + StartConfiguration *string `json:"startConfiguration,omitempty"` // +} + // Account . type Account struct { MarketID int `json:"marketId"` @@ -277,17 +298,6 @@ type ServiceRequest struct { Vin string `json:"vin"` // 4S4BTGND8L3137058 } -// ErrorResponse . -// "dataName":"errorResponse" -// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}} -// {"success":false,"errorCode":"vehicleNotInAccount","dataName":null,"data":null} -// {"httpCode":500,"errorCode":"error","errorMessage":"java.lang.NullPointerException - null"} -// {"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"}} -type ErrorResponse struct { - ErrorLabel string `json:"errorLabel"` // "404-soa-unableToParseResponseBody" - ErrorDescription string `json:"errorDescription,omitempty"` // null -} - // climateSettings: [ climateSettings ] // climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1)] // climateZoneFrontTemp: [for _ in range(60, 85 + 1)] @@ -313,3 +323,14 @@ type VehicleHealthItem struct { OnDates []int64 `json:"onDates,omitempty"` // List of the timestamps WarningCode int `json:"warningCode"` } + +// ErrorResponse . +// "dataName":"errorResponse" +// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}} +// {"success":false,"errorCode":"vehicleNotInAccount","dataName":null,"data":null} +// {"httpCode":500,"errorCode":"error","errorMessage":"java.lang.NullPointerException - null"} +// {"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"}} +type ErrorResponse struct { + ErrorLabel string `json:"errorLabel"` // "404-soa-unableToParseResponseBody" + ErrorDescription string `json:"errorDescription,omitempty"` // null +} diff --git a/vehicle.go b/vehicle.go index 3f065c1..4ac5e71 100644 --- a/vehicle.go +++ b/vehicle.go @@ -2,6 +2,7 @@ package mysubaru import ( "encoding/json" + "errors" "fmt" "reflect" "regexp" @@ -212,230 +213,365 @@ func (v *Vehicle) String() string { // Lock . // Sends a command to lock doors. -func (v *Vehicle) Lock() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - params := map[string]string{ - "delay": "0", - "vin": v.Vin, - "pin": v.client.credentials.PIN, - "forceKeyInCar": "false"} - reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCK"], v.getAPIGen()) - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") +func (v *Vehicle) Lock() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN, + "forceKeyInCar": "false"} + reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCK"], v.getAPIGen()) + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + + return ch, nil } // Unlock . // Send command to unlock doors. -func (v *Vehicle) Unlock() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - params := map[string]string{ - "delay": "0", - "vin": v.Vin, - "pin": v.client.credentials.PIN, - "unlockDoorType": "ALL_DOORS_CMD"} // FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD - reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_UNLOCK"], v.getAPIGen()) - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") +func (v *Vehicle) Unlock() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } - // 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) + + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN, + "unlockDoorType": "ALL_DOORS_CMD"} // FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD + reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_UNLOCK"], v.getAPIGen()) + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + + return ch, nil } // EngineStart . // Sends a command to start engine and set climate control. -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, - "pin": v.client.credentials.PIN, - "horn": "true", - "climateSettings": "climateSettings", // climateSettings - "climateZoneFrontTemp": "65", // 60-86 - "climateZoneFrontAirMode": "WINDOW", // FEET_FACE_BALANCED | FEET_WINDOW | WINDOW | FEET - "climateZoneFrontAirVolume": "6", // 1-7 - "heatedSeatFrontLeft": "OFF", // OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT | low_cool | medium_cool | high_cool - "heatedSeatFrontRight": "OFF", // ---//--- - "heatedRearWindowActive": "true", // boolean - "outerAirCirculation": "outsideAir", // outsideAir | recirculation - "airConditionOn": "false", // boolean - "runTimeMinutes": "10", // 1-10 - "startConfiguration": START_CONFIG_DEFAULT_RES, // START_ENGINE_ALLOW_KEY_IN_IGNITION | ONLY FOR PHEV > START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION - } - reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_START"] - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") +func (v *Vehicle) EngineStart() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + // TODO: Get Quick Climate Preset from the Currect Car + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN, + "horn": "true", + "climateSettings": "climateSettings", // climateSettings + "climateZoneFrontTemp": "65", // 60-86 + "climateZoneFrontAirMode": "WINDOW", // FEET_FACE_BALANCED | FEET_WINDOW | WINDOW | FEET + "climateZoneFrontAirVolume": "6", // 1-7 + "heatedSeatFrontLeft": "OFF", // OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT | low_cool | medium_cool | high_cool + "heatedSeatFrontRight": "OFF", // ---//--- + "heatedRearWindowActive": "true", // boolean + "outerAirCirculation": "outsideAir", // outsideAir | recirculation + "airConditionOn": "false", // boolean + "runTimeMinutes": "10", // 1-10 + "startConfiguration": START_CONFIG_DEFAULT_RES, // START_ENGINE_ALLOW_KEY_IN_IGNITION | ONLY FOR PHEV > START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION + } + reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_START"] + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + + return ch, nil } // EngineStop . // Sends a command to stop engine. -func (v *Vehicle) EngineStop() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - params := map[string]string{ - "delay": "0", - "vin": v.Vin, - "pin": v.client.credentials.PIN} - reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_STOP"] - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("Active STARLINK Security Plus subscription required") +func (v *Vehicle) EngineStop() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN} + reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_STOP"] + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + + return ch, nil } // LightsStart . // Sends a command to flash lights. -func (v *Vehicle) LightsStart() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - params := map[string]string{ - "delay": "0", - "vin": v.Vin, - "pin": v.client.credentials.PIN} - reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS"], v.getAPIGen()) - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - if v.getAPIGen() == FEATURE_G1_TELEMATICS { - pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] - } - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") +func (v *Vehicle) LightsStart() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN} + reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS"], v.getAPIGen()) + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + if v.getAPIGen() == FEATURE_G1_TELEMATICS { + pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] + } + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + return ch, nil } // LightsStop . // Sends a command to stop flash lights. -func (v *Vehicle) LightsStop() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - params := map[string]string{ - "delay": "0", - "vin": v.Vin, - "pin": v.client.credentials.PIN} - reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS_STOP"], v.getAPIGen()) - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - if v.getAPIGen() == FEATURE_G1_TELEMATICS { - pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] - } - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") +func (v *Vehicle) LightsStop() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN} + reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS_STOP"], v.getAPIGen()) + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + if v.getAPIGen() == FEATURE_G1_TELEMATICS { + pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] + } + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + + return ch, nil } // HornStart . // Send command to sound horn. -func (v *Vehicle) HornStart() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - params := map[string]string{ - "delay": "0", - "vin": v.Vin, - "pin": v.client.credentials.PIN} - reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS"], v.getAPIGen()) - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - if v.getAPIGen() == FEATURE_G1_TELEMATICS { - pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] - } - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") +func (v *Vehicle) HornStart() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN} + reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS"], v.getAPIGen()) + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + if v.getAPIGen() == FEATURE_G1_TELEMATICS { + pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] + } + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + + return ch, nil } // HornStop . // Send command to sound horn. -func (v *Vehicle) HornStop() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - params := map[string]string{ - "delay": "0", - "vin": v.Vin, - "pin": v.client.credentials.PIN} - reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS_STOP"], v.getAPIGen()) - pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - if v.getAPIGen() == FEATURE_G1_TELEMATICS { - pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] - } - v.client.execute(reqURL, POST, params, pollingURL, true) - } else { - v.client.logger.Error("Active STARLINK Security Plus subscription required") +func (v *Vehicle) HornStop() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + params := map[string]string{ + "delay": "0", + "vin": v.Vin, + "pin": v.client.credentials.PIN} + reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS_STOP"], v.getAPIGen()) + pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] + if v.getAPIGen() == FEATURE_G1_TELEMATICS { + pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] + } + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + + return ch, nil } // ChargeStart . -func (v *Vehicle) ChargeOn() { +func (v *Vehicle) ChargeOn() (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } if v.isEV() { - v.selectVehicle() + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } params := map[string]string{ "delay": "0", "vin": v.Vin, "pin": v.client.credentials.PIN} reqURL := MOBILE_API_VERSION + apiURLs["API_EV_CHARGE_NOW"] pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] - v.client.execute(reqURL, POST, params, pollingURL, true) + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + return ch, nil + } else { + return nil, errors.New("not an EV car") } } // GetLocation . -func (v *Vehicle) GetLocation(force bool) { - if force { // Sends a locate command to the vehicle to get real time position +func (v *Vehicle) GetLocation(force bool) (chan string, error) { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + + var reqURL, pollingURL string + var params map[string]string + if v.Vin != (v.client).currentVin { v.selectVehicle() - reqURL := MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_UPDATE"] - pollingURL := MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_STATUS"] - params := map[string]string{ + } + if force { // Sends a locate command to the vehicle to get real time position + reqURL = MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_UPDATE"] + pollingURL = MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_STATUS"] + params = map[string]string{ "vin": v.Vin, "pin": v.client.credentials.PIN} if v.getAPIGen() == FEATURE_G1_TELEMATICS { reqURL = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_UPDATE"] pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_STATUS"] } - v.client.execute(reqURL, POST, params, pollingURL, true) } else { // Reports the last location the vehicle has reported to Subaru - v.selectVehicle() - params := map[string]string{ + params = map[string]string{ "vin": v.Vin, "pin": v.client.credentials.PIN} - reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen()) - v.client.execute(reqURL, GET, params, "", false) + reqURL = MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen()) } + + // Simulate sending multiple messages to the channel + // go func() { + // defer close(msgChan) // Close the channel when the goroutine finishes + + // for i := 1; i <= numMessages; i++ { + // // Simulate some work or delay before sending the message + // time.Sleep(100 * time.Millisecond) + + // msg := Message{ + // Content: fmt.Sprintf("Message %d", i), + // Success: true, // Indicate success for each message + // } + // msgChan <- msg + // fmt.Printf("Sent: %s (Success: %t)\n", msg.Content, msg.Success) + // } + // }() + + ch := make(chan string) + go func() { + defer close(ch) + v.executeServiceRequest(params, reqURL, pollingURL, ch) + }() + return ch, nil +} + +// executeServiceRequest +func (v *Vehicle) executeServiceRequest(params map[string]string, reqURL, pollingURL string, ch chan string) error { + if v.Vin != v.client.currentVin { + v.selectVehicle() + } + resp, _ := v.client.execute(reqURL, POST, params, true) + + // dataName field has the list of the states [ remoteServiceStatus | errorResponse ] + if resp.DataName == "remoteServiceStatus" { + if sr, ok := v.parseServiceRequest([]byte(resp.Data)); ok { + switch { + case sr.RemoteServiceState == "finished": + // Finished RemoteServiceState Service Request does not include Service Request ID + v.client.logger.Debug("Remote service request completed successfully") + ch <- sr.RemoteServiceState + case sr.RemoteServiceState == "started": + time.Sleep(5 * time.Second) + v.client.logger.Debug("Subaru API reports remote service request (started) is in progress", "id", sr.ServiceRequestID) + v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqURL, pollingURL, ch) + ch <- sr.RemoteServiceState + case sr.RemoteServiceState == "stopping": + time.Sleep(5 * time.Second) + v.client.logger.Debug("Subaru API reports remote service request (stopping) is in progress", "id", sr.ServiceRequestID) + v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqURL, pollingURL, ch) + ch <- sr.RemoteServiceState + default: + time.Sleep(5 * time.Second) + v.client.logger.Debug("Subaru API reports remote service request (stopping) is in progress") + v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqURL, pollingURL, ch) + ch <- sr.RemoteServiceState + } + return nil + } + } + return errors.New("response is not a service request") +} + +// parseServiceRequest . +func (v *Vehicle) parseServiceRequest(b []byte) (ServiceRequest, bool) { + var sr ServiceRequest + err := json.Unmarshal(b, &sr) + if err != nil { + v.client.logger.Error("error while parsing service request json", "error", err.Error()) + return sr, false + } + return sr, true } // GetClimatePresets connects to the MySubaru API to download available climate presets. @@ -443,274 +579,287 @@ func (v *Vehicle) GetLocation(force bool) { // If successful and climate presets are found for the user's vehicle, // it downloads them. If no presets are available, or if the connection fails, // appropriate handling should be implemented within the function. -func (v *Vehicle) GetClimatePresets() { - if v.getRemoteOptionsStatus() { +func (v *Vehicle) GetClimatePresets() error { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() - reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"] - resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) + } + reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"] + resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false) - if r, ok := v.client.parseResponse(resp); ok { + re1 := regexp.MustCompile(`\"`) + result := re1.ReplaceAllString(string(resp.Data), "") + re2 := regexp.MustCompile(`\\`) + result = re2.ReplaceAllString(result, `"`) // \u0022 - re1 := regexp.MustCompile(`\"`) - result := re1.ReplaceAllString(string(r.Data), "") - re2 := regexp.MustCompile(`\\`) - result = re2.ReplaceAllString(result, `"`) // \u0022 + var cProfiles []ClimateProfile + err := json.Unmarshal([]byte(result), &cProfiles) + if err != nil { + v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) + } - var cProfiles []ClimateProfile - err := json.Unmarshal([]byte(result), &cProfiles) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) - } + if len(cProfiles) > 0 { + for _, cp := range cProfiles { + re := regexp.MustCompile(`([A-Z])`) + cpn := strings.ToLower(re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_")) - if len(cProfiles) > 0 { - for _, cp := range cProfiles { - re := regexp.MustCompile(`([A-Z])`) - cpn := strings.ToLower(re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_")) - - if v.isEV() && cp.VehicleType == "phev" { - if _, ok := v.ClimateProfiles[cpn]; ok { - v.ClimateProfiles[cpn] = cp - } else { - if _, ok := v.ClimateProfiles[cpn]; ok { - v.ClimateProfiles[cpn] = cp - } else { - v.ClimateProfiles[cpn] = cp - } - } - } - if !v.isEV() && cp.VehicleType == "gas" { - if _, ok := v.ClimateProfiles[cpn]; ok { - v.ClimateProfiles[cpn] = cp - } else { - if _, ok := v.ClimateProfiles[cpn]; ok { - v.ClimateProfiles[cpn] = cp - } else { - v.ClimateProfiles[cpn] = cp - } - } + if v.isEV() && cp.VehicleType == "phev" { + if _, ok := v.ClimateProfiles[cpn]; ok { + v.ClimateProfiles[cpn] = cp + } else { + if _, ok := v.ClimateProfiles[cpn]; ok { + v.ClimateProfiles[cpn] = cp + } else { + v.ClimateProfiles[cpn] = cp + } + } + } + if !v.isEV() && cp.VehicleType == "gas" { + if _, ok := v.ClimateProfiles[cpn]; ok { + v.ClimateProfiles[cpn] = cp + } else { + if _, ok := v.ClimateProfiles[cpn]; ok { + v.ClimateProfiles[cpn] = cp + } else { + v.ClimateProfiles[cpn] = cp } } - } else { - v.client.logger.Debug("couldn't find any subaru climate presets") } - v.Updated = time.Now() } } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") + v.client.logger.Debug("couldn't find any subaru climate presets") } + v.Updated = time.Now() + return nil } // GetClimateQuickPresets // Used while user uses "quick start engine" button in the app -func (v *Vehicle) GetClimateQuickPresets() { - if v.getRemoteOptionsStatus() { +func (v *Vehicle) GetClimateQuickPresets() error { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() - reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"] - resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) - // v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp) + } + reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"] + resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false) + // v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp) - if r, ok := v.client.parseResponse(resp); ok { + re1 := regexp.MustCompile(`\"`) + result := re1.ReplaceAllString(string(resp.Data), "") + re2 := regexp.MustCompile(`\\`) + result = re2.ReplaceAllString(result, `"`) // \u0022 - re1 := regexp.MustCompile(`\"`) - result := re1.ReplaceAllString(string(r.Data), "") - re2 := regexp.MustCompile(`\\`) - result = re2.ReplaceAllString(result, `"`) // \u0022 + var cp ClimateProfile + err := json.Unmarshal([]byte(result), &cp) + if err != nil { + v.client.logger.Error("error while parsing climate quick presets json", "request", "GetClimateQuickPresets", "error", err.Error()) + } - var cp ClimateProfile - err := json.Unmarshal([]byte(result), &cp) - if err != nil { - v.client.logger.Error("error while parsing climate quick presets json", "request", "GetClimateQuickPresets", "error", err.Error()) - } + re := regexp.MustCompile(`([A-Z])`) + cpn := strings.ToLower("quick_" + re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_")) + if _, ok := v.ClimateProfiles[cpn]; ok { + v.ClimateProfiles[cpn] = cp + } else { + v.ClimateProfiles[cpn] = cp + } + v.Updated = time.Now() + return nil +} + +// GetClimateUserPresets . +func (v *Vehicle) GetClimateUserPresets() error { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } + reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_USER_PRESETS"] + resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false) + + re1 := regexp.MustCompile(`\"`) + result := re1.ReplaceAllString(string(resp.Data), "") + re2 := regexp.MustCompile(`\\`) + result = re2.ReplaceAllString(result, `"`) // \u0022 + + var cProfiles []ClimateProfile + err := json.Unmarshal([]byte(result), &cProfiles) + if err != nil { + v.client.logger.Error("error while parsing json", "request", "GetClimateUserPresets", "error", err.Error()) + } + + if len(cProfiles) > 0 { + for _, cp := range cProfiles { re := regexp.MustCompile(`([A-Z])`) - cpn := strings.ToLower("quick_" + re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_")) + cpn := strings.ToLower(re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_")) if _, ok := v.ClimateProfiles[cpn]; ok { v.ClimateProfiles[cpn] = cp } else { - v.ClimateProfiles[cpn] = cp - } - v.Updated = time.Now() - } - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") - } -} - -// GetClimateUserPresets . -func (v *Vehicle) GetClimateUserPresets() { - if v.getRemoteOptionsStatus() { - v.selectVehicle() - reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_USER_PRESETS"] - resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) - - if r, ok := v.client.parseResponse(resp); ok { - - re1 := regexp.MustCompile(`\"`) - result := re1.ReplaceAllString(string(r.Data), "") - re2 := regexp.MustCompile(`\\`) - result = re2.ReplaceAllString(result, `"`) // \u0022 - - var cProfiles []ClimateProfile - err := json.Unmarshal([]byte(result), &cProfiles) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetClimateUserPresets", "error", err.Error()) - } - - if len(cProfiles) > 0 { - for _, cp := range cProfiles { - re := regexp.MustCompile(`([A-Z])`) - cpn := strings.ToLower(re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_")) - - if _, ok := v.ClimateProfiles[cpn]; ok { - v.ClimateProfiles[cpn] = cp - } else { - if _, ok := v.ClimateProfiles[cpn]; ok { - v.ClimateProfiles[cpn] = cp - } else { - v.ClimateProfiles[cpn] = cp - } - } + if _, ok := v.ClimateProfiles[cpn]; ok { + v.ClimateProfiles[cpn] = cp + } else { + v.ClimateProfiles[cpn] = cp } - } else { - v.client.logger.Debug("couldn't find any user climate presets") } - v.Updated = time.Now() } } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") + v.client.logger.Debug("couldn't find any user climate presets") } + v.Updated = time.Now() + return nil } // GetVehicleStatus . -func (v *Vehicle) GetVehicleStatus() { - v.selectVehicle() +func (v *Vehicle) GetVehicleStatus() error { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_VEHICLE_STATUS"], v.getAPIGen()) - resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) + resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false) // v.client.logger.Info("http request output", "request", "GetVehicleStatus", "body", resp) - if r, ok := v.client.parseResponse(resp); ok { - var vs VehicleStatus - err := json.Unmarshal(r.Data, &vs) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error()) - } - // v.client.logger.Debug("http request output", "request", "GetVehicleStatus", "body", vs) + var vs VehicleStatus + err := json.Unmarshal(resp.Data, &vs) + if err != nil { + v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error()) + } + // v.client.logger.Debug("http request output", "request", "GetVehicleStatus", "body", vs) - v.EngineState = vs.VehicleStateType - v.Odometer.Miles = vs.OdometerValue - v.Odometer.Kilometers = vs.OdometerValueKm - v.DistanceToEmpty.Miles = int(vs.DistanceToEmptyFuelMiles) - v.DistanceToEmpty.Kilometers = vs.DistanceToEmptyFuelKilometers - v.DistanceToEmpty.Miles10s = vs.DistanceToEmptyFuelMiles10s - v.DistanceToEmpty.Kilometers10s = vs.DistanceToEmptyFuelKilometers10s - if vs.RemainingFuelPercent > 0 && vs.RemainingFuelPercent <= 101 { - v.DistanceToEmpty.Percentage = vs.RemainingFuelPercent - } - v.FuelConsumptionAvg.MPG = float64(vs.AvgFuelConsumptionMpg) - v.FuelConsumptionAvg.LP100Km = float64(vs.AvgFuelConsumptionLitersPer100Kilometers) + v.EngineState = vs.VehicleStateType + v.Odometer.Miles = vs.OdometerValue + v.Odometer.Kilometers = vs.OdometerValueKm + v.DistanceToEmpty.Miles = int(vs.DistanceToEmptyFuelMiles) + v.DistanceToEmpty.Kilometers = vs.DistanceToEmptyFuelKilometers + v.DistanceToEmpty.Miles10s = vs.DistanceToEmptyFuelMiles10s + v.DistanceToEmpty.Kilometers10s = vs.DistanceToEmptyFuelKilometers10s + if vs.RemainingFuelPercent > 0 && vs.RemainingFuelPercent <= 101 { + v.DistanceToEmpty.Percentage = vs.RemainingFuelPercent + } + v.FuelConsumptionAvg.MPG = float64(vs.AvgFuelConsumptionMpg) + v.FuelConsumptionAvg.LP100Km = float64(vs.AvgFuelConsumptionLitersPer100Kilometers) - v.GeoLocation.Latitude = float64(vs.Latitude) - v.GeoLocation.Longitude = float64(vs.Longitude) - v.GeoLocation.Heading = vs.Heading + v.GeoLocation.Latitude = float64(vs.Latitude) + v.GeoLocation.Longitude = float64(vs.Longitude) + v.GeoLocation.Heading = vs.Heading - val := reflect.ValueOf(vs) - typeOfS := val.Type() + val := reflect.ValueOf(vs) + typeOfS := val.Type() - for i := range val.NumField() { - // v.client.logger.Debug("vehicle status >> parsing a car part", "field", typeOfS.Field(i).Name, "value", val.Field(i).Interface(), "type", val.Field(i).Type()) - if slices.Contains(badValues, val.Field(i).Interface()) { - continue - } else { - if strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "Position") || - strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "LockStatus") || - strings.HasPrefix(typeOfS.Field(i).Name, "Window") && strings.HasSuffix(typeOfS.Field(i).Name, "Status") || - strings.HasPrefix(typeOfS.Field(i).Name, "TirePressure") && strings.HasSuffix(typeOfS.Field(i).Name, "Psi") { - v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface()) - } + for i := range val.NumField() { + // v.client.logger.Debug("vehicle status >> parsing a car part", "field", typeOfS.Field(i).Name, "value", val.Field(i).Interface(), "type", val.Field(i).Type()) + if slices.Contains(badValues, val.Field(i).Interface()) { + continue + } else { + if strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "Position") || + strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "LockStatus") || + strings.HasPrefix(typeOfS.Field(i).Name, "Window") && strings.HasSuffix(typeOfS.Field(i).Name, "Status") || + strings.HasPrefix(typeOfS.Field(i).Name, "TirePressure") && strings.HasSuffix(typeOfS.Field(i).Name, "Psi") { + v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface()) } } - v.Updated = time.Now() } + v.Updated = time.Now() + return nil } // GetVehicleCondition . -func (v *Vehicle) GetVehicleCondition() { - v.selectVehicle() +func (v *Vehicle) GetVehicleCondition() error { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_CONDITION"], v.getAPIGen()) - resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) + resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false) // v.client.logger.Info("http request output", "request", "GetVehicleCondition", "body", resp) - if r, ok := v.client.parseResponse(resp); ok { - var sr ServiceRequest - err := json.Unmarshal(r.Data, &sr) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) - } - // v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp) - - var vc VehicleCondition - err = json.Unmarshal(sr.Result, &vc) - if err != nil { - v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) - } - // v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp) - - val := reflect.ValueOf(vc) - typeOfS := val.Type() - - for i := range val.NumField() { - // v.client.logger.Debug("vehicle condition >> parsing a car part", "field", typeOfS.Field(i).Name, "value", val.Field(i).Interface(), "type", val.Field(i).Type()) - if slices.Contains(badValues, val.Field(i).Interface()) { - continue - } else { - if strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "Position") || - strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "LockStatus") || - strings.HasPrefix(typeOfS.Field(i).Name, "Window") && strings.HasSuffix(typeOfS.Field(i).Name, "Status") { - v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface()) - } - // if strings.HasPrefix(typeOfS.Field(i).Name, "TirePressure") { - // v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface()) - // } - } - } - v.Updated = time.Now() + var sr ServiceRequest + err := json.Unmarshal(resp.Data, &sr) + if err != nil { + v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) } + // v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp) + + var vc VehicleCondition + err = json.Unmarshal(sr.Result, &vc) + if err != nil { + v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) + } + // v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp) + + val := reflect.ValueOf(vc) + typeOfS := val.Type() + + for i := range val.NumField() { + // v.client.logger.Debug("vehicle condition >> parsing a car part", "field", typeOfS.Field(i).Name, "value", val.Field(i).Interface(), "type", val.Field(i).Type()) + if slices.Contains(badValues, val.Field(i).Interface()) { + continue + } else { + if strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "Position") || + strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "LockStatus") || + strings.HasPrefix(typeOfS.Field(i).Name, "Window") && strings.HasSuffix(typeOfS.Field(i).Name, "Status") { + v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface()) + } + // if strings.HasPrefix(typeOfS.Field(i).Name, "TirePressure") { + // v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface()) + // } + } + } + v.Updated = time.Now() + return nil } // GetVehicleHealth . -func (v *Vehicle) GetVehicleHealth() { - v.selectVehicle() +func (v *Vehicle) GetVehicleHealth() error { + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + if v.Vin != (v.client).currentVin { + v.selectVehicle() + } params := map[string]string{ "vin": v.Vin, "_": timestamp()} reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_HEALTH"] - resp := v.client.execute(reqURL, GET, params, "", false) + resp, _ := v.client.execute(GET, reqURL, params, false) // v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "body", resp) - if r, ok := v.client.parseResponse(resp); ok { - var vh VehicleHealth - 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) + var vh VehicleHealth + err := json.Unmarshal(resp.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) - for i, vhi := range vh.VehicleHealthItems { - // v.client.logger.Debug("vehicle health item", "id", i, "item", vhi) - if vhi.IsTrouble { - if _, ok := troubles[vhi.FeatureCode]; ok { - t := Trouble{ - Description: troubles[vhi.FeatureCode], - } - v.Troubles[vhi.FeatureCode] = t - v.client.logger.Debug("found troubled vehicle health item", "id", i, "item", vhi.FeatureCode, "description", troubles[vhi.FeatureCode]) + for i, vhi := range vh.VehicleHealthItems { + // v.client.logger.Debug("vehicle health item", "id", i, "item", vhi) + if vhi.IsTrouble { + if _, ok := troubles[vhi.FeatureCode]; ok { + t := Trouble{ + Description: troubles[vhi.FeatureCode], } + v.Troubles[vhi.FeatureCode] = t + v.client.logger.Debug("found troubled vehicle health item", "id", i, "item", vhi.FeatureCode, "description", troubles[vhi.FeatureCode]) } } - } else { - v.client.logger.Error("active STARLINK Security Plus subscription required") } + return nil } // GetFeaturesList . @@ -727,7 +876,10 @@ func (v *Vehicle) GetFeaturesList() { // selectVehicle . func (v *Vehicle) selectVehicle() { if v.client.currentVin != v.Vin { - vData := (*v.client).SelectVehicle(v.Vin) + vData, err := (v.client).SelectVehicle(v.Vin) + if err != nil { + v.client.logger.Debug("cannot get vehicle data") + } v.SubscriptionStatus = vData.SubscriptionStatus v.GeoLocation.Latitude = vData.VehicleGeoPosition.Latitude v.GeoLocation.Longitude = vData.VehicleGeoPosition.Longitude