From 30bd0bde44dc240d0ee00b91a5b9192c9de473d6 Mon Sep 17 00:00:00 2001 From: Alex Savin Date: Tue, 8 Jul 2025 15:34:07 -0400 Subject: [PATCH] Refactor session validation and enhance error handling in vehicle commands --- client.go | 30 ++++++++++- mysubaru.go | 35 ++++++++++--- vehicle.go | 143 ++++++++++++++++++++++------------------------------ 3 files changed, 116 insertions(+), 92 deletions(-) diff --git a/client.go b/client.go index 28fcff7..56a4935 100644 --- a/client.go +++ b/client.go @@ -437,8 +437,8 @@ func (c *Client) parseResponse(b []byte) (Response, bool) { } // ValidateSession checks if the current session is valid by making a request to the vehicle status API. -func (c *Client) ValidateSession() bool { - reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_STATUS"] +func (c *Client) validateSession() bool { + reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"] resp, err := c.execute(GET, reqURL, map[string]string{}, false) if err != nil { c.logger.Error("error while executing validateSession request", "request", "validateSession", "error", err.Error()) @@ -446,9 +446,35 @@ func (c *Client) ValidateSession() bool { } c.logger.Debug("http request output", "request", "validateSession", "body", resp) + if resp.Success { + _, err := c.SelectVehicle(c.currentVin) + if err != nil { + c.logger.Error("error while selecting vehicle", "request", "validateSession", "error", err.Error()) + return false + } + } + if !resp.Success { + _, err := c.auth() + if err != nil { + c.logger.Error("error while re-authenticating", "request", "validateSession", "error", err.Error()) + return false + } + _, err = c.SelectVehicle(c.currentVin) + if err != nil { + c.logger.Error("error while selecting vehicle", "request", "validateSession", "error", err.Error()) + return false + } + } + return true } +// isPINRequired . +// Return if a vehicle with an active remote service subscription exists. +// func (v *Vehicle) isPINRequired() bool { +// return v.getRemoteOptionsStatus() +// } + // func isPINRequired() {} // func getEVStatus() {} // func getRemoteOptionsStatus() {} diff --git a/mysubaru.go b/mysubaru.go index 7e8e317..66bb665 100644 --- a/mysubaru.go +++ b/mysubaru.go @@ -377,6 +377,28 @@ type ServiceRequest struct { UpdateTime UnixTime `json:"updateTime,omitempty"` // timestamp // is empty if the request is started } +// parse parses the JSON response from the MySubaru API into a ServiceRequest struct. +func (sr *ServiceRequest) parse(b []byte, logger *slog.Logger) error { + err := json.Unmarshal(b, &sr) + if err != nil { + logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) + } + if !sr.Success && sr.ErrorCode != "" { + logger.Error("error in response", "request", "GetVehicleCondition", "errorCode", sr.ErrorCode, "remoteServiceType", sr.RemoteServiceType) + switch sr.ErrorCode { + case API_ERRORS["API_ERROR_SERVICE_ALREADY_STARTED"]: + return errors.New("error in response: Service already started") + case API_ERRORS["API_ERROR_VEHICLE_NOT_IN_ACCOUNT"]: + return errors.New("error in response: Vehicle not in account") + case API_ERRORS["API_ERROR_SOA_403"]: + return errors.New("error in response: Unable to parse response body, SOA 403 error") + default: + return errors.New("error in response: " + sr.ErrorCode) + } + } + return nil +} + // climateSettings: [ climateSettings ] // climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1)] // climateZoneFrontTemp: [for _ in range(60, 85 + 1)] @@ -395,12 +417,12 @@ type VehicleHealth struct { LastUpdatedDate int64 `json:"lastUpdatedDate"` } type VehicleHealthItem struct { - B2cCode string `json:"b2cCode"` - FeatureCode string `json:"featureCode"` - IsTrouble bool `json:"isTrouble"` - OnDaiID int `json:"onDaiId"` // Has a number, probably id, but I couldn't find it purpose - OnDates []int64 `json:"onDates,omitempty"` // List of the timestamps - WarningCode int `json:"warningCode"` + WarningCode int `json:"warningCode"` // internal code used by MySubaru, not documented + B2cCode string `json:"b2cCode"` // oilTemp | airbag | oilLevel | etc. + FeatureCode string `json:"featureCode"` // SRS_MIL | CEL_MIL | ATF_MIL | etc. + IsTrouble bool `json:"isTrouble"` // false | true + OnDaiID int `json:"onDaiId"` // Has a number, probably internal record id + OnDates []UnixTime `json:"onDates,omitempty"` // List of the timestamps } // ErrorResponse . @@ -454,6 +476,7 @@ type CustomTime2 struct { time.Time } +// UnmarshalJSON implements the json.Unmarshaler interface func (ct *CustomTime2) UnmarshalJSON(b []byte) (err error) { const layout = "2006-01-02T15:04:05-0700" ct.Time, err = time.Parse(layout, string(b)) // Parse the string using the custom layout diff --git a/vehicle.go b/vehicle.go index eed726f..edcfaab 100644 --- a/vehicle.go +++ b/vehicle.go @@ -214,11 +214,6 @@ func (v *Vehicle) String() string { // Lock // Sends a command to lock doors. 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"]) - } - params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -239,11 +234,6 @@ func (v *Vehicle) Lock() (chan string, error) { // Unlock // Send command to unlock doors. 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"]) - } - params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -264,11 +254,6 @@ func (v *Vehicle) Unlock() (chan string, error) { // EngineStart // Sends a command to start engine and set climate control. 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"]) - } - // TODO: Get Quick Climate Preset from the Currect Car params := map[string]string{ "delay": "0", @@ -302,11 +287,6 @@ func (v *Vehicle) EngineStart() (chan string, error) { // EngineStop // Sends a command to stop engine. 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"]) - } - params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -326,11 +306,6 @@ func (v *Vehicle) EngineStop() (chan string, error) { // LightsStart // Sends a command to flash lights. 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"]) - } - params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -353,11 +328,6 @@ func (v *Vehicle) LightsStart() (chan string, error) { // LightsStop // Sends a command to stop flash lights. 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"]) - } - params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -380,11 +350,6 @@ func (v *Vehicle) LightsStop() (chan string, error) { // HornStart // Send command to sound horn. 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"]) - } - params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -407,11 +372,6 @@ func (v *Vehicle) HornStart() (chan string, error) { // HornStop // Send command to sound horn. 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"]) - } - params := map[string]string{ "delay": "0", "vin": v.Vin, @@ -433,10 +393,6 @@ func (v *Vehicle) HornStop() (chan string, error) { // ChargeStart 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() { params := map[string]string{ "delay": "0", @@ -456,11 +412,6 @@ func (v *Vehicle) ChargeOn() (chan string, error) { // GetLocation 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 force { // Sends a locate command to the vehicle to get real time position @@ -498,6 +449,13 @@ func (v *Vehicle) GetClimatePresets() error { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + // Validate session before executing the request + if v.client.validateSession() { + v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"]) + return errors.New(APP_ERRORS["SESSION_EXPIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() } @@ -557,6 +515,13 @@ func (v *Vehicle) GetClimateQuickPresets() error { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + // Validate session before executing the request + if v.client.validateSession() { + v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"]) + return errors.New(APP_ERRORS["SESSION_EXPIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() } @@ -593,6 +558,13 @@ func (v *Vehicle) GetClimateUserPresets() error { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + // Validate session before executing the request + if v.client.validateSession() { + v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"]) + return errors.New(APP_ERRORS["SESSION_EXPIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() } @@ -638,6 +610,13 @@ func (v *Vehicle) GetVehicleStatus() error { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + // Validate session before executing the request + if v.client.validateSession() { + v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"]) + return errors.New(APP_ERRORS["SESSION_EXPIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() } @@ -699,6 +678,13 @@ func (v *Vehicle) GetVehicleCondition() error { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + // Validate session before executing the request + if v.client.validateSession() { + v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"]) + return errors.New(APP_ERRORS["SESSION_EXPIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() } @@ -742,12 +728,20 @@ func (v *Vehicle) GetVehicleCondition() error { return nil } -// GetVehicleHealth . +// GetVehicleHealth +// Retrieves the vehicle health status from MySubaru API. func (v *Vehicle) GetVehicleHealth() error { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } + + // Validate session before executing the request + if v.client.validateSession() { + v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"]) + return errors.New(APP_ERRORS["SESSION_EXPIRED"]) + } + if v.Vin != (v.client).currentVin { v.selectVehicle() } @@ -795,13 +789,24 @@ func (v *Vehicle) GetFeaturesList() { // Executes a service request to the Subaru API and handles the response. func (v *Vehicle) executeServiceRequest(params map[string]string, reqUrl, pollingUrl string, ch chan string, attempt int) error { var maxAttempts = 15 - if attempt >= maxAttempts { v.client.logger.Error("maximum attempts reached for service request", "request", reqUrl, "attempts", attempt) ch <- "error" return errors.New("maximum attempts reached for service request") } + // Check if the vehicle has a valid subscription for remote services + if !v.getRemoteOptionsStatus() { + v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) + } + + // Validate session before executing the request + if v.client.validateSession() { + v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"]) + return errors.New(APP_ERRORS["SESSION_EXPIRED"]) + } + if v.Vin != v.client.currentVin { v.selectVehicle() } @@ -836,16 +841,16 @@ func (v *Vehicle) executeServiceRequest(params map[string]string, reqUrl, pollin case "started": time.Sleep(5 * time.Second) - v.client.logger.Debug("Subaru API reports remote service request (started) is in progress", "id", sr.ServiceRequestID) + v.client.logger.Debug("MySubaru API reports remote service request (started) is in progress", "id", sr.ServiceRequestID) v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1) case "stopping": time.Sleep(5 * time.Second) - v.client.logger.Debug("Subaru API reports remote service request (stopping) is in progress", "id", sr.ServiceRequestID) + v.client.logger.Debug("MySubaru API reports remote service request (stopping) is in progress", "id", sr.ServiceRequestID) v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1) default: - v.client.logger.Debug("Subaru API reports remote service request (default)") + v.client.logger.Debug("MySubaru API reports remote service request (default)") v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1) } return nil @@ -899,12 +904,6 @@ func (v *Vehicle) getAPIGen() string { return "unknown" } -// isPINRequired . -// Return if a vehicle with an active remote service subscription exists. -// func (v *Vehicle) isPINRequired() bool { -// return v.getRemoteOptionsStatus() -// } - // isEV . // Get whether the specified car is an Electric Vehicle. func (v *Vehicle) isEV() bool { @@ -1022,27 +1021,3 @@ func (v *Vehicle) parseParts(name string, value any) { // func (v *Vehicle) getSubscriptionStatus() bool { // return slices.Contains(v.SubscriptionFeatures, FEATURE_ACTIVE) // } - -// // getVehicleName . -// // Get the nickname of a specified VIN. -// func (v *Vehicle) getVehicleName() string { -// return v.CarName -// } - -// func getClimateData() {} -// func saveClimateSettings() {} - -// "vhsId": 914631252, - -// "odometerValue": 23865, -// "odometerValueKilometers": 38399, - -// "tirePressureFrontLeft": "2344", -// "tirePressureFrontRight": "2344", -// "tirePressureRearLeft": "2413", -// "tirePressureRearRight": "2344", - -// "tirePressureFrontLeftPsi": "34", -// "tirePressureFrontRightPsi": "34", -// "tirePressureRearLeftPsi": "35", -// "tirePressureRearRightPsi": "34",