Files
mysubaru/vehicle.go
Alex Savin d7944123dd
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 22s
Update climate control settings for improved performance and user comfort
2025-07-21 19:12:42 -04:00

1063 lines
36 KiB
Go

package mysubaru
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"time"
)
// Vehicle represents a Subaru vehicle with various attributes and methods to interact with it.
type Vehicle struct {
CarId int64
Vin string // SELECT CAR REQUEST > "vin": "4S4BTGND8L3137058"
CarName string // SELECT CAR REQUEST > "vehicleName": "Subaru Outback LXT"
CarNickname string // SELECT CAR REQUEST > "nickname": "Subaru Outback LXT"
ExtDescrip string // SELECT CAR REQUEST > "extDescrip": "Abyss Blue Pearl"
IntDescrip string // SELECT CAR REQUEST > "intDescrip": "Gray"
ModelName string // SELECT CAR REQUEST > "modelName": "Outback",
ModelYear string // SELECT CAR REQUEST > "modelYear": "2020"
ModelCode string // SELECT CAR REQUEST > "modelCode": "LDJ"
TransCode string // SELECT CAR REQUEST > "transCode": "CVT"
EngineSize float64 // SELECT CAR REQUEST > "engineSize": 2.4
VehicleKey int64 // SELECT CAR REQUEST > "vehicleKey": 3832950
EV bool // SELECT CAR REQUEST >
LicensePlate string // SELECT CAR REQUEST > "licensePlate": "8KV8"
LicensePlateState string // SELECT CAR REQUEST > "licensePlateState": "NJ"
Features []string // SELECT CAR REQUEST > "features": ["ATF_MIL","11.6MMAN","ABS_MIL","CEL_MIL","ACCS","RCC","REARBRK","TEL_MIL","VDC_MIL","TPMS_MIL","WASH_MIL","BSDRCT_MIL","OPL_MIL","EYESIGHT","RAB_MIL","SRS_MIL","ESS_MIL","RESCC","EOL_MIL","BSD","EBD_MIL","EPB_MIL","RES","RHSF","AWD_MIL","NAV_TOMTOM","ISS_MIL","RPOIA","EPAS_MIL","RPOI","AHBL_MIL","SRH_MIL","g2"],
SubscriptionFeatures []string // SELECT CAR REQUEST > "subscriptionFeatures": ["REMOTE","SAFETY","Retail"]
SubscriptionStatus string // SELECT CAR REQUEST > "subscriptionStatus": "ACTIVE"
EngineState string // STATUS REQUEST > "vehicleStateType": "IGNITION_OFF"
Odometer struct {
Miles int // STATUS REQUEST > "odometerValue": 24999
Kilometers int // STATUS REQUEST > "odometerValueKilometers": 40223
}
DistanceToEmpty struct {
Miles int // STATUS REQUEST > "distanceToEmptyFuelMiles": 149.75
Kilometers int // STATUS REQUEST > "distanceToEmptyFuelKilometers": 241
Miles10s int // STATUS REQUEST > "distanceToEmptyFuelMiles10s": 150
Kilometers10s int // STATUS REQUEST > "distanceToEmptyFuelKilometers10s": 240
Percentage int // > "remainingFuelPercent": 66
}
FuelConsumptionAvg struct {
MPG float64 // STATUS REQUEST > "avgFuelConsumptionMpg": 18.5
LP100Km float64 // STATUS REQUEST > "avgFuelConsumptionLitersPer100Kilometers": 12.7
}
ClimateProfiles map[string]ClimateProfile
Doors map[string]Door // CONDITION REQUEST >
Windows map[string]Window // CONDITION REQUEST >
Tires map[string]Tire // CONDITION AND STATUS REQUEST >
Troubles map[string]Trouble //
GeoLocation GeoLocation
Updated time.Time
client *Client
// "evStateOfChargePercent": null,
// "evDistanceToEmptyMiles": null,
// "evDistanceToEmptyKilometers": null,
// "evDistanceToEmptyByStateMiles": null,
// "evDistanceToEmptyByStateKilometers": null,
}
// Door represents a door of a Subaru vehicle with its position, sub-position, status, and lock state.
type Door struct {
Position string // front | rear | boot | enginehood
SubPosition string // right | left
Status string // CLOSED | OPEN
Lock string // LOCKED | UNLOCKED
Updated time.Time
}
// Window represents a window of a Subaru vehicle with its position, sub-position, status, and last updated time.
type Window struct {
Position string
SubPosition string
Status string // CLOSE | VENTED | OPEN
Updated time.Time
}
// Tire represents a tire of a Subaru vehicle with its position, sub-position, pressure, pressure in PSI, and last updated time.
type Tire struct {
Position string
SubPosition string
Pressure int
PressurePsi int
Updated time.Time
// Status string
}
// Trouble represents a trouble or issue with a Subaru vehicle, containing a description of the trouble.
type Trouble struct {
Description string
}
func (v *Vehicle) String() string {
var vString string
vString += "=== INFORMATION =====================\n"
vString += "Nickname: " + v.CarNickname + "\n"
vString += "Car Name: " + v.CarName + "\n"
vString += "Model: " + v.ModelName + "\n"
vString += "=== ODOMETER =====================\n"
vString += "Miles: " + strconv.Itoa(v.Odometer.Miles) + "\n"
vString += "Kilometers: " + strconv.Itoa(v.Odometer.Kilometers) + "\n"
vString += "=== DISTANCE TO EMPTY =====================\n"
vString += "Miles: " + strconv.Itoa(v.DistanceToEmpty.Miles) + "\n"
vString += "Kilometers: " + strconv.Itoa(v.DistanceToEmpty.Kilometers) + "\n"
vString += "=== FUEL =============================\n"
vString += "Tank (%): " + fmt.Sprintf("%v", v.DistanceToEmpty.Percentage) + "\n"
vString += "MPG: " + fmt.Sprintf("%v", v.FuelConsumptionAvg.MPG) + "\n"
vString += "Litres per 100 km: " + fmt.Sprintf("%v", v.FuelConsumptionAvg.LP100Km) + "\n"
vString += "=== GPS LOCATION ==============\n"
vString += "Lantitude: " + fmt.Sprintf("%v", v.GeoLocation.Latitude) + "\n"
vString += "Longitude: " + fmt.Sprintf("%v", v.GeoLocation.Longitude) + "\n"
vString += "Heading: " + fmt.Sprintf("%v", v.GeoLocation.Heading) + "\n"
vString += "=== WINDOWS ===================\n"
for k, v := range v.Windows {
vString += fmt.Sprintf("%s >> %+v\n", k, v)
}
vString += "=== DOORS =====================\n"
for k, v := range v.Doors {
vString += fmt.Sprintf("%s >> %+v\n", k, v)
}
vString += "=== TIRES =====================\n"
for k, v := range v.Tires {
vString += fmt.Sprintf("%s >> %+v\n", k, v)
}
vString += "=== CLIMATE PROFILES ==========\n"
for k, v := range v.ClimateProfiles {
vString += fmt.Sprintf("%s >> %+v\n", k, v)
}
vString += "=== TROUBLES =====================\n"
for k, v := range v.Troubles {
vString += fmt.Sprintf("%s >> %+v\n", k, v)
}
vString += "=== FEATURES =====================\n"
for i, f := range v.Features {
if !strings.HasSuffix(f, "_MIL") {
if _, ok := features[f]; ok {
vString += fmt.Sprintf("%d >> %+v || %s\n", i+1, f, features[f])
} else {
vString += fmt.Sprintf("%d >> %+v\n", i+1, f)
}
}
}
return vString
}
// Lock
// Sends a command to lock doors.
func (v *Vehicle) Lock() (chan string, error) {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN,
"forceKeyInCar": "false"}
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCK"], v.getAPIGen())
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
}
// Unlock
// Send command to unlock doors.
func (v *Vehicle) Unlock() (chan string, error) {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN,
"unlockDoorType": "ALL_DOORS_CMD"} // FRONT_LEFT_DOOR_CMD | ALL_DOORS_CMD
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_UNLOCK"], v.getAPIGen())
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
}
// EngineStart
// Sends a command to start engine and set climate control.
func (v *Vehicle) EngineStart(run, delay int, horn bool) (chan string, error) {
if slices.Contains([]int{0, 1, 5, 10}, run) {
return nil, errors.New("run time must be 0, 1, 5 or 10 minutes")
}
var startConfig string
if v.EV {
startConfig = START_CONFIG_DEFAULT_EV
} else {
startConfig = START_CONFIG_DEFAULT_RES
}
params := map[string]string{
"delay": strconv.Itoa(delay),
"vin": v.Vin,
"pin": v.client.credentials.PIN,
"horn": strconv.FormatBool(horn),
"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": "true", // boolean
"runTimeMinutes": strconv.Itoa(run), // 1-10
"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"]
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
}
// EngineStop
// Sends a command to stop engine.
func (v *Vehicle) EngineStop() (chan string, error) {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN}
reqUrl := MOBILE_API_VERSION + apiURLs["API_G2_REMOTE_ENGINE_STOP"]
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
}
// LightsStart
// Sends a command to flash lights.
func (v *Vehicle) LightsStart() (chan string, error) {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN}
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS"], v.getAPIGen())
pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
}
// LightsStop
// Sends a command to stop flash lights.
func (v *Vehicle) LightsStop() (chan string, error) {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN}
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_LIGHTS_STOP"], v.getAPIGen())
pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
}
// HornStart
// Send command to sound horn.
func (v *Vehicle) HornStart() (chan string, error) {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN}
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS"], v.getAPIGen())
pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
}
// HornStop
// Send command to sound horn.
func (v *Vehicle) HornStop() (chan string, error) {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN}
reqUrl := MOBILE_API_VERSION + urlToGen(apiURLs["API_HORN_LIGHTS_STOP"], v.getAPIGen())
pollingUrl := MOBILE_API_VERSION + apiURLs["API_REMOTE_SVC_STATUS"]
if v.getAPIGen() == FEATURE_G1_TELEMATICS {
pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_HORN_LIGHTS_STATUS"]
}
ch := make(chan string)
go func() {
defer close(ch)
v.executeServiceRequest(params, reqUrl, pollingUrl, ch, 1)
}()
return ch, nil
}
// ChargeStart
func (v *Vehicle) ChargeOn() (chan string, error) {
if v.isEV() {
params := map[string]string{
"delay": "0",
"vin": v.Vin,
"pin": v.client.credentials.PIN}
reqUrl := MOBILE_API_VERSION + apiURLs["API_EV_CHARGE_NOW"]
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
}
return nil, errors.New("not an EV car")
}
// GetLocation
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
reqUrl = MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_UPDATE"]
pollingUrl = MOBILE_API_VERSION + apiURLs["API_G2_LOCATE_STATUS"]
params = map[string]string{
"vin": v.Vin,
"pin": v.client.credentials.PIN}
if v.getAPIGen() == FEATURE_G1_TELEMATICS {
reqUrl = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_UPDATE"]
pollingUrl = MOBILE_API_VERSION + apiURLs["API_G1_LOCATE_STATUS"]
}
} else { // Reports the last location the vehicle has reported to Subaru
params = map[string]string{
"vin": v.Vin,
"pin": v.client.credentials.PIN}
reqUrl = MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen())
}
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.
// It first attempts to establish a connection with the MySubaru API.
// 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,
// appropriate handling should be implemented within the function.
func (v *Vehicle) GetClimatePresets() 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_SUBARU_PRESETS"]
resp, _ := v.client.execute(GET, reqUrl, map[string]string{}, false)
re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022
var cProfiles []ClimateProfile
err := json.Unmarshal([]byte(result), &cProfiles)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error())
}
if len(cProfiles) > 0 {
for _, cp := range cProfiles {
re := regexp.MustCompile(`([A-Z])`)
cpn := strings.ToLower(re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_"))
if v.isEV() && cp.VehicleType == "phev" {
if _, ok := v.ClimateProfiles[cpn]; ok {
v.ClimateProfiles[cpn] = cp
} else {
if _, ok := v.ClimateProfiles[cpn]; ok {
v.ClimateProfiles[cpn] = cp
} else {
v.ClimateProfiles[cpn] = cp
}
}
}
if !v.isEV() && cp.VehicleType == "gas" {
if _, ok := v.ClimateProfiles[cpn]; ok {
v.ClimateProfiles[cpn] = cp
} else {
if _, ok := v.ClimateProfiles[cpn]; ok {
v.ClimateProfiles[cpn] = cp
} else {
v.ClimateProfiles[cpn] = cp
}
}
}
}
} else {
v.client.logger.Debug("couldn't find any subaru climate presets")
}
v.Updated = time.Now()
return nil
}
// GetClimateQuickPresets
// Used while user uses "quick start engine" button in the app
func (v *Vehicle) GetClimateQuickPresets() 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_QUICK_START_SETTINGS"]
resp, _ := v.client.execute(GET, reqUrl, map[string]string{}, false)
// v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp)
re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022
var cp ClimateProfile
err := json.Unmarshal([]byte(result), &cp)
if err != nil {
v.client.logger.Error("error while parsing climate quick presets json", "request", "GetClimateQuickPresets", "error", err.Error())
}
re := regexp.MustCompile(`([A-Z])`)
cpn := strings.ToLower("quick_" + re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_"))
if _, ok := v.ClimateProfiles[cpn]; ok {
v.ClimateProfiles[cpn] = cp
} else {
v.ClimateProfiles[cpn] = cp
}
v.Updated = time.Now()
return nil
}
// UpdateClimateQuickPresets
// Updates the quick climate presets by fetching them from the MySubaru API.
// {"success":true,"data":null}
func (v *Vehicle) UpdateClimateQuickPresets() 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()
}
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(`\"`)
result := re1.ReplaceAllString(string(resp.Data), "")
re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022
var cProfiles []ClimateProfile
err := json.Unmarshal([]byte(result), &cProfiles)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetClimateUserPresets", "error", err.Error())
}
if len(cProfiles) > 0 {
for _, cp := range cProfiles {
re := regexp.MustCompile(`([A-Z])`)
cpn := strings.ToLower(re.ReplaceAllString(cp.PresetType, "_$1") + "_" + strings.ReplaceAll(cp.Name, " ", "_"))
if _, ok := v.ClimateProfiles[cpn]; ok {
v.ClimateProfiles[cpn] = cp
} else {
if _, ok := v.ClimateProfiles[cpn]; ok {
v.ClimateProfiles[cpn] = cp
} else {
v.ClimateProfiles[cpn] = cp
}
}
}
} else {
v.client.logger.Debug("couldn't find any user climate presets")
}
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"])
}
// 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
}
// GetVehicleStatus .
func (v *Vehicle) GetVehicleStatus() 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 + 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)
var vs VehicleStatus
err = json.Unmarshal(resp.Data, &vs)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error())
}
// v.client.logger.Debug("http request output", "request", "GetVehicleStatus", "body", vs)
v.EngineState = vs.VehicleStateType
v.Odometer.Miles = vs.OdometerValue
v.Odometer.Kilometers = vs.OdometerValueKm
v.DistanceToEmpty.Miles = int(vs.DistanceToEmptyFuelMiles)
v.DistanceToEmpty.Kilometers = vs.DistanceToEmptyFuelKilometers
v.DistanceToEmpty.Miles10s = vs.DistanceToEmptyFuelMiles10s
v.DistanceToEmpty.Kilometers10s = vs.DistanceToEmptyFuelKilometers10s
if vs.RemainingFuelPercent > 0 && vs.RemainingFuelPercent <= 101 {
v.DistanceToEmpty.Percentage = vs.RemainingFuelPercent
}
v.FuelConsumptionAvg.MPG = float64(vs.AvgFuelConsumptionMpg)
v.FuelConsumptionAvg.LP100Km = float64(vs.AvgFuelConsumptionLitersPer100Kilometers)
v.GeoLocation.Latitude = float64(vs.Latitude)
v.GeoLocation.Longitude = float64(vs.Longitude)
v.GeoLocation.Heading = vs.Heading
val := reflect.ValueOf(vs)
typeOfS := val.Type()
for i := range val.NumField() {
// v.client.logger.Debug("vehicle status >> parsing a car part", "field", typeOfS.Field(i).Name, "value", val.Field(i).Interface(), "type", val.Field(i).Type())
if slices.Contains(badValues, val.Field(i).Interface()) {
continue
} else {
if strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "Position") ||
strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "LockStatus") ||
strings.HasPrefix(typeOfS.Field(i).Name, "Window") && strings.HasSuffix(typeOfS.Field(i).Name, "Status") ||
strings.HasPrefix(typeOfS.Field(i).Name, "TirePressure") && strings.HasSuffix(typeOfS.Field(i).Name, "Psi") {
v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface())
}
}
}
v.Updated = time.Now()
return nil
}
// GetVehicleCondition .
func (v *Vehicle) GetVehicleCondition() 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 + 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)
var sr ServiceRequest
err := json.Unmarshal(resp.Data, &sr)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error())
}
// v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp)
var vc VehicleCondition
err = json.Unmarshal(sr.Result, &vc)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error())
}
// v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp)
val := reflect.ValueOf(vc)
typeOfS := val.Type()
for i := range val.NumField() {
// v.client.logger.Debug("vehicle condition >> parsing a car part", "field", typeOfS.Field(i).Name, "value", val.Field(i).Interface(), "type", val.Field(i).Type())
if slices.Contains(badValues, val.Field(i).Interface()) {
continue
} else {
if strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "Position") ||
strings.HasPrefix(typeOfS.Field(i).Name, "Door") && strings.HasSuffix(typeOfS.Field(i).Name, "LockStatus") ||
strings.HasPrefix(typeOfS.Field(i).Name, "Window") && strings.HasSuffix(typeOfS.Field(i).Name, "Status") {
v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface())
}
// if strings.HasPrefix(typeOfS.Field(i).Name, "TirePressure") {
// v.parseParts(typeOfS.Field(i).Name, val.Field(i).Interface())
// }
}
}
v.Updated = time.Now()
return nil
}
// 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()
}
params := map[string]string{
"vin": v.Vin,
"_": timestamp()}
reqUrl := MOBILE_API_VERSION + apiURLs["API_VEHICLE_HEALTH"]
resp, _ := v.client.execute(GET, reqUrl, params, false)
// v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "body", resp)
var vh VehicleHealth
err := json.Unmarshal(resp.Data, &vh)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error())
}
// v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "vehicle health", vh)
for i, vhi := range vh.VehicleHealthItems {
// v.client.logger.Debug("vehicle health item", "id", i, "item", vhi)
if vhi.IsTrouble {
if _, ok := troubles[vhi.FeatureCode]; ok {
t := Trouble{
Description: troubles[vhi.FeatureCode],
}
v.Troubles[vhi.FeatureCode] = t
v.client.logger.Debug("found troubled vehicle health item", "id", i, "item", vhi.FeatureCode, "description", troubles[vhi.FeatureCode])
}
}
}
return nil
}
// GetFeaturesList .
func (v *Vehicle) GetFeaturesList() {
for _, f := range v.Features {
if _, ok := features[f]; ok {
v.client.logger.Debug("vehicle features", "id", f, "feature", features[f])
} else {
v.client.logger.Debug("vehicle features", "id", f)
}
}
}
// 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 .
func (v *Vehicle) selectVehicle() {
if v.client.currentVin != 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.GeoLocation.Latitude = vData.VehicleGeoPosition.Latitude
v.GeoLocation.Longitude = vData.VehicleGeoPosition.Longitude
v.GeoLocation.Heading = vData.VehicleGeoPosition.Heading
v.GeoLocation.Speed = vData.VehicleGeoPosition.Speed
v.GeoLocation.Updated = vData.VehicleGeoPosition.Timestamp
v.Updated = time.Now()
}
}
// getAPIGen
// Get the Subaru telematics API generation of a specified VIN
func (v *Vehicle) getAPIGen() string {
if slices.Contains(v.Features, FEATURE_G1_TELEMATICS) {
return "g1"
}
if slices.Contains(v.Features, FEATURE_G2_TELEMATICS) {
return "g2"
}
if slices.Contains(v.Features, FEATURE_G3_TELEMATICS) {
return "g3"
}
return "unknown"
}
// isEV .
// Get whether the specified car is an Electric Vehicle.
func (v *Vehicle) isEV() bool {
return slices.Contains(v.Features, FEATURE_PHEV)
}
// getRemoteOptionsStatus .
// Get whether the specified VIN has remote locks/horn/light service available
func (v *Vehicle) getRemoteOptionsStatus() bool {
return slices.Contains(v.SubscriptionFeatures, FEATURE_REMOTE)
}
// parseDoor .
func (v *Vehicle) parseParts(name string, value any) {
// 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)?`)
grps := re.FindStringSubmatch(name)
pn := grps[1] + "_" + grps[2]
if len(grps[3]) > 0 {
pn = pn + "_" + grps[3]
}
pn = strings.ToLower(pn)
// v.client.logger.Debug("VEHICLE COND", "key", name, "value", value, "number", len(submatchall))
switch grps[1] {
case "Door", "door":
if d, ok := v.Doors[pn]; ok {
if grps[4] == "Position" {
d.Status = value.(string)
}
if grps[4] == "LockStatus" {
d.Lock = value.(string)
}
d.Updated = time.Now()
v.Doors[pn] = d
} else {
d = Door{
Position: grps[2],
Updated: time.Now(),
}
if grps[4] == "Position" {
d.Status = value.(string)
}
if grps[4] == "LockStatus" {
d.Lock = value.(string)
}
v.Doors[pn] = d
if len(grps) >= 2 {
if d, ok := v.Doors[pn]; ok {
d.SubPosition = grps[3]
v.Doors[pn] = d
}
}
}
case "Window", "window":
if w, ok := v.Windows[pn]; ok {
w.Status = value.(string)
w.Updated = time.Now()
v.Windows[pn] = w
} else {
v.Windows[pn] = Window{
Position: grps[2],
Status: value.(string),
Updated: time.Now(),
}
if len(grps) >= 2 {
if w, ok := v.Windows[pn]; ok {
w.SubPosition = grps[3]
v.Windows[pn] = w
}
}
}
case "Tire", "tire":
if t, ok := v.Tires[pn]; ok {
switch v := value.(type) {
case int:
t.PressurePsi = v
case float64:
t.PressurePsi = int(v)
}
t.Updated = time.Now()
v.Tires[pn] = t
} else {
t = Tire{
Position: grps[2],
SubPosition: grps[3],
Updated: time.Now(),
}
switch v := value.(type) {
case int:
t.PressurePsi = v
case float64:
t.PressurePsi = int(v)
}
v.Tires[pn] = t
}
}
}
// // getRemoteStartStatus .
// // Get whether the specified VIN has remote engine start service available.
// func (v *Vehicle) getRemoteStartStatus() bool {
// return slices.Contains(v.Features, FEATURE_REMOTE_START)
// }
// // getSafetyStatus .
// // Get whether the specified VIN is has an active Starlink Safety Plus service plan.
// func (v *Vehicle) getSafetyStatus() bool {
// return slices.Contains(v.SubscriptionFeatures, FEATURE_SAFETY)
// }
// // getSubscriptionStatus .
// // Get whether the specified VIN has an active service plan.
// func (v *Vehicle) getSubscriptionStatus() bool {
// return slices.Contains(v.SubscriptionFeatures, FEATURE_ACTIVE)
// }