package mysubaru import ( "encoding/json" "errors" "fmt" "reflect" "regexp" "slices" "strconv" "strings" "time" ) // var parts = map[string]map[string][]string{ // "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 { 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, } // ClimateProfile . 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 { Position string // front | rear | boot | enginehood SubPosition string // right | left Status string // CLOSED | OPEN Lock string // LOCKED | UNLOCKED Updated time.Time } // Window . type Window struct { Position string SubPosition string Status string // CLOSE | VENTED | OPEN Updated time.Time } // Tire . type Tire struct { Position string SubPosition string Pressure int PressurePsi int Updated time.Time // Status string } // 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) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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() (chan string, error) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } // TODO: Get Quick Climate Preset from the Currect Car params := map[string]string{ "delay": "0", "vin": v.Vin, "pin": v.client.credentials.PIN, "horn": "true", "climateSettings": "climateSettings", // climateSettings "climateZoneFrontTemp": "65", // 60-86 "climateZoneFrontAirMode": "WINDOW", // FEET_FACE_BALANCED | FEET_WINDOW | WINDOW | FEET "climateZoneFrontAirVolume": "6", // 1-7 "heatedSeatFrontLeft": "OFF", // OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT | low_cool | medium_cool | high_cool "heatedSeatFrontRight": "OFF", // ---//--- "heatedRearWindowActive": "true", // boolean "outerAirCirculation": "outsideAir", // outsideAir | recirculation "airConditionOn": "false", // boolean "runTimeMinutes": "10", // 1-10 "startConfiguration": START_CONFIG_DEFAULT_RES, // 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) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } 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 } else { return nil, errors.New("not an EV car") } } // GetLocation . func (v *Vehicle) GetLocation(force bool) (chan string, error) { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return nil, errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } var reqUrl, pollingUrl string var 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"]) } 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"]) } 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 } // GetClimateUserPresets . func (v *Vehicle) GetClimateUserPresets() error { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } if v.Vin != (v.client).currentVin { v.selectVehicle() } 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 } // GetVehicleStatus . func (v *Vehicle) GetVehicleStatus() error { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } if v.Vin != (v.client).currentVin { v.selectVehicle() } 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"]) } 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 . func (v *Vehicle) GetVehicleHealth() error { if !v.getRemoteOptionsStatus() { v.client.logger.Error(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) return errors.New(APP_ERRORS["SUBSCRIBTION_REQUIRED"]) } if v.Vin != (v.client).currentVin { v.selectVehicle() } 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 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") } 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("Subaru 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("Subaru 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("Subaru 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 = time.Now() 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" } // isPINRequired . // Return if a vehicle with an active remote service subscription exists. // func (v *Vehicle) isPINRequired() bool { // return v.getRemoteOptionsStatus() // } // 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) // } // // 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",