Compare commits

...

49 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
21a928bf70 Debuging response outputs
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 25s
2025-06-09 12:46:14 -04:00
cb008f61e7 Enabled some Debug as an Info output
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 23s
2025-06-06 17:37:48 -04:00
fd26de9b82 Updated go.mod for the example
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 24s
2025-06-06 17:29:31 -04:00
017d7de3a2 Changes Tire Pressure (PSI) at Vehicle Status from int to float64
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 27s
2025-06-06 17:28:15 -04:00
91ab2ddf00 Updated dependencies
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 22s
2025-06-05 17:03:09 -04:00
af42a5971e Fixed parts parsing regexp
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 22s
2025-06-05 17:02:00 -04:00
14 changed files with 2863 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" import "git.savin.nyc/alex/mysubaru"
``` ```
#### Create a new MySubaru connection and get a car by VIN #### Create a new MySubaru API Client
```go ```go
// Create a MySubaru Client // Create a MySubaru Client
mysubaru, _ := New() msc, err := mysubaru.New(cfg)
outback := mysubaru.GetVehicleByVIN("VIN-CODE-HERE") 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 #### Start/Stop Lights request
```go ```go
outback.LightsStart() // Execute a LightsStart command
time.Sleep(30 * time.Second) events, err := outback.LightsStart()
outback.LightsStop() 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)
}
``` ```

609
client.go
View File

@ -2,8 +2,10 @@ package mysubaru
import ( import (
"encoding/json" "encoding/json"
"errors"
"io" "io"
"log/slog" "log/slog"
"regexp"
"slices" "slices"
"sync" "sync"
"time" "time"
@ -12,22 +14,24 @@ import (
"resty.dev/v3" "resty.dev/v3"
) )
// Client . // Client represents a MySubaru API client that interacts with the MySubaru API.
type Client struct { type Client struct {
credentials config.Credentials credentials config.Credentials
httpClient *resty.Client httpClient *resty.Client
country string // USA | CA country string // USA | CA
updateInterval int // 7200 contactMethods dataMap // List of contact methods for 2FA
fetchInterval int // 360
currentVin string currentVin string
listOfVins []string listOfVins []string
isAuthenticated bool isAuthenticated bool
isRegistered bool isRegistered bool
isAlive bool
updateInterval int // 7200
fetchInterval int // 360
logger *slog.Logger logger *slog.Logger
sync.RWMutex sync.RWMutex
} }
// New function creates a New MySubaru client // New function creates a New MySubaru API client
func New(config *config.Config) (*Client, error) { func New(config *config.Config) (*Client, error) {
client := &Client{ client := &Client{
@ -47,115 +51,138 @@ func New(config *config.Config) (*Client, error) {
"X-Requested-With": MOBILE_APP[client.country], "X-Requested-With": MOBILE_APP[client.country],
"Accept-Language": "en-US,en;q=0.9", "Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate", "Accept-Encoding": "gzip, deflate",
"Accept": "*/*"}) "Accept": "*/*"},
)
client.httpClient = httpClient client.httpClient = httpClient
resp := client.auth()
if r, ok := client.parseResponse(resp); ok { if ok, err := client.auth(); !ok {
var sd SessionData client.logger.Error("error while executing auth request", "request", "auth", "error", err.Error())
err := json.Unmarshal(r.Data, &sd) return nil, errors.New("error while executing auth request: " + err.Error())
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
}
return client, nil return client, nil
} }
// SelectVehicle . // auth authenticates the client with the MySubaru API using the provided credentials.
func (c *Client) SelectVehicle(vin string) VehicleData { func (c *Client) auth() (bool, error) {
params := map[string]string{
"env": "cloudprod",
"deviceType": "android",
"loginUsername": c.credentials.Username,
"password": c.credentials.Password,
"deviceId": c.credentials.DeviceID,
"passwordToken": "",
"selectedVin": "",
"pushToken": ""}
reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"]
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
}
// 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 == "" { if vin == "" {
vin = c.currentVin vin = c.currentVin
} }
vinCheck(vin) vinCheck(vin)
params := map[string]string{ params := map[string]string{
"vin": vin, "vin": vin,
"_": timestamp()} "_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp := c.execute(reqURL, GET, params, "", false) 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) // c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
if r, ok := c.parseResponse(resp); ok {
var vd VehicleData var vd VehicleData
err := json.Unmarshal(r.Data, &vd) err = json.Unmarshal(resp.Data, &vd)
if err != nil { if err != nil {
c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error()) c.logger.Error("error while parsing json", "request", "SelectVehicle", "error", err.Error())
return nil, errors.New("error while parsing json while vehicle selection")
} }
// c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp) // c.logger.Debug("http request output", "request", "SelectVehicle", "body", resp)
return vd return &vd, nil
} else {
return VehicleData{}
}
} }
// GetVehicles . // GetVehicles retrieves a list of vehicles associated with the client's account.
func (c *Client) GetVehicles() []*Vehicle { func (c *Client) GetVehicles() ([]*Vehicle, error) {
var vehicles []*Vehicle var vehicles []*Vehicle
for _, vin := range c.listOfVins { for _, vin := range c.listOfVins {
vehicle := c.GetVehicleByVIN(vin) vehicle, err := c.GetVehicleByVin(vin)
if err != nil {
c.logger.Error("cannot get vehicle data", "request", "GetVehicles", "error", err.Error())
return nil, errors.New("cannot get vehicle data: " + err.Error())
}
vehicles = append(vehicles, vehicle) vehicles = append(vehicles, vehicle)
} }
return vehicles return vehicles, nil
} }
// GetVehicleByVIN . // GetVehicleByVin retrieves a vehicle by its VIN from the client's list of vehicles.
func (c *Client) GetVehicleByVIN(vin string) *Vehicle { func (c *Client) GetVehicleByVin(vin string) (*Vehicle, error) {
var vehicle *Vehicle var vehicle *Vehicle
if slices.Contains(c.listOfVins, vin) { if slices.Contains(c.listOfVins, vin) {
params := map[string]string{ params := map[string]string{
"vin": vin, "vin": vin,
"_": timestamp()} "_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"]
resp := c.execute(reqURL, GET, params, "", false) resp, err := c.execute(GET, reqURL, params, false)
// c.logger.Debug("http request output", "request", "GetVehicleByVIN", "body", resp)
if r, ok := c.parseResponse(resp); ok {
var vd VehicleData
err := json.Unmarshal(r.Data, &vd)
if err != nil { if err != nil {
c.logger.Error("error while parsing json", "request", "GetVehicleByVIN", "error", err.Error()) 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) // 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{ vehicle = &Vehicle{
Vin: vin, Vin: vin,
@ -194,86 +221,194 @@ func (c *Client) GetVehicleByVIN(vin string) *Vehicle {
vehicle.GetClimateUserPresets() vehicle.GetClimateUserPresets()
vehicle.GetClimateQuickPresets() vehicle.GetClimateQuickPresets()
return vehicle return vehicle, nil
} }
} c.logger.Error("vin code is not in the list of the available vin codes", "request", "GetVehicleByVIN")
c.logger.Error("error while parsing json", "request", "GetVehicleByVIN") return nil, errors.New("vin code is not in the list of the available vin codes")
return &Vehicle{}
} }
// func isPINRequired() {} // RefreshVehicles refreshes the list of vehicles associated with the client's account.
// func getVehicles() {} // {"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 getEVStatus() {} func (c *Client) RefreshVehicles() error {
// func getRemoteOptionsStatus() {} params := map[string]string{}
// func getRemoteStartStatus() {} reqURL := MOBILE_API_VERSION + apiURLs["API_REFRESH_VEHICLES"]
// func getSafetyStatus() {} resp, err := c.execute(GET, reqURL, params, false)
// func getSubscriptionStatus() {} if err != nil {
// func getClimateData() {} c.logger.Error("error while executing RefreshVehicles request", "request", "RefreshVehicles", "error", err.Error())
// func saveClimateSettings() {} return errors.New("error while executing RefreshVehicles request: " + err.Error())
}
c.logger.Debug("http request output", "request", "RefreshVehicles", "body", resp)
// Exec method executes a Client instance with the API URL // var vd VehicleData
func (c *Client) execute(requestUrl string, method string, params map[string]string, pollingUrl string, j bool, attempts ...int) []byte { // 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") // defer timeTrack("[TIMETRK] Executing HTTP Request")
var resp *resty.Response var resp *resty.Response
var err error
// c.logger.Debug("executing http request", "method", method, "url", url, "params", params)
// GET Requests // GET Requests
if method == GET { if method == GET {
resp, _ = c.httpClient. resp, err = c.httpClient.
R(). R().
SetQueryParams(params). SetQueryParams(params).
Get(requestUrl) 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 // POST Requests
if method == POST { if method == POST {
if j { // POST > JSON Body if j { // POST > JSON Body
resp, _ = c.httpClient. resp, err = c.httpClient.
R(). R().
SetBody(params). SetBody(params).
Post(requestUrl) 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 } else { // POST > Form Data
resp, _ = c.httpClient. resp, err = c.httpClient.
R(). R().
SetFormData(params). SetFormData(params).
Post(requestUrl) 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) resBytes, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
c.logger.Error("error while getting body", "error", err.Error()) c.logger.Error("error while getting body", "error", err.Error())
} }
c.logger.Debug("parsed http request output", "data", string(resBytes))
c.httpClient.SetCookies(resp.Cookies())
if r, ok := c.parseResponse(resBytes); ok { if r, ok := c.parseResponse(resBytes); ok {
// c.logger.Debug("parsed http request output", "data", r.Data) c.isAlive = true
c.Unlock()
// dataName field has the list of the states [ remoteServiceStatus | errorResponse ] return &r, nil
if r.DataName == "remoteServiceStatus" {
var sr ServiceRequest
err := json.Unmarshal(r.Data, &sr)
if err != nil {
c.logger.Error("error while parsing json", "request", "HTTP POLLING", "error", err.Error())
}
if pollingUrl != "" {
switch {
case sr.RemoteServiceState == "finished":
// Finished RemoteServiceState Service Request does not include Service Request ID
c.logger.Debug("Remote service request completed successfully")
case sr.RemoteServiceState == "started":
time.Sleep(5 * time.Second)
c.logger.Debug("Subaru API reports remote service request (started) is in progress", "id", sr.ServiceRequestID)
c.execute(pollingUrl, GET, map[string]string{"serviceRequestId": sr.ServiceRequestID}, pollingUrl, false)
case sr.RemoteServiceState == "stopping":
time.Sleep(5 * time.Second)
c.logger.Debug("Subaru API reports remote service request (stopping) is in progress", "id", sr.ServiceRequestID)
c.execute(pollingUrl, GET, map[string]string{"serviceRequestId": sr.ServiceRequestID}, pollingUrl, false)
default:
time.Sleep(5 * time.Second)
c.logger.Debug("Subaru API reports remote service request (stopping) is in progress")
c.execute(pollingUrl, GET, map[string]string{"serviceRequestId": sr.ServiceRequestID}, pollingUrl, false, 1)
}
}
}
} else { } else {
if r.DataName == "errorResponse" { if r.DataName == "errorResponse" {
var er ErrorResponse var er ErrorResponse
@ -282,61 +417,89 @@ func (c *Client) execute(requestUrl string, method string, params map[string]str
c.logger.Error("error while parsing json", "request", "errorResponse", "error", err.Error()) c.logger.Error("error while parsing json", "request", "errorResponse", "error", err.Error())
} }
if _, ok := API_ERRORS[er.ErrorLabel]; ok { if _, ok := API_ERRORS[er.ErrorLabel]; ok {
c.logger.Error("request got an error", "request", "execute", "method", method, "url", requestUrl, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription) c.logger.Error("request got an error", "request", "execute", "method", method, "url", url, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
} }
c.logger.Error("request got an unknown error", "request", "execute", "method", method, "url", requestUrl, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription) c.logger.Error("request got an unknown error", "request", "execute", "method", method, "url", url, "label", er.ErrorLabel, "descrip[tion", er.ErrorDescription)
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", requestUrl, "error", err.Error()) c.logger.Error("request is not successfull", "request", "execute", "method", method, "url", url, "error", err.Error())
} }
return resBytes }
c.isAlive = false
c.Unlock()
return nil, errors.New("request is not successfull, HTTP code: " + resp.Status())
} }
// auth . // parseResponse parses the JSON response from the MySubaru API into a Response struct.
func (c *Client) auth() []byte {
params := map[string]string{
"env": "cloudprod",
"deviceType": "android",
"loginUsername": c.credentials.Username,
"password": c.credentials.Password,
"deviceId": c.credentials.DeviceID,
"passwordToken": "",
"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
}
// parseResponse .
func (c *Client) parseResponse(b []byte) (Response, bool) { func (c *Client) parseResponse(b []byte) (Response, bool) {
var r Response var r Response
err := json.Unmarshal(b, &r) err := json.Unmarshal(b, &r)
if err != nil { if err != nil {
c.logger.Error("error while parsing json", "error", err.Error()) c.logger.Error("error while parsing json", "error", err.Error())
return r, false
} }
return r, true return r, true
} }
// validateSession . // ValidateSession checks if the current session is valid by making a request to the vehicle status API.
// func (c *Client) validateSession() bool { func (c *Client) validateSession() bool {
// // { reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"]
// // "success": true, resp, err := c.execute(GET, reqURL, map[string]string{}, false)
// // "errorCode": null, if err != nil {
// // "dataName": null, c.logger.Error("error while executing validateSession request", "request", "validateSession", "error", err.Error())
// // "data": null return false
// // } }
// reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"] c.logger.Debug("http request output", "request", "validateSession", "body", resp)
// resp := c.execute(reqURL, GET, map[string]string{}, "", false)
// c.logger.Debug("http request output", "request", "validateSession", "body", resp)
// var r Response if resp.Success {
// err := json.Unmarshal(resp, &r) _, err := c.SelectVehicle(c.currentVin)
// if err != nil { if err != nil {
// c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error()) 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()
// } // }
// if r.Success { // 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 {
// 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)
// if r, ok := c.parseResponse(resp); ok {
// c.logger.Error("error while parsing json", "request", "validateSession", "error", err.Error())
// return true
// } else {
// resp := c.auth()
// return true // return true
// } // }
// return false // return false
@ -357,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":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]}
// {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8210723,"deviceName":"Hassio Golang Integration","createdDate":"2021-12-22T01:38:43.000+0000","modifiedDate":"2021-12-22T01:38:43.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]} // {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8210723,"deviceName":"Hassio Golang Integration","createdDate":"2021-12-22T01:38:43.000+0000","modifiedDate":"2021-12-22T01:38:43.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]}
// registerDevice . // // registerDevice .
func (c *Client) registerDevice() bool { // func (c *Client) registerDevice() bool {
// c.httpClient. // // c.httpClient.
// SetBaseURL(WEB_API_SERVER[c.country]). // // SetBaseURL(WEB_API_SERVER[c.country]).
// R(). // // R().
// SetFormData(map[string]string{ // // SetFormData(map[string]string{
// "username": c.credentials.username, // // "username": c.credentials.username,
// "password": c.credentials.password, // // "password": c.credentials.password,
// "deviceId": c.credentials.deviceId, // // "deviceId": c.credentials.deviceId,
// }). // // }).
// Post(apiURLs["WEB_API_LOGIN"]) // // Post(apiURLs["WEB_API_LOGIN"])
params := map[string]string{ // params := map[string]string{
"username": c.credentials.Username, // "username": c.credentials.Username,
"password": c.credentials.Password, // "password": c.credentials.Password,
"deviceId": c.credentials.DeviceID} // "deviceId": c.credentials.DeviceID}
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"] // reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"]
c.execute(reqURL, POST, params, "", true) // resp, _ := c.execute(POST, reqURL, params, true)
// Authorizing device via web API // // Authorizing device via web API
// c.httpClient. // // c.httpClient.
// SetBaseURL(WEB_API_SERVER[c.country]). // // SetBaseURL(WEB_API_SERVER[c.country]).
// R(). // // R().
// SetQueryParams(map[string]string{ // // SetQueryParams(map[string]string{
// "deviceId": c.credentials.deviceId, // // "deviceId": c.credentials.deviceId,
// }). // // }).
// Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"]) // // Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"])
params = map[string]string{ // params = map[string]string{
"deviceId": c.credentials.DeviceID} // "deviceId": c.credentials.DeviceID}
reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"] // reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"]
c.execute(reqURL, GET, params, "", false) // c.execute(reqURL, GET, params, "", false)
return c.setDeviceName() // return c.setDeviceName()
} // }
// setDeviceName . // // setDeviceName .
func (c *Client) setDeviceName() bool { // func (c *Client) setDeviceName() bool {
params := map[string]string{ // params := map[string]string{
"deviceId": c.credentials.DeviceID, // "deviceId": c.credentials.DeviceID,
"deviceName": c.credentials.DeviceName} // "deviceName": c.credentials.DeviceName}
reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"] // reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"]
c.execute(reqURL, GET, params, "", false) // c.execute(reqURL, GET, params, "", false)
return true // return true
} // }
// // listDevices . // // listDevices .
// func (c *Client) listDevices() { // func (c *Client) listDevices() {
@ -456,3 +619,45 @@ func (c *Client) setDeviceName() bool {
// // logger.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp)) // // logger.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp))
// // } // // }
// } // }
// c.logger.Debug("parsed http request output", "data", r.Data)
// ERROR
// {"httpCode":500,"errorCode":"error","errorMessage":"org.springframework.web.HttpMediaTypeNotSupportedException - Content type 'application/x-www-form-urlencoded' not supported"}
// {"success":true,"errorCode":null,"dataName":"remoteServiceStatus","data":{"serviceRequestId":"4S4BTGND8L3137058_1640203129607_19_@NGTP","success":false,"cancelled":false,"remoteServiceType":"unlock","remoteServiceState":"started","subState":null,"errorCode":null,"result":null,"updateTime":null,"vin":"4S4BTGND8L3137058"}}
// API_REMOTE_SVC_STATUS
// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}}
// if js_resp["errorCode"] == sc.ERROR_SOA_403:
// raise RemoteServiceFailure("Backend session expired, please try again")
// if js_resp["data"]["remoteServiceState"] == "finished":
// if js_resp["data"]["success"]:
// _LOGGER.info("Remote service request completed successfully: %s", req_id)
// return True, js_resp
// _LOGGER.error(
// "Remote service request completed but failed: %s Error: %s",
// req_id,
// js_resp["data"]["errorCode"],
// )
// raise RemoteServiceFailure(
// "Remote service request completed but failed: %s" % js_resp["data"]["errorCode"]
// )
// if js_resp["data"].get("remoteServiceState") == "started":
// _LOGGER.info(
// "Subaru API reports remote service request is in progress: %s",
// req_id,
// )
// attempts_left -= 1
// await asyncio.sleep(2)
// TODO: Work on errors
// error, _ := respParsed.Path("errorCode").Data().(string)
// switch {
// case error == apiErrors["ERROR_INVALID_ACCOUNT"]:
// client.logger.Debug("Invalid account")
// case error == apiErrors["ERROR_INVALID_CREDENTIALS"]:
// client.logger.Debug("Client authentication failed")
// case error == apiErrors["ERROR_PASSWORD_WARNING"]:
// client.logger.Debug("Multiple Password Failures.")
// default:
// client.logger.Debug("Uknown error")
// }

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 package mysubaru
var MOBILE_API_VERSION = "/g2v30" var MOBILE_API_VERSION = "/g2v31"
var MOBILE_API_SERVER = map[string]string{ var MOBILE_API_SERVER = map[string]string{
"USA": "https://mobileapi.prod.subarucs.com", "USA": "https://mobileapi.prod.subarucs.com",
@ -30,12 +30,12 @@ var apiURLs = map[string]string{
"API_2FA_AUTH_VERIFY": "/twoStepAuthVerify.json", "API_2FA_AUTH_VERIFY": "/twoStepAuthVerify.json",
"API_LOGIN": "/login.json", // Same API for g1 and g2 "API_LOGIN": "/login.json", // Same API for g1 and g2
"API_REFRESH_VEHICLES": "/refreshVehicles.json", "API_REFRESH_VEHICLES": "/refreshVehicles.json",
"API_SELECT_VEHICLE": "/selectVehicle.json", "API_SELECT_VEHICLE": "/selectVehicle.json", // Covered by test
"API_VALIDATE_SESSION": "/validateSession.json", "API_VALIDATE_SESSION": "/validateSession.json", // Covered by test
"API_VEHICLE_STATUS": "/vehicleStatus.json", "API_VEHICLE_STATUS": "/vehicleStatus.json", // Covered by test
"API_AUTHORIZE_DEVICE": "/authenticateDevice.json", "API_AUTHORIZE_DEVICE": "/authenticateDevice.json",
"API_NAME_DEVICE": "/nameThisDevice.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_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_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", "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": "/service/api_gen/lightsOnly/execute.json",
"API_LIGHTS_CANCEL": "/service/api_gen/lightsOnly/cancel.json", "API_LIGHTS_CANCEL": "/service/api_gen/lightsOnly/cancel.json",
"API_LIGHTS_STOP": "/service/api_gen/lightsOnly/stop.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_UPDATE": "/service/g1/vehicleLocate/execute.json", // Different API for g1 and g2
"API_G1_LOCATE_STATUS": "/service/g1/vehicleLocate/status.json", "API_G1_LOCATE_STATUS": "/service/g1/vehicleLocate/status.json",
"API_G1_HORN_LIGHTS_STATUS": "/service/g1/hornLights/status.json", // g1-Only API "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_UPDATE": "/service/g2/vehicleStatus/execute.json",
"API_G2_LOCATE_STATUS": "/service/g2/vehicleStatus/locationStatus.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_SEND_POI": "/service/g2/sendPoi/execute.json", // g2-Only API
"API_G2_SPEEDFENCE": "/service/g2/speedFence/execute.json", "API_G2_SPEEDFENCE": "/service/g2/speedFence/execute.json",
"API_G2_GEOFENCE": "/service/g2/geoFence/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_FETCH_CHARGE_SETTINGS": "/service/g2/phevGetTimerSettings/execute.json",
"API_EV_SAVE_CHARGE_SETTINGS": "/service/g2/phevSendTimerSetting/execute.json", "API_EV_SAVE_CHARGE_SETTINGS": "/service/g2/phevSendTimerSetting/execute.json",
"API_EV_DELETE_CHARGE_SCHEDULE": "/service/g2/phevDeleteTimerSetting/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 // 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{ var API_ERRORS = map[string]string{
"403-soa-unableToParseResponseBody": "ERROR_SOA_403", // G2 Error Codes "API_ERROR_SOA_403": "403-soa-unableToParseResponseBody", // G2
"InvalidCredentials": "ERROR_INVALID_CREDENTIALS", "API_ERROR_NO_ACCOUNT": "accountNotFound", // G2
"ServiceAlreadyStarted": "ERROR_SERVICE_ALREADY_STARTED", "API_ERROR_INVALID_ACCOUNT": "invalidAccount", // G2
"invalidAccount": "ERROR_INVALID_ACCOUNT", "API_ERROR_INVALID_CREDENTIALS": "InvalidCredentials", // G2
"passwordWarning": "ERROR_PASSWORD_WARNING", "API_ERROR_INVALID_TOKEN": "InvalidToken", // G2
"accountLocked": "ERROR_ACCOUNT_LOCKED", "API_ERROR_PASSWORD_WARNING": "passwordWarning", // G2
"noVehiclesOnAccount": "ERROR_NO_VEHICLES", "API_ERROR_TOO_MANY_ATTEMPTS": "tooManyAttempts", // G2
"accountNotFound": "ERROR_NO_ACCOUNT", "API_ERROR_ACCOUNT_LOCKED": "accountLocked", // G2
"tooManyAttempts": "ERROR_TOO_MANY_ATTEMPTS", "API_ERROR_NO_VEHICLES": "noVehiclesOnAccount", // G2
"vehicleNotInAccount": "ERROR_VEHICLE_NOT_IN_ACCOUNT", "API_ERROR_VEHICLE_SETUP": "VEHICLESETUPERROR", // G2
"SXM40004": "ERROR_G1_NO_SUBSCRIPTION", // G1 Error Codes "API_ERROR_VEHICLE_NOT_IN_ACCOUNT": "vehicleNotInAccount", // G2
"SXM40005": "ERROR_G1_STOLEN_VEHICLE", "API_ERROR_SERVICE_ALREADY_STARTED": "ServiceAlreadyStarted", // G2
"SXM40006": "ERROR_G1_INVALID_PIN", "API_ERROR_G1_NO_SUBSCRIPTION": "SXM40004", // G1
"SXM40009": "ERROR_G1_SERVICE_ALREADY_STARTED", "API_ERROR_G1_STOLEN_VEHICLE": "SXM40005", // G1
"SXM40017": "ERROR_G1_PIN_LOCKED", "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 // TODO: Get back and add error wrapper
@ -146,7 +150,7 @@ var features = map[string]string{
"PANPM-TUIRWAOC": "Power Moonroof", "PANPM-TUIRWAOC": "Power Moonroof",
"PANPM-DG2G": "Panoramic Moonroof", "PANPM-DG2G": "Panoramic Moonroof",
"PHEV": "Electric Vehicle", "PHEV": "Electric Vehicle",
"RES": "Remote Start", "RES": "Remote Engine Start",
"REARBRK": "Reverse Auto Braking", "REARBRK": "Reverse Auto Braking",
"TIF_35": "Tire Pressure Front 35", "TIF_35": "Tire Pressure Front 35",
"TIR_33": "Tire Pressure Rear 35", "TIR_33": "Tire Pressure Rear 35",
@ -157,6 +161,13 @@ var features = map[string]string{
"RCC": "Remote Climate Control", "RCC": "Remote Climate Control",
"ACCS": "Adaptive Cruise Control", "ACCS": "Adaptive Cruise Control",
"SXM360L": "SiriusXM with 360L", "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{ var troubles = map[string]string{
@ -261,7 +272,7 @@ const (
REAR_AC_ON = "true" REAR_AC_ON = "true"
REAR_AC_OFF = "false" REAR_AC_OFF = "false"
START_CONFIG = "startConfiguration" 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" START_CONFIG_DEFAULT_RES = "START_ENGINE_ALLOW_KEY_IN_IGNITION"
WHICH_DOOR = "unlockDoorType" // Unlock doors constants WHICH_DOOR = "unlockDoorType" // Unlock doors constants
ALL_DOORS = "ALL_DOORS_CMD" ALL_DOORS = "ALL_DOORS_CMD"
@ -405,3 +416,32 @@ const (
// ] // ]
// BAD_BINARY_SENSOR_VALUES = [UNKNOWN, VENTED, NOT_EQUIPPED] // 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" timezone: "America/New_York"
logging: logging:
level: INFO level: INFO
output: json output: JSON
source: false source: false

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"time"
"git.savin.nyc/alex/mysubaru" "git.savin.nyc/alex/mysubaru"
"git.savin.nyc/alex/mysubaru/config" "git.savin.nyc/alex/mysubaru/config"
@ -21,11 +22,13 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
cfg.Logger.Debug("printing config", "config", cfg) // cfg.Logger.Debug("printing config", "config", cfg)
ms, _ := mysubaru.New(cfg) msc, err := mysubaru.New(cfg)
if err != nil {
// subaru := ms.SelectVehicle("4S4BTGPD0P3199198") 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 // 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") // subaru1 := mysubaru.GetVehicleByVIN("4S4BTGND8L3137058")
@ -37,48 +40,84 @@ func main() {
// fmt.Printf("GEN #1: %+v\n\n", subaru1.getAPIGen()) // 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 // 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") outback, err := msc.GetVehicleByVin("4S4BTGPD0P3199198")
// subaru.GetLocation(true) 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): %s\n", outback)
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: %+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() // Wait for a while to see the lights on
// subaru.GetLocation(false) time.Sleep(20 * time.Second)
// subaru.GetVehicleStatus() // Execute a LightsStop command
// subaru.GetVehicleCondition() 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() // // Execute a Unlock command
// subaru.GetClimatePresets() // events, err := outback.Unlock()
// subaru.GetClimateUserPresets() // 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() // // Wait for a while to see the lights on
// subaru.GetClimateSettings()
// subaru.EngineStart()
// time.Sleep(15 * time.Second)
// subaru.EngineStop()
// subaru.Unlock()
// time.Sleep(20 * time.Second) // time.Sleep(20 * time.Second)
// subaru.Lock()
// subaru.GetLocation() // // Execute a Lock command
fmt.Printf("SUBARU: %+v\n", outback) // 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 go 1.24
require git.savin.nyc/alex/mysubaru v0.0.0-20250604165849-c3536512873b require git.savin.nyc/alex/mysubaru v0.0.0-20250722205404-92d4266f8b5a
require ( require (
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect
golang.org/x/net v0.40.0 // indirect golang.org/x/net v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
resty.dev/v3 v3.0.0-beta.3 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.40.0 // indirect golang.org/x/net v0.42.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
) )

View File

@ -2,9 +2,14 @@ package mysubaru
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"log/slog"
"strings"
"time"
) )
// Response . // Response represents the structure of a response from the MySubaru API.
type Response struct { type Response struct {
Success bool `json:"success"` // true | false Success bool `json:"success"` // true | false
ErrorCode string `json:"errorCode,omitempty"` // string | Error message if Success is false ErrorCode string `json:"errorCode,omitempty"` // string | Error message if Success is false
@ -12,34 +17,96 @@ type Response struct {
Data json.RawMessage `json:"data"` // Data struct Data json.RawMessage `json:"data"` // Data struct
} }
// parse . // parse parses the JSON response from the MySubaru API into a Response struct.
// func (r *Response) parse(b []byte, logger *slog.Logger) bool { func (r *Response) parse(b []byte, logger *slog.Logger) (*Response, error) {
// err := json.Unmarshal(b, &r) err := json.Unmarshal(b, &r)
// if err != nil { if err != nil {
// logger.Error("error while parsing json", "error", err.Error()) logger.Error("error while parsing json", "error", err.Error())
// return false return nil, errors.New("error while parsing json: " + err.Error())
// } }
// return true
// }
// Unmarshal . if !r.Success && r.ErrorCode != "" {
// func (r *Response) Unmarshal(b []byte) {} 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 . return r, nil
type Account struct { }
// 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"` MarketID int `json:"marketId"`
AccountKey int `json:"accountKey"` AccountKey int `json:"accountKey"`
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
LastName string `json:"lastName"` LastName string `json:"lastName"`
ZipCode string `json:"zipCode"` ZipCode string `json:"zipCode"`
ZipCode5 string `json:"zipCode5"` ZipCode5 string `json:"zipCode5"`
LastLoginDate int64 `json:"lastLoginDate"` LastLoginDate UnixTime `json:"lastLoginDate"`
CreatedDate int64 `json:"createdDate"` CreatedDate UnixTime `json:"createdDate"`
} }
// Customer . // Customer .
type Customer struct { type Customer struct {
SessionCustomer string `json:"sessionCustomer"` SessionCustomer SessionCustomer `json:"sessionCustomer,omitempty"` // struct | Only by performing a RefreshVehicles request
Email string `json:"email"` Email string `json:"email"`
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
LastName string `json:"lastName"` LastName string `json:"lastName"`
@ -48,18 +115,60 @@ type Customer struct {
Phone string `json:"phone"` 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 . // SessionData .
// "dataName": "sessionData" // "dataName": "sessionData"
type SessionData struct { type SessionData struct {
SessionChanged bool `json:"sessionChanged"` Account account `json:"account"`
VehicleInactivated bool `json:"vehicleInactivated"`
Account Account `json:"account"`
ResetPassword bool `json:"resetPassword"`
DeviceID string `json:"deviceId"`
SessionID string `json:"sessionId"`
DeviceRegistered bool `json:"deviceRegistered"`
PasswordToken string `json:"passwordToken"` 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"` Vehicles []VehicleData `json:"vehicles"`
VehicleInactivated bool `json:"vehicleInactivated"`
RightToRepairEnabled bool `json:"rightToRepairEnabled"` RightToRepairEnabled bool `json:"rightToRepairEnabled"`
RightToRepairStates string `json:"rightToRepairStates"` RightToRepairStates string `json:"rightToRepairStates"`
CurrentVehicleIndex int `json:"currentVehicleIndex"` CurrentVehicleIndex int `json:"currentVehicleIndex"`
@ -72,21 +181,20 @@ type SessionData struct {
DigitalGlobeTransparentTileService string `json:"digitalGlobeTransparentTileService"` DigitalGlobeTransparentTileService string `json:"digitalGlobeTransparentTileService"`
TomtomKey string `json:"tomtomKey"` TomtomKey string `json:"tomtomKey"`
SatelliteViewEnabled bool `json:"satelliteViewEnabled"` SatelliteViewEnabled bool `json:"satelliteViewEnabled"`
RegisteredDevicePermanent bool `json:"registeredDevicePermanent"`
} }
// Vehicle . // Vehicle .
// "dataName": "vehicle" // "dataName": "vehicle"
type VehicleData struct { type VehicleData struct {
Customer Customer `json:"customer"` // Customer struct Customer Customer `json:"customer"` // Customer struct
UserOemCustID string `json:"userOemCustId"` // CRM-631-HQN48K
OemCustID string `json:"oemCustId"` // CRM-631-HQN48K OemCustID string `json:"oemCustId"` // CRM-631-HQN48K
UserOemCustID string `json:"userOemCustId"` // CRM-631-HQN48K
Active bool `json:"active"` // true | false Active bool `json:"active"` // true | false
Email string `json:"email"` // null | email@address.com Email string `json:"email"` // null | email@address.com
FirstName string `json:"firstName"` // null | First Name FirstName string `json:"firstName,omitempty"` // null | First Name
LastName string `json:"lastName"` // null | Last Name LastName string `json:"lastName,omitempty"` // null | Last Name
Zip string `json:"zip"` // 12345 Zip string `json:"zip"` // 12345
Phone string `json:"phone"` // null | 123-456-7890 Phone string `json:"phone,omitempty"` // null | 123-456-7890
StolenVehicle bool `json:"stolenVehicle"` // true | false StolenVehicle bool `json:"stolenVehicle"` // true | false
VehicleName string `json:"vehicleName"` // Subaru Outback LXT 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" 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"
@ -106,10 +214,10 @@ type VehicleData struct {
LicensePlateState string `json:"licensePlateState"` // ABCDEF LicensePlateState string `json:"licensePlateState"` // ABCDEF
SubscriptionStatus string `json:"subscriptionStatus"` // ACTIVE SubscriptionStatus string `json:"subscriptionStatus"` // ACTIVE
SubscriptionFeatures []string `json:"subscriptionFeatures"` // "[ REMOTE ], [ SAFETY ], [ Retail | Finance3 | RetailPHEV ]"" SubscriptionFeatures []string `json:"subscriptionFeatures"` // "[ REMOTE ], [ SAFETY ], [ Retail | Finance3 | RetailPHEV ]""
SubscriptionPlans []string `json:"subscriptionPlans"` // [] SubscriptionPlans []string `json:"subscriptionPlans,omitempty"` // []
VehicleGeoPosition GeoPosition `json:"vehicleGeoPosition"` // GeoPosition struct VehicleGeoPosition GeoPosition `json:"vehicleGeoPosition"` // GeoPosition struct
AccessLevel int `json:"accessLevel"` // -1 AccessLevel int `json:"accessLevel"` // -1
VehicleMileage int `json:"vehicleMileage"` // null VehicleMileage int `json:"vehicleMileage,omitempty"` // null
CrmRightToRepair bool `json:"crmRightToRepair"` // true | false CrmRightToRepair bool `json:"crmRightToRepair"` // true | false
AuthorizedVehicle bool `json:"authorizedVehicle"` // false | true AuthorizedVehicle bool `json:"authorizedVehicle"` // false | true
NeedMileagePrompt bool `json:"needMileagePrompt"` // false | true NeedMileagePrompt bool `json:"needMileagePrompt"` // false | true
@ -119,16 +227,17 @@ type VehicleData struct {
Provisioned bool `json:"provisioned"` // true | false Provisioned bool `json:"provisioned"` // true | false
TimeZone string `json:"timeZone"` // America/New_York TimeZone string `json:"timeZone"` // America/New_York
SunsetUpgraded bool `json:"sunsetUpgraded"` // true | false SunsetUpgraded bool `json:"sunsetUpgraded"` // true | false
PreferredDealer string `json:"preferredDealer"` // null | PreferredDealer string `json:"preferredDealer,omitempty"` // null |
VehicleBranded bool `json:"vehicleBranded"`
} }
// GeoPosition . // GeoPosition .
type GeoPosition struct { type GeoPosition struct {
Latitude float64 `json:"latitude"` // 40.700184 Latitude float64 `json:"latitude"` // 40.700184
Longitude float64 `json:"longitude"` // -74.401375 Longitude float64 `json:"longitude"` // -74.401375
Speed float64 `json:"speed,omitempty"` // 62 Speed int `json:"speed,omitempty"` // 62
Heading int `json:"heading,omitempty"` // 155 Heading int `json:"heading,omitempty"` // 155
Timestamp string `json:"timestamp"` // "2021-12-22T13:14:47" Timestamp CustomTime1 `json:"timestamp"` // "2021-12-22T13:14:47"
} }
// type GeoPositionTime time.Time // type GeoPositionTime time.Time
@ -145,20 +254,13 @@ type GeoPosition struct {
// } // }
// VehicleStatus . // 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 { type VehicleStatus struct {
VehicleId int64 `json:"vhsId"` // + 9969776690 5198812434 VehicleId int64 `json:"vhsId"` // + 9969776690 5198812434
OdometerValue int `json:"odometerValue"` // + 23787 OdometerValue int `json:"odometerValue"` // + 23787
OdometerValueKm int `json:"odometerValueKilometers"` // + 38273 OdometerValueKm int `json:"odometerValueKilometers"` // + 38273
EventDate int64 `json:"eventDate"` // + 1701896993000 EventDate UnixTime `json:"eventDate"` // + 1701896993000
EventDateStr string `json:"eventDateStr"` // + 2023-12-06T21:09+0000 EventDateStr string `json:"eventDateStr"` // + 2023-12-06T21:09+0000
EventDateCarUser int64 `json:"eventDateCarUser"` // + 1701896993000 EventDateCarUser UnixTime `json:"eventDateCarUser"` // + 1701896993000
EventDateStrCarUser string `json:"eventDateStrCarUser"` // + 2023-12-06T21:09+0000 EventDateStrCarUser string `json:"eventDateStrCarUser"` // + 2023-12-06T21:09+0000
Latitude float64 `json:"latitude"` // + 40.700183 Latitude float64 `json:"latitude"` // + 40.700183
Longitude float64 `json:"longitude"` // + -74.401372 Longitude float64 `json:"longitude"` // + -74.401372
@ -174,10 +276,10 @@ type VehicleStatus struct {
TirePressureFrontRight int `json:"tirePressureFrontRight,string,omitempty"` // + "2344" TirePressureFrontRight int `json:"tirePressureFrontRight,string,omitempty"` // + "2344"
TirePressureRearLeft int `json:"tirePressureRearLeft,string,omitempty"` // + "2413" TirePressureRearLeft int `json:"tirePressureRearLeft,string,omitempty"` // + "2413"
TirePressureRearRight int `json:"tirePressureRearRight,string,omitempty"` // + "2344" TirePressureRearRight int `json:"tirePressureRearRight,string,omitempty"` // + "2344"
TirePressureFrontLeftPsi int `json:"tirePressureFrontLeftPsi,string,omitempty"` // + "33" TirePressureFrontLeftPsi float64 `json:"tirePressureFrontLeftPsi,string,omitempty"` // + "33"
TirePressureFrontRightPsi int `json:"tirePressureFrontRightPsi,string,omitempty"` // + "34" TirePressureFrontRightPsi float64 `json:"tirePressureFrontRightPsi,string,omitempty"` // + "34"
TirePressureRearLeftPsi int `json:"tirePressureRearLeftPsi,string,omitempty"` // + "35" TirePressureRearLeftPsi float64 `json:"tirePressureRearLeftPsi,string,omitempty"` // + "35"
TirePressureRearRightPsi int `json:"tirePressureRearRightPsi,string,omitempty"` // + "34" TirePressureRearRightPsi float64 `json:"tirePressureRearRightPsi,string,omitempty"` // + "34"
TyreStatusFrontLeft string `json:"tyreStatusFrontLeft"` // + "UNKNOWN" TyreStatusFrontLeft string `json:"tyreStatusFrontLeft"` // + "UNKNOWN"
TyreStatusFrontRight string `json:"tyreStatusFrontRight"` // + "UNKNOWN" TyreStatusFrontRight string `json:"tyreStatusFrontRight"` // + "UNKNOWN"
TyreStatusRearLeft string `json:"tyreStatusRearLeft"` // + "UNKNOWN" TyreStatusRearLeft string `json:"tyreStatusRearLeft"` // + "UNKNOWN"
@ -247,25 +349,41 @@ type VehicleCondition struct {
LastUpdatedTime string `json:"lastUpdatedTime"` // "2023-04-10T17:50:54+0000", LastUpdatedTime string `json:"lastUpdatedTime"` // "2023-04-10T17:50:54+0000",
} }
// ClimateSettings . // ClimateProfile represents a climate control profile for a Subaru vehicle.
// "dataName":null type ClimateProfile struct {
// type ClimateSettings struct { Name string `json:"name"`
// RunTimeMinutes string `json:"runTimeMinutes"` VehicleType string `json:"vehicleType,omitempty"` // vehicleType [ gas | phev ]
// StartConfiguration string `json:"startConfiguration"` PresetType string `json:"presetType"` // presetType [ subaruPreset | userPreset ]
// AirConditionOn string `json:"airConditionOn"` StartConfiguration string `json:"startConfiguration"` // startConfiguration [ START_ENGINE_ALLOW_KEY_IN_IGNITION (gas) | START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION (phev) ]
// OuterAirCirculation string `json:"outerAirCirculation"` RunTimeMinutes int `json:"runTimeMinutes,string"` // runTimeMinutes [ 0 | 1 | 5 | 10 ]
// ClimateZoneFrontAirMode string `json:"climateZoneFrontAirMode"` HeatedRearWindowActive string `json:"heatedRearWindowActive"` // heatedRearWindowActive: [ false | true ]
// ClimateZoneFrontTemp string `json:"climateZoneFrontTemp"` HeatedSeatFrontRight string `json:"heatedSeatFrontRight"` // heatedSeatFrontRight: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ]
// ClimateZoneFrontAirVolume string `json:"climateZoneFrontAirVolume"` HeatedSeatFrontLeft string `json:"heatedSeatFrontLeft"` // heatedSeatFrontLeft: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ]
// HeatedSeatFrontLeft string `json:"heatedSeatFrontLeft"` ClimateZoneFrontTemp int `json:"climateZoneFrontTemp,string"` // climateZoneFrontTemp: [ for _ in range(60, 85 + 1)] // climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1) ]
// HeatedSeatFrontRight string `json:"heatedSeatFrontRight"` ClimateZoneFrontAirMode string `json:"climateZoneFrontAirMode"` // climateZoneFrontAirMode: [ WINDOW | FEET_WINDOW | FACE | FEET | FEET_FACE_BALANCED | AUTO ]
// HeatedRearWindowActive string `json:"heatedRearWindowActive"` 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 . // ServiceRequest .
// "dataName": "remoteServiceStatus" // "dataName": "remoteServiceStatus"
type ServiceRequest struct { type ServiceRequest struct {
ServiceRequestID string `json:"serviceRequestId,omitempty"` // 4S4BTGND8L3137058_1640294426029_19_@NGTP 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 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 Cancelled bool `json:"cancelled"` // false | true
RemoteServiceType string `json:"remoteServiceType"` // vehicleStatus | condition | locate | unlock | lock | lightsOnly | engineStart | engineStop | phevChargeNow RemoteServiceType string `json:"remoteServiceType"` // vehicleStatus | condition | locate | unlock | lock | lightsOnly | engineStart | engineStop | phevChargeNow
@ -273,18 +391,29 @@ type ServiceRequest struct {
SubState string `json:"subState,omitempty"` // null SubState string `json:"subState,omitempty"` // null
ErrorCode string `json:"errorCode,omitempty"` // null:null ErrorCode string `json:"errorCode,omitempty"` // null:null
Result json.RawMessage `json:"result,omitempty"` // struct Result json.RawMessage `json:"result,omitempty"` // struct
UpdateTime int64 `json:"updateTime,omitempty"` // timestamp UpdateTime UnixTime `json:"updateTime,omitempty"` // timestamp // is empty if the request is started
Vin string `json:"vin"` // 4S4BTGND8L3137058
} }
// ErrorResponse . // parse parses the JSON response from the MySubaru API into a ServiceRequest struct.
// "dataName":"errorResponse" func (sr *ServiceRequest) parse(b []byte, logger *slog.Logger) error {
// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}} err := json.Unmarshal(b, &sr)
// {"success":false,"errorCode":"vehicleNotInAccount","dataName":null,"data":null} if err != nil {
// {"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"}} logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error())
type ErrorResponse struct { }
ErrorLabel string `json:"errorLabel"` // "404-soa-unableToParseResponseBody" if !sr.Success && sr.ErrorCode != "" {
ErrorDescription string `json:"errorDescription,omitempty"` // null logger.Error("error in response", "request", "GetVehicleCondition", "errorCode", sr.ErrorCode, "remoteServiceType", sr.RemoteServiceType)
switch sr.ErrorCode {
case API_ERRORS["API_ERROR_SERVICE_ALREADY_STARTED"]:
return errors.New("error in response: Service already started")
case API_ERRORS["API_ERROR_VEHICLE_NOT_IN_ACCOUNT"]:
return errors.New("error in response: Vehicle not in account")
case API_ERRORS["API_ERROR_SOA_403"]:
return errors.New("error in response: Unable to parse response body, SOA 403 error")
default:
return errors.New("error in response: " + sr.ErrorCode)
}
}
return nil
} }
// climateSettings: [ climateSettings ] // climateSettings: [ climateSettings ]
@ -305,10 +434,79 @@ type VehicleHealth struct {
LastUpdatedDate int64 `json:"lastUpdatedDate"` LastUpdatedDate int64 `json:"lastUpdatedDate"`
} }
type VehicleHealthItem struct { type VehicleHealthItem struct {
B2cCode string `json:"b2cCode"` WarningCode int `json:"warningCode"` // internal code used by MySubaru, not documented
FeatureCode string `json:"featureCode"` B2cCode string `json:"b2cCode"` // oilTemp | airbag | oilLevel | etc.
IsTrouble bool `json:"isTrouble"` FeatureCode string `json:"featureCode"` // SRS_MIL | CEL_MIL | ATF_MIL | etc.
OnDaiID int `json:"onDaiId"` // Has a number, probably id, but I couldn't find it purpose IsTrouble bool `json:"isTrouble"` // false | true
OnDates []int64 `json:"onDates,omitempty"` // List of the timestamps OnDaiID int `json:"onDaiId"` // Has a number, probably internal record id
WarningCode int `json:"warningCode"` OnDates []UnixTime `json:"onDates,omitempty"` // List of the timestamps
}
// ErrorResponse .
// "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 package mysubaru
import ( import (
"fmt"
"math" "math"
"net/mail"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -58,7 +61,7 @@ func vinCheck(vin string) (bool, string) {
return valid, retVin return valid, retVin
} }
// transcodeDigits . // transcodeDigits transcodes VIN digits to a numeric value
func transcodeDigits(vin string) int { func transcodeDigits(vin string) int {
var digitSum = 0 var digitSum = 0
var code int var code int
@ -114,30 +117,67 @@ func transcodeDigits(vin string) int {
return digitSum return digitSum
} }
// isNilFixed . // emailMasking takes an email address as input and returns a version of the email
// func isNil(i interface{}) bool { // with the username part partially hidden for privacy. Only the first and last
// if i == nil { // characters of the username are visible, with the middle characters replaced by asterisks.
// return true // The function validates the email format before processing.
// } // Returns the obfuscated email or an error if the input is not a valid email address.
// switch reflect.TypeOf(i).Kind() { func emailMasking(email string) (string, error) {
// case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: _, err := mail.ParseAddress(email)
// return reflect.ValueOf(i).IsNil() if err != nil {
// } return "", fmt.Errorf("invalid email address: %s", email)
// return false }
// }
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 . // timeTrack .
// func timeTrack(name string) { // func timeTrack(name string) {
// start := time.Now() // start := time.Now()
// fmt.Printf("%s took %v\n", name, time.Since(start)) // 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)
}
}
}

View File

@ -2,6 +2,7 @@ package mysubaru
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"regexp" "regexp"
@ -11,31 +12,7 @@ import (
"time" "time"
) )
// var parts = map[string]map[string][]string{ // Vehicle represents a Subaru vehicle with various attributes and methods to interact with it.
// "door": {
// "suffix": {"position", "status"},
// "position1": {"front", "rear", "boot", "enginehood"},
// "position2": {"right", "left"},
// },
// "window": {
// "suffix": {"status"},
// "position1": {"front", "rear", "sunroof"},
// "position2": {"right", "left"},
// },
// "tire": {
// "prefix": {"status"},
// "position1": {"front", "rear"},
// "position2": {"right", "left"},
// },
// "tyre": {
// "prefix": {"pressure"},
// "suffix": {"psi", "unit"},
// "position1": {"front", "rear"},
// "position2": {"right", "left"},
// },
// }
// Vehicle .
type Vehicle struct { type Vehicle struct {
CarId int64 CarId int64
Vin string // SELECT CAR REQUEST > "vin": "4S4BTGND8L3137058" Vin string // SELECT CAR REQUEST > "vin": "4S4BTGND8L3137058"
@ -87,35 +64,7 @@ type Vehicle struct {
} }
// ClimateProfile . // Door represents a door of a Subaru vehicle with its position, sub-position, status, and lock state.
type ClimateProfile struct {
Name string `json:"name,omitempty"`
VehicleType string `json:"vehicleType,omitempty"` // vehicleType [ gas | phev ]
PresetType string `json:"presetType,omitempty"` // presetType [ subaruPreset | userPreset ]
CanEdit bool `json:"canEdit,string,omitempty"` // canEdit [ false | true ]
Disabled bool `json:"disabled,string,omitempty"` // disabled [ false | true ]
RunTimeMinutes int `json:"runTimeMinutes,string"` // runTimeMinutes [ 5 | 10 ]
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"` // outerAirCirculation: [ outsideAir, recirculation ]
HeatedRearWindowActive bool `json:"heatedRearWindowActive,string"` // heatedRearWindowActive: [ false | true ]
AirConditionOn bool `json:"airConditionOn,string"` // airConditionOn: [ false | true ]
HeatedSeatFrontLeft string `json:"heatedSeatFrontLeft"` // heatedSeatFrontLeft: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ]
HeatedSeatFrontRight string `json:"heatedSeatFrontRight"` // heatedSeatFrontRight: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ]
StartConfiguration string `json:"startConfiguration"` // startConfiguration [ START_ENGINE_ALLOW_KEY_IN_IGNITION (gas) | START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION (phev) ]
}
// GeoLocation .
type GeoLocation struct {
Latitude float64 // 40.700184
Longitude float64 // -74.401375
Heading int // 189
Speed float64 // 0.00
Updated time.Time
}
// Door .
type Door struct { type Door struct {
Position string // front | rear | boot | enginehood Position string // front | rear | boot | enginehood
SubPosition string // right | left SubPosition string // right | left
@ -124,7 +73,7 @@ type Door struct {
Updated time.Time Updated time.Time
} }
// Window . // Window represents a window of a Subaru vehicle with its position, sub-position, status, and last updated time.
type Window struct { type Window struct {
Position string Position string
SubPosition string SubPosition string
@ -132,7 +81,7 @@ type Window struct {
Updated time.Time Updated time.Time
} }
// Tire . // Tire represents a tire of a Subaru vehicle with its position, sub-position, pressure, pressure in PSI, and last updated time.
type Tire struct { type Tire struct {
Position string Position string
SubPosition string SubPosition string
@ -142,7 +91,7 @@ type Tire struct {
// Status string // Status string
} }
// Trouble . // Trouble represents a trouble or issue with a Subaru vehicle, containing a description of the trouble.
type Trouble struct { type Trouble struct {
Description string Description string
} }
@ -210,232 +159,242 @@ func (v *Vehicle) String() string {
return vString return vString
} }
// Lock . // Lock
// Sends a command to lock doors. // Sends a command to lock doors.
func (v *Vehicle) Lock() { func (v *Vehicle) Lock() (chan string, error) {
if v.getRemoteOptionsStatus() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN, "pin": v.client.credentials.PIN,
"forceKeyInCar": "false"} "forceKeyInCar": "false"}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCK"], v.getAPIGen()) reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCK"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true)
} else { ch := make(chan string)
v.client.logger.Error("active STARLINK Security Plus subscription required") go func() {
} defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// Unlock . // Unlock
// Send command to unlock doors. // Send command to unlock doors.
func (v *Vehicle) Unlock() { func (v *Vehicle) Unlock() (chan string, error) {
if v.getRemoteOptionsStatus() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN, "pin": v.client.credentials.PIN,
"unlockDoorType": "ALL_DOORS_CMD"} // FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD "unlockDoorType": "ALL_DOORS_CMD"} // FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_UNLOCK"], v.getAPIGen()) reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_UNLOCK"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true)
} else { ch := make(chan string)
v.client.logger.Error("active STARLINK Security Plus subscription required") go func() {
} defer close(ch)
// ERROR v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
// {"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 return ch, nil
// {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}}
// if js_resp["errorCode"] == sc.ERROR_SOA_403:
// raise RemoteServiceFailure("Backend session expired, please try again")
// if js_resp["data"]["remoteServiceState"] == "finished":
// if js_resp["data"]["success"]:
// _LOGGER.info("Remote service request completed successfully: %s", req_id)
// return True, js_resp
// _LOGGER.error(
// "Remote service request completed but failed: %s Error: %s",
// req_id,
// js_resp["data"]["errorCode"],
// )
// raise RemoteServiceFailure(
// "Remote service request completed but failed: %s" % js_resp["data"]["errorCode"]
// )
// if js_resp["data"].get("remoteServiceState") == "started":
// _LOGGER.info(
// "Subaru API reports remote service request is in progress: %s",
// req_id,
// )
// attempts_left -= 1
// await asyncio.sleep(2)
} }
// EngineStart . // EngineStart
// Sends a command to start engine and set climate control. // Sends a command to start engine and set climate control.
func (v *Vehicle) EngineStart() { func (v *Vehicle) EngineStart(run, delay int, horn bool) (chan string, error) {
if v.getRemoteOptionsStatus() { if slices.Contains([]int{0, 1, 5, 10}, run) {
v.selectVehicle() return nil, errors.New("run time must be 0, 1, 5 or 10 minutes")
// TODO: Get Quick Climate Preset from the Currect Car }
var startConfig string
if v.EV {
startConfig = START_CONFIG_DEFAULT_EV
} else {
startConfig = START_CONFIG_DEFAULT_RES
}
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": strconv.Itoa(delay),
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN, "pin": v.client.credentials.PIN,
"horn": "true", "horn": strconv.FormatBool(horn),
"climateSettings": "climateSettings", // climateSettings "climateSettings": "climateSettings", // climateSettings
"climateZoneFrontTemp": "65", // 60-86 "climateZoneFrontTemp": "65", // 60-86
"climateZoneFrontAirMode": "WINDOW", // FEET_FACE_BALANCED | FEET_WINDOW | WINDOW | FEET "climateZoneFrontAirMode": "FEET_WINDOW", // FEET_FACE_BALANCED | FEET_WINDOW | WINDOW | FEET
"climateZoneFrontAirVolume": "6", // 1-7 "climateZoneFrontAirVolume": "7", // 1-7
"heatedSeatFrontLeft": "OFF", // OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT | low_cool | medium_cool | high_cool "heatedSeatFrontLeft": "HIGH_COOL", // OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT | LOW_COOL | MEDIUM_COOL | HIGH_COOL
"heatedSeatFrontRight": "OFF", // ---//--- "heatedSeatFrontRight": "HIGH_COOL", // ---//---
"heatedRearWindowActive": "true", // boolean "heatedRearWindowActive": "false", // boolean
"outerAirCirculation": "outsideAir", // outsideAir | recirculation "outerAirCirculation": "outsideAir", // outsideAir | recirculation
"airConditionOn": "false", // boolean "airConditionOn": "true", // boolean
"runTimeMinutes": "10", // 1-10 "runTimeMinutes": strconv.Itoa(run), // 1-10
"startConfiguration": START_CONFIG_DEFAULT_RES, // START_ENGINE_ALLOW_KEY_IN_IGNITION | ONLY FOR PHEV > START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION "startConfiguration": startConfig, // START_ENGINE_ALLOW_KEY_IN_IGNITION | ONLY FOR PHEV > START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION
}
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_START"]
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true)
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
} }
reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_START"]
pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// EngineStop . // EngineStop
// Sends a command to stop engine. // Sends a command to stop engine.
func (v *Vehicle) EngineStop() { func (v *Vehicle) EngineStop() (chan string, error) {
if v.getRemoteOptionsStatus() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_STOP"] reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_STOP"]
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true)
} else { ch := make(chan string)
v.client.logger.Error("Active STARLINK Security Plus subscription required") go func() {
} defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// LightsStart . // LightsStart
// Sends a command to flash lights. // Sends a command to flash lights.
func (v *Vehicle) LightsStart() { func (v *Vehicle) LightsStart() (chan string, error) {
if v.getRemoteOptionsStatus() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS"], v.getAPIGen()) reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
v.client.execute(reqURL, POST, params, pollingURL, true)
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
} }
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// LightsStop . // LightsStop
// Sends a command to stop flash lights. // Sends a command to stop flash lights.
func (v *Vehicle) LightsStop() { func (v *Vehicle) LightsStop() (chan string, error) {
if v.getRemoteOptionsStatus() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS_STOP"], v.getAPIGen()) reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS_STOP"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
v.client.execute(reqURL, POST, params, pollingURL, true)
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
} }
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// HornStart . // HornStart
// Send command to sound horn. // Send command to sound horn.
func (v *Vehicle) HornStart() { func (v *Vehicle) HornStart() (chan string, error) {
if v.getRemoteOptionsStatus() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS"], v.getAPIGen()) reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
v.client.execute(reqURL, POST, params, pollingURL, true)
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
} }
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// HornStop . // HornStop
// Send command to sound horn. // Send command to sound horn.
func (v *Vehicle) HornStop() { func (v *Vehicle) HornStop() (chan string, error) {
if v.getRemoteOptionsStatus() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS_STOP"], v.getAPIGen()) reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS_STOP"], v.getAPIGen())
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"] pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
v.client.execute(reqURL, POST, params, pollingURL, true)
} else {
v.client.logger.Error("Active STARLINK Security Plus subscription required")
} }
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// ChargeStart . // ChargeStart
func (v *Vehicle) ChargeOn() { func (v *Vehicle) ChargeOn() (chan string, error) {
if v.isEV() { if v.isEV() {
v.selectVehicle()
params := map[string]string{ params := map[string]string{
"delay": "0", "delay": "0",
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + apiURLs["API_EV_CHARGE_NOW"] reqUrl := MOBILE_API_VERSION + apiURLs["API_EV_CHARGE_NOW"]
pollingURL := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"] pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
v.client.execute(reqURL, POST, params, pollingURL, true) ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
return nil, errors.New("not an EV car")
} }
// GetLocation . // GetLocation
func (v *Vehicle) GetLocation(force bool) { func (v *Vehicle) GetLocation(force bool) (chan string, error) {
var reqUrl, pollingUrl string
var params map[string]string
if force { // Sends a locate command to the vehicle to get real time position if force { // Sends a locate command to the vehicle to get real time position
v.selectVehicle() reqUrl = MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_UPDATE"]
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_UPDATE"] pollingUrl = MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_STATUS"]
pollingURL := MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_STATUS"] params = map[string]string{
params := map[string]string{
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
if v.getAPIGen() == FEATURE_G1_TELEMATICS { if v.getAPIGen() == FEATURE_G1_TELEMATICS {
reqURL = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_UPDATE"] reqUrl = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_UPDATE"]
pollingURL = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_STATUS"] pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_STATUS"]
} }
v.client.execute(reqURL, POST, params, pollingURL, true)
} else { // Reports the last location the vehicle has reported to Subaru } else { // Reports the last location the vehicle has reported to Subaru
v.selectVehicle() params = map[string]string{
params := map[string]string{
"vin": v.Vin, "vin": v.Vin,
"pin": v.client.credentials.PIN} "pin": v.client.credentials.PIN}
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen()) reqUrl = MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen())
v.client.execute(reqURL, GET, params, "", false)
} }
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
} }
// GetClimatePresets connects to the MySubaru API to download available climate presets. // GetClimatePresets connects to the MySubaru API to download available climate presets.
@ -443,16 +402,26 @@ func (v *Vehicle) GetLocation(force bool) {
// If successful and climate presets are found for the user's vehicle, // If successful and climate presets are found for the user's vehicle,
// it downloads them. If no presets are available, or if the connection fails, // it downloads them. If no presets are available, or if the connection fails,
// appropriate handling should be implemented within the function. // appropriate handling should be implemented within the function.
func (v *Vehicle) GetClimatePresets() { func (v *Vehicle) GetClimatePresets() error {
if v.getRemoteOptionsStatus() { if !v.getRemoteOptionsStatus() {
v.selectVehicle() v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"] return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) }
if r, ok := v.client.parseResponse(resp); ok { // Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != (v.client).currentVin {
v.selectVehicle()
}
reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"]
resp, _ := v.client.execute(GET, reqUrl, map[string]string{}, false)
re1 := regexp.MustCompile(`\"`) re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.Data), "") result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`) re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022 result = re2.ReplaceAllString(result, `"`) // \u0022
@ -494,25 +463,32 @@ func (v *Vehicle) GetClimatePresets() {
v.client.logger.Debug("couldn't find any subaru climate presets") v.client.logger.Debug("couldn't find any subaru climate presets")
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
} }
// GetClimateQuickPresets // GetClimateQuickPresets
// Used while user uses "quick start engine" button in the app // Used while user uses "quick start engine" button in the app
func (v *Vehicle) GetClimateQuickPresets() { func (v *Vehicle) GetClimateQuickPresets() error {
if v.getRemoteOptionsStatus() { if !v.getRemoteOptionsStatus() {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
}
// Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"] }
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"]
resp, _ := v.client.execute(GET, reqUrl, map[string]string{}, false)
// v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp) // v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
re1 := regexp.MustCompile(`\"`) re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.Data), "") result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`) re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022 result = re2.ReplaceAllString(result, `"`) // \u0022
@ -531,23 +507,71 @@ func (v *Vehicle) GetClimateQuickPresets() {
v.ClimateProfiles[cpn] = cp v.ClimateProfiles[cpn] = cp
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
} }
// GetClimateUserPresets . // UpdateClimateQuickPresets
func (v *Vehicle) GetClimateUserPresets() { // Updates the quick climate presets by fetching them from the MySubaru API.
if v.getRemoteOptionsStatus() { // {"success":true,"data":null}
v.selectVehicle() func (v *Vehicle) UpdateClimateQuickPresets() error {
reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_USER_PRESETS"] if !v.getRemoteOptionsStatus() {
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
}
if r, ok := v.client.parseResponse(resp); ok { // Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != (v.client).currentVin {
v.selectVehicle()
}
params := map[string]string{
"name": "Cooling",
"runTimeMinutes": "10",
"climateSettings": "climateSettings", // climateSettings
"climateZoneFrontTemp": "65", // 60-86
"climateZoneFrontAirMode": "FEET_WINDOW", // FEET_FACE_BALANCED | FEET_WINDOW | WINDOW | FEET
"climateZoneFrontAirVolume": "7", // 1-7
"heatedSeatFrontLeft": "HIGH_COOL", // OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT | LOW_COOL | MEDIUM_COOL | HIGH_COOL
"heatedSeatFrontRight": "HIGH_COOL", // ---//---
"heatedRearWindowActive": "false", // boolean
"outerAirCirculation": "outsideAir", // outsideAir | recirculation
"airConditionOn": "false", // boolean
"startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION", // START_ENGINE_ALLOW_KEY_IN_IGNITION | ONLY FOR PHEV > START_CLIMATE_CONTROL_ONLY_ALLOW_KEY_IN_IGNITION
}
reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_SAVE_RES_QUICK_START_SETTINGS"]
resp, _ := v.client.execute(POST, reqUrl, params, true)
v.client.logger.Debug("http request output", "request", "UpdateClimateUserPresets", "body", resp)
return nil
}
// GetClimateUserPresets
func (v *Vehicle) GetClimateUserPresets() error {
if !v.getRemoteOptionsStatus() {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
}
// Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != (v.client).currentVin {
v.selectVehicle()
}
reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_USER_PRESETS"]
resp, _ := v.client.execute(GET, reqUrl, map[string]string{}, false)
re1 := regexp.MustCompile(`\"`) re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.Data), "") result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`) re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022 result = re2.ReplaceAllString(result, `"`) // \u0022
@ -576,22 +600,79 @@ func (v *Vehicle) GetClimateUserPresets() {
v.client.logger.Debug("couldn't find any user climate presets") v.client.logger.Debug("couldn't find any user climate presets")
} }
v.Updated = time.Now() v.Updated = time.Now()
return nil
}
// UpdateClimateUserPresets
// Updates the user's climate presets by fetching them from the MySubaru API.
func (v *Vehicle) UpdateClimateUserPresets() error {
if !v.getRemoteOptionsStatus() {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
} }
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required") // Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
} }
if v.Vin != (v.client).currentVin {
v.selectVehicle()
}
params := map[string]string{
"presetType": "userPreset",
"name": "Cooling",
"runTimeMinutes": "10",
"climateZoneFrontTemp": "65",
"climateZoneFrontAirMode": "FEET_FACE_BALANCED",
"climateZoneFrontAirVolume": "7",
"outerAirCirculation": "outsideAir",
"heatedRearWindowActive": "false",
"heatedSeatFrontLeft": "HIGH_COOL",
"airConditionOn": "false",
"startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION",
// "canEdit": "true",
// "disabled": "false",
}
reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_SAVE_RES_SETTINGS"]
resp, _ := v.client.execute(POST, reqUrl, params, false)
v.client.logger.Debug("http request output", "request", "UpdateClimateUserPresets", "body", resp)
return nil
}
func (v *Vehicle) DeleteClimateUserPresets() error {
return nil
} }
// GetVehicleStatus . // GetVehicleStatus .
func (v *Vehicle) GetVehicleStatus() { func (v *Vehicle) GetVehicleStatus() error {
v.selectVehicle() if !v.getRemoteOptionsStatus() {
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_VEHICLE_STATUS"], v.getAPIGen()) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
// v.client.logger.Debug("http request output", "request", "GetVehicleStatus", "body", resp) }
// Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != (v.client).currentVin {
v.selectVehicle()
}
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_VEHICLE_STATUS"], v.getAPIGen())
resp, err := v.client.execute(GET, reqUrl, map[string]string{}, false)
if err != nil {
v.client.logger.Error("error while executing GetVehicleStatus request", "request", "GetVehicleStatus", "error", err.Error())
return err
}
// v.client.logger.Info("http request output", "request", "GetVehicleStatus", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var vs VehicleStatus var vs VehicleStatus
err := json.Unmarshal(r.Data, &vs) err = json.Unmarshal(resp.Data, &vs)
if err != nil { if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error()) v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error())
} }
@ -631,19 +712,31 @@ func (v *Vehicle) GetVehicleStatus() {
} }
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} }
// GetVehicleCondition . // GetVehicleCondition .
func (v *Vehicle) GetVehicleCondition() { func (v *Vehicle) GetVehicleCondition() error {
v.selectVehicle() if !v.getRemoteOptionsStatus() {
reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_CONDITION"], v.getAPIGen()) v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
// v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp) }
// Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != (v.client).currentVin {
v.selectVehicle()
}
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_CONDITION"], v.getAPIGen())
resp, _ := v.client.execute(GET, reqUrl, map[string]string{}, false)
// v.client.logger.Info("http request output", "request", "GetVehicleCondition", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var sr ServiceRequest var sr ServiceRequest
err := json.Unmarshal(r.Data, &sr) err := json.Unmarshal(resp.Data, &sr)
if err != nil { if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error())
} }
@ -675,22 +768,35 @@ func (v *Vehicle) GetVehicleCondition() {
} }
} }
v.Updated = time.Now() v.Updated = time.Now()
} return nil
} }
// GetVehicleHealth . // GetVehicleHealth
func (v *Vehicle) GetVehicleHealth() { // Retrieves the vehicle health status from MySubaru API.
func (v *Vehicle) GetVehicleHealth() error {
if !v.getRemoteOptionsStatus() {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
}
// Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != (v.client).currentVin {
v.selectVehicle() v.selectVehicle()
}
params := map[string]string{ params := map[string]string{
"vin": v.Vin, "vin": v.Vin,
"_": timestamp()} "_": timestamp()}
reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_HEALTH"] reqUrl := MOBILE_API_VERSION + apiURLs["API_VEHICLE_HEALTH"]
resp := v.client.execute(reqURL, GET, params, "", false) resp, _ := v.client.execute(GET, reqUrl, params, false)
// v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "body", resp) // v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var vh VehicleHealth var vh VehicleHealth
err := json.Unmarshal(r.Data, &vh) err := json.Unmarshal(resp.Data, &vh)
if err != nil { if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error()) v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error())
} }
@ -708,9 +814,7 @@ func (v *Vehicle) GetVehicleHealth() {
} }
} }
} }
} else { return nil
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
} }
// GetFeaturesList . // GetFeaturesList .
@ -724,16 +828,106 @@ func (v *Vehicle) GetFeaturesList() {
} }
} }
// executeServiceRequest
// Executes a service request to the Subaru API and handles the response.
func (v *Vehicle) executeServiceRequest(params map[string]string, reqUrl, pollingUrl string, ch chan string, attempt int) error {
var maxAttempts = 15
if attempt >= maxAttempts {
v.client.logger.Error("maximum attempts reached for service request", "request", reqUrl, "attempts", attempt)
ch <- "error"
return errors.New("maximum attempts reached for service request")
}
// Check if the vehicle has a valid subscription for remote services
if !v.getRemoteOptionsStatus() {
v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"])
}
// Validate session before executing the request
if !v.client.validateSession() {
v.client.logger.Error(APP_ERRORS["SESSION_EXPIRED"])
return errors.New(APP_ERRORS["SESSION_EXPIRED"])
}
if v.Vin != v.client.currentVin {
v.selectVehicle()
}
var resp *Response
var err error
if attempt == 1 {
resp, err = v.client.execute(POST, reqUrl, params, true)
if err != nil {
v.client.logger.Error("error while executing service request", "request", reqUrl, "error", err.Error())
ch <- "error"
return err
}
} else {
resp, err = v.client.execute(GET, pollingUrl, params, false)
if err != nil {
v.client.logger.Error("error while executing service request status polling", "request", reqUrl, "error", err.Error())
ch <- "error"
return err
}
}
// dataName field has the list of the states [ remoteServiceStatus | errorResponse ]
if resp.DataName == "remoteServiceStatus" {
if sr, ok := v.parseServiceRequest([]byte(resp.Data)); ok {
ch <- sr.RemoteServiceState
switch sr.RemoteServiceState {
case "finished":
// Finished RemoteServiceState Service Request does not include Service Request ID
v.client.logger.Debug("Remote service request completed successfully")
case "started":
time.Sleep(5 * time.Second)
v.client.logger.Debug("MySubaru API reports remote service request (started) is in progress", "id", sr.ServiceRequestID)
v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1)
case "stopping":
time.Sleep(5 * time.Second)
v.client.logger.Debug("MySubaru API reports remote service request (stopping) is in progress", "id", sr.ServiceRequestID)
v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1)
default:
v.client.logger.Debug("MySubaru API reports remote service request (default)")
v.executeServiceRequest(map[string]string{"serviceRequestId": sr.ServiceRequestID}, reqUrl, pollingUrl, ch, attempt+1)
}
return nil
}
v.client.logger.Error("error while parsing service request json", "request", reqUrl, "response", resp.Data)
return errors.New("error while parsing service request json")
}
return errors.New("response is not a service request")
}
// parseServiceRequest .
func (v *Vehicle) parseServiceRequest(b []byte) (ServiceRequest, bool) {
var sr ServiceRequest
err := json.Unmarshal(b, &sr)
if err != nil {
v.client.logger.Error("error while parsing service request json", "error", err.Error())
return sr, false
}
return sr, true
}
// selectVehicle . // selectVehicle .
func (v *Vehicle) selectVehicle() { func (v *Vehicle) selectVehicle() {
if v.client.currentVin != v.Vin { if v.client.currentVin != v.Vin {
vData := (*v.client).SelectVehicle(v.Vin) vData, err := (v.client).SelectVehicle(v.Vin)
if err != nil {
v.client.logger.Debug("cannot get vehicle data")
}
v.SubscriptionStatus = vData.SubscriptionStatus v.SubscriptionStatus = vData.SubscriptionStatus
v.GeoLocation.Latitude = vData.VehicleGeoPosition.Latitude v.GeoLocation.Latitude = vData.VehicleGeoPosition.Latitude
v.GeoLocation.Longitude = vData.VehicleGeoPosition.Longitude v.GeoLocation.Longitude = vData.VehicleGeoPosition.Longitude
v.GeoLocation.Heading = vData.VehicleGeoPosition.Heading v.GeoLocation.Heading = vData.VehicleGeoPosition.Heading
v.GeoLocation.Speed = vData.VehicleGeoPosition.Speed v.GeoLocation.Speed = vData.VehicleGeoPosition.Speed
v.GeoLocation.Updated = time.Now() v.GeoLocation.Updated = vData.VehicleGeoPosition.Timestamp
v.Updated = time.Now() v.Updated = time.Now()
} }
} }
@ -753,12 +947,6 @@ func (v *Vehicle) getAPIGen() string {
return "unknown" return "unknown"
} }
// isPINRequired .
// Return if a vehicle with an active remote service subscription exists.
// func (v *Vehicle) isPINRequired() bool {
// return v.getRemoteOptionsStatus()
// }
// isEV . // isEV .
// Get whether the specified car is an Electric Vehicle. // Get whether the specified car is an Electric Vehicle.
func (v *Vehicle) isEV() bool { func (v *Vehicle) isEV() bool {
@ -774,7 +962,7 @@ func (v *Vehicle) getRemoteOptionsStatus() bool {
// parseDoor . // parseDoor .
func (v *Vehicle) parseParts(name string, value any) { func (v *Vehicle) parseParts(name string, value any) {
// re := regexp.MustCompile(`[A-Z][^A-Z]*`) // re := regexp.MustCompile(`[A-Z][^A-Z]*`)
re := regexp.MustCompile(`([Dd]oor|[Ww]indow|[Tt]ire)(?:[Pp]ressure)?([Ff]ront|[Rr]ear|[Bb]oot|[Ee]ngine[Hh]ood|[Ss]unroof)([Ll]eft|[Rr]ight)?(?:[Pp]osition|[Ss]tatus|[Ll]ock[Ss]tatus|[Pp]si)?`) re := regexp.MustCompile(`([Dd]oor|[Ww]indow|[Tt]ire)(?:[Pp]ressure)?([Ff]ront|[Rr]ear|[Bb]oot|[Ee]ngine[Hh]ood|[Ss]unroof)([Ll]eft|[Rr]ight)?([Pp]osition|[Ss]tatus|[Ll]ock[Ss]tatus|[Pp]si)?`)
grps := re.FindStringSubmatch(name) grps := re.FindStringSubmatch(name)
pn := grps[1] + "_" + grps[2] pn := grps[1] + "_" + grps[2]
@ -876,27 +1064,3 @@ func (v *Vehicle) parseParts(name string, value any) {
// func (v *Vehicle) getSubscriptionStatus() bool { // func (v *Vehicle) getSubscriptionStatus() bool {
// return slices.Contains(v.SubscriptionFeatures, FEATURE_ACTIVE) // return slices.Contains(v.SubscriptionFeatures, FEATURE_ACTIVE)
// } // }
// // getVehicleName .
// // Get the nickname of a specified VIN.
// func (v *Vehicle) getVehicleName() string {
// return v.CarName
// }
// func getClimateData() {}
// func saveClimateSettings() {}
// "vhsId": 914631252,
// "odometerValue": 23865,
// "odometerValueKilometers": 38399,
// "tirePressureFrontLeft": "2344",
// "tirePressureFrontRight": "2344",
// "tirePressureRearLeft": "2413",
// "tirePressureRearRight": "2344",
// "tirePressureFrontLeftPsi": "34",
// "tirePressureFrontRightPsi": "34",
// "tirePressureRearLeftPsi": "35",
// "tirePressureRearRightPsi": "34",

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)
}
}