Compare commits

...

43 Commits

Author SHA1 Message Date
143d100793 Update go.mod to use latest mysubaru module version and update net dependency
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-22 16:56:11 -04:00
92d4266f8b Add multi-car success test, update feature descriptions, and enhance climate profile structure
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-22 16:54:04 -04:00
d7944123dd Update climate control settings for improved performance and user comfort
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 22s
2025-07-21 19:12:42 -04:00
0e5b9aae19 Add new feature status mappings and update run time validation in vehicle functions
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 23s
2025-07-21 19:11:31 -04:00
3809ed5883 Update climate settings in UpdateClimateQuickPresets for improved functionality
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 24s
2025-07-21 16:03:00 -04:00
32dfb8fb6e Update climateZoneFrontTemp preset value for improved cooling efficiency
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-10 16:28:15 -04:00
6604b8ccc3 Update climate control preset temperatures for improved user experience
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 23s
2025-07-10 16:22:18 -04:00
20f7ab5aa2 Refactor ClimateProfile struct to change boolean fields to string for improved consistency in JSON representation
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 24s
2025-07-10 16:17:22 -04:00
d41a2a7618 Change ClimateZoneFrontAirVolume type from int to string for consistency with JSON tags
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 22s
2025-07-10 16:09:50 -04:00
cbca67a61d Update golang.org/x/net dependency to v0.42.0
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 23s
2025-07-10 16:08:04 -04:00
c3d0fa53f1 Refactor ClimateProfile struct for improved readability and consistency in JSON tags
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 26s
2025-07-10 16:07:05 -04:00
89b3d44d82 Refactor vehicle climate control methods to improve parameter handling and add new climate preset updates
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 24s
2025-07-10 08:36:31 -04:00
ebe98e685a Remove debug logging from JSON unmarshalling in CustomTime types
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-09 09:48:38 -04:00
ed6ee5aacc Enhance JSON unmarshalling in CustomTime types to improve string parsing and add debug logging
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-09 09:43:31 -04:00
7f5b092c64 Refactor JSON unmarshalling in CustomTime types to simplify quote handling and improve null value checks
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 23s
2025-07-08 22:38:09 -04:00
c018850e34 Fix JSON unmarshalling in CustomTime types to correctly handle escaped quotes
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-08 22:33:46 -04:00
7ec4dc5f1a Fix JSON unmarshalling for CustomTime types to correctly handle escaped quotes
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 27s
2025-07-08 22:32:30 -04:00
a455733f8b Enhance JSON unmarshalling for CustomTime types to handle null values and improve string parsing
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-08 22:21:02 -04:00
0b2ed38ca3 Enhance session validation handling in API endpoints and refactor vehicle-related structures for improved clarity and functionality.
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-08 22:11:16 -04:00
07e3005e9c Remove session validation checks from vehicle selection and contact methods
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 24s
2025-07-08 16:42:39 -04:00
e8a1d8f54e Add session validation checks before executing vehicle commands
Some checks failed
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Failing after 27s
2025-07-08 16:32:45 -04:00
30bd0bde44 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
2025-07-08 15:34:07 -04:00
aec4b8435b Enhance MySubaru API integration with improved error handling and new utility functions
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
- Refactor Response struct's parse method to return detailed error messages based on API error codes.
- Introduce UnixTime type for handling Unix timestamps in JSON marshaling and unmarshaling.
- Add email masking utility function to obfuscate email addresses for privacy.
- Implement containsValueInStruct function to check for substring presence in struct fields.
- Create comprehensive unit tests for UnixTime, email masking, and struct value checking.
- Update vehicle service request method documentation for clarity.
2025-07-08 11:26:45 -04:00
1d8d175be0 Refactor vehicle command functions to improve readability and maintainability
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-06 17:33:34 -04:00
ac19db1271 Update mobile API version to v2.31 2025-07-06 17:33:26 -04:00
f850b55b52 Add methods for two-factor authentication and vehicle refresh functionality 2025-07-06 17:33:22 -04:00
699dc13118 Fix vehicle VIN case sensitivity and update module dependency version
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-06 15:17:56 -04:00
3c927fd83b Enhance documentation for API client and vehicle structures; improve test function naming for clarity
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 24s
2025-07-06 15:15:12 -04:00
d8cf2c3fd7 Remove unused indirect dependency on github.com/neilotoole/slogt
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 26s
2025-07-06 15:00:39 -04:00
152eb2c7b7 Refactor code structure and remove redundant sections for improved readability and maintainability
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 26s
2025-07-06 14:23:35 -04:00
b61b5664b7 Add ValidateSession method to check vehicle status and log errors
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 27s
2025-07-05 16:14:10 -04:00
cff0624807 Ensure channel closure in LightsStart, LightsStop, and GetLocation methods; improve error handling in executeServiceRequest
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 27s
2025-07-05 13:50:31 -04:00
3f426d206d Some testing
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 26s
2025-07-05 13:34:23 -04:00
fffb194bf5 Refactor service request execution to improve error handling and remove unused message simulation
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 28s
2025-07-05 13:16:44 -04:00
85ae2658a2 Fix parameter order in service request execution for consistency
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 27s
2025-07-05 12:36:57 -04:00
f06a46b3cc Refactor service request URLs and enhance retry logic in vehicle service methods
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-05 12:33:34 -04:00
b2a76f83cc Enhance authentication error handling and logging in auth function
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-07-02 17:28:07 +03:00
d616854f4e Improve error handling and logging in HTTP request execution
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 26s
2025-07-02 17:24:57 +03:00
16af99d38e More debug
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 26s
2025-07-02 16:16:12 +03:00
3afa650200 More debug
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 46s
2025-07-02 16:12:50 +03:00
23e242be8a Enabled auth debug output
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 26s
2025-07-02 16:08:39 +03:00
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
14 changed files with 2861 additions and 1047 deletions

View File

@ -20,16 +20,47 @@ The following samples will assist you to become as comfortable as possible with
import "git.savin.nyc/alex/mysubaru"
```
#### Create a new MySubaru connection and get a car by VIN
#### Create a new MySubaru API Client
```go
// Create a MySubaru Client
mysubaru, _ := New()
outback := mysubaru.GetVehicleByVIN("VIN-CODE-HERE")
msc, err := mysubaru.New(cfg)
if err != nil {
cfg.Logger.Error("cannot create MySubaru client", "error", err)
os.Exit(1)
}
```
#### Get a car by VIN
```go
outback, err := msc.GetVehicleByVIN("1HGCM82633A004352")
if err != nil {
cfg.Logger.Error("cannot get a vehicle by VIN", "error", err)
os.Exit(1)
}
```
#### Start/Stop Lights request
```go
outback.LightsStart()
time.Sleep(30 * time.Second)
outback.LightsStop()
```
// Execute a LightsStart command
events, err := outback.LightsStart()
if err != nil {
cfg.Logger.Error("cannot execute LightsStart command", "error", err)
os.Exit(1)
}
for event := range events {
fmt.Printf("Lights Start Event: %+v\n", event)
}
// Wait for a while to see the lights on
time.Sleep(20 * time.Second)
// Execute a LightsStop command
events, err = outback.LightsStop()
if err != nil {
cfg.Logger.Error("cannot execute LightsStop command", "error", err)
os.Exit(1)
}
for event := range events {
fmt.Printf("Lights Stop Event: %+v\n", event)
}
```

808
client.go
View File

