Compare commits

..

2 Commits

Author SHA1 Message Date
0ff98f1f1f Add tests for timestamp, URL generation, VIN validation, and vehicle charging functionality
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 1m13s
- Implement unit tests for timestamp generation to ensure it returns numeric and increasing values.
- Add tests for URL transformation based on generation type.
- Create tests for VIN validation, including valid, invalid check digits, and incorrect lengths.
- Introduce tests for vehicle charging functionality, covering scenarios for electric vehicles, non-electric vehicles, and missing remote features.
- Log errors for JSON parsing issues in the client.
2025-07-02 12:07:03 +03:00
0744d16401 Refactor some functions 2025-07-02 12:06:23 +03:00
6 changed files with 892 additions and 588 deletions

269
client.go
View File

@ -2,11 +2,11 @@ package mysubaru
import ( import (
"encoding/json" "encoding/json"
"errors"
"io" "io"
"log/slog" "log/slog"
"slices" "slices"
"sync" "sync"
"time"
"git.savin.nyc/alex/mysubaru/config" "git.savin.nyc/alex/mysubaru/config"
"resty.dev/v3" "resty.dev/v3"
@ -23,6 +23,7 @@ type Client struct {
listOfVins []string listOfVins []string
isAuthenticated bool isAuthenticated bool
isRegistered bool isRegistered bool
isAlive bool
logger *slog.Logger logger *slog.Logger
sync.RWMutex sync.RWMutex
} }
@ -52,9 +53,8 @@ func New(config *config.Config) (*Client, error) {
client.httpClient = httpClient client.httpClient = httpClient
resp := client.auth() resp := client.auth()
if r, ok := client.parseResponse(resp); ok {
var sd SessionData var sd SessionData
err := json.Unmarshal(r.Data, &sd) err := json.Unmarshal(resp.Data, &sd)
if err != nil { if err != nil {
client.logger.Error("error while parsing json", "request", "auth", "error", err.Error()) client.logger.Error("error while parsing json", "request", "auth", "error", err.Error())
} }
@ -64,10 +64,12 @@ func New(config *config.Config) (*Client, error) {
// client.logger.Debug("client authentication successful") // client.logger.Debug("client authentication successful")
client.isAuthenticated = true client.isAuthenticated = true
client.isRegistered = true client.isRegistered = true
} else {
// client.logger.Debug("client authentication successful, but devices is not registered")
client.registerDevice()
} }
// 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)) // client.logger.Debug("parsing cars assigned to the account", "quantity", len(sd.Vehicles))
if len(sd.Vehicles) > 0 { if len(sd.Vehicles) > 0 {
@ -80,28 +82,11 @@ func New(config *config.Config) (*Client, error) {
client.logger.Error("there no cars assigned to the account") client.logger.Error("there no cars assigned to the account")
return nil, err return nil, err
} }
} else {
// TODO: Work on errors
// error, _ := respParsed.Path("errorCode").Data().(string)
// switch {
// case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
// client.logger.Debug("Invalid account")
// case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
// client.logger.Debug("Client authentication failed")
// case error == apiErrors["ERROR_PASSWORD_WARNING"]:
// client.logger.Debug("Multiple Password Failures.")
// default:
// client.logger.Debug("Uknown error")
// }
client.logger.Error("request was not successfull", "request", "auth")
// TODO: Work on providing error
return nil, nil
}
return client, nil return client, nil
} }
// SelectVehicle . // SelectVehicle .
func (c *Client) SelectVehicle(vin string) VehicleData { func (c *Client) SelectVehicle(vin string) (*VehicleData, error) {
if vin == "" { if vin == "" {
vin = c.currentVin vin = c.currentVin
} }
@ -112,46 +97,47 @@ func (c *Client) SelectVehicle(vin string) VehicleData {
"vin": vin, "vin": vin,
"_": timestamp()} "_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] 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) // c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
if r, ok := c.parseResponse(resp); ok {
var vd VehicleData var vd VehicleData
err := json.Unmarshal(r.Data, &vd) err := json.Unmarshal(resp.Data, &vd)
if err != nil { if err != nil {
c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error()) 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) // c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
return vd return &vd, nil
} else {
return VehicleData{}
}
} }
// GetVehicles . // GetVehicles .
func (c *Client) GetVehicles() []*Vehicle { func (c *Client) GetVehicles() []*Vehicle {
var vehicles []*Vehicle var vehicles []*Vehicle
for _, vin := range c.listOfVins { 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) vehicles = append(vehicles, vehicle)
} }
return vehicles return vehicles
} }
// GetVehicleByVIN . // GetVehicleByVIN .
func (c *Client) GetVehicleByVIN(vin string) *Vehicle { func (c *Client) GetVehicleByVIN(vin string) (*Vehicle, error) {
var vehicle *Vehicle var vehicle *Vehicle
if slices.Contains(c.listOfVins, vin) { if slices.Contains(c.listOfVins, vin) {
params := map[string]string{ params := map[string]string{
"vin": vin, "vin": vin,
"_": timestamp()} "_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] 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) // c.logger.Debug("http request output", "request", "GetVehicleByVIN", "body", resp)
if r, ok := c.parseResponse(resp); ok {
var vd VehicleData var vd VehicleData
err := json.Unmarshal(r.Data, &vd) err := json.Unmarshal(resp.Data, &vd)
if err != nil { if err != nil {
c.logger.Error("error while parsing json", "request", "GetVehicleByVIN", "error", err.Error()) c.logger.Error("error while parsing json", "request", "GetVehicleByVIN", "error", err.Error())
} }
@ -194,11 +180,10 @@ func (c *Client) GetVehicleByVIN(vin string) *Vehicle {
vehicle.GetClimateUserPresets() vehicle.GetClimateUserPresets()
vehicle.GetClimateQuickPresets() vehicle.GetClimateQuickPresets()
return vehicle return vehicle, nil
} }
} c.logger.Error("vin code is not in the list of the available vin codes", "request", "GetVehicleByVIN")
c.logger.Error("error while parsing json", "request", "GetVehicleByVIN") return nil, errors.New("vin code is not in the list of the available vin codes")
return &Vehicle{}
} }
// func isPINRequired() {} // func isPINRequired() {}
@ -211,8 +196,12 @@ func (c *Client) GetVehicleByVIN(vin string) *Vehicle {
// func getClimateData() {} // func getClimateData() {}
// func saveClimateSettings() {} // func saveClimateSettings() {}
func (c *Client) IsAlive() bool {
return c.isAlive
}
// Exec method executes a Client instance with the API URL // 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") // defer timeTrack("[TIMETRK] Executing HTTP Request")
var resp *resty.Response var resp *resty.Response
@ -221,7 +210,7 @@ func (c *Client) execute(requestUrl string, method string, params map[string]str
resp, _ = c.httpClient. resp, _ = c.httpClient.
R(). R().
SetQueryParams(params). SetQueryParams(params).
Get(requestUrl) Get(url)
} }
// POST Requests // POST Requests
@ -230,14 +219,16 @@ func (c *Client) execute(requestUrl string, method string, params map[string]str
resp, _ = c.httpClient. resp, _ = c.httpClient.
R(). R().
SetBody(params). SetBody(params).
Post(requestUrl) Post(url)
} else { // POST > Form Data } else { // POST > Form Data
resp, _ = c.httpClient. resp, _ = c.httpClient.
R(). R().
SetFormData(params). SetFormData(params).
Post(requestUrl) Post(url)
} }
} }
if resp.IsSuccess() {
resBytes, err := io.ReadAll(resp.Body) resBytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
c.logger.Error("error while getting body", "error", err.Error()) c.logger.Error("error while getting body", "error", err.Error())
@ -245,36 +236,8 @@ func (c *Client) execute(requestUrl string, method string, params map[string]str
c.logger.Debug("parsed http request output", "data", string(resBytes)) c.logger.Debug("parsed http request output", "data", string(resBytes))
if r, ok := c.parseResponse(resBytes); ok { if r, ok := c.parseResponse(resBytes); ok {
// c.logger.Debug("parsed http request output", "data", r.Data) c.isAlive = true
return &r, nil
// 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)
}
}
}
} else { } else {
if r.DataName == "errorResponse" { if r.DataName == "errorResponse" {
var er ErrorResponse var er ErrorResponse
@ -283,17 +246,20 @@ func (c *Client) execute(requestUrl string, method string, params map[string]str
c.logger.Error("error while parsing json", "request", "errorResponse", "error", err.Error()) c.logger.Error("error while parsing json", "request", "errorResponse", "error", err.Error())
} }
if _, ok := API_ERRORS[er.ErrorLabel]; ok { 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 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", requestUrl, "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", requestUrl, "error", err.Error()) c.logger.Error("request is not successfull", "request", "execute", "method", method, "url", url, "error", err.Error())
} }
return resBytes }
c.isAlive = false
return nil, errors.New("request is not successfull, HTTP code: " + resp.Status())
} }
// auth . // auth .
func (c *Client) auth() []byte { func (c *Client) auth() *Response {
params := map[string]string{ params := map[string]string{
"env": "cloudprod", "env": "cloudprod",
"deviceType": "android", "deviceType": "android",
@ -304,7 +270,8 @@ func (c *Client) auth() []byte {
"selectedVin": "", "selectedVin": "",
"pushToken": ""} "pushToken": ""}
reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"] 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))) // c.logger.Debug("AUTH HTTP OUTPUT", "body", string([]byte(resp)))
return resp return resp
} }
@ -315,29 +282,23 @@ func (c *Client) parseResponse(b []byte) (Response, bool) {
err := json.Unmarshal(b, &r) err := json.Unmarshal(b, &r)
if err != nil { if err != nil {
c.logger.Error("error while parsing json", "error", err.Error()) c.logger.Error("error while parsing json", "error", err.Error())
return r, false
} }
return r, true return r, true
} }
// validateSession . // validateSession .
// TODO: add session validation process and add it to the proper functions
// func (c *Client) validateSession() bool { // func (c *Client) validateSession() bool {
// // {
// // "success": true,
// // "errorCode": null,
// // "dataName": null,
// // "data": null
// // }
// reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"] // reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"]
// resp := c.execute(reqURL, GET, map[string]string{}, "", false) // 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 // if r, ok := c.parseResponse(resp); ok {
// err := json.Unmarshal(resp, &r)
// if err != nil {
// c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error()) // c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error())
// } // return true
// } else {
// if r.Success { // resp := c.auth()
// return true // return true
// } // }
// return false // 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":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"}]} // {"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 . // // registerDevice .
func (c *Client) registerDevice() bool { // func (c *Client) registerDevice() bool {
// c.httpClient. // // c.httpClient.
// SetBaseURL(WEB_API_SERVER[c.country]). // // SetBaseURL(WEB_API_SERVER[c.country]).
// R(). // // R().
// SetFormData(map[string]string{ // // SetFormData(map[string]string{
// "username": c.credentials.username, // // "username": c.credentials.username,
// "password": c.credentials.password, // // "password": c.credentials.password,
// "deviceId": c.credentials.deviceId, // // "deviceId": c.credentials.deviceId,
// }). // // }).
// Post(apiURLs["WEB_API_LOGIN"]) // // Post(apiURLs["WEB_API_LOGIN"])
params := map[string]string{ // params := map[string]string{
"username": c.credentials.Username, // "username": c.credentials.Username,
"password": c.credentials.Password, // "password": c.credentials.Password,
"deviceId": c.credentials.DeviceID} // "deviceId": c.credentials.DeviceID}
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"] // reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"]
c.execute(reqURL, POST, params, "", true) // resp, _ := c.execute(POST, reqURL, params, true)
// Authorizing device via web API // // Authorizing device via web API
// c.httpClient. // // c.httpClient.
// SetBaseURL(WEB_API_SERVER[c.country]). // // SetBaseURL(WEB_API_SERVER[c.country]).
// R(). // // R().
// SetQueryParams(map[string]string{ // // SetQueryParams(map[string]string{
// "deviceId": c.credentials.deviceId, // // "deviceId": c.credentials.deviceId,
// }). // // }).
// Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"]) // // Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"])
params = map[string]string{ // params = map[string]string{
"deviceId": c.credentials.DeviceID} // "deviceId": c.credentials.DeviceID}
reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"] // reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"]
c.execute(reqURL, GET, params, "", false) // c.execute(reqURL, GET, params, "", false)
return c.setDeviceName() // return c.setDeviceName()
} // }
// setDeviceName . // // setDeviceName .
func (c *Client) setDeviceName() bool { // func (c *Client) setDeviceName() bool {
params := map[string]string{ // params := map[string]string{
"deviceId": c.credentials.DeviceID, // "deviceId": c.credentials.DeviceID,
"deviceName": c.credentials.DeviceName} // "deviceName": c.credentials.DeviceName}
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"] // reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"]
c.execute(reqURL, GET, params, "", false) // c.execute(reqURL, GET, params, "", false)
return true // return true
} // }
// // listDevices . // // listDevices .
// func (c *Client) listDevices() { // func (c *Client) listDevices() {
@ -457,3 +418,45 @@ func (c *Client) setDeviceName() bool {
// // logger.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp)) // // logger.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp))
// // } // // }
// } // }
// c.logger.Debug("parsed http request output", "data", r.Data)
// ERROR
// {"httpCode":500,"errorCode":"error","errorMessage":"org.springframework.web.HttpMediaTypeNotSupportedException - Content type 'application/x-www-form-urlencoded' not supported"}
// {"success":true,"errorCode":null,"dataName":"remoteServiceStatus","data":{"serviceRequestId":"4S4BTGND8L3137058_1640203129607_19_@NGTP","success":false,"cancelled":false,"remoteServiceType":"unlock","remoteServiceState":"started","subState":null,"errorCode":null,"result":null,"updateTime":null,"vin":"4S4BTGND8L3137058"}}
// API_REMOTE_SVC_STATUS
// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}}
// if js_resp["errorCode"] == sc.ERROR_SOA_403:
// raise RemoteServiceFailure("Backend session expired, please try again")
// if js_resp["data"]["remoteServiceState"] == "finished":
// if js_resp["data"]["success"]:
// _LOGGER.info("Remote service request completed successfully: %s", req_id)
// return True, js_resp
// _LOGGER.error(
// "Remote service request completed but failed: %s Error: %s",
// req_id,
// js_resp["data"]["errorCode"],
// )
// raise RemoteServiceFailure(
// "Remote service request completed but failed: %s" % js_resp["data"]["errorCode"]
// )
// if js_resp["data"].get("remoteServiceState") == "started":
// _LOGGER.info(
// "Subaru API reports remote service request is in progress: %s",
// req_id,
// )
// attempts_left -= 1
// await asyncio.sleep(2)
// TODO: Work on errors
// error, _ := respParsed.Path("errorCode").Data().(string)
// switch {
// case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
// client.logger.Debug("Invalid account")
// case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
// client.logger.Debug("Client authentication failed")
// case error == apiErrors["ERROR_PASSWORD_WARNING"]:
// client.logger.Debug("Multiple Password Failures.")
// default:
// client.logger.Debug("Uknown error")
// }

View File

@ -102,6 +102,8 @@ var API_ERRORS = map[string]string{
"passwordWarning": "ERROR_PASSWORD_WARNING", "passwordWarning": "ERROR_PASSWORD_WARNING",
"accountLocked": "ERROR_ACCOUNT_LOCKED", "accountLocked": "ERROR_ACCOUNT_LOCKED",
"noVehiclesOnAccount": "ERROR_NO_VEHICLES", "noVehiclesOnAccount": "ERROR_NO_VEHICLES",
"noVehiclesAvailable": "ERROR_NO_VEHICLE_AVAILABLE",
"VEHICLESETUPERROR": "ERROR_VEHICLE_SETUP_ERROR", // Vehicle Select
"accountNotFound": "ERROR_NO_ACCOUNT", "accountNotFound": "ERROR_NO_ACCOUNT",
"tooManyAttempts": "ERROR_TOO_MANY_ATTEMPTS", "tooManyAttempts": "ERROR_TOO_MANY_ATTEMPTS",
"vehicleNotInAccount": "ERROR_VEHICLE_NOT_IN_ACCOUNT", "vehicleNotInAccount": "ERROR_VEHICLE_NOT_IN_ACCOUNT",
@ -112,6 +114,10 @@ var API_ERRORS = map[string]string{
"SXM40017": "ERROR_G1_PIN_LOCKED", "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 // TODO: Get back and add error wrapper
// var apiErrors = map[string]string{ // var apiErrors = map[string]string{
// "ERROR_SOA_403": "403-soa-unableToParseResponseBody", // G2 Error Codes // "ERROR_SOA_403": "403-soa-unableToParseResponseBody", // G2 Error Codes

1
go.mod
View File

@ -11,6 +11,7 @@ require (
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.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 github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.41.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect

View File

@ -25,6 +25,27 @@ type Response struct {
// Unmarshal . // Unmarshal .
// func (r *Response) Unmarshal(b []byte) {} // 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 . // Account .
type Account struct { type Account struct {
MarketID int `json:"marketId"` MarketID int `json:"marketId"`
@ -277,17 +298,6 @@ type ServiceRequest struct {
Vin string `json:"vin"` // 4S4BTGND8L3137058 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 ] // 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)]
@ -313,3 +323,14 @@ type VehicleHealthItem struct {
OnDates []int64 `json:"onDates,omitempty"` // List of the timestamps OnDates []int64 `json:"onDates,omitempty"` // List of the timestamps
WarningCode int `json:"warningCode"` 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
}

121
utils_test.go Normal file
View File

@ -0,0 +1,121 @@
package mysubaru
import (
"regexp"
"strconv"
"testing"
"time"
)
func TestTimestamp(t *testing.T) {
ts1 := timestamp()
time.Sleep(1 * time.Millisecond)
ts2 := timestamp()
// Should be numeric
if _, err := strconv.ParseInt(ts1, 10, 64); err != nil {
t.Errorf("timestamp() returned non-numeric string: %s", ts1)
}
// Should be increasing
if ts1 >= ts2 {
t.Errorf("timestamp() not increasing: %s >= %s", ts1, ts2)
}
}
func TestUrlToGen(t *testing.T) {
tests := []struct {
url, gen, want string
}{
{"https://host/api_gen/endpoint", "g1", "https://host/g1/endpoint"},
{"https://host/api_gen/endpoint", "g2", "https://host/g2/endpoint"},
{"https://host/api_gen/endpoint", "g3", "https://host/g2/endpoint"}, // g3 special case
{"https://host/api_gen/api_gen", "g1", "https://host/g1/g1"},
{"https://host/other/endpoint", "g1", "https://host/other/endpoint"},
}
for _, tt := range tests {
got := urlToGen(tt.url, tt.gen)
if got != tt.want {
t.Errorf("urlToGen(%q, %q) = %q, want %q", tt.url, tt.gen, got, tt.want)
}
}
}
func TestVinCheck_Valid(t *testing.T) {
// Example valid VIN: 1HGCM82633A004352 (check digit is '3')
vin := "1HGCM82633A004352"
valid, corrected := vinCheck(vin)
if !valid {
t.Errorf("vinCheck(%q) = false, want true", vin)
}
if corrected != vin {
t.Errorf("vinCheck(%q) corrected VIN = %q, want %q", vin, corrected, vin)
}
}
func TestVinCheck_InvalidCheckDigit(t *testing.T) {
vin := "1HGCM82633A004352"
// Change check digit (9th char) to '9'
badVin := vin[:8] + "9" + vin[9:]
valid, corrected := vinCheck(badVin)
if valid {
t.Errorf("vinCheck(%q) = true, want false", badVin)
}
// Should correct to original VIN
if corrected != vin {
t.Errorf("vinCheck(%q) corrected VIN = %q, want %q", badVin, corrected, vin)
}
}
func TestVinCheck_WrongLength(t *testing.T) {
vin := "1234567890123456" // 16 chars
valid, corrected := vinCheck(vin)
if valid {
t.Errorf("vinCheck(%q) = true, want false", vin)
}
if corrected != "" {
t.Errorf("vinCheck(%q) corrected VIN = %q, want empty string", vin, corrected)
}
}
func TestTranscodeDigits(t *testing.T) {
// Use a known VIN and manually compute the sum
vin := "1HGCM82633A004352"
sum := transcodeDigits(vin)
// Precomputed sum for this VIN is 311 (from online VIN calculator)
want := 311
if sum != want {
t.Errorf("transcodeDigits(%q) = %d, want %d", vin, sum, want)
}
}
func TestVinCheck_XCheckDigit(t *testing.T) {
// VIN with check digit 'X'
vin := "1M8GDM9AXKP042788"
valid, corrected := vinCheck(vin)
if !valid {
t.Errorf("vinCheck(%q) = false, want true", vin)
}
if corrected != vin {
t.Errorf("vinCheck(%q) corrected VIN = %q, want %q", vin, corrected, vin)
}
}
func TestUrlToGen_NoApiGen(t *testing.T) {
url := "https://host/endpoint"
gen := "g1"
got := urlToGen(url, gen)
if got != url {
t.Errorf("urlToGen(%q, %q) = %q, want %q", url, gen, got, url)
}
}
func TestTimestamp_Format(t *testing.T) {
ts := timestamp()
matched, err := regexp.MatchString(`^\d+$`, ts)
if err != nil {
t.Fatalf("regexp error: %v", err)
}
if !matched {
t.Errorf("timestamp() = %q, want only digits", ts)
}
}

View File

@ -2,6 +2,7 @@ package mysubaru
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
@ -212,9 +213,15 @@ func (v *Vehicle) String() string {
// Lock . // Lock .
// Sends a command to lock doors. // Sends a command to lock doors.
func (v *Vehicle) Lock() { func (v *Vehicle) Lock() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -222,17 +229,26 @@ func (v *Vehicle) Lock() {
"forceKeyInCar": "false"} "forceKeyInCar": "false"}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCK"], v.getAPIGen()) reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCK"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
} else { go func() {
v.client.logger.Error("active STARLINK Security Plus subscription required") defer close(ch)
} v.executeServiceRequest(params, reqURL, pollingURL, ch)
}()
return ch, nil
} }
// Unlock . // Unlock .
// Send command to unlock doors. // Send command to unlock doors.
func (v *Vehicle) Unlock() { func (v *Vehicle) Unlock() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -240,43 +256,26 @@ func (v *Vehicle) Unlock() {
"unlockDoorType": "ALL_DOORS_CMD"} // FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD "unlockDoorType": "ALL_DOORS_CMD"} // FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_UNLOCK"], v.getAPIGen()) reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_UNLOCK"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
} else { go func() {
v.client.logger.Error("active STARLINK Security Plus subscription required") defer close(ch)
} v.executeServiceRequest(params, reqURL, pollingURL, ch)
// 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"}} return ch, nil
// 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)
} }
// 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() { func (v *Vehicle) EngineStart() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
// 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",
@ -297,34 +296,52 @@ func (v *Vehicle) EngineStart() {
} }
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_START"] reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_START"]
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
} else { go func() {
v.client.logger.Error("active STARLINK Security Plus subscription required") defer close(ch)
} v.executeServiceRequest(params, reqURL, pollingURL, ch)
}()
return ch, nil
} }
// EngineStop . // EngineStop .
// Sends a command to stop engine. // Sends a command to stop engine.
func (v *Vehicle) EngineStop() { func (v *Vehicle) EngineStop() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_STOP"] reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_STOP"]
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true)
} else { ch := make(chan string)
v.client.logger.Error("Active STARLINK Security Plus subscription required") go func() {
} defer close(ch)
v.executeServiceRequest(params, reqURL, pollingURL, ch)
}()
return ch, nil
} }
// LightsStart . // LightsStart .
// Sends a command to flash lights. // Sends a command to flash lights.
func (v *Vehicle) LightsStart() { func (v *Vehicle) LightsStart() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -334,17 +351,25 @@ func (v *Vehicle) LightsStart() {
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
} }
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
} else { go func() {
v.client.logger.Error("active STARLINK Security Plus subscription required") defer close(ch)
} v.executeServiceRequest(params, reqURL, pollingURL, ch)
}()
return ch, nil
} }
// LightsStop . // LightsStop .
// Sends a command to stop flash lights. // Sends a command to stop flash lights.
func (v *Vehicle) LightsStop() { func (v *Vehicle) LightsStop() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -354,17 +379,25 @@ func (v *Vehicle) LightsStop() {
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
} }
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
} else { go func() {
v.client.logger.Error("active STARLINK Security Plus subscription required") defer close(ch)
} v.executeServiceRequest(params, reqURL, pollingURL, ch)
}()
return ch, nil
} }
// HornStart . // HornStart .
// Send command to sound horn. // Send command to sound horn.
func (v *Vehicle) HornStart() { func (v *Vehicle) HornStart() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -374,17 +407,26 @@ func (v *Vehicle) HornStart() {
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
} }
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
} else { go func() {
v.client.logger.Error("active STARLINK Security Plus subscription required") defer close(ch)
} v.executeServiceRequest(params, reqURL, pollingURL, ch)
}()
return ch, nil
} }
// HornStop . // HornStop .
// Send command to sound horn. // Send command to sound horn.
func (v *Vehicle) HornStop() { func (v *Vehicle) HornStop() (chan string, error) {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
@ -394,48 +436,142 @@ func (v *Vehicle) HornStop() {
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
} }
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
} else { go func() {
v.client.logger.Error("Active STARLINK Security Plus subscription required") defer close(ch)
} v.executeServiceRequest(params, reqURL, pollingURL, ch)
}()
return ch, nil
} }
// ChargeStart . // 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() { if v.isEV() {
if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + apiURLs["API_EV_CHARGE_NOW"] reqURL := MOBILE_API_VERSION + apiURLs["API_EV_CHARGE_NOW"]
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] 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 . // GetLocation .
func (v *Vehicle) GetLocation(force bool) { func (v *Vehicle) GetLocation(force bool) (chan string, error) {
if force { // Sends a locate command to the vehicle to get real time position 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() v.selectVehicle()
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_UPDATE"] }
pollingURL := MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_STATUS"] if force { // Sends a locate command to the vehicle to get real time position
params := map[string]string{ 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, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
reqURL = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_UPDATE"] reqURL = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_UPDATE"]
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_STATUS"] 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 } else { // Reports the last location the vehicle has reported to Subaru
v.selectVehicle() params = map[string]string{
params := map[string]string{
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen()) reqURL = MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen())
v.client.execute(reqURL, GET, params, "", false)
} }
// 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. // GetClimatePresets connects to the MySubaru API to download available climate presets.
@ -443,16 +579,19 @@ func (v *Vehicle) GetLocation(force bool) {
// If successful and climate presets are found for the user's vehicle, // 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, // it downloads them. If no presets are available, or if the connection fails,
// appropriate handling should be implemented within the function. // appropriate handling should be implemented within the function.
func (v *Vehicle) GetClimatePresets() { func (v *Vehicle) GetClimatePresets() error {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"] reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"]
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false)
if r, ok := v.client.parseResponse(resp); ok {
re1 := regexp.MustCompile(`\"`) re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.Data), "") result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`) re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022 result = re2.ReplaceAllString(result, `"`) // \u0022
@ -494,25 +633,25 @@ func (v *Vehicle) GetClimatePresets() {
v.client.logger.Debug("couldn't find any subaru climate presets") v.client.logger.Debug("couldn't find any subaru climate presets")
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
} }
// GetClimateQuickPresets // GetClimateQuickPresets
// Used while user uses "quick start engine" button in the app // Used while user uses "quick start engine" button in the app
func (v *Vehicle) GetClimateQuickPresets() { func (v *Vehicle) GetClimateQuickPresets() error {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"] reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"]
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false)
// v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp) // v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
re1 := regexp.MustCompile(`\"`) re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.Data), "") result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`) re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022 result = re2.ReplaceAllString(result, `"`) // \u0022
@ -531,23 +670,23 @@ func (v *Vehicle) GetClimateQuickPresets() {
v.ClimateProfiles[cpn] = cp v.ClimateProfiles[cpn] = cp
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
} }
// GetClimateUserPresets . // GetClimateUserPresets .
func (v *Vehicle) GetClimateUserPresets() { func (v *Vehicle) GetClimateUserPresets() error {
if v.getRemoteOptionsStatus() { 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() v.selectVehicle()
}
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_USER_PRESETS"] reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_USER_PRESETS"]
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) resp, _ := v.client.execute(GET, reqURL, map[string]string{}, false)
if r, ok := v.client.parseResponse(resp); ok {
re1 := regexp.MustCompile(`\"`) re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.Data), "") result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`) re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022 result = re2.ReplaceAllString(result, `"`) // \u0022
@ -576,22 +715,24 @@ func (v *Vehicle) GetClimateUserPresets() {
v.client.logger.Debug("couldn't find any user climate presets") v.client.logger.Debug("couldn't find any user climate presets")
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
} }
// GetVehicleStatus . // GetVehicleStatus .
func (v *Vehicle) GetVehicleStatus() { 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() v.selectVehicle()
}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_VEHICLE_STATUS"], v.getAPIGen()) 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) // v.client.logger.Info("http request output", "request", "GetVehicleStatus", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var vs VehicleStatus var vs VehicleStatus
err := json.Unmarshal(r.Data, &vs) err := json.Unmarshal(resp.Data, &vs)
if err != nil { if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error()) v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error())
} }
@ -631,19 +772,24 @@ func (v *Vehicle) GetVehicleStatus() {
} }
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} }
// GetVehicleCondition . // GetVehicleCondition .
func (v *Vehicle) GetVehicleCondition() { 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() v.selectVehicle()
}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_CONDITION"], v.getAPIGen()) 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) // v.client.logger.Info("http request output", "request", "GetVehicleCondition", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var sr ServiceRequest var sr ServiceRequest
err := json.Unmarshal(r.Data, &sr) err := json.Unmarshal(resp.Data, &sr)
if err != nil { if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error())
} }
@ -675,22 +821,27 @@ func (v *Vehicle) GetVehicleCondition() {
} }
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} }
// GetVehicleHealth . // GetVehicleHealth .
func (v *Vehicle) GetVehicleHealth() { 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() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"vin": v.Vin, "vin": v.Vin,
"_": timestamp()} "_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_HEALTH"] 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) // v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var vh VehicleHealth var vh VehicleHealth
err := json.Unmarshal(r.Data, &vh) err := json.Unmarshal(resp.Data, &vh)
if err != nil { if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error()) v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error())
} }
@ -708,9 +859,7 @@ func (v *Vehicle) GetVehicleHealth() {
} }
} }
} }
} else { return nil
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
} }
// GetFeaturesList . // GetFeaturesList .
@ -727,7 +876,10 @@ func (v *Vehicle) GetFeaturesList() {
// selectVehicle . // selectVehicle .
func (v *Vehicle) selectVehicle() { func (v *Vehicle) selectVehicle() {
if v.client.currentVin != v.Vin { 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.SubscriptionStatus = vData.SubscriptionStatus
v.GeoLocation.Latitude = vData.VehicleGeoPosition.Latitude v.GeoLocation.Latitude = vData.VehicleGeoPosition.Latitude
v.GeoLocation.Longitude = vData.VehicleGeoPosition.Longitude v.GeoLocation.Longitude = vData.VehicleGeoPosition.Longitude