Refactor session validation and enhance error handling in vehicle commands
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 24s

This commit is contained in:
2025-07-08 15:34:07 -04:00
parent aec4b8435b
commit 30bd0bde44
3 changed files with 116 additions and 92 deletions

View File

@ -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. // ValidateSession checks if the current session is valid by making a request to the vehicle status API.
func (c *Client) ValidateSession() bool { func (c *Client) validateSession() bool {
reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_STATUS"] reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"]
resp, err := c.execute(GET, reqURL, map[string]string{}, false) resp, err := c.execute(GET, reqURL, map[string]string{}, false)
if err != nil { if err != nil {
c.logger.Error("error while executing validateSession request", "request", "validateSession", "error", err.Error()) 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) 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 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 isPINRequired() {}
// func getEVStatus() {} // func getEVStatus() {}
// func getRemoteOptionsStatus() {} // func getRemoteOptionsStatus() {}

View File

@ -377,6 +377,28 @@ type ServiceRequest struct {
UpdateTime UnixTime `json:"updateTime,omitempty"` // timestamp // is empty if the request is started 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 ] // climateSettings: [ climateSettings ]
// climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1)] // climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1)]
// climateZoneFrontTemp: [for _ in range(60, 85 + 1)] // climateZoneFrontTemp: [for _ in range(60, 85 + 1)]
@ -395,12 +417,12 @@ type VehicleHealth struct {
LastUpdatedDate int64 `json:"lastUpdatedDate"` LastUpdatedDate int64 `json:"lastUpdatedDate"`
} }
type VehicleHealthItem struct { type VehicleHealthItem struct {
B2cCode string `json:"b2cCode"` WarningCode int `json:"warningCode"` // internal code used by MySubaru, not documented
FeatureCode string `json:"featureCode"` B2cCode string `json:"b2cCode"` // oilTemp | airbag | oilLevel | etc.
IsTrouble bool `json:"isTrouble"` FeatureCode string `json:"featureCode"` // SRS_MIL | CEL_MIL | ATF_MIL | etc.
OnDaiID int `json:"onDaiId"` // Has a number, probably id, but I couldn't find it purpose IsTrouble bool `json:"isTrouble"` // false | true
OnDates []int64 `json:"onDates,omitempty"` // List of the timestamps OnDaiID int `json:"onDaiId"` // Has a number, probably internal record id
WarningCode int `json:"warningCode"` OnDates []UnixTime `json:"onDates,omitempty"` // List of the timestamps
} }
// ErrorResponse . // ErrorResponse .
@ -454,6 +476,7 @@ type CustomTime2 struct {
time.Time time.Time
} }
// UnmarshalJSON implements the json.Unmarshaler interface
func (ct *CustomTime2) UnmarshalJSON(b []byte) (err error) { func (ct *CustomTime2) UnmarshalJSON(b []byte) (err error) {
const layout = "2006-01-02T15:04:05-0700" const layout = "2006-01-02T15:04:05-0700"
ct.Time, err = time.Parse(layout, string(b)) // Parse the string using the custom layout ct.Time, err = time.Parse(layout, string(b)) // Parse the string using the custom layout

View File

@ -214,11 +214,6 @@ func (v *Vehicle) String() string {
// Lock // Lock
// Sends a command to lock doors. // Sends a command to lock doors.
func (v *Vehicle) Lock() (chan string, error) { 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{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -239,11 +234,6 @@ func (v *Vehicle) Lock() (chan string, error) {
// Unlock // Unlock
// Send command to unlock doors. // Send command to unlock doors.
func (v *Vehicle) Unlock() (chan string, error) { 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{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -264,11 +254,6 @@ func (v *Vehicle) Unlock() (chan string, error) {
// EngineStart // EngineStart
// Sends a command to start engine and set climate control. // Sends a command to start engine and set climate control.
func (v *Vehicle) EngineStart() (chan string, error) { 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 // TODO: Get Quick Climate Preset from the Currect Car
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
@ -302,11 +287,6 @@ func (v *Vehicle) EngineStart() (chan string, error) {
// EngineStop // EngineStop
// Sends a command to stop engine. // Sends a command to stop engine.
func (v *Vehicle) EngineStop() (chan string, error) { 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{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -326,11 +306,6 @@ func (v *Vehicle) EngineStop() (chan string, error) {
// LightsStart // LightsStart
// Sends a command to flash lights. // Sends a command to flash lights.
func (v *Vehicle) LightsStart() (chan string, error) { 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{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -353,11 +328,6 @@ func (v *Vehicle) LightsStart() (chan string, error) {
// LightsStop // LightsStop
// Sends a command to stop flash lights. // Sends a command to stop flash lights.
func (v *Vehicle) LightsStop() (chan string, error) { 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{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -380,11 +350,6 @@ func (v *Vehicle) LightsStop() (chan string, error) {
// HornStart // HornStart
// Send command to sound horn. // Send command to sound horn.
func (v *Vehicle) HornStart() (chan string, error) { 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{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -407,11 +372,6 @@ func (v *Vehicle) HornStart() (chan string, error) {
// HornStop // HornStop
// Send command to sound horn. // Send command to sound horn.
func (v *Vehicle) HornStop() (chan string, error) { 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{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -433,10 +393,6 @@ func (v *Vehicle) HornStop() (chan string, error) {
// ChargeStart // ChargeStart
func (v *Vehicle) ChargeOn() (chan string, error) { 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() { if v.isEV() {
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
@ -456,11 +412,6 @@ func (v *Vehicle) ChargeOn() (chan string, error) {
// GetLocation // GetLocation
func (v *Vehicle) GetLocation(force bool) (chan string, error) { 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 reqUrl, pollingUrl string
var params map[string]string var params map[string]string
if force { // Sends a locate command to the vehicle to get real time position 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"]) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(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 { if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
} }
@ -557,6 +515,13 @@ func (v *Vehicle) GetClimateQuickPresets() error {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(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 { if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
} }
@ -593,6 +558,13 @@ func (v *Vehicle) GetClimateUserPresets() error {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(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 { if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
} }
@ -638,6 +610,13 @@ func (v *Vehicle) GetVehicleStatus() error {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(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 { if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
} }
@ -699,6 +678,13 @@ func (v *Vehicle) GetVehicleCondition() error {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(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 { if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
} }
@ -742,12 +728,20 @@ func (v *Vehicle) GetVehicleCondition() error {
return nil return nil
} }
// GetVehicleHealth . // GetVehicleHealth
// Retrieves the vehicle health status from MySubaru API.
func (v *Vehicle) GetVehicleHealth() error { func (v *Vehicle) GetVehicleHealth() error {
if !v.getRemoteOptionsStatus() { if !v.getRemoteOptionsStatus() {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(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 { if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
} }
@ -795,13 +789,24 @@ func (v *Vehicle) GetFeaturesList() {
// Executes a service request to the Subaru API and handles the response. // 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 { func (v *Vehicle) executeServiceRequest(params map[string]string, reqUrl, pollingUrl string, ch chan string, attempt int) error {
var maxAttempts = 15 var maxAttempts = 15
if attempt >= maxAttempts { if attempt >= maxAttempts {
v.client.logger.Error("maximum attempts reached for service request", "request", reqUrl, "attempts", attempt) v.client.logger.Error("maximum attempts reached for service request", "request", reqUrl, "attempts", attempt)
ch <- "error" ch <- "error"
return errors.New("maximum attempts reached for service request") 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 { if v.Vin != v.client.currentVin {
v.selectVehicle() v.selectVehicle()
} }
@ -836,16 +841,16 @@ func (v *Vehicle) executeServiceRequest(params map[string]string, reqUrl, pollin
case "started": case "started":
time.Sleep(5 * time.Second) 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) v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1)
case "stopping": case "stopping":
time.Sleep(5 * time.Second) 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) v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1)
default: 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) v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1)
} }
return nil return nil
@ -899,12 +904,6 @@ func (v *Vehicle) getAPIGen() string {
return "unknown" return "unknown"
} }
// isPINRequired .
// Return if a vehicle with an active remote service subscription exists.
// func (v *Vehicle) isPINRequired() bool {
// return v.getRemoteOptionsStatus()
// }
// isEV . // isEV .
// Get whether the specified car is an Electric Vehicle. // Get whether the specified car is an Electric Vehicle.
func (v *Vehicle) isEV() bool { func (v *Vehicle) isEV() bool {
@ -1022,27 +1021,3 @@ func (v *Vehicle) parseParts(name string, value any) {
// func (v *Vehicle) getSubscriptionStatus() bool { // func (v *Vehicle) getSubscriptionStatus() bool {
// return slices.Contains(v.SubscriptionFeatures, FEATURE_ACTIVE) // 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",