@ -2,8 +2,10 @@ package mysubaru
import (
"encoding/json"
"errors"
"io"
"log/slog"
"regexp"
"slices"
"sync"
"time"
@ -12,22 +14,24 @@ import (
"resty.dev/v3"
)
// Client .
// Client represents a MySubaru API client that interacts with the MySubaru API.
type Client struct {
credentials config.Credentials
httpClient *resty.Client
country string // USA | CA
updateInterval int // 7200
fetchInterval int // 360
country string // USA | CA
contactMethods dataMap // List of contact methods for 2FA
currentVin string
listOfVins []string
isAuthenticated bool
isRegistered bool
isAlive bool
updateInterval int // 7200
fetchInterval int // 360
logger *slog.Logger
sync.RWMutex
}
// New function creates a New MySubaru client
// New function creates a New MySubaru API client
func New(config *config.Config) (*Client, error) {
client := &Client{
@ -47,253 +51,21 @@ func New(config *config.Config) (*Client, error) {
"X-Requested-With": MOBILE_APP[client.country],
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate",
"Accept": "*/*"})
"Accept": "*/*"},
)
client.httpClient = httpClient
resp := client.auth()
if r, ok := client.parseResponse(resp); ok {
var sd SessionData
err := json.Unmarshal(r.Data, &sd)
if err != nil {
client.logger.Error("error while parsing json", "request", "auth", "error", err.Error())
}
// client.logger.Debug("unmarshaled json data", "request", "auth", "type", "sessionData", "body", sd)
if sd.DeviceRegistered && sd.RegisteredDevicePermanent {
// client.logger.Debug("client authentication successful")
client.isAuthenticated = true
client.isRegistered = true
} else {
// client.logger.Debug("client authentication successful, but devices is not registered")
client.registerDevice()
}
// client.logger.Debug("parsing cars assigned to the account", "quantity", len(sd.Vehicles))
if len(sd.Vehicles) > 0 {
for _, vehicle := range sd.Vehicles {
// client.logger.Debug("parsing car", "vin", vehicle.Vin)
client.listOfVins = append(client.listOfVins, vehicle.Vin)
}
client.currentVin = client.listOfVins[0]
} else {
client.logger.Error("there no cars assigned to the account")
return nil, err
}
} else {
// TODO: Work on errors
// error, _ := respParsed.Path("errorCode").Data().(string)
// switch {
// case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
// client.logger.Debug("Invalid account")
// case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
// client.logger.Debug("Client authentication failed")
// case error == apiErrors["ERROR_PASSWORD_WARNING"]:
// client.logger.Debug("Multiple Password Failures.")
// default:
// client.logger.Debug("Uknown error")
// }
client.logger.Error("request was not successfull", "request", "auth")
// TODO: Work on providing error
return nil, nil
if ok, err := client.auth(); !ok {
client.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
return nil, errors.New("error while executing auth request: " + err.Error())
}
return client, nil
}
// SelectVehicle .
func (c *Client) SelectVehicle(vin string) VehicleData {
if vin == "" {
vin = c.currentVin
}
vinCheck(vin)
params := map[string]string{
"vin": vin,
"_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp := c.execute(reqURL, GET, params, "", false)
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
if r, ok := c.parseResponse(resp); ok {
var vd VehicleData
err := json.Unmarshal(r.Data, &vd)
if err != nil {
c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error())
}
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
return vd
} else {
return VehicleData{}
}
}
// GetVehicles .
func (c *Client) GetVehicles() []*Vehicle {
var vehicles []*Vehicle
for _, vin := range c.listOfVins {
vehicle := c.GetVehicleByVIN(vin)
vehicles = append(vehicles, vehicle)
}
return vehicles
}
// GetVehicleByVIN .
func (c *Client) GetVehicleByVIN(vin string) *Vehicle {
var vehicle *Vehicle
if slices.Contains(c.listOfVins, vin) {
params := map[string]string{
"vin": vin,
"_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp := c.execute(reqURL, GET, params, "", false)
// c.logger.Debug("http request output", "request", "GetVehicleByVIN", "body", resp)
if r, ok := c.parseResponse(resp); ok {
var vd VehicleData
err := json.Unmarshal(r.Data, &vd)
if err != nil {
c.logger.Error("error while parsing json", "request", "GetVehicleByVIN", "error", err.Error())
}
// c.logger.Debug("http request output", "request", "GetVehicleByVIN", "body", resp)
vehicle = &Vehicle{
Vin: vin,
CarName: vd.VehicleName,
CarNickname: vd.Nickname,
ModelName: vd.ModelName,
ModelYear: vd.ModelYear,
ModelCode: vd.ModelCode,
ExtDescrip: vd.ExtDescrip,
IntDescrip: vd.IntDescrip,
TransCode: vd.TransCode,
EngineSize: vd.EngineSize,
VehicleKey: vd.VehicleKey,
LicensePlate: vd.LicensePlate,
LicensePlateState: vd.LicensePlateState,
Features: vd.Features,
SubscriptionFeatures: vd.SubscriptionFeatures,
client: c,
}
vehicle.Doors = make(map[string]Door)
vehicle.Windows = make(map[string]Window)
vehicle.Tires = make(map[string]Tire)
vehicle.ClimateProfiles = make(map[string]ClimateProfile)
vehicle.Troubles = make(map[string]Trouble)
if vehicle.isEV() {
vehicle.EV = true
} else {
vehicle.EV = false
}
vehicle.GetVehicleStatus()
vehicle.GetVehicleCondition()
vehicle.GetVehicleHealth()
vehicle.GetClimatePresets()
vehicle.GetClimateUserPresets()
vehicle.GetClimateQuickPresets()
return vehicle
}
}
c.logger.Error("error while parsing json", "request", "GetVehicleByVIN")
return &Vehicle{}
}
// func isPINRequired() {}
// func getVehicles() {}
// func getEVStatus() {}
// func getRemoteOptionsStatus() {}
// func getRemoteStartStatus() {}
// func getSafetyStatus() {}
// func getSubscriptionStatus() {}
// func getClimateData() {}
// func saveClimateSettings() {}
// 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 {
// defer timeTrack("[TIMETRK] Executing HTTP Request")
var resp *resty.Response
// GET Requests
if method == GET {
resp, _ = c.httpClient.
R().
SetQueryParams(params).
Get(requestUrl)
}
// POST Requests
if method == POST {
if j { // POST > JSON Body
resp, _ = c.httpClient.
R().
SetBody(params).
Post(requestUrl)
} else { // POST > Form Data
resp, _ = c.httpClient.
R().
SetFormData(params).
Post(requestUrl)
}
}
resBytes, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Error("error while getting body", "error", err.Error())
}
c.logger.Debug("parsed http request output", "data", string(resBytes))
if r, ok := c.parseResponse(resBytes); ok {
// c.logger.Debug("parsed http request output", "data", r.Data)
// 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 {
if r.DataName == "errorResponse" {
var er ErrorResponse
err := json.Unmarshal(r.Data, &er)
if err != nil {
c.logger.Error("error while parsing json", "request", "errorResponse", "error", err.Error())
}
if _, ok := API_ERRORS[er.ErrorLabel]; ok {
c.logger.Error("request got an error", "request", "execute", "method", method, "url", requestUrl, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
}
c.logger.Error("request got an unknown error", "request", "execute", "method", method, "url", requestUrl, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
}
c.logger.Error("request is not successfull", "request", "execute", "method", method, "url", requestUrl, "error", err.Error())
}
return resBytes
}
// auth .
func (c *Client) auth() []byte {
// auth authenticates the client with the MySubaru API using the provided credentials.
func (c *Client) auth() (bool, error) {
params := map[string]string{
"env": "cloudprod",
"deviceType": "android",
@ -304,40 +76,430 @@ func (c *Client) auth() []byte {
"selectedVin": "",
"pushToken": ""}
reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"]
resp := c.execute(reqURL, POST, params, "", false)
// c.logger.Debug("AUTH HTTP OUTPUT", "body", string([]byte(resp)))
return resp
resp, err := c.execute(POST, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
return false, errors.New("error while executing auth request: " + err.Error())
}
c.logger.Debug("http request output", "request", "auth", "body", resp)
var sd SessionData
err = json.Unmarshal(resp.Data, &sd)
if err != nil {
c.logger.Error("error while parsing json", "request", "auth", "error", err.Error())
}
// client.logger.Debug("unmarshaled json data", "request", "auth", "type", "sessionData", "body", sd)
if !sd.DeviceRegistered {
err := c.getContactMethods()
if err != nil {
c.logger.Error("error while getting contact methods", "request", "auth", "error", err.Error())
return false, errors.New("error while getting contact methods: " + err.Error())
}
c.logger.Error("device is not registered", "request", "auth", "deviceId", c.credentials.DeviceID)
return false, errors.New("device is not registered: " + c.credentials.DeviceID)
}
if sd.DeviceRegistered && sd.RegisteredDevicePermanent {
c.isAuthenticated = true
c.isRegistered = true
c.isAlive = true
}
c.logger.Debug("MySubaru API client authenticated")
if len(sd.Vehicles) > 0 {
for _, vehicle := range sd.Vehicles {
c.listOfVins = append(c.listOfVins, vehicle.Vin)
}
c.currentVin = c.listOfVins[0]
} else {
c.logger.Error("there are no vehicles associated with the account", "request", "auth", "error", "no vehicles found")
return false, err
}
return true, nil
}
// parseResponse .
// SelectVehicle selects a vehicle by its VIN. If no VIN is provided, it uses the current VIN.
func (c *Client) SelectVehicle(vin string) (*VehicleData, error) {
if vin == "" {
vin = c.currentVin
}
vinCheck(vin)
params := map[string]string{
"vin": vin,
"_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp, err := c.execute(GET, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing SelectVehicle request", "request", "SelectVehicle", "error", err.Error())
return nil, errors.New("error while executing SelectVehicle request: " + err.Error())
}
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
var vd VehicleData
err = json.Unmarshal(resp.Data, &vd)
if err != nil {
c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error())
return nil, errors.New("error while parsing json while vehicle selection")
}
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
return &vd, nil
}
// GetVehicles retrieves a list of vehicles associated with the client's account.
func (c *Client) GetVehicles() ([]*Vehicle, error) {
var vehicles []*Vehicle
for _, vin := range c.listOfVins {
vehicle, err := c.GetVehicleByVin(vin)
if err != nil {
c.logger.Error("cannot get vehicle data", "request", "GetVehicles", "error", err.Error())
return nil, errors.New("cannot get vehicle data: " + err.Error())
}
vehicles = append(vehicles, vehicle)
}
return vehicles, nil
}
// GetVehicleByVin retrieves a vehicle by its VIN from the client's list of vehicles.
func (c *Client) GetVehicleByVin(vin string) (*Vehicle, error) {
var vehicle *Vehicle
if slices.Contains(c.listOfVins, vin) {
params := map[string]string{
"vin": vin,
"_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp, err := c.execute(GET, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing GetVehicleByVin request", "request", "GetVehicleByVin", "error", err.Error())
return nil, errors.New("error while executing GetVehicleByVin request: " + err.Error())
}
// c.logger.Debug("http request output", "request", "GetVehicleByVin", "body", resp)
var vd VehicleData
err = json.Unmarshal(resp.Data, &vd)
if err != nil {
c.logger.Error("error while parsing json", "request", "GetVehicleByVin", "error", err.Error())
}
// c.logger.Debug("http request output", "request", "GetVehicleByVin", "body", resp)
vehicle = &Vehicle{
Vin: vin,
CarName: vd.VehicleName,
CarNickname: vd.Nickname,
ModelName: vd.ModelName,
ModelYear: vd.ModelYear,
ModelCode: vd.ModelCode,
ExtDescrip: vd.ExtDescrip,
IntDescrip: vd.IntDescrip,
TransCode: vd.TransCode,
EngineSize: vd.EngineSize,
VehicleKey: vd.VehicleKey,
LicensePlate: vd.LicensePlate,
LicensePlateState: vd.LicensePlateState,
Features: vd.Features,
SubscriptionFeatures: vd.SubscriptionFeatures,
client: c,
}
vehicle.Doors = make(map[string]Door)
vehicle.Windows = make(map[string]Window)
vehicle.Tires = make(map[string]Tire)
vehicle.ClimateProfiles = make(map[string]ClimateProfile)
vehicle.Troubles = make(map[string]Trouble)
if vehicle.isEV() {
vehicle.EV = true
} else {
vehicle.EV = false
}
vehicle.GetVehicleStatus()
vehicle.GetVehicleCondition()
vehicle.GetVehicleHealth()
vehicle.GetClimatePresets()
vehicle.GetClimateUserPresets()
vehicle.GetClimateQuickPresets()
return vehicle, nil
}
c.logger.Error("vin code is not in the list of the available vin codes", "request", "GetVehicleByVIN")
return nil, errors.New("vin code is not in the list of the available vin codes")
}
// RefreshVehicles refreshes the list of vehicles associated with the client's account.
// {"success":true,"dataName":"sessionData","data":{"sessionChanged":true,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751835464000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"0E154A99FA3D014D866840123E5E666A","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":{"firstName":"Tatiana","lastName":"Savin","title":"","suffix":"","email":"tanya@savin.nyc","address":"29a Marion Ave","address2":"","city":"New Providence","state":"NJ","zip":"07974-1906","cellularPhone":"","workPhone":"","homePhone":"4013050505","countryCode":"USA","relationshipType":null,"gender":"","dealerCode":null,"oemCustId":"CRM-41PLM-5TYE","createMysAccount":null,"sourceSystemCode":"mys","vehicles":[{"vin":"4S4BSETC4H3265676","siebelVehicleRelationship":"Previous Owner","primary":false,"oemCustId":"1-8K7OBOJ","status":""},{"vin":"4S4BSETC4H3265676","siebelVehicleRelationship":"Previous TM Subscriber","primary":false,"oemCustId":"1-8JY3UVS","status":"Inactive"},{"vin":"4S4BTGND8L3137058","siebelVehicleRelationship":"Previous TM Subscriber","primary":false,"oemCustId":"CRM-44UFUA14-V","status":"Draft"},{"vin":"4S4BTGPD0P3199198","siebelVehicleRelationship":"TM Subscriber","primary":true,"oemCustId":"CRM-41PLM-5TYE","status":"Active"}],"phone":"","zip5Digits":"07974","primaryPersonalCountry":"USA"},"email":"","firstName":"Tatiana","lastName":"Savin","zip":"07974-1906","oemCustId":"CRM-41PLM-5TYE","phone":""},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"4S4BTGPD0P3199198","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":"tanya@savin.nyc","firstName":"Tatiana","lastName":"Savin","subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":"07974-1906","oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":"","timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false,"vehicleBranded":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$EvONydxMhrhVgFsv4OgX3O8QaOu3naCFYex2Crqhl27cPQwJYXera$1751837730624","satelliteViewEnabled":true,"registeredDevicePermanent":true}}
func (c *Client) RefreshVehicles() error {
params := map[string]string{}
reqURL := MOBILE_API_VERSION + apiURLs["API_REFRESH_VEHICLES"]
resp, err := c.execute(GET, reqURL, params, false)
if err != nil {
c.logger.Error("error while executing RefreshVehicles request", "request", "RefreshVehicles", "error", err.Error())
return errors.New("error while executing RefreshVehicles request: " + err.Error())
}
c.logger.Debug("http request output", "request", "RefreshVehicles", "body", resp)
// var vd VehicleData
// err = json.Unmarshal(resp.Data, &vd)
// if err != nil {
// c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error())
// return errors.New("error while parsing json while vehicle selection")
// }
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
return nil
}
// RequestAuthCode requests an authentication code for two-factor authentication (2FA).
// (?!^).(?=.*@)
// (?!^): This is a negative lookbehind assertion. It ensures that the matched character is not at the beginning of the string.
// .: This matches any single character (except newline, by default).
// (?=.*@): This is a positive lookahead assertion. It ensures that the matched character is followed by any characters (.*) and then an "@" symbol. This targets the username part of the email address.
func (c *Client) RequestAuthCode(email string) error {
email, err := emailMasking(email)
if err != nil {
c.logger.Error("error while hiding email", "request", "RequestAuthCode", "error", err.Error())
return errors.New("error while hiding email: " + err.Error())
}
if !containsValueInStruct(c.contactMethods, email) {
c.logger.Error("email is not in the list of contact methods", "request", "RequestAuthCode", "email", email)
return errors.New("email is not in the list of contact methods: " + email)
}
params := map[string]string{
"contactMethod": email,
"languagePreference": "EN"}
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_SEND_VERIFICATION"]
resp, err := c.execute(POST, reqUrl, params, false)
if err != nil {
c.logger.Error("error while executing RequestAuthCode request", "request", "RequestAuthCode", "error", err.Error())
return errors.New("error while executing RequestAuthCode request: " + err.Error())
}
c.logger.Debug("http request output", "request", "RequestAuthCode", "body", resp)
return nil
}
// SubmitAuthCode submits the authentication code received from the RequestAuthCode method.
func (c *Client) SubmitAuthCode(code string, permanent bool) error {
regex := regexp.MustCompile(`^\d{6}$`)
if !regex.MatchString(code) {
c.logger.Error("invalid verification code format", "request", "SubmitAuthCode", "code", code)
return errors.New("invalid verification code format, must be 6 digits")
}
params := map[string]string{
"deviceId": c.credentials.DeviceID,
"deviceName": c.credentials.DeviceName,
"verificationCode": code}
if permanent {
params["rememberDevice"] = "on"
}
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_AUTH_VERIFY"]
resp, err := c.execute(POST, reqUrl, params, false)
if err != nil {
c.logger.Error("error while executing SubmitAuthCode request", "request", "SubmitAuthCode", "error", err.Error())
return errors.New("error while executing SubmitAuthCode request: " + err.Error())
}
c.logger.Debug("http request output", "request", "SubmitAuthCode", "body", resp)
// Device registration does not always immediately take effect
time.Sleep(time.Second * 3)
// Reauthenticate after submitting the code
if ok, err := c.auth(); !ok {
c.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
return errors.New("error while executing auth request: " + err.Error())
}
return nil
}
// getContactMethods retrieves the available contact methods for two-factor authentication (2FA).
// {"success":true,"dataName":"dataMap","data":{"userName":"a**x@savin.nyc","email":"t***a@savin.nyc"}}
func (c *Client) getContactMethods() error {
// // Validate session before executing the request
// if !c.validateSession() {
// c.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
// return errors.New(APP_ERRORS["SESSION_EXPIRED"])
// }
params := map[string]string{}
reqUrl := MOBILE_API_VERSION + apiURLs["API_2FA_CONTACT"]
resp, err := c.execute(POST, reqUrl, params, false)
if err != nil {
c.logger.Error("error while executing getContactMethods request", "request", "getContactMethods", "error", err.Error())
return errors.New("error while executing getContactMethods request: " + err.Error())
}
c.logger.Debug("http request output", "request", "getContactMethods", "body", resp)
var dm dataMap
err = json.Unmarshal(resp.Data, &dm)
if err != nil {
c.logger.Error("error while parsing json", "request", "getContactMethods", "error", err.Error())
return errors.New("error while parsing json while getting contact methods: " + err.Error())
}
c.contactMethods = dm
c.logger.Debug("contact methods successfully retrieved", "request", "getContactMethods", "methods", dm)
return nil
}
// IsAlive checks if the Client instance is alive
func (c *Client) IsAlive() bool {
return c.isAlive
}
// execute executes an HTTP request based on the method, URL, and parameters provided.
func (c *Client) execute(method string, url string, params map[string]string, j bool) (*Response, error) {
c.Lock()
// defer timeTrack("[TIMETRK] Executing HTTP Request")
var resp *resty.Response
var err error
// c.logger.Debug("executing http request", "method", method, "url", url, "params", params)
// GET Requests
if method == GET {
resp, err = c.httpClient.
R().
SetQueryParams(params).
Get(url)
if err != nil {
c.logger.Error("error while executing GET request", "request", "execute", "method", method, "url", url, "error", err.Error())
return nil, err
}
c.logger.Debug("executed GET request", "method", method, "url", url, "params", params)
}
// POST Requests
if method == POST {
if j { // POST > JSON Body
resp, err = c.httpClient.
R().
SetBody(params).
Post(url)
if err != nil {
c.logger.Error("error while executing POST request", "request", "execute", "method", method, "url", url, "error", err.Error())
return nil, err
}
} else { // POST > Form Data
resp, err = c.httpClient.
R().
SetFormData(params).
Post(url)
if err != nil {
c.logger.Error("error while executing POST request", "request", "execute", "method", method, "url", url, "error", err.Error())
return nil, err
}
}
c.logger.Debug("executed POST request", "method", method, "url", url, "params", params)
}
if resp.IsSuccess() {
resBytes, err := io.ReadAll(resp.Body)
if err != nil {
c.logger.Error("error while getting body", "error", err.Error())
}
c.logger.Debug("parsed http request output", "data", string(resBytes))
c.httpClient.SetCookies(resp.Cookies())
if r, ok := c.parseResponse(resBytes); ok {
c.isAlive = true
c.Unlock()
return &r, nil
} else {
if r.DataName == "errorResponse" {
var er ErrorResponse
err := json.Unmarshal(r.Data, &er)
if err != nil {
c.logger.Error("error while parsing json", "request", "errorResponse", "error", err.Error())
}
if _, ok := API_ERRORS[er.ErrorLabel]; ok {
c.logger.Error("request got an error", "request", "execute", "method", method, "url", url, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
}
c.logger.Error("request got an unknown error", "request", "execute", "method", method, "url", url, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
c.Unlock()
return nil, errors.New("request is not successfull, HTTP code: " + resp.Status())
}
c.logger.Error("request is not successfull", "request", "execute", "method", method, "url", url, "error", err.Error())
}
}
c.isAlive = false
c.Unlock()
return nil, errors.New("request is not successfull, HTTP code: " + resp.Status())
}
// parseResponse parses the JSON response from the MySubaru API into a Response struct.
func (c *Client) parseResponse(b []byte) (Response, bool) {
var r Response
err := json.Unmarshal(b, &r)
if err != nil {
c.logger.Error("error while parsing json", "error", err.Error())
return r, false
}
return r, true
}
// 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_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())
return false
}
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() {}
// func getRemoteStartStatus() {}
// func getSafetyStatus() {}
// func getSubscriptionStatus() {}
// validateSession .
// TODO: add session validation process and add it to the proper functions
// func (c *Client) validateSession() bool {
// // {
// // "success": true,
// // "errorCode": null,
// // "dataName": null,
// // "data": null
// // }
// reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"]
// resp := c.execute(reqURL, GET, map[string]string{}, "", false)
// c.logger.Debug("http request output", "request", "validateSession", "body", resp)
// // c.logger.Debug("http request output", "request", "validateSession", "body", resp)
// var r Response
// err := json.Unmarshal(resp, &r)
// if err != nil {
// if r, ok := c.parseResponse(resp); ok {
// c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error())
// }
// if r.Success {
// return true
// } else {
// resp := c.auth()
// return true
// }
// return false
@ -358,50 +520,50 @@ func (c *Client) parseResponse(b []byte) (Response, bool) {
// {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]}
// {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8210723,"deviceName":"Hassio Golang Integration","createdDate":"2021-12-22T01:38:43.000+0000","modifiedDate":"2021-12-22T01:38:43.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]}
// registerDevice .
func (c *Client) registerDevice() bool {
// c.httpClient.
// SetBaseURL(WEB_API_SERVER[c.country]).
// R().
// SetFormData(map[string]string{
// "username": c.credentials.username,
// "password": c.credentials.password,
// "deviceId": c.credentials.deviceId,
// }).
// Post(apiURLs["WEB_API_LOGIN"])
params := map[string]string{
"username": c.credentials.Username,
"password": c.credentials.Password,
"deviceId": c.credentials.DeviceID}
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"]
c.execute(reqURL, POST, params, "", true)
// // registerDevice .
// func (c *Client) registerDevice() bool {
// // c.httpClient.
// // SetBaseURL(WEB_API_SERVER[c.country]).
// // R().
// // SetFormData(map[string]string{
// // "username": c.credentials.username,
// // "password": c.credentials.password,
// // "deviceId": c.credentials.deviceId,
// // }).
// // Post(apiURLs["WEB_API_LOGIN"])
// params := map[string]string{
// "username": c.credentials.Username,
// "password": c.credentials.Password,
// "deviceId": c.credentials.DeviceID}
// reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"]
// resp, _ := c.execute(POST, reqURL, params, true)
// Authorizing device via web API
// c.httpClient.
// SetBaseURL(WEB_API_SERVER[c.country]).
// R().
// SetQueryParams(map[string]string{
// "deviceId": c.credentials.deviceId,
// }).
// Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"])
params = map[string]string{
"deviceId": c.credentials.DeviceID}
reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"]
c.execute(reqURL, GET, params, "", false)
// // Authorizing device via web API
// // c.httpClient.
// // SetBaseURL(WEB_API_SERVER[c.country]).
// // R().
// // SetQueryParams(map[string]string{
// // "deviceId": c.credentials.deviceId,
// // }).
// // Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"])
// params = map[string]string{
// "deviceId": c.credentials.DeviceID}
// reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"]
// c.execute(reqURL, GET, params, "", false)
return c.setDeviceName()
}
// return c.setDeviceName()
// }
// setDeviceName .
func (c *Client) setDeviceName() bool {
params := map[string]string{
"deviceId": c.credentials.DeviceID,
"deviceName": c.credentials.DeviceName}
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"]
c.execute(reqURL, GET, params, "", false)
// // setDeviceName .
// func (c *Client) setDeviceName() bool {
// params := map[string]string{
// "deviceId": c.credentials.DeviceID,
// "deviceName": c.credentials.DeviceName}
// reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"]
// c.execute(reqURL, GET, params, "", false)
return true
}
// return true
// }
// // listDevices .
// func (c *Client) listDevices() {
@ -457,3 +619,45 @@ func (c *Client) setDeviceName() bool {
// // logger.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp))
// // }
// }
// c.logger.Debug("parsed http request output", "data", r.Data)
// ERROR
// {"httpCode":500,"errorCode":"error","errorMessage":"org.springframework.web.HttpMediaTypeNotSupportedException - Content type 'application/x-www-form-urlencoded' not supported"}
// {"success":true,"errorCode":null,"dataName":"remoteServiceStatus","data":{"serviceRequestId":"4S4BTGND8L3137058_1640203129607_19_@NGTP","success":false,"cancelled":false,"remoteServiceType":"unlock","remoteServiceState":"started","subState":null,"errorCode":null,"result":null,"updateTime":null,"vin":"4S4BTGND8L3137058"}}
// API_REMOTE_SVC_STATUS
// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}}
// if js_resp["errorCode"] == sc.ERROR_SOA_403:
// raise RemoteServiceFailure("Backend session expired, please try again")
// if js_resp["data"]["remoteServiceState"] == "finished":
// if js_resp["data"]["success"]:
// _LOGGER.info("Remote service request completed successfully: %s", req_id)
// return True, js_resp
// _LOGGER.error(
// "Remote service request completed but failed: %s Error: %s",
// req_id,
// js_resp["data"]["errorCode"],
// )
// raise RemoteServiceFailure(
// "Remote service request completed but failed: %s" % js_resp["data"]["errorCode"]
// )
// if js_resp["data"].get("remoteServiceState") == "started":
// _LOGGER.info(
// "Subaru API reports remote service request is in progress: %s",
// req_id,
// )
// attempts_left -= 1
// await asyncio.sleep(2)
// TODO: Work on errors
// error, _ := respParsed.Path("errorCode").Data().(string)
// switch {
// case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
// client.logger.Debug("Invalid account")
// case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
// client.logger.Debug("Client authentication failed")
// case error == apiErrors["ERROR_PASSWORD_WARNING"]:
// client.logger.Debug("Multiple Password Failures.")
// default:
// client.logger.Debug("Uknown error")
// }

372
client_test.go Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
package mysubaru
var MOBILE_API_VERSION = "/g2v30"
var MOBILE_API_VERSION = "/g2v31"
var MOBILE_API_SERVER = map[string]string{
"USA": "https://mobileapi.prod.subarucs.com",
@ -30,12 +30,12 @@ var apiURLs = map[string]string{
"API_2FA_AUTH_VERIFY": "/twoStepAuthVerify.json",
"API_LOGIN": "/login.json", // Same API for g1 and g2
"API_REFRESH_VEHICLES": "/refreshVehicles.json",
"API_SELECT_VEHICLE": "/selectVehicle.json",
"API_VALIDATE_SESSION": "/validateSession.json",
"API_VEHICLE_STATUS": "/vehicleStatus.json",
"API_SELECT_VEHICLE": "/selectVehicle.json", // Covered by test
"API_VALIDATE_SESSION": "/validateSession.json", // Covered by test
"API_VEHICLE_STATUS": "/vehicleStatus.json", // Covered by test
"API_AUTHORIZE_DEVICE": "/authenticateDevice.json",
"API_NAME_DEVICE": "/nameThisDevice.json",
"API_VEHICLE_HEALTH": "/vehicleHealth.json",
"API_VEHICLE_HEALTH": "/vehicleHealth.json", // Covered by test
"API_CONDITION": "/service/api_gen/condition/execute.json", // Similar API for g1 and g2 -- controller should replace 'api_gen' with either 'g1' or 'g2'
"API_LOCATE": "/service/api_gen/locate/execute.json", // Get the last location the vehicle has reported to Subaru
"API_LOCK": "/service/api_gen/lock/execute.json",
@ -48,12 +48,12 @@ var apiURLs = map[string]string{
"API_LIGHTS": "/service/api_gen/lightsOnly/execute.json",
"API_LIGHTS_CANCEL": "/service/api_gen/lightsOnly/cancel.json",
"API_LIGHTS_STOP": "/service/api_gen/lightsOnly/stop.json",
"API_REMOTE_SVC_STATUS": "/service/g2/remoteService/status.json",
"API_G1_LOCATE_UPDATE": "/service/g1/vehicleLocate/execute.json", // Different API for g1 and g2
"API_G1_LOCATE_STATUS": "/service/g1/vehicleLocate/status.json",
"API_G1_HORN_LIGHTS_STATUS": "/service/g1/hornLights/status.json", // g1-Only API
"API_G2_LOCATE_UPDATE": "/service/g2/vehicleStatus/execute.json",
"API_G2_LOCATE_STATUS": "/service/g2/vehicleStatus/locationStatus.json",
"API_REMOTE_SVC_STATUS": "/service/g2/remoteService/status.json",
"API_G2_SEND_POI": "/service/g2/sendPoi/execute.json", // g2-Only API
"API_G2_SPEEDFENCE": "/service/g2/speedFence/execute.json",
"API_G2_GEOFENCE": "/service/g2/geoFence/execute.json",
@ -70,8 +70,6 @@ var apiURLs = map[string]string{
"API_EV_FETCH_CHARGE_SETTINGS": "/service/g2/phevGetTimerSettings/execute.json",
"API_EV_SAVE_CHARGE_SETTINGS": "/service/g2/phevSendTimerSetting/execute.json",
"API_EV_DELETE_CHARGE_SCHEDULE": "/service/g2/phevDeleteTimerSetting/execute.json",
// "API_G2_FETCH_CLIMATE_SETTINGS": "/service/g2/remoteEngineStart/fetch.json",
// "API_G2_SAVE_CLIMATE_SETTINGS": "/service/g2/remoteEngineStart/save.json",
}
// TODO: Get back and add wrapper to support Feature List
@ -95,21 +93,27 @@ var apiURLs = map[string]string{
// }
var API_ERRORS = map[string]string{
"403-soa-unableToParseResponseBody": "ERROR_SOA_403", // G2 Error Codes
"InvalidCredentials": "ERROR_INVALID_CREDENTIALS",
"ServiceAlreadyStarted": "ERROR_SERVICE_ALREADY_STARTED",
"invalidAccount": "ERROR_INVALID_ACCOUNT",
"passwordWarning": "ERROR_PASSWORD_WARNING",
"accountLocked": "ERROR_ACCOUNT_LOCKED",
"noVehiclesOnAccount": "ERROR_NO_VEHICLES",
"accountNotFound": "ERROR_NO_ACCOUNT",
"tooManyAttempts": "ERROR_TOO_MANY_ATTEMPTS",
"vehicleNotInAccount": "ERROR_VEHICLE_NOT_IN_ACCOUNT",
"SXM40004": "ERROR_G1_NO_SUBSCRIPTION", // G1 Error Codes
"SXM40005": "ERROR_G1_STOLEN_VEHICLE",
"SXM40006": "ERROR_G1_INVALID_PIN",
"SXM40009": "ERROR_G1_SERVICE_ALREADY_STARTED",
"SXM40017": "ERROR_G1_PIN_LOCKED",
"API_ERROR_SOA_403": "403-soa-unableToParseResponseBody", // G2
"API_ERROR_NO_ACCOUNT": "accountNotFound", // G2
"API_ERROR_INVALID_ACCOUNT": "invalidAccount", // G2
"API_ERROR_INVALID_CREDENTIALS": "InvalidCredentials", // G2
"API_ERROR_INVALID_TOKEN": "InvalidToken", // G2
"API_ERROR_PASSWORD_WARNING": "passwordWarning", // G2
"API_ERROR_TOO_MANY_ATTEMPTS": "tooManyAttempts", // G2
"API_ERROR_ACCOUNT_LOCKED": "accountLocked", // G2
"API_ERROR_NO_VEHICLES": "noVehiclesOnAccount", // G2
"API_ERROR_VEHICLE_SETUP": "VEHICLESETUPERROR", // G2
"API_ERROR_VEHICLE_NOT_IN_ACCOUNT": "vehicleNotInAccount", // G2
"API_ERROR_SERVICE_ALREADY_STARTED": "ServiceAlreadyStarted", // G2
"API_ERROR_G1_NO_SUBSCRIPTION": "SXM40004", // G1
"API_ERROR_G1_STOLEN_VEHICLE": "SXM40005", // G1
"API_ERROR_G1_INVALID_PIN": "SXM40006", // G1
"API_ERROR_G1_SERVICE_ALREADY_STARTED": "SXM40009", // G1
"API_ERROR_G1_PIN_LOCKED": "SXM40017", // G1
}
var APP_ERRORS = map[string]string{
"SUBSCRIBTION_REQUIRED": "active STARLINK Security Plus subscription required",
}
// TODO: Get back and add error wrapper
@ -146,7 +150,7 @@ var features = map[string]string{
"PANPM-TUIRWAOC": "Power Moonroof",
"PANPM-DG2G": "Panoramic Moonroof",
"PHEV": "Electric Vehicle",
"RES": "Remote Start",
"RES": "Remote Engine Start",
"REARBRK": "Reverse Auto Braking",
"TIF_35": "Tire Pressure Front 35",
"TIR_33": "Tire Pressure Rear 35",
@ -157,6 +161,13 @@ var features = map[string]string{
"RCC": "Remote Climate Control",
"ACCS": "Adaptive Cruise Control",
"SXM360L": "SiriusXM with 360L",
"WDWSTAT": "Window Status",
"MOONSTAT": "Moonroof Status",
"RTGU": "Remote Trunk / Rear Gate Unlock",
"RVFS": "Remote Vehicle Find System",
"TLD": "Tire Pressure Low Detection",
"DOOR_LU_STAT": "Door Lock/Unlock Status",
"RPOI": "Remote Geo Point of Interest",
}
var troubles = map[string]string{
@ -261,7 +272,7 @@ const (
REAR_AC_ON = "true"
REAR_AC_OFF = "false"
START_CONFIG = "startConfiguration"
START_CONFIG_DEFAULT_EV = "start_Climate_Control_only_allow_key_in_ignition"
START_CONFIG_DEFAULT_EV = "START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION"
START_CONFIG_DEFAULT_RES = "START_ENGINE_ALLOW_KEY_IN_IGNITION"
WHICH_DOOR = "unlockDoorType" // Unlock doors constants
ALL_DOORS = "ALL_DOORS_CMD"
@ -405,3 +416,32 @@ const (
// ]
// BAD_BINARY_SENSOR_VALUES = [UNKNOWN, VENTED, NOT_EQUIPPED]
)
// RAW_API_FIELDS_TO_REDACT = [
// "cachedStateCode",
// "customer",
// "email",
// "firstName",
// "lastName",
// "latitude",
// "licensePlate",
// "licensePlateState",
// "longitude",
// "nickname",
// "odometer",
// "odometerValue",
// "odometerValueKilometers",
// "oemCustId",
// "phone",
// "preferredDealer",
// "sessionCustomer",
// "timeZone",
// "userOemCustId",
// "vehicleGeoPosition",
// "vehicleKey",
// "vehicleMileage",
// "vehicleName",
// "vhsId",
// "vin",
// "zip",
// ]

View File

@ -10,5 +10,5 @@ mysubaru:
timezone: "America/New_York"
logging:
level: INFO
output: json
output: JSON
source: false

View File

@ -4,6 +4,7 @@ import (
"fmt"
"log"
"os"
"time"
"git.savin.nyc/alex/mysubaru"
"git.savin.nyc/alex/mysubaru/config"
@ -21,11 +22,13 @@ func main() {
if err != nil {
log.Fatal(err)
}
cfg.Logger.Debug("printing config", "config", cfg)
// cfg.Logger.Debug("printing config", "config", cfg)
ms, _ := mysubaru.New(cfg)
// subaru := ms.SelectVehicle("4S4BTGPD0P3199198")
msc, err := mysubaru.New(cfg)
if err != nil {
cfg.Logger.Error("cannot create MySubaru client", "error", err)
os.Exit(1)
}
// 11.6MMAN ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL NAV_TOMTOM OPL_MIL RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA SRS_MIL TEL_MIL TIF_35 TIR_33 TPMS_MIL VDC_MIL WASH_MIL g2
// subaru1 := mysubaru.GetVehicleByVIN("4S4BTGND8L3137058")
@ -37,48 +40,84 @@ func main() {
// fmt.Printf("GEN #1: %+v\n\n", subaru1.getAPIGen())
// ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL OPL_MIL PANPM-TUIRWAOC PWAAADWWAP RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA RTGU RVFS SRH_MIL SRS_MIL TEL_MIL TIF_35 TIR_33 TLD TPMS_MIL VALET VDC_MIL WASH_MIL g3
outback := ms.GetVehicleByVIN("4S4BTGPD0P3199198")
// subaru.GetLocation(true)
outback, err := msc.GetVehicleByVin("4S4BTGPD0P3199198")
if err != nil {
cfg.Logger.Error("cannot get a vehicle by VIN", "error", err)
os.Exit(1)
}
// subaru.EngineStart()
fmt.Printf("SUBARU #1 (Vehicle Status):\n")
// subaru.GetVehicleStatus()
// fmt.Printf("SUBARU #1 (Vehicle Condition):\n")
// subaru.GetVehicleCondition()
// fmt.Printf("SUBARU #1: %+v\n", subaru)
// subaru.GetClimatePresets()
// subaru.GetClimateUserPresets()
// fmt.Printf("SUBARU #2: %+v\n", subaru)
// subaru.GetVehicleHealth()
// subaru.GetFeaturesList()
// subaru := mysubaru.GetVehicles()[0]
fmt.Printf("SUBARU #1 (Vehicle Status): %s\n", outback)
// fmt.Printf("SUBARU: %+v\n", subaru)
// // Execute a Lock command
// events, err := outback.EngineStart(5, 0, true)
// if err != nil {
// cfg.Logger.Error("cannot execute Lock command", "error", err)
// os.Exit(1)
// }
// for event := range events {
// fmt.Printf("Lock Event: %+v\n", event)
// }
// fmt.Printf("Subaru Gen: %+v\n\n", subaru.getAPIGen())
// Execute a LightsStart command
events, err := outback.LightsStart()
if err != nil {
cfg.Logger.Error("cannot execute LightsStart command", "error", err)
os.Exit(1)
}
for event := range events {
fmt.Printf("Lights Start Event: %+v\n", event)
}
// subaru.EngineOn()
// subaru.GetLocation(false)
// Wait for a while to see the lights on
time.Sleep(20 * time.Second)
// subaru.GetVehicleStatus()
// subaru.GetVehicleCondition()
// Execute a LightsStop command
events, err = outback.LightsStop()
if err != nil {
cfg.Logger.Error("cannot execute LightsStop command", "error", err)
os.Exit(1)
}
for event := range events {
fmt.Printf("Lights Stop Event: %+v\n", event)
}
// subaru.GetClimateQuickPresets()
// subaru.GetClimatePresets()
// subaru.GetClimateUserPresets()
// // Execute a Unlock command
// events, err := outback.Unlock()
// if err != nil {
// cfg.Logger.Error("cannot execute Unlock command", "error", err)
// os.Exit(1)
// }
// for event := range events {
// fmt.Printf("Unlock Event: %+v\n", event)
// }
// subaru.GetVehicleStatus()
// subaru.GetClimateSettings()
// subaru.EngineStart()
// time.Sleep(15 * time.Second)
// subaru.EngineStop()
// subaru.Unlock()
// // Wait for a while to see the lights on
// time.Sleep(20 * time.Second)
// subaru.Lock()
// subaru.GetLocation()
fmt.Printf("SUBARU: %+v\n", outback)
// // Execute a Lock command
// events, err = outback.Lock()
// if err != nil {
// cfg.Logger.Error("cannot execute Lock command", "error", err)
// os.Exit(1)
// }
// for event := range events {
// fmt.Printf("Lock Event: %+v\n", event)
// }
// Execute a forced GetLocation command
// events, err = outback.GetLocation(true)
// if err != nil {
// cfg.Logger.Error("cannot execute forced GetLocation command", "error", err)
// os.Exit(1)
// }
// for event := range events {
// fmt.Printf("GeoLocation Event: %+v\n", event)
// }
}
// {"time":"2025-07-21T18:05:30.987314-04:00","level":"DEBUG","source":{"function":"git.savin.nyc/alex/mysubaru.(*Client).execute","file":"/Users/alex/go/pkg/mod/git.savin.nyc/alex/mysubaru@v0.0.0-20250721200300-3809ed5883b4/client.go","line":372},"msg":"executed GET request","method":"GET","url":"/g2v31/service/g2/remoteEngineQuickStartSettings/fetch.json","params":{}}
// {"time":"2025-07-21T18:05:31.014014-04:00","level":"DEBUG","source":{"function":"git.savin.nyc/alex/mysubaru.(*Client).execute","file":"/Users/alex/go/pkg/mod/git.savin.nyc/alex/mysubaru@v0.0.0-20250721200300-3809ed5883b4/client.go","line":404},"msg":"parsed http request output","data":"{\"success\":true,\"errorCode\":null,\"dataName\":null,\"data\":\"{\\\"name\\\":\\\"Cooling\\\",\\\"runTimeMinutes\\\":\\\"10\\\",\\\"climateZoneFrontTemp\\\":\\\"65\\\",\\\"climateZoneFrontAirMode\\\":\\\"FEET_FACE_BALANCED\\\",\\\"climateZoneFrontAirVolume\\\":\\\"7\\\",\\\"outerAirCirculation\\\":\\\"outsideAir\\\",\\\"heatedRearWindowActive\\\":\\\"false\\\",\\\"heatedSeatFrontLeft\\\":\\\"HIGH_COOL\\\",\\\"heatedSeatFrontRight\\\":\\\"HIGH_COOL\\\",\\\"airConditionOn\\\":\\\"false\\\",\\\"canEdit\\\":\\\"true\\\",\\\"disabled\\\":\\\"false\\\",\\\"presetType\\\":\\\"userPreset\\\",\\\"startConfiguration\\\":\\\"START_ENGINE_ALLOW_KEY_IN_IGNITION\\\"}\"}"}
// {"time":"2025-07-21T18:03:44.461646-04:00","level":"DEBUG","source":{"function":"git.savin.nyc/alex/mysubaru.(*Client).execute","file":"/Users/alex/go/pkg/mod/git.savin.nyc/alex/mysubaru@v0.0.0-20250721200300-3809ed5883b4/client.go","line":372},"msg":"executed GET request","method":"GET","url":"/g2v31/service/g2/remoteEngineQuickStartSettings/fetch.json","params":{}}
// {"time":"2025-07-21T18:03:44.475745-04:00","level":"DEBUG","source":{"function":"git.savin.nyc/alex/mysubaru.(*Client).execute","file":"/Users/alex/go/pkg/mod/git.savin.nyc/alex/mysubaru@v0.0.0-20250721200300-3809ed5883b4/client.go","line":404},"msg":"parsed http request output","data":"{\"success\":true,\"errorCode\":null,\"dataName\":null,\"data\":\"{\\\"airConditionOn\\\":\\\"false\\\",\\\"climateSettings\\\":\\\"climateSettings\\\",\\\"climateZoneFrontAirMode\\\":\\\"FEET_WINDOW\\\",\\\"climateZoneFrontAirVolume\\\":\\\"7\\\",\\\"climateZoneFrontTemp\\\":\\\"65\\\",\\\"heatedRearWindowActive\\\":\\\"false\\\",\\\"heatedSeatFrontLeft\\\":\\\"HIGH_COOL\\\",\\\"heatedSeatFrontRight\\\":\\\"HIGH_COOL\\\",\\\"name\\\":\\\"Cooling\\\",\\\"outerAirCirculation\\\":\\\"outsideAir\\\",\\\"runTimeMinutes\\\":\\\"10\\\",\\\"startConfiguration\\\":\\\"START_ENGINE_ALLOW_KEY_IN_IGNITION\\\"}\\n\"}"}
// {"time":"2025-07-21T18:03:44.475885-04:00","level":"ERROR","source":{"function":"git.savin.nyc/alex/mysubaru.(*Vehicle).GetClimateQuickPresets","file":"/Users/alex/go/pkg/mod/git.savin.nyc/alex/mysubaru@v0.0.0-20250721200300-3809ed5883b4/vehicle.go","line":498},"msg":"error while parsing climate quick presets json","request":"GetClimateQuickPresets","error":"invalid character '\"' after top-level value"}

View File

@ -2,12 +2,12 @@ module example
go 1.24
require git.savin.nyc/alex/mysubaru v0.0.0-20250606212815-017d7de3a224
require git.savin.nyc/alex/mysubaru v0.0.0-20250722205404-92d4266f8b5a
require (
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/net v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
resty.dev/v3 v3.0.0-beta.3 // indirect
)

2
go.mod
View File

@ -12,6 +12,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/net v0.42.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

View File

@ -2,9 +2,14 @@ package mysubaru
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"
"time"
)
// Response .
// Response represents the structure of a response from the MySubaru API.
type Response struct {
Success bool `json:"success"` // true | false
ErrorCode string `json:"errorCode,omitempty"` // string | Error message if Success is false
@ -12,54 +17,158 @@ type Response struct {
Data json.RawMessage `json:"data"` // Data struct
}
// parse .
// func (r *Response) parse(b []byte, logger *slog.Logger) bool {
// err := json.Unmarshal(b, &r)
// if err != nil {
// logger.Error("error while parsing json", "error", err.Error())
// return false
// }
// return true
// }
// parse parses the JSON response from the MySubaru API into a Response struct.
func (r *Response) parse(b []byte, logger *slog.Logger) (*Response, error) {
err := json.Unmarshal(b, &r)
if err != nil {
logger.Error("error while parsing json", "error", err.Error())
return nil, errors.New("error while parsing json: " + err.Error())
}
// Unmarshal .
// func (r *Response) Unmarshal(b []byte) {}
if !r.Success && r.ErrorCode != "" {
logger.Error("error in response", "errorCode", r.ErrorCode, "dataName", r.DataName)
switch r.ErrorCode {
// G2 API errors
case API_ERRORS["API_ERROR_NO_ACCOUNT"]:
return r, errors.New("error in response: Account not found")
case API_ERRORS["API_ERROR_INVALID_ACCOUNT"]:
return r, errors.New("error in response: Invalid Account")
case API_ERRORS["API_ERROR_INVALID_CREDENTIALS"]:
return r, errors.New("error in response: Invalid Credentials")
case API_ERRORS["API_ERROR_INVALID_TOKEN"]:
return r, errors.New("error in response: Invalid Token")
case API_ERRORS["API_ERROR_PASSWORD_WARNING"]:
return r, errors.New("error in response: Mutiple failed login attempts, password warning")
case API_ERRORS["API_ERROR_TOO_MANY_ATTEMPTS"]:
return r, errors.New("error in response: Too many attempts, please try again later")
case API_ERRORS["API_ERROR_ACCOUNT_LOCKED"]:
return r, errors.New("error in response: Account Locked")
case API_ERRORS["API_ERROR_NO_VEHICLES"]:
return r, errors.New("error in response: No vehicles found for the account")
case API_ERRORS["API_ERROR_VEHICLE_SETUP"]:
return r, errors.New("error in response: Vehicle setup is not complete")
case API_ERRORS["API_ERROR_VEHICLE_NOT_IN_ACCOUNT"]:
return r, errors.New("error in response: Vehicle not in account")
case API_ERRORS["API_ERROR_SERVICE_ALREADY_STARTED"]:
return r, errors.New("error in response: Service already started")
case API_ERRORS["API_ERROR_SOA_403"]:
return r, errors.New("error in response: Unable to parse response body, SOA 403 error")
// G1 API errors
case API_ERRORS["API_ERROR_G1_NO_SUBSCRIPTION"]:
return r, errors.New("error in response: No subscription found for the vehicle")
case API_ERRORS["API_ERROR_G1_STOLEN_VEHICLE"]:
return r, errors.New("error in response: Car is reported as stolen")
case API_ERRORS["API_ERROR_G1_INVALID_PIN"]:
return r, errors.New("error in response: Invalid PIN")
case API_ERRORS["API_ERROR_G1_PIN_LOCKED"]:
return r, errors.New("error in response: PIN is locked")
case API_ERRORS["API_ERROR_G1_SERVICE_ALREADY_STARTED"]:
return r, errors.New("error in response: Service already started")
}
return r, errors.New("error in response: " + r.ErrorCode)
}
// Account .
type Account struct {
MarketID int `json:"marketId"`
AccountKey int `json:"accountKey"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
ZipCode string `json:"zipCode"`
ZipCode5 string `json:"zipCode5"`
LastLoginDate int64 `json:"lastLoginDate"`
CreatedDate int64 `json:"createdDate"`
return r, nil
}
// Request represents the structure of a request to the MySubaru API.
type Request struct {
Vin string `json:"vin"` //
Pin string `json:"pin"` //
Delay int `json:"delay,string,omitempty"` //
ForceKeyInCar *bool `json:"forceKeyInCar,string,omitempty"` //
UnlockDoorType *string `json:"unlockDoorType,omitempty"` // [ ALL_DOORS_CMD | FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD ]
Horn *string `json:"horn,omitempty"` //
ClimateSettings *string `json:"climateSettings,omitempty"` //
ClimateZoneFrontTemp *string `json:"climateZoneFrontTemp,omitempty"` //
ClimateZoneFrontAirMode *string `json:"climateZoneFrontAirMode,omitempty"` //
ClimateZoneFrontAirVolume *string `json:"climateZoneFrontAirVolume,omitempty"` //
HeatedSeatFrontLeft *string `json:"heatedSeatFrontLeft,omitempty"` //
HeatedSeatFrontRight *string `json:"heatedSeatFrontRight,omitempty"` //
HeatedRearWindowActive *string `json:"heatedRearWindowActive,omitempty"` //
OuterAirCirculation *string `json:"outerAirCirculation,omitempty"` //
AirConditionOn *string `json:"airConditionOn,omitempty"` //
RunTimeMinutes *string `json:"runTimeMinutes,omitempty"` //
StartConfiguration *string `json:"startConfiguration,omitempty"` //
}
// account .
type account struct {
MarketID int `json:"marketId"`
AccountKey int `json:"accountKey"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
ZipCode string `json:"zipCode"`
ZipCode5 string `json:"zipCode5"`
LastLoginDate UnixTime `json:"lastLoginDate"`
CreatedDate UnixTime `json:"createdDate"`
}
// Customer .
type Customer struct {
SessionCustomer string `json:"sessionCustomer"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Zip string `json:"zip"`
OemCustID string `json:"oemCustId"`
Phone string `json:"phone"`
SessionCustomer SessionCustomer `json:"sessionCustomer,omitempty"` // struct | Only by performing a RefreshVehicles request
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Zip string `json:"zip"`
OemCustID string `json:"oemCustId"`
Phone string `json:"phone"`
}
// SessionCustomer .
type SessionCustomer struct {
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
Title string `json:"title,omitempty"`
Suffix string `json:"suffix,omitempty"`
Email string `json:"email"`
Address string `json:"address"`
Address2 string `json:"address2,omitempty"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip"`
CellularPhone string `json:"cellularPhone,omitempty"`
WorkPhone string `json:"workPhone,omitempty"`
HomePhone string `json:"homePhone,omitempty"`
CountryCode string `json:"countryCode"`
RelationshipType any `json:"relationshipType,omitempty"`
Gender string `json:"gender,omitempty"`
DealerCode any `json:"dealerCode,omitempty"`
OemCustID string `json:"oemCustId"`
CreateMysAccount any `json:"createMysAccount,omitempty"`
SourceSystemCode string `json:"sourceSystemCode"`
Vehicles []struct {
Vin string `json:"vin"`
SiebelVehicleRelationship string `json:"siebelVehicleRelationship"` // TM Subscriber | Previous TM Subscriber | Previous Owner
Primary bool `json:"primary"` // true | false
OemCustID string `json:"oemCustId"` // CRM-41PLM-5TYE | 1-8K7OBOJ | 1-8JY3UVS | CRM-44UFUA14-V
Status string `json:"status,omitempty"` // "Active" | "Draft" | "Inactive"
} `json:"vehicles"`
Phone string `json:"phone,omitempty"`
Zip5Digits string `json:"zip5Digits"`
PrimaryPersonalCountry string `json:"primaryPersonalCountry"`
}
// DataMap .
// "dataName": "dataMap"
type dataMap struct {
Username string `json:"userName"`
Email string `json:"email"`
}
// SessionData .
// "dataName": "sessionData"
type SessionData struct {
SessionChanged bool `json:"sessionChanged"`
VehicleInactivated bool `json:"vehicleInactivated"`
Account Account `json:"account"`
ResetPassword bool `json:"resetPassword"`
DeviceID string `json:"deviceId"`
SessionID string `json:"sessionId"`
DeviceRegistered bool `json:"deviceRegistered"`
Account account `json:"account"`
PasswordToken string `json:"passwordToken"`
ResetPassword bool `json:"resetPassword"`
SessionID string `json:"sessionId"`
SessionChanged bool `json:"sessionChanged"`
DeviceID string `json:"deviceId"`
DeviceRegistered bool `json:"deviceRegistered"`
RegisteredDevicePermanent bool `json:"registeredDevicePermanent"`
Vehicles []VehicleData `json:"vehicles"`
VehicleInactivated bool `json:"vehicleInactivated"`
RightToRepairEnabled bool `json:"rightToRepairEnabled"`
RightToRepairStates string `json:"rightToRepairStates"`
CurrentVehicleIndex int `json:"currentVehicleIndex"`
@ -72,63 +181,63 @@ type SessionData struct {
DigitalGlobeTransparentTileService string `json:"digitalGlobeTransparentTileService"`
TomtomKey string `json:"tomtomKey"`
SatelliteViewEnabled bool `json:"satelliteViewEnabled"`
RegisteredDevicePermanent bool `json:"registeredDevicePermanent"`
}
// Vehicle .
// "dataName": "vehicle"
type VehicleData struct {
Customer Customer `json:"customer"` // Customer struct
UserOemCustID string `json:"userOemCustId"` // CRM-631-HQN48K
OemCustID string `json:"oemCustId"` // CRM-631-HQN48K
Active bool `json:"active"` // true | false
Email string `json:"email"` // null | email@address.com
FirstName string `json:"firstName"` // null | First Name
LastName string `json:"lastName"` // null | Last Name
Zip string `json:"zip"` // 12345
Phone string `json:"phone"` // null | 123-456-7890
StolenVehicle bool `json:"stolenVehicle"` // true | false
VehicleName string `json:"vehicleName"` // Subaru Outback LXT
Features []string `json:"features"` // "11.6MMAN", "ABS_MIL", "ACCS", "AHBL_MIL", "ATF_MIL", "AWD_MIL", "BSD", "BSDRCT_MIL", "CEL_MIL", "EBD_MIL", "EOL_MIL", "EPAS_MIL", "EPB_MIL", "ESS_MIL", "EYESIGHT", "ISS_MIL", "NAV_TOMTOM", "OPL_MIL", "RAB_MIL", "RCC", "REARBRK", "RES", "RESCC", "RHSF", "RPOI", "RPOIA", "SRH_MIL", "SRS_MIL", "TEL_MIL", "TPMS_MIL", "VDC_MIL", "WASH_MIL", "g2"
Vin string `json:"vin"` // 4Y1SL65848Z411439
VehicleKey int64 `json:"vehicleKey"` // 3832950
Nickname string `json:"nickname"` // Subaru Outback LXT
ModelName string `json:"modelName"` // Outback
ModelYear string `json:"modelYear"` // 2020
ModelCode string `json:"modelCode"` // LDJ
ExtDescrip string `json:"extDescrip"` // Abyss Blue Pearl (ext color)
IntDescrip string `json:"intDescrip"` // Gray (int color)
TransCode string `json:"transCode"` // CVT
EngineSize float64 `json:"engineSize"` // 2.4
Phev bool `json:"phev"` // null
CachedStateCode string `json:"cachedStateCode"` // NJ
LicensePlate string `json:"licensePlate"` // NJ
LicensePlateState string `json:"licensePlateState"` // ABCDEF
SubscriptionStatus string `json:"subscriptionStatus"` // ACTIVE
SubscriptionFeatures []string `json:"subscriptionFeatures"` // "[ REMOTE ], [ SAFETY ], [ Retail | Finance3 | RetailPHEV ]""
SubscriptionPlans []string `json:"subscriptionPlans"` // []
VehicleGeoPosition GeoPosition `json:"vehicleGeoPosition"` // GeoPosition struct
AccessLevel int `json:"accessLevel"` // -1
VehicleMileage int `json:"vehicleMileage"` // null
CrmRightToRepair bool `json:"crmRightToRepair"` // true | false
AuthorizedVehicle bool `json:"authorizedVehicle"` // false | true
NeedMileagePrompt bool `json:"needMileagePrompt"` // false | true
RemoteServicePinExist bool `json:"remoteServicePinExist"` // true | false
NeedEmergencyContactPrompt bool `json:"needEmergencyContactPrompt"` // false | true
Show3GSunsetBanner bool `json:"show3gSunsetBanner"` // false | true
Provisioned bool `json:"provisioned"` // true | false
TimeZone string `json:"timeZone"` // America/New_York
SunsetUpgraded bool `json:"sunsetUpgraded"` // true | false
PreferredDealer string `json:"preferredDealer"` // null |
Customer Customer `json:"customer"` // Customer struct
OemCustID string `json:"oemCustId"` // CRM-631-HQN48K
UserOemCustID string `json:"userOemCustId"` // CRM-631-HQN48K
Active bool `json:"active"` // true | false
Email string `json:"email"` // null | email@address.com
FirstName string `json:"firstName,omitempty"` // null | First Name
LastName string `json:"lastName,omitempty"` // null | Last Name
Zip string `json:"zip"` // 12345
Phone string `json:"phone,omitempty"` // null | 123-456-7890
StolenVehicle bool `json:"stolenVehicle"` // true | false
VehicleName string `json:"vehicleName"` // Subaru Outback LXT
Features []string `json:"features"` // "11.6MMAN", "ABS_MIL", "ACCS", "AHBL_MIL", "ATF_MIL", "AWD_MIL", "BSD", "BSDRCT_MIL", "CEL_MIL", "EBD_MIL", "EOL_MIL", "EPAS_MIL", "EPB_MIL", "ESS_MIL", "EYESIGHT", "ISS_MIL", "NAV_TOMTOM", "OPL_MIL", "RAB_MIL", "RCC", "REARBRK", "RES", "RESCC", "RHSF", "RPOI", "RPOIA", "SRH_MIL", "SRS_MIL", "TEL_MIL", "TPMS_MIL", "VDC_MIL", "WASH_MIL", "g2"
Vin string `json:"vin"` // 4Y1SL65848Z411439
VehicleKey int64 `json:"vehicleKey"` // 3832950
Nickname string `json:"nickname"` // Subaru Outback LXT
ModelName string `json:"modelName"` // Outback
ModelYear string `json:"modelYear"` // 2020
ModelCode string `json:"modelCode"` // LDJ
ExtDescrip string `json:"extDescrip"` // Abyss Blue Pearl (ext color)
IntDescrip string `json:"intDescrip"` // Gray (int color)
TransCode string `json:"transCode"` // CVT
EngineSize float64 `json:"engineSize"` // 2.4
Phev bool `json:"phev"` // null
CachedStateCode string `json:"cachedStateCode"` // NJ
LicensePlate string `json:"licensePlate"` // NJ
LicensePlateState string `json:"licensePlateState"` // ABCDEF
SubscriptionStatus string `json:"subscriptionStatus"` // ACTIVE
SubscriptionFeatures []string `json:"subscriptionFeatures"` // "[ REMOTE ], [ SAFETY ], [ Retail | Finance3 | RetailPHEV ]""
SubscriptionPlans []string `json:"subscriptionPlans,omitempty"` // []
VehicleGeoPosition GeoPosition `json:"vehicleGeoPosition"` // GeoPosition struct
AccessLevel int `json:"accessLevel"` // -1
VehicleMileage int `json:"vehicleMileage,omitempty"` // null
CrmRightToRepair bool `json:"crmRightToRepair"` // true | false
AuthorizedVehicle bool `json:"authorizedVehicle"` // false | true
NeedMileagePrompt bool `json:"needMileagePrompt"` // false | true
RemoteServicePinExist bool `json:"remoteServicePinExist"` // true | false
NeedEmergencyContactPrompt bool `json:"needEmergencyContactPrompt"` // false | true
Show3GSunsetBanner bool `json:"show3gSunsetBanner"` // false | true
Provisioned bool `json:"provisioned"` // true | false
TimeZone string `json:"timeZone"` // America/New_York
SunsetUpgraded bool `json:"sunsetUpgraded"` // true | false
PreferredDealer string `json:"preferredDealer,omitempty"` // null |
VehicleBranded bool `json:"vehicleBranded"`
}
// GeoPosition .
type GeoPosition struct {
Latitude float64 `json:"latitude"` // 40.700184
Longitude float64 `json:"longitude"` // -74.401375
Speed float64 `json:"speed,omitempty"` // 62
Heading int `json:"heading,omitempty"` // 155
Timestamp string `json:"timestamp"` // "2021-12-22T13:14:47"
Latitude float64 `json:"latitude"` // 40.700184
Longitude float64 `json:"longitude"` // -74.401375
Speed int `json:"speed,omitempty"` // 62
Heading int `json:"heading,omitempty"` // 155
Timestamp CustomTime1 `json:"timestamp"` // "2021-12-22T13:14:47"
}
// type GeoPositionTime time.Time
@ -145,65 +254,58 @@ type GeoPosition struct {
// }
// VehicleStatus .
// "dataName":null
// parts = []{"door", "tire", "tyre", "window"}
// prefix = []{"pressure", "status"}
// suffix = []{"status", "position", "unit", "psi"}
// positions = []{"front", "rear", "sunroof", "boot", "enginehood"}
// subPositions []{"left", "right"}
type VehicleStatus struct {
VehicleId int64 `json:"vhsId"` // + 9969776690 5198812434
OdometerValue int `json:"odometerValue"` // + 23787
OdometerValueKm int `json:"odometerValueKilometers"` // + 38273
EventDate int64 `json:"eventDate"` // + 1701896993000
EventDateStr string `json:"eventDateStr"` // + 2023-12-06T21:09+0000
EventDateCarUser int64 `json:"eventDateCarUser"` // + 1701896993000
EventDateStrCarUser string `json:"eventDateStrCarUser"` // + 2023-12-06T21:09+0000
Latitude float64 `json:"latitude"` // + 40.700183
Longitude float64 `json:"longitude"` // + -74.401372
Heading int `json:"positionHeadingDegree,string"` // + "154"
DistanceToEmptyFuelMiles float64 `json:"distanceToEmptyFuelMiles"` // + 209.4
DistanceToEmptyFuelKilometers int `json:"distanceToEmptyFuelKilometers"` // + 337
DistanceToEmptyFuelMiles10s int `json:"distanceToEmptyFuelMiles10s"` // + 210
DistanceToEmptyFuelKilometers10s int `json:"distanceToEmptyFuelKilometers10s"` // + 340
AvgFuelConsumptionMpg float64 `json:"avgFuelConsumptionMpg"` // + 18.4
AvgFuelConsumptionLitersPer100Kilometers float64 `json:"avgFuelConsumptionLitersPer100Kilometers"` // + 12.8
RemainingFuelPercent int `json:"remainingFuelPercent"` // + 82
TirePressureFrontLeft int `json:"tirePressureFrontLeft,string,omitempty"` // + "2275"
TirePressureFrontRight int `json:"tirePressureFrontRight,string,omitempty"` // + "2344"
TirePressureRearLeft int `json:"tirePressureRearLeft,string,omitempty"` // + "2413"
TirePressureRearRight int `json:"tirePressureRearRight,string,omitempty"` // + "2344"
TirePressureFrontLeftPsi float64 `json:"tirePressureFrontLeftPsi,string,omitempty"` // + "33"
TirePressureFrontRightPsi float64 `json:"tirePressureFrontRightPsi,string,omitempty"` // + "34"
TirePressureRearLeftPsi float64 `json:"tirePressureRearLeftPsi,string,omitempty"` // + "35"
TirePressureRearRightPsi float64 `json:"tirePressureRearRightPsi,string,omitempty"` // + "34"
TyreStatusFrontLeft string `json:"tyreStatusFrontLeft"` // + "UNKNOWN"
TyreStatusFrontRight string `json:"tyreStatusFrontRight"` // + "UNKNOWN"
TyreStatusRearLeft string `json:"tyreStatusRearLeft"` // + "UNKNOWN"
TyreStatusRearRight string `json:"tyreStatusRearRight"` // + "UNKNOWN"
EvStateOfChargePercent float64 `json:"evStateOfChargePercent,omitempty"` // + null
EvDistanceToEmptyMiles int `json:"evDistanceToEmptyMiles,omitempty"` // + null
EvDistanceToEmptyKilometers int `json:"evDistanceToEmptyKilometers,omitempty"` // + null
EvDistanceToEmptyByStateMiles int `json:"evDistanceToEmptyByStateMiles,omitempty"` // + null
EvDistanceToEmptyByStateKilometers int `json:"evDistanceToEmptyByStateKilometers,omitempty"` // + null
VehicleStateType string `json:"vehicleStateType"` // + "IGNITION_OFF | IGNITION_ON"
WindowFrontLeftStatus string `json:"windowFrontLeftStatus"` // CLOSE | VENTED | OPEN
WindowFrontRightStatus string `json:"windowFrontRightStatus"` // CLOSE | VENTED | OPEN
WindowRearLeftStatus string `json:"windowRearLeftStatus"` // CLOSE | VENTED | OPEN
WindowRearRightStatus string `json:"windowRearRightStatus"` // CLOSE | VENTED | OPEN
WindowSunroofStatus string `json:"windowSunroofStatus"` // CLOSE | SLIDE_PARTLY_OPEN | OPEN | TILT
DoorBootPosition string `json:"doorBootPosition"` // CLOSED | OPEN
DoorEngineHoodPosition string `json:"doorEngineHoodPosition"` // CLOSED | OPEN
DoorFrontLeftPosition string `json:"doorFrontLeftPosition"` // CLOSED | OPEN
DoorFrontRightPosition string `json:"doorFrontRightPosition"` // CLOSED | OPEN
DoorRearLeftPosition string `json:"doorRearLeftPosition"` // CLOSED | OPEN
DoorRearRightPosition string `json:"doorRearRightPosition"` // CLOSED | OPEN
DoorBootLockStatus string `json:"doorBootLockStatus"` // LOCKED | UNLOCKED
DoorFrontLeftLockStatus string `json:"doorFrontLeftLockStatus"` // LOCKED | UNLOCKED
DoorFrontRightLockStatus string `json:"doorFrontRightLockStatus"` // LOCKED | UNLOCKED
DoorRearLeftLockStatus string `json:"doorRearLeftLockStatus"` // LOCKED | UNLOCKED
DoorRearRightLockStatus string `json:"doorRearRightLockStatus"` // LOCKED | UNLOCKED
VehicleId int64 `json:"vhsId"` // + 9969776690 5198812434
OdometerValue int `json:"odometerValue"` // + 23787
OdometerValueKm int `json:"odometerValueKilometers"` // + 38273
EventDate UnixTime `json:"eventDate"` // + 1701896993000
EventDateStr string `json:"eventDateStr"` // + 2023-12-06T21:09+0000
EventDateCarUser UnixTime `json:"eventDateCarUser"` // + 1701896993000
EventDateStrCarUser string `json:"eventDateStrCarUser"` // + 2023-12-06T21:09+0000
Latitude float64 `json:"latitude"` // + 40.700183
Longitude float64 `json:"longitude"` // + -74.401372
Heading int `json:"positionHeadingDegree,string"` // + "154"
DistanceToEmptyFuelMiles float64 `json:"distanceToEmptyFuelMiles"` // + 209.4
DistanceToEmptyFuelKilometers int `json:"distanceToEmptyFuelKilometers"` // + 337
DistanceToEmptyFuelMiles10s int `json:"distanceToEmptyFuelMiles10s"` // + 210
DistanceToEmptyFuelKilometers10s int `json:"distanceToEmptyFuelKilometers10s"` // + 340
AvgFuelConsumptionMpg float64 `json:"avgFuelConsumptionMpg"` // + 18.4
AvgFuelConsumptionLitersPer100Kilometers float64 `json:"avgFuelConsumptionLitersPer100Kilometers"` // + 12.8
RemainingFuelPercent int `json:"remainingFuelPercent"` // + 82
TirePressureFrontLeft int `json:"tirePressureFrontLeft,string,omitempty"` // + "2275"
TirePressureFrontRight int `json:"tirePressureFrontRight,string,omitempty"` // + "2344"
TirePressureRearLeft int `json:"tirePressureRearLeft,string,omitempty"` // + "2413"
TirePressureRearRight int `json:"tirePressureRearRight,string,omitempty"` // + "2344"
TirePressureFrontLeftPsi float64 `json:"tirePressureFrontLeftPsi,string,omitempty"` // + "33"
TirePressureFrontRightPsi float64 `json:"tirePressureFrontRightPsi,string,omitempty"` // + "34"
TirePressureRearLeftPsi float64 `json:"tirePressureRearLeftPsi,string,omitempty"` // + "35"
TirePressureRearRightPsi float64 `json:"tirePressureRearRightPsi,string,omitempty"` // + "34"
TyreStatusFrontLeft string `json:"tyreStatusFrontLeft"` // + "UNKNOWN"
TyreStatusFrontRight string `json:"tyreStatusFrontRight"` // + "UNKNOWN"
TyreStatusRearLeft string `json:"tyreStatusRearLeft"` // + "UNKNOWN"
TyreStatusRearRight string `json:"tyreStatusRearRight"` // + "UNKNOWN"
EvStateOfChargePercent float64 `json:"evStateOfChargePercent,omitempty"` // + null
EvDistanceToEmptyMiles int `json:"evDistanceToEmptyMiles,omitempty"` // + null
EvDistanceToEmptyKilometers int `json:"evDistanceToEmptyKilometers,omitempty"` // + null
EvDistanceToEmptyByStateMiles int `json:"evDistanceToEmptyByStateMiles,omitempty"` // + null
EvDistanceToEmptyByStateKilometers int `json:"evDistanceToEmptyByStateKilometers,omitempty"` // + null
VehicleStateType string `json:"vehicleStateType"` // + "IGNITION_OFF | IGNITION_ON"
WindowFrontLeftStatus string `json:"windowFrontLeftStatus"` // CLOSE | VENTED | OPEN
WindowFrontRightStatus string `json:"windowFrontRightStatus"` // CLOSE | VENTED | OPEN
WindowRearLeftStatus string `json:"windowRearLeftStatus"` // CLOSE | VENTED | OPEN
WindowRearRightStatus string `json:"windowRearRightStatus"` // CLOSE | VENTED | OPEN
WindowSunroofStatus string `json:"windowSunroofStatus"` // CLOSE | SLIDE_PARTLY_OPEN | OPEN | TILT
DoorBootPosition string `json:"doorBootPosition"` // CLOSED | OPEN
DoorEngineHoodPosition string `json:"doorEngineHoodPosition"` // CLOSED | OPEN
DoorFrontLeftPosition string `json:"doorFrontLeftPosition"` // CLOSED | OPEN
DoorFrontRightPosition string `json:"doorFrontRightPosition"` // CLOSED | OPEN
DoorRearLeftPosition string `json:"doorRearLeftPosition"` // CLOSED | OPEN
DoorRearRightPosition string `json:"doorRearRightPosition"` // CLOSED | OPEN
DoorBootLockStatus string `json:"doorBootLockStatus"` // LOCKED | UNLOCKED
DoorFrontLeftLockStatus string `json:"doorFrontLeftLockStatus"` // LOCKED | UNLOCKED
DoorFrontRightLockStatus string `json:"doorFrontRightLockStatus"` // LOCKED | UNLOCKED
DoorRearLeftLockStatus string `json:"doorRearLeftLockStatus"` // LOCKED | UNLOCKED
DoorRearRightLockStatus string `json:"doorRearRightLockStatus"` // LOCKED | UNLOCKED
}
// VehicleCondition .
@ -247,25 +349,41 @@ type VehicleCondition struct {
LastUpdatedTime string `json:"lastUpdatedTime"` // "2023-04-10T17:50:54+0000",
}
// ClimateSettings .
// "dataName":null
// type ClimateSettings struct {
// RunTimeMinutes string `json:"runTimeMinutes"`
// StartConfiguration string `json:"startConfiguration"`
// AirConditionOn string `json:"airConditionOn"`
// OuterAirCirculation string `json:"outerAirCirculation"`
// ClimateZoneFrontAirMode string `json:"climateZoneFrontAirMode"`
// ClimateZoneFrontTemp string `json:"climateZoneFrontTemp"`
// ClimateZoneFrontAirVolume string `json:"climateZoneFrontAirVolume"`
// HeatedSeatFrontLeft string `json:"heatedSeatFrontLeft"`
// HeatedSeatFrontRight string `json:"heatedSeatFrontRight"`
// HeatedRearWindowActive string `json:"heatedRearWindowActive"`
// }
// ClimateProfile represents a climate control profile for a Subaru vehicle.
type ClimateProfile struct {
Name string `json:"name"`
VehicleType string `json:"vehicleType,omitempty"` // vehicleType [ gas | phev ]
PresetType string `json:"presetType"` // presetType [ subaruPreset | userPreset ]
StartConfiguration string `json:"startConfiguration"` // startConfiguration [ START_ENGINE_ALLOW_KEY_IN_IGNITION (gas) | START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION (phev) ]
RunTimeMinutes int `json:"runTimeMinutes,string"` // runTimeMinutes [ 0 | 1 | 5 | 10 ]
HeatedRearWindowActive string `json:"heatedRearWindowActive"` // heatedRearWindowActive: [ false | true ]
HeatedSeatFrontRight string `json:"heatedSeatFrontRight"` // heatedSeatFrontRight: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ]
HeatedSeatFrontLeft string `json:"heatedSeatFrontLeft"` // heatedSeatFrontLeft: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ]
ClimateZoneFrontTemp int `json:"climateZoneFrontTemp,string"` // climateZoneFrontTemp: [ for _ in range(60, 85 + 1)] // climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1) ]
ClimateZoneFrontAirMode string `json:"climateZoneFrontAirMode"` // climateZoneFrontAirMode: [ WINDOW | FEET_WINDOW | FACE | FEET | FEET_FACE_BALANCED | AUTO ]
ClimateZoneFrontAirVolume string `json:"climateZoneFrontAirVolume"` // climateZoneFrontAirVolume: [ AUTO | 2 | 4 | 7 ]
OuterAirCirculation string `json:"outerAirCirculation"` // airConditionOn: [ auto | outsideAir | true ]
AirConditionOn string `json:"airConditionOn"` // airConditionOn: [ false | true ]
CanEdit string `json:"canEdit"` // canEdit [ false | true ]
Disabled string `json:"disabled"` // disabled [ false | true ]
}
type ClimateProfiles map[string]ClimateProfile
// GeoLocation represents the geographical location of a Subaru vehicle.
type GeoLocation struct {
Latitude float64 `json:"latitude"` // 40.700184
Longitude float64 `json:"longitude"` // -74.401375
Heading int `json:"heading,omitempty"` // 189
Speed int `json:"speed,omitempty"` // 0.00
Updated CustomTime1 `json:"timestamp"` // "2025-07-08T19:05:07"
}
// ServiceRequest .
// "dataName": "remoteServiceStatus"
type ServiceRequest struct {
ServiceRequestID string `json:"serviceRequestId,omitempty"` // 4S4BTGND8L3137058_1640294426029_19_@NGTP
Vin string `json:"vin"` // 4S4BTGND8L3137058
Success bool `json:"success"` // false | true // Could be in the false state while the executed request in the progress
Cancelled bool `json:"cancelled"` // false | true
RemoteServiceType string `json:"remoteServiceType"` // vehicleStatus | condition | locate | unlock | lock | lightsOnly | engineStart | engineStop | phevChargeNow
@ -273,19 +391,29 @@ type ServiceRequest struct {
SubState string `json:"subState,omitempty"` // null
ErrorCode string `json:"errorCode,omitempty"` // null:null
Result json.RawMessage `json:"result,omitempty"` // struct
UpdateTime int64 `json:"updateTime,omitempty"` // timestamp
Vin string `json:"vin"` // 4S4BTGND8L3137058
UpdateTime UnixTime `json:"updateTime,omitempty"` // timestamp // is empty if the request is started
}
// 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
// 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 ]
@ -306,10 +434,79 @@ 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 .
// "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
}
// UnixTime is a wrapper around time.Time that allows us to marshal and unmarshal Unix timestamps
type UnixTime struct {
time.Time
}
// UnmarshalJSON is the method that satisfies the Unmarshaller interface
// Note that it uses a pointer receiver. It needs this because it will be modifying the embedded time.Time instance
func (u *UnixTime) UnmarshalJSON(b []byte) error {
var timestamp int64
err := json.Unmarshal(b, &timestamp)
if err != nil {
return err
}
u.Time = time.Unix(timestamp, 0)
return nil
}
// MarshalJSON turns our time.Time back into an int
func (u UnixTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%d", (u.Time.Unix()))), nil
}
// CustomTime1 "2021-12-22T13:14:47" is a custom type for unmarshalling time strings
type CustomTime1 struct {
time.Time
}
// UnmarshalJSON implements the json.Unmarshaler interface
func (ct *CustomTime1) UnmarshalJSON(b []byte) (err error) {
// Use the correct layout string for the desired format
const layout = "2006-01-02T15:04:05"
s := strings.Trim(string(b), "\\\"") // Remove surrounding escapes and quotes (parsing time \"\\\"2025-07-09T00:23:19\\\"\" as \"2006-01-02T15:04:05\")
if string(b) == "null" {
ct.Time = time.Time{}
return nil
}
ct.Time, err = time.Parse(layout, s) // Parse the string with your custom layout
return
}
// CustomTime2 "2023-04-10T17:50:54+0000" is a custom type for unmarshalling time strings
type CustomTime2 struct {
time.Time
}
// UnmarshalJSON implements the json.Unmarshaler interface
func (ct *CustomTime2) UnmarshalJSON(b []byte) (err error) {
// Use the correct layout string for the desired format
const layout = "2006-01-02T15:04:05-0700"
s := strings.Trim(string(b), "\\\"") // Remove surrounding escapes and quotes ((parsing time \"\\\"2025-07-09T00:23:19\\\"\" as \"2006-01-02T15:04:05\"))
if string(b) == "null" {
ct.Time = time.Time{}
return nil
}
ct.Time, err = time.Parse(layout, s) // Parse the string with your custom layout
return
}

204
mysubaru_test.go Normal file
View File

@ -0,0 +1,204 @@
package mysubaru
import (
"encoding/json"
"testing"
"time"
)
func TestUnixTime_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
input string
wantTime time.Time
wantError bool
}{
{
name: "valid unix timestamp",
input: "1700000000",
wantTime: time.Unix(1700000000, 0),
},
{
name: "invalid string",
input: "\"notanumber\"",
wantError: true,
},
{
name: "empty input",
input: "",
wantError: true,
},
{
name: "float value",
input: "1700000000.123",
wantError: true,
},
{
name: "zero timestamp",
input: "0",
wantTime: time.Unix(0, 0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var ut UnixTime
err := ut.UnmarshalJSON([]byte(tt.input))
if (err != nil) != tt.wantError {
t.Errorf("UnmarshalJSON() error = %v, wantError %v", err, tt.wantError)
return
}
if !tt.wantError && !ut.Time.Equal(tt.wantTime) {
t.Errorf("UnmarshalJSON() got = %v, want %v", ut.Time, tt.wantTime)
}
})
}
}
func TestUnixTime_UnmarshalJSON_withJSONUnmarshal(t *testing.T) {
type testStruct struct {
Time UnixTime `json:"time"`
}
input := `{"time":1700000000}`
var ts testStruct
err := json.Unmarshal([]byte(input), &ts)
if err != nil {
t.Fatalf("json.Unmarshal failed: %v", err)
}
want := time.Unix(1700000000, 0)
if !ts.Time.Time.Equal(want) {
t.Errorf("UnmarshalJSON() got = %v, want %v", ts.Time.Time, want)
}
}
func TestUnixTime_MarshalJSON(t *testing.T) {
tests := []struct {
name string
input time.Time
want string
}{
{
name: "epoch",
input: time.Unix(0, 0),
want: "0",
},
{
name: "positive unix time",
input: time.Unix(1700000000, 0),
want: "1700000000",
},
{
name: "negative unix time",
input: time.Unix(-100, 0),
want: "-100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ut := UnixTime{Time: tt.input}
got, err := ut.MarshalJSON()
if err != nil {
t.Fatalf("MarshalJSON() error = %v", err)
}
if string(got) != tt.want {
t.Errorf("MarshalJSON() = %s, want %s", string(got), tt.want)
}
})
}
}
func TestUnixTime_MarshalJSON_withJSONMarshal(t *testing.T) {
type testStruct struct {
Time UnixTime `json:"time"`
}
ts := testStruct{Time: UnixTime{Time: time.Unix(1700000000, 0)}}
b, err := json.Marshal(ts)
if err != nil {
t.Fatalf("json.Marshal failed: %v", err)
}
want := `{"time":1700000000}`
if string(b) != want {
t.Errorf("json.Marshal() = %s, want %s", string(b), want)
}
}
// func TestResponse_parse(t *testing.T) {
// tests := []struct {
// name string
// input string
// wantErr error
// wantCode string
// wantLog string
// }{
// {
// name: "success response",
// input: `{"success":true,"dataName":"foo","data":{}}`,
// wantErr: nil,
// },
// {
// name: "invalid json",
// input: `{"success":tru`,
// wantErr: errors.New("error while parsing json:"),
// wantLog: "error while parsing json",
// },
// {
// name: "API_ERROR_NO_ACCOUNT",
// input: `{"success":false,"errorCode":"noAccount","dataName":"errorResponse","data":{}}`,
// wantErr: errors.New("error in response: Account not found"),
// wantCode: "noAccount",
// wantLog: "error in response",
// },
// {
// name: "API_ERROR_INVALID_CREDENTIALS",
// input: `{"success":false,"errorCode":"invalidCredentials","dataName":"errorResponse","data":{}}`,
// wantErr: errors.New("error in response: Invalid Credentials"),
// wantCode: "invalidCredentials",
// wantLog: "error in response",
// },
// {
// name: "API_ERROR_SOA_403",
// input: `{"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{}}`,
// wantErr: errors.New("error in response: Unable to parse response body, SOA 403 error"),
// wantCode: "404-soa-unableToParseResponseBody",
// wantLog: "error in response",
// },
// {
// name: "unknown error code",
// input: `{"success":false,"errorCode":"somethingElse","dataName":"errorResponse","data":{}}`,
// wantErr: errors.New("error in response: somethingElse"),
// wantCode: "somethingElse",
// wantLog: "error in response",
// },
// {
// name: "no errorCode but not success",
// input: `{"success":false,"dataName":"errorResponse","data":{}}`,
// wantErr: nil,
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// var resp Response
// logger := slog.New(slog.NewTextHandler(nil, nil))
// got, err := resp.parse([]byte(tt.input), logger)
// if tt.wantErr != nil {
// if err == nil {
// t.Fatalf("expected error, got nil")
// }
// if !contains(err.Error(), tt.wantErr.Error()) {
// t.Errorf("parse() error = %v, want %v", err, tt.wantErr)
// }
// } else if err != nil {
// t.Errorf("parse() unexpected error: %v", err)
// }
// if tt.wantCode != "" && got != nil && got.ErrorCode != tt.wantCode {
// t.Errorf("parse() got.ErrorCode = %v, want %v", got.ErrorCode, tt.wantCode)
// }
// })
// }
// }
// // contains is a helper for substring matching.
// func contains(s, substr string) bool {
// return bytes.Contains([]byte(s), []byte(substr))
// }

View File

@ -1,7 +1,10 @@
package mysubaru
import (
"fmt"
"math"
"net/mail"
"reflect"
"regexp"
"strconv"
"strings"
@ -58,7 +61,7 @@ func vinCheck(vin string) (bool, string) {
return valid, retVin
}
// transcodeDigits .
// transcodeDigits transcodes VIN digits to a numeric value
func transcodeDigits(vin string) int {
var digitSum = 0
var code int
@ -114,30 +117,67 @@ func transcodeDigits(vin string) int {
return digitSum
}
// isNilFixed .
// func isNil(i interface{}) bool {
// if i == nil {
// return true
// }
// switch reflect.TypeOf(i).Kind() {
// case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
// return reflect.ValueOf(i).IsNil()
// }
// return false
// }
// emailMasking takes an email address as input and returns a version of the email
// with the username part partially hidden for privacy. Only the first and last
// characters of the username are visible, with the middle characters replaced by asterisks.
// The function validates the email format before processing.
// Returns the obfuscated email or an error if the input is not a valid email address.
func emailMasking(email string) (string, error) {
_, err := mail.ParseAddress(email)
if err != nil {
return "", fmt.Errorf("invalid email address: %s", email)
}
re1 := regexp.MustCompile(`^(.*?)@(.*)$`)
matches := re1.FindStringSubmatch(email)
var username, domain string
if len(matches) == 3 { // Expecting the full match, username, and domain
username = matches[1]
domain = matches[2]
} else {
return "", fmt.Errorf("invalid email format: %s", email)
}
re2 := regexp.MustCompile(`(.)(.*)(.)`)
replacedString := re2.ReplaceAllStringFunc(username, func(s string) string {
firstChar := string(s[0])
lastChar := string(s[len(s)-1])
middleCharsCount := len(s) - 2
if middleCharsCount < 0 { // Should not happen with the length check above, but for robustness
return s
}
return firstChar + strings.Repeat("*", middleCharsCount) + lastChar
})
return replacedString + "@" + domain, nil
}
// containsValueInStruct checks if any string field in the given struct 's' contains the specified 'search' substring (case-insensitive).
// It returns true if at least one string field contains the substring, and false otherwise.
// If 's' is not a struct, it returns false.
func containsValueInStruct(s any, search string) bool {
val := reflect.ValueOf(s)
if val.Kind() != reflect.Struct {
return false // Not a struct
}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
if field.Kind() == reflect.String {
if strings.Contains(strings.ToLower(field.String()), strings.ToLower(search)) {
return true
}
}
}
return false
}
// timeTrack .
// func timeTrack(name string) {
// start := time.Now()
// fmt.Printf("%s took %v\n", name, time.Since(start))
// }
// contains .
// func contains(s []string, str string) bool {
// for _, v := range s {
// if v == str {
// return true
// }
// }
// return false
// }

216
utils_test.go Normal file
View File

@ -0,0 +1,216 @@
package mysubaru
import (
"regexp"
"strconv"
"testing"
"time"
)
// timestamp returns the current time in milliseconds since epoch as a string.
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)
}
}
// timestamp returns the current time in milliseconds since epoch as a string.
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)
}
}
// urlToGen replaces "api_gen" in the URL with the specified generation.
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)
}
}
}
// vinCheck validates the VIN check digit and returns the corrected VIN.
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)
}
}
// TestVinCheck_InvalidCheckDigit tests a VIN with an incorrect check digit.
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)
}
}
// TestVinCheck_WrongLength tests a VIN that is not 17 characters long.
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)
}
}
// transcodeDigits computes the sum of the VIN digits according to the VIN rules.
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)
}
}
// TestVinCheck_XCheckDigit tests a VIN with 'X' as the check digit.
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)
}
}
// TestUrlToGen_NoApiGen tests the case where the URL does not contain "api_gen".
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 TestEmailHidder(t *testing.T) {
tests := []struct {
email string
expected string
wantErr bool
}{
{"alex@example.com", "a**x@example.com", false},
{"a@example.com", "a@example.com", false},
{"ab@example.com", "ab@example.com", false},
{"", "", true},
{"notanemail", "", true},
}
for _, tt := range tests {
got, err := emailMasking(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("emailHidder(%q) error = %v, wantErr %v", tt.email, err, tt.wantErr)
continue
}
if got != tt.expected {
t.Errorf("emailHidder(%q) = %q, want %q", tt.email, got, tt.expected)
}
}
}
func TestContainsValueInStruct(t *testing.T) {
type TestStruct struct {
Name string
Address string
Age int
Note string
}
tests := []struct {
s any
search string
want bool
}{
{
s: TestStruct{Name: "Alice", Address: "123 Main St", Age: 30, Note: "VIP customer"},
search: "alice",
want: true,
},
{
s: TestStruct{Name: "Bob", Address: "456 Elm St", Age: 25, Note: "Regular"},
search: "elm",
want: true,
},
{
s: TestStruct{Name: "Charlie", Address: "789 Oak St", Age: 40, Note: "VIP"},
search: "vip",
want: true,
},
{
s: TestStruct{Name: "Diana", Address: "101 Pine St", Age: 22, Note: ""},
search: "xyz",
want: false,
},
{
s: TestStruct{Name: "", Address: "", Age: 0, Note: ""},
search: "",
want: true, // empty string is contained in all strings
},
{
s: struct{ Foo int }{Foo: 42},
search: "42",
want: false,
},
{
s: "not a struct",
search: "struct",
want: false,
},
{
s: struct{ S string }{S: "CaseInsensitive"},
search: "caseinsensitive",
want: true,
},
}
for i, tt := range tests {
got := containsValueInStruct(tt.s, tt.search)
if got != tt.want {
t.Errorf("Test %d: containsStringInStruct(%#v, %q) = %v, want %v", i, tt.s, tt.search, got, tt.want)
}
}
}

1114
vehicle.go

File diff suppressed because it is too large Load Diff

307
vehicle_test.go Normal file
View File

@ -0,0 +1,307 @@
package mysubaru
import (
"fmt"
"net/http"
"testing"
)
// TestGetClimateQuickPresets_Success tests the retrieval of quick climate presets.
func TestGetClimatePresets_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
// Handle API_LOGIN endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_LOGIN"] && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":"BIOMETRICS_DISABLED","dataName":"sessionData","data":{"sessionChanged":false,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751738613000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"9D7FCDF274794346689D3FA0D693CBBF","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":null,"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":null,"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":null,"authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$rOb/uqhm8I3QtSel2phOCOxNM51w43eqXDDksMkJ.1a5KsaQuLvEu$1751745334477","satelliteViewEnabled":true,"registeredDevicePermanent":true}}`)
}
// Handle API_VALIDATE_SESSION endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VALIDATE_SESSION"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":null}`)
}
// Handle SELECT_VEHICLE endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_SELECT_VEHICLE"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":"vehicle","data":{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":"2023","modelCode":"PDL","engineSize":2.4,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"8KV8","licensePlateState":"NJ","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":"Outback","subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":"Cosmic Blue Pearl","sunsetUpgraded":true,"intDescrip":"Black","transCode":"CVT","provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}}`)
}
// Handle API_G2_FETCH_RES_SUBARU_PRESETS endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":["{\"name\": \"Auto\", \"runTimeMinutes\": \"10\", \"climateZoneFrontTemp\": \"74\", \"climateZoneFrontAirMode\": \"AUTO\", \"climateZoneFrontAirVolume\": \"AUTO\", \"outerAirCirculation\": \"auto\", \"heatedRearWindowActive\": \"false\", \"airConditionOn\": \"false\", \"heatedSeatFrontLeft\": \"off\", \"heatedSeatFrontRight\": \"off\", \"startConfiguration\": \"START_ENGINE_ALLOW_KEY_IN_IGNITION\", \"canEdit\": \"true\", \"disabled\": \"false\", \"vehicleType\": \"gas\", \"presetType\": \"subaruPreset\" }","{\"name\":\"Full Cool\",\"runTimeMinutes\":\"10\",\"climateZoneFrontTemp\":\"60\",\"climateZoneFrontAirMode\":\"feet_face_balanced\",\"climateZoneFrontAirVolume\":\"7\",\"airConditionOn\":\"true\",\"heatedSeatFrontLeft\":\"high_cool\",\"heatedSeatFrontRight\":\"high_cool\",\"heatedRearWindowActive\":\"false\",\"outerAirCirculation\":\"outsideAir\",\"startConfiguration\":\"START_ENGINE_ALLOW_KEY_IN_IGNITION\",\"canEdit\":\"true\",\"disabled\":\"true\",\"vehicleType\":\"gas\",\"presetType\":\"subaruPreset\"}","{\"name\": \"Full Heat\", \"runTimeMinutes\": \"10\", \"climateZoneFrontTemp\": \"85\", \"climateZoneFrontAirMode\": \"feet_window\", \"climateZoneFrontAirVolume\": \"7\", \"airConditionOn\": \"false\", \"heatedSeatFrontLeft\": \"high_heat\", \"heatedSeatFrontRight\": \"high_heat\", \"heatedRearWindowActive\": \"true\", \"outerAirCirculation\": \"outsideAir\", \"startConfiguration\": \"START_ENGINE_ALLOW_KEY_IN_IGNITION\", \"canEdit\": \"true\", \"disabled\": \"true\", \"vehicleType\": \"gas\", \"presetType\": \"subaruPreset\" }","{\"name\": \"Full Cool\", \"runTimeMinutes\": \"10\", \"climateZoneFrontTemp\": \"60\", \"climateZoneFrontAirMode\": \"feet_face_balanced\", \"climateZoneFrontAirVolume\": \"7\", \"airConditionOn\": \"true\", \"heatedSeatFrontLeft\": \"OFF\", \"heatedSeatFrontRight\": \"OFF\", \"heatedRearWindowActive\": \"false\", \"outerAirCirculation\": \"outsideAir\", \"startConfiguration\": \"START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION\", \"canEdit\": \"true\", \"disabled\": \"true\", \"vehicleType\": \"phev\", \"presetType\": \"subaruPreset\" }","{\"name\": \"Full Heat\", \"runTimeMinutes\": \"10\", \"climateZoneFrontTemp\": \"85\", \"climateZoneFrontAirMode\": \"feet_window\", \"climateZoneFrontAirVolume\": \"7\", \"airConditionOn\": \"false\", \"heatedSeatFrontLeft\": \"high_heat\", \"heatedSeatFrontRight\": \"high_heat\", \"heatedRearWindowActive\": \"true\", \"outerAirCirculation\": \"outsideAir\", \"startConfiguration\": \"START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION\", \"canEdit\": \"true\", \"disabled\": \"true\", \"vehicleType\": \"phev\", \"presetType\": \"subaruPreset\" }"]}`)
}
}
ts := mockMySubaruApi(t, handler)
ts.Start()
defer ts.Close()
cfg := mockConfig(t)
msc, err := New(cfg)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if msc == nil {
t.Fatalf("expected MySubaru API client, got nil")
}
if !msc.isAuthenticated || !msc.isRegistered {
t.Errorf("expected authenticated and registered true, got %v %v", msc.isAuthenticated, msc.isRegistered)
}
if msc.currentVin != "1HGCM82633A004352" {
t.Errorf("expected currentVin 1HGCM82633A004352, got %v", msc.currentVin)
}
}
// TestGetClimateQuickPresets_Success tests the retrieval of quick climate presets.
func TestGetClimateQuickPresets_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
// Handle API_LOGIN endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_LOGIN"] && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":"BIOMETRICS_DISABLED","dataName":"sessionData","data":{"sessionChanged":false,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751738613000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"9D7FCDF274794346689D3FA0D693CBBF","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":null,"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":null,"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":null,"authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$rOb/uqhm8I3QtSel2phOCOxNM51w43eqXDDksMkJ.1a5KsaQuLvEu$1751745334477","satelliteViewEnabled":true,"registeredDevicePermanent":true}}`)
}
// Handle API_VALIDATE_SESSION endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VALIDATE_SESSION"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":null}`)
}
// Handle SELECT_VEHICLE endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_SELECT_VEHICLE"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":"vehicle","data":{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":"2023","modelCode":"PDL","engineSize":2.4,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"8KV8","licensePlateState":"NJ","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":"Outback","subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":"Cosmic Blue Pearl","sunsetUpgraded":true,"intDescrip":"Black","transCode":"CVT","provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}}`)
}
// Handle API_G2_FETCH_RES_QUICK_START_SETTINGS endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":"{\"name\":\"Cooling\",\"runTimeMinutes\":\"10\",\"climateZoneFrontTemp\":\"65\",\"climateZoneFrontAirMode\":\"FEET_FACE_BALANCED\",\"climateZoneFrontAirVolume\":\"7\",\"outerAirCirculation\":\"outsideAir\",\"heatedRearWindowActive\":\"false\",\"heatedSeatFrontLeft\":\"HIGH_COOL\",\"airConditionOn\":\"false\",\"canEdit\":\"true\",\"disabled\":\"false\",\"presetType\":\"userPreset\",\"startConfiguration\":\"START_ENGINE_ALLOW_KEY_IN_IGNITION\"}"}`)
}
}
ts := mockMySubaruApi(t, handler)
ts.Start()
defer ts.Close()
cfg := mockConfig(t)
msc, err := New(cfg)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if msc == nil {
t.Fatalf("expected MySubaru API client, got nil")
}
if !msc.isAuthenticated || !msc.isRegistered {
t.Errorf("expected authenticated and registered true, got %v %v", msc.isAuthenticated, msc.isRegistered)
}
if msc.currentVin != "1HGCM82633A004352" {
t.Errorf("expected currentVin 1HGCM82633A004352, got %v", msc.currentVin)
}
}
// TestGetClimateUserPresets_Success tests the retrieval of user-defined climate presets.
func TestGetClimateUserPresets_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
// Handle API_LOGIN endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_LOGIN"] && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":"BIOMETRICS_DISABLED","dataName":"sessionData","data":{"sessionChanged":false,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751738613000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"9D7FCDF274794346689D3FA0D693CBBF","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":null,"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":null,"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":null,"authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$rOb/uqhm8I3QtSel2phOCOxNM51w43eqXDDksMkJ.1a5KsaQuLvEu$1751745334477","satelliteViewEnabled":true,"registeredDevicePermanent":true}}`)
}
// Handle API_VALIDATE_SESSION endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VALIDATE_SESSION"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":null}`)
}
// Handle SELECT_VEHICLE endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_SELECT_VEHICLE"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":"vehicle","data":{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":"2023","modelCode":"PDL","engineSize":2.4,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"8KV8","licensePlateState":"NJ","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":"Outback","subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":"Cosmic Blue Pearl","sunsetUpgraded":true,"intDescrip":"Black","transCode":"CVT","provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}}`)
}
// Handle API_G2_FETCH_RES_USER_PRESETS endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_G2_FETCH_RES_USER_PRESETS"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":"[{\"name\":\"Cooling\",\"runTimeMinutes\":\"10\",\"climateZoneFrontTemp\":\"65\",\"climateZoneFrontAirMode\":\"FEET_FACE_BALANCED\",\"climateZoneFrontAirVolume\":\"7\",\"outerAirCirculation\":\"outsideAir\",\"heatedRearWindowActive\":\"false\",\"heatedSeatFrontLeft\":\"HIGH_COOL\",\"heatedSeatFrontRight\":\"HIGH_COOL\",\"airConditionOn\":\"false\",\"canEdit\":\"true\",\"disabled\":\"false\",\"presetType\":\"userPreset\",\"startConfiguration\":\"START_ENGINE_ALLOW_KEY_IN_IGNITION\"}]"}`)
}
}
ts := mockMySubaruApi(t, handler)
ts.Start()
defer ts.Close()
cfg := mockConfig(t)
msc, err := New(cfg)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if msc == nil {
t.Fatalf("expected MySubaru API client, got nil")
}
if !msc.isAuthenticated || !msc.isRegistered {
t.Errorf("expected authenticated and registered true, got %v %v", msc.isAuthenticated, msc.isRegistered)
}
if msc.currentVin != "1HGCM82633A004352" {
t.Errorf("expected currentVin 1HGCM82633A004352, got %v", msc.currentVin)
}
}
// TestGetVehicleCondition_Success tests the GetVehicleCondition method
func TestGetVehicleStatus_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
// Handle API_LOGIN endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_LOGIN"] && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":"BIOMETRICS_DISABLED","dataName":"sessionData","data":{"sessionChanged":false,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751738613000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"9D7FCDF274794346689D3FA0D693CBBF","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":null,"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":null,"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":null,"authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$rOb/uqhm8I3QtSel2phOCOxNM51w43eqXDDksMkJ.1a5KsaQuLvEu$1751745334477","satelliteViewEnabled":true,"registeredDevicePermanent":true}}`)
}
// Handle API_VALIDATE_SESSION endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VALIDATE_SESSION"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":null}`)
}
// Handle SELECT_VEHICLE endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_SELECT_VEHICLE"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":"vehicle","data":{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":"2023","modelCode":"PDL","engineSize":2.4,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"8KV8","licensePlateState":"NJ","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":"Outback","subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":"Cosmic Blue Pearl","sunsetUpgraded":true,"intDescrip":"Black","transCode":"CVT","provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}}`)
}
// Handle API_VEHICLE_STATUS endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VEHICLE_STATUS"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":{"vhsId":14662115789,"odometerValue":31694,"odometerValueKilometers":50996,"eventDate":1751742945000,"eventDateStr":"2025-07-05T19:15+0000","eventDateCarUser":1751742945000,"eventDateStrCarUser":"2025-07-05T19:15+0000","latitude":40.700153,"longitude":-74.401405,"positionHeadingDegree":"154","tirePressureFrontLeft":"2482","tirePressureFrontRight":"2482","tirePressureRearLeft":"2413","tirePressureRearRight":"2482","tirePressureFrontLeftPsi":"36","tirePressureFrontRightPsi":"36","tirePressureRearLeftPsi":"35","tirePressureRearRightPsi":"36","doorBootPosition":"CLOSED","doorEngineHoodPosition":"CLOSED","doorFrontLeftPosition":"CLOSED","doorFrontRightPosition":"CLOSED","doorRearLeftPosition":"CLOSED","doorRearRightPosition":"CLOSED","doorBootLockStatus":"LOCKED","doorFrontLeftLockStatus":"LOCKED","doorFrontRightLockStatus":"LOCKED","doorRearLeftLockStatus":"LOCKED","doorRearRightLockStatus":"LOCKED","distanceToEmptyFuelMiles":259.73,"distanceToEmptyFuelKilometers":418,"avgFuelConsumptionMpg":102.2,"avgFuelConsumptionLitersPer100Kilometers":2.3,"evStateOfChargePercent":null,"evDistanceToEmptyMiles":null,"evDistanceToEmptyKilometers":null,"evDistanceToEmptyByStateMiles":null,"evDistanceToEmptyByStateKilometers":null,"vehicleStateType":"IGNITION_OFF","windowFrontLeftStatus":"CLOSE","windowFrontRightStatus":"CLOSE","windowRearLeftStatus":"CLOSE","windowRearRightStatus":"CLOSE","windowSunroofStatus":"CLOSE","tyreStatusFrontLeft":"UNKNOWN","tyreStatusFrontRight":"UNKNOWN","tyreStatusRearLeft":"UNKNOWN","tyreStatusRearRight":"UNKNOWN","remainingFuelPercent":90,"distanceToEmptyFuelMiles10s":260,"distanceToEmptyFuelKilometers10s":420}}`)
}
}
ts := mockMySubaruApi(t, handler)
ts.Start()
defer ts.Close()
cfg := mockConfig(t)
msc, err := New(cfg)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if msc == nil {
t.Fatalf("expected MySubaru API client, got nil")
}
if !msc.isAuthenticated || !msc.isRegistered {
t.Errorf("expected authenticated and registered true, got %v %v", msc.isAuthenticated, msc.isRegistered)
}
if msc.currentVin != "1HGCM82633A004352" {
t.Errorf("expected currentVin 1HGCM82633A004352, got %v", msc.currentVin)
}
}
// TestGetVehicleCondition_Success tests the GetVehicleCondition method
func TestGetVehicleCondition_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
// Handle API_LOGIN endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_LOGIN"] && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":"BIOMETRICS_DISABLED","dataName":"sessionData","data":{"sessionChanged":false,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751738613000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"9D7FCDF274794346689D3FA0D693CBBF","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":null,"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":null,"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":null,"authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$rOb/uqhm8I3QtSel2phOCOxNM51w43eqXDDksMkJ.1a5KsaQuLvEu$1751745334477","satelliteViewEnabled":true,"registeredDevicePermanent":true}}`)
}
// Handle API_VALIDATE_SESSION endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VALIDATE_SESSION"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":null}`)
}
// Handle SELECT_VEHICLE endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_SELECT_VEHICLE"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":"vehicle","data":{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":"2023","modelCode":"PDL","engineSize":2.4,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"8KV8","licensePlateState":"NJ","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":"Outback","subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":"Cosmic Blue Pearl","sunsetUpgraded":true,"intDescrip":"Black","transCode":"CVT","provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}}`)
}
// Handle API_CONDITION endpoint
if (r.URL.Path == MOBILE_API_VERSION+urlToGen(apiURLs["API_CONDITION"], "g1") || r.URL.Path == MOBILE_API_VERSION+urlToGen(apiURLs["API_CONDITION"], "g2")) && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":"remoteServiceStatus","data":{"serviceRequestId":null,"success":true,"cancelled":false,"remoteServiceType":"condition","remoteServiceState":"finished","subState":null,"errorCode":null,"result":{"avgFuelConsumption":null,"avgFuelConsumptionUnit":"MPG","distanceToEmptyFuel":null,"distanceToEmptyFuelUnit":"MILES","odometer":31692,"odometerUnit":"MILES","tirePressureFrontLeft":null,"tirePressureFrontLeftUnit":"PSI","tirePressureFrontRight":null,"tirePressureFrontRightUnit":"PSI","tirePressureRearLeft":null,"tirePressureRearLeftUnit":"PSI","tirePressureRearRight":null,"tirePressureRearRightUnit":"PSI","lastUpdatedTime":"2025-07-05T19:15:45.000+0000","windowFrontLeftStatus":"CLOSE","windowFrontRightStatus":"CLOSE","windowRearLeftStatus":"CLOSE","windowRearRightStatus":"CLOSE","windowSunroofStatus":"CLOSE","remainingFuelPercent":"90","evDistanceToEmpty":null,"evDistanceToEmptyUnit":null,"evChargerStateType":null,"evIsPluggedIn":null,"evStateOfChargeMode":null,"evTimeToFullyCharged":null,"evStateOfChargePercent":null,"vehicleStateType":"IGNITION_OFF","doorBootLockStatus":"LOCKED","doorBootPosition":"CLOSED","doorEngineHoodPosition":"CLOSED","doorFrontLeftLockStatus":"LOCKED","doorFrontLeftPosition":"CLOSED","doorFrontRightLockStatus":"LOCKED","doorFrontRightPosition":"CLOSED","doorRearLeftLockStatus":"LOCKED","doorRearLeftPosition":"CLOSED","doorRearRightLockStatus":"LOCKED","doorRearRightPosition":"CLOSED"},"updateTime":null,"vin":"1HGCM82633A004352","errorDescription":null}}`)
}
}
ts := mockMySubaruApi(t, handler)
ts.Start()
defer ts.Close()
cfg := mockConfig(t)
msc, err := New(cfg)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if msc == nil {
t.Fatalf("expected MySubaru API client, got nil")
}
if !msc.isAuthenticated || !msc.isRegistered {
t.Errorf("expected authenticated and registered true, got %v %v", msc.isAuthenticated, msc.isRegistered)
}
if msc.currentVin != "1HGCM82633A004352" {
t.Errorf("expected currentVin 1HGCM82633A004352, got %v", msc.currentVin)
}
}
// TestGetVehicleHealth_Success tests the successful retrieval of vehicle health data.
func TestGetVehicleHealth_Success(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
// Handle API_LOGIN endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_LOGIN"] && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":"BIOMETRICS_DISABLED","dataName":"sessionData","data":{"sessionChanged":false,"vehicleInactivated":false,"account":{"marketId":1,"createdDate":1476984644000,"firstName":"Tatiana","lastName":"Savin","zipCode":"07974","accountKey":765268,"lastLoginDate":1751738613000,"zipCode5":"07974"},"resetPassword":false,"deviceId":"JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ","sessionId":"9D7FCDF274794346689D3FA0D693CBBF","deviceRegistered":true,"passwordToken":null,"vehicles":[{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":null,"modelCode":null,"engineSize":null,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"","licensePlateState":"","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":null,"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":null,"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":null,"authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":null,"subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":null,"sunsetUpgraded":true,"intDescrip":null,"transCode":null,"provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}],"rightToRepairEnabled":true,"rightToRepairStartYear":2022,"rightToRepairStates":"MA","enableXtime":true,"termsAndConditionsAccepted":true,"digitalGlobeConnectId":"0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeImageTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","digitalGlobeTransparentTileService":"https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132","tomtomKey":"DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs","currentVehicleIndex":0,"handoffToken":"$2a$08$rOb/uqhm8I3QtSel2phOCOxNM51w43eqXDDksMkJ.1a5KsaQuLvEu$1751745334477","satelliteViewEnabled":true,"registeredDevicePermanent":true}}`)
}
// Handle API_VALIDATE_SESSION endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VALIDATE_SESSION"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":null}`)
}
// Handle SELECT_VEHICLE endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_SELECT_VEHICLE"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":"vehicle","data":{"customer":{"sessionCustomer":null,"email":null,"firstName":null,"lastName":null,"zip":null,"oemCustId":null,"phone":null},"vehicleName":"Subaru Outback TXT","stolenVehicle":false,"vin":"1HGCM82633A004352","modelYear":"2023","modelCode":"PDL","engineSize":2.4,"nickname":"Subaru Outback TXT","vehicleKey":8211380,"active":true,"licensePlate":"8KV8","licensePlateState":"NJ","email":null,"firstName":null,"lastName":null,"subscriptionFeatures":["REMOTE","SAFETY","Retail3"],"accessLevel":-1,"zip":null,"oemCustId":"CRM-41PLM-5TYE","vehicleMileage":null,"phone":null,"timeZone":"America/New_York","features":["ABS_MIL","ACCS","AHBL_MIL","ATF_MIL","AWD_MIL","BSD","BSDRCT_MIL","CEL_MIL","CP1_5HHU","EBD_MIL","EOL_MIL","EPAS_MIL","EPB_MIL","ESS_MIL","EYESIGHT","ISS_MIL","MOONSTAT","OPL_MIL","PANPM-TUIRWAOC","PWAAADWWAP","RAB_MIL","RCC","REARBRK","RES","RESCC","RES_HVAC_HFS","RES_HVAC_VFS","RHSF","RPOI","RPOIA","RTGU","RVFS","SRH_MIL","SRS_MIL","SXM360L","T23DCM","TEL_MIL","TIF_35","TIR_33","TLD","TPMS_MIL","VALET","VDC_MIL","WASH_MIL","WDWSTAT","g3"],"userOemCustId":"CRM-41PLM-5TYE","subscriptionStatus":"ACTIVE","authorizedVehicle":false,"preferredDealer":null,"cachedStateCode":"NJ","modelName":"Outback","subscriptionPlans":[],"crmRightToRepair":false,"needMileagePrompt":false,"phev":null,"extDescrip":"Cosmic Blue Pearl","sunsetUpgraded":true,"intDescrip":"Black","transCode":"CVT","provisioned":true,"remoteServicePinExist":true,"needEmergencyContactPrompt":false,"vehicleGeoPosition":null,"show3gSunsetBanner":false}}`)
}
// Handle API_VEHICLE_HEALTH endpoint
if r.URL.Path == MOBILE_API_VERSION+apiURLs["API_VEHICLE_HEALTH"] && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"success":true,"errorCode":null,"dataName":null,"data":{"lastUpdatedDate":1751742945000,"vehicleHealthItems":[{"warningCode":10,"b2cCode":"airbag","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"SRS_MIL"},{"warningCode":4,"b2cCode":"oilTemp","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"ATF_MIL"},{"warningCode":39,"b2cCode":"blindspot","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"BSDRCT_MIL"},{"warningCode":2,"b2cCode":"engineFail","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"CEL_MIL"},{"warningCode":44,"b2cCode":"pkgBrake","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"EPB_MIL"},{"warningCode":8,"b2cCode":"ebd","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"EBD_MIL"},{"warningCode":3,"b2cCode":"oilWarning","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"EOL_MIL"},{"warningCode":1,"b2cCode":"washer","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"WASH_MIL"},{"warningCode":50,"b2cCode":"iss","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"ISS_MIL"},{"warningCode":53,"b2cCode":"oilPres","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"OPL_MIL"},{"warningCode":11,"b2cCode":"epas","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"EPAS_MIL"},{"warningCode":69,"b2cCode":"revBrake","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"RAB_MIL"},{"warningCode":14,"b2cCode":"telematics","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"TEL_MIL"},{"warningCode":9,"b2cCode":"tpms","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"TPMS_MIL"},{"warningCode":7,"b2cCode":"vdc","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"VDC_MIL"},{"warningCode":6,"b2cCode":"abs","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"ABS_MIL"},{"warningCode":5,"b2cCode":"awd","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"AWD_MIL"},{"warningCode":12,"b2cCode":"eyesight","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"ESS_MIL"},{"warningCode":30,"b2cCode":"ahbl","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"AHBL_MIL"},{"warningCode":31,"b2cCode":"srh","isTrouble":false,"onDates":[],"onDaiId":0,"featureCode":"SRH_MIL"}]}}`)
}
}
ts := mockMySubaruApi(t, handler)
ts.Start()
defer ts.Close()
cfg := mockConfig(t)
msc, err := New(cfg)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if msc == nil {
t.Fatalf("expected MySubaru API client, got nil")
}
if !msc.isAuthenticated || !msc.isRegistered {
t.Errorf("expected authenticated and registered true, got %v %v", msc.isAuthenticated, msc.isRegistered)
}
if msc.currentVin != "1HGCM82633A004352" {
t.Errorf("expected currentVin 1HGCM82633A004352, got %v", msc.currentVin)
}
}