package mysubaru import ( "encoding/json" "fmt" "regexp" "strconv" "strings" "time" "github.com/Jeffail/gabs/v2" ) 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 > 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"` 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) ] CanEdit bool `json:"canEdit,string,omitempty"` // canEdit [ false | true ] Disabled bool `json:"disabled,string,omitempty"` // disabled [ false | true ] VehicleType string `json:"vehicleType,omitempty"` // vehicleType [ gas | phev ] PresetType string `json:"presetType,omitempty"` // presetType [ subaruPreset | userPreset ] } // func (cp *ClimateProfile) New() {} // GeoLocation . type GeoLocation struct { Latitude float64 // 40.700184 Longitude float64 // -74.401375 Speed float64 // 0.00 Heading int // 189 Updated time.Time } // Door . type Door struct { Position string // front | rear | boot | enginehood SubPosition string // right | left Status string // CLOSED | Lock string // LOCKED | Updated time.Time } // Window . type Window struct { Position string SubPosition string Status string Updated time.Time } // Tire . type Tire struct { Position string SubPosition string Pressure int PressurePsi string Status string Updated time.Time } 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 += "=== 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() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // Unlock . // Send command to unlock doors. func (v *Vehicle) Unlock() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } // 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) } // EngineStart . // Sends a command to start engine and set climate control. func (v *Vehicle) EngineStart() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // EngineStop . // Sends a command to stop engine. func (v *Vehicle) EngineStop() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("Active STARLINK Security Plus subscription required") } } // LightsStart . // Sends a command to flash lights. func (v *Vehicle) LightsStart() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] } v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // LightsStop . // Sends a command to stop flash lights. func (v *Vehicle) LightsStop() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] } v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // HornStart . // Send command to sound horn. func (v *Vehicle) HornStart() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] } v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // HornStop . // Send command to sound horn. func (v *Vehicle) HornStop() { if v.getRemoteOptionsStatus() { v.selectVehicle() 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"] } v.client.execute(reqURL, POST, params, pollingURL, true) } else { v.client.logger.Error("Active STARLINK Security Plus subscription required") } } // ChargeStart . func (v *Vehicle) ChargeOn() { if v.isEV() { v.selectVehicle() 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"] v.client.execute(reqURL, POST, params, pollingURL, true) } } // GetLocation . func (v *Vehicle) GetLocation(force bool) { 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"] 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"] } v.client.execute(reqURL, POST, params, pollingURL, true) } else { // Reports the last location the vehicle has reported to Subaru v.selectVehicle() params := map[string]string{ "vin": v.Vin, "pin": v.client.credentials.PIN} reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_LOCATE"], v.getAPIGen()) v.client.execute(reqURL, GET, params, "", false) } } // GetClimateQuickPresets . func (v *Vehicle) GetClimateQuickPresets() { if v.getRemoteOptionsStatus() { v.selectVehicle() reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_QUICK_START_SETTINGS"] resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) respParsed, err := gabs.ParseJSON(resp) if err != nil { v.client.logger.Error("error while parsing http output json", "request", "GetClimateQuickPresets", "error", err.Error()) } v.client.logger.Debug("CLIMATE SETTINGS OUTPUT", "body", respParsed) // ONLY FOR THAT REQUEST BECAUSE OF API SENDS BACK ESCAPING DATA IN DATA FIELD data, ok := respParsed.Path("data").Data().(string) if !ok { v.client.logger.Error("error while parsing data json", "request", "GetClimateQuickPresets", "error", err.Error()) } cProfiles := []ClimateProfile{} err = json.Unmarshal([]byte(data), &cProfiles) v.client.logger.Debug("climate quick presets", "data", data) if err != nil { v.client.logger.Error("error while parsing climate quick presets json", "request", "GetClimateQuickPresets", "error", err.Error()) } if len(cProfiles) > 0 { for _, cp := range cProfiles { if _, ok := v.ClimateProfiles[cp.PresetType+cp.Name]; ok { v.ClimateProfiles[cp.PresetType+cp.Name] = cp } else { v.ClimateProfiles[cp.PresetType+cp.Name] = cp } } } } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // GetClimatePresets . func (v *Vehicle) GetClimatePresets() { if v.getRemoteOptionsStatus() { v.selectVehicle() reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_SUBARU_PRESETS"] resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) respParsed, err := gabs.ParseJSON(resp) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error()) } // ONLY FOR THAT REQUEST BECAUSE OF API SENDS BACK ESCAPED DATA IN DATA FIELD for _, child := range respParsed.S("data").Children() { // logger.Debugf("key: %v, value: %v\n", key, child.Data().(string)) var climateProfile ClimateProfile json.Unmarshal([]byte(child.Data().(string)), &climateProfile) // if v.isEV() && climateProfile.VehicleType == "phev" { // v.ClimateProfiles = append(v.ClimateProfiles, climateProfile) // } // if !v.isEV() && climateProfile.VehicleType == "gas" { // v.ClimateProfiles = append(v.ClimateProfiles, climateProfile) // } v.Updated = time.Now() } } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // GetClimateUserPresets . func (v *Vehicle) GetClimateUserPresets() { if v.getRemoteOptionsStatus() { v.selectVehicle() reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_RES_USER_PRESETS"] resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) respParsed, err := gabs.ParseJSON(resp) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetClimateUserPresets", "error", err.Error()) } v.client.logger.Debug("CLIMATE USER SETTINGS OUTPUT", "body", respParsed) // ONLY FOR THAT REQUEST BECAUSE OF API SENDS BACK ESCAPED DATA IN DATA FIELD for _, child := range respParsed.S("data").Children() { // logger.Debugf("key: %v, value: %v\n", key, child.Data().(string)) var climateProfile ClimateProfile json.Unmarshal([]byte(child.Data().(string)), &climateProfile) // if v.isEV() && climateProfile.VehicleType == "phev" { // v.ClimateProfiles = append(v.ClimateProfiles, climateProfile) // } // if !v.isEV() && climateProfile.VehicleType == "gas" { // v.ClimateProfiles = append(v.ClimateProfiles, climateProfile) // } } v.Updated = time.Now() // // ONLY FOR THAT REQUEST BECAUSE OF API SENDS BACK ESCAPING DATA IN DATA FIELD // data, ok := respParsed.Path("data").Data().(string) // // rawIn := json.RawMessage(in) // // bytes, err := rawIn.MarshalJSON() // // if err != nil { // // panic(err) // // } // // value == string, ok == false // if !ok { // // TODO: Work with errorCode // panic(data) // } // logger.Debugf("PRESETS: %+v\n", data) } else { v.client.logger.Error("active STARLINK Security Plus subscription required") } } // GetVehicleStatus . func (v *Vehicle) GetVehicleStatus() { v.selectVehicle() reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_VEHICLE_STATUS"], v.getAPIGen()) resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) v.client.logger.Debug("http request output", "request", "GetVehicleStatus", "body", resp) if v.client.isResponseSuccessfull(resp) { respParsed, err := gabs.ParseJSON(resp) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetVehicleStatus", "error", err.Error()) } vSta := VehicleStatus{} vsString := respParsed.Path("data").String() json.Unmarshal([]byte(vsString), &vSta) fmt.Printf("CAR STATUS: %+v\n", vSta) v.EngineState = vSta.VehicleStateType v.Odometer.Miles = vSta.OdometerValue v.Odometer.Kilometers = vSta.OdometerValueKm v.DistanceToEmpty.Miles = int(vSta.DistanceToEmptyFuelMiles) v.DistanceToEmpty.Kilometers = vSta.DistanceToEmptyFuelKilometers v.DistanceToEmpty.Miles10s = vSta.DistanceToEmptyFuelMiles10s v.DistanceToEmpty.Kilometers10s = vSta.DistanceToEmptyFuelKilometers10s v.DistanceToEmpty.Percentage = vSta.RemainingFuelPercent v.FuelConsumptionAvg.MPG = float64(vSta.AvgFuelConsumptionMpg) v.FuelConsumptionAvg.LP100Km = float64(vSta.AvgFuelConsumptionLitersPer100Kilometers) v.GeoLocation.Latitude = float64(vSta.Latitude) v.GeoLocation.Longitude = float64(vSta.Longitude) v.GeoLocation.Heading = vSta.Heading re := regexp.MustCompile(`[A-Z][^A-Z]*`) for key, child := range respParsed.S("data").ChildrenMap() { fmt.Printf("key: %v, value: %v\n", key, child.Data()) if child.Data() == "NOT_EQUIPPED" || child.Data() == "UNKNOWN" || child.Data() == "16383" || child.Data() == "65535" || child.Data() == "None" || child.Data() == "-64.0" || child.Data() == nil { // fmt.Println("Skipping") continue } if strings.HasPrefix(key, "door") && strings.HasSuffix(key, "Position") { pos := strings.TrimPrefix(key, "door") pos = strings.TrimSuffix(pos, "Position") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if door, ok := v.Doors[pos]; ok { door.Status = child.Data().(string) door.Updated = time.Now() } else { door.Status = child.Data().(string) door.Updated = time.Now() v.Doors[pos] = Door{ Position: submatchall[0], Status: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if d, ok := v.Doors[pos]; ok { d.SubPosition = submatchall[1] v.Doors[pos] = d } } } } if strings.HasPrefix(key, "door") && strings.HasSuffix(key, "LockStatus") { pos := strings.TrimPrefix(key, "door") pos = strings.TrimSuffix(pos, "LockStatus") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if door, ok := v.Doors[pos]; ok { door.Lock = child.Data().(string) door.Updated = time.Now() } else { door.Lock = child.Data().(string) door.Updated = time.Now() v.Doors[pos] = Door{ Position: submatchall[0], Lock: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if d, ok := v.Doors[pos]; ok { d.SubPosition = submatchall[1] v.Doors[pos] = d } } } } if strings.HasPrefix(key, "window") && strings.HasSuffix(key, "Status") { pos := strings.TrimPrefix(key, "window") pos = strings.TrimSuffix(pos, "Status") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if window, ok := v.Windows[pos]; ok { window.Status = child.Data().(string) window.Updated = time.Now() } else { window.Status = child.Data().(string) window.Updated = time.Now() v.Windows[pos] = Window{ Position: submatchall[0], Status: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if w, ok := v.Windows[pos]; ok { w.SubPosition = submatchall[1] v.Windows[pos] = w } } } } if strings.HasPrefix(key, "tirePressure") && strings.HasSuffix(key, "Psi") { pos := strings.TrimPrefix(key, "tirePressure") pos = strings.TrimSuffix(pos, "Psi") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if tire, ok := v.Tires[pos]; ok { tire.PressurePsi = child.Data().(string) tire.Updated = time.Now() } else { tire.PressurePsi = child.Data().(string) tire.Updated = time.Now() v.Tires[pos] = Tire{ Position: submatchall[0], PressurePsi: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if t, ok := v.Tires[pos]; ok { t.SubPosition = submatchall[1] v.Tires[pos] = t } } } } } fmt.Printf("PARTS: %+v\n", parts) v.Updated = time.Now() } // vData := VehicleData{} // vdString := respParsed.Path("data").String() // json.Unmarshal([]byte(vdString), &vData) // { "data": { // "vhsId": 1038682433, // "odometerValue": 24999, // "odometerValueKilometers": 40223, // "eventDate": 1642538410000, // "eventDateStr": "2022-01-18T20:40+0000", // "latitude": 40.70018, // "longitude": -74.40139, // "positionHeadingDegree": "155", // "tirePressureFrontLeft": "2275", // "tirePressureFrontRight": "2206", // "tirePressureRearLeft": "2344", // "tirePressureRearRight": "2275", // "tirePressureFrontLeftPsi": "33", // "tirePressureFrontRightPsi": "32", // "tirePressureRearLeftPsi": "34", // "tirePressureRearRightPsi": "33", // "distanceToEmptyFuelMiles": 149.75, // "distanceToEmptyFuelKilometers": 241, // "avgFuelConsumptionMpg": 18.5, // "avgFuelConsumptionLitersPer100Kilometers": 12.7, // "evStateOfChargePercent": null, // "evDistanceToEmptyMiles": null, // "evDistanceToEmptyKilometers": null, // "evDistanceToEmptyByStateMiles": null, // "evDistanceToEmptyByStateKilometers": null, // "vehicleStateType": "IGNITION_OFF", // "distanceToEmptyFuelMiles10s": 150, // "distanceToEmptyFuelKilometers10s": 240 // } // } } // GetVehicleCondition . func (v *Vehicle) GetVehicleCondition() { v.selectVehicle() reqURL := MOBILE_API_VERSION + urlToGen(apiURLs["API_CONDITION"], v.getAPIGen()) resp := v.client.execute(reqURL, GET, map[string]string{}, "", false) if v.client.isResponseSuccessfull(resp) { respParsed, err := gabs.ParseJSON(resp) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetVehicleCondition", "error", err.Error()) } re := regexp.MustCompile(`[A-Z][^A-Z]*`) for key, child := range respParsed.S("data").S("result").ChildrenMap() { fmt.Printf("key: %v, value: %v\n", key, child.Data()) if child.Data() == "NOT_EQUIPPED" || child.Data() == "UNKNOWN" || child.Data() == "16383" || child.Data() == "65535" || child.Data() == "None" || child.Data() == "-64.0" || child.Data() == nil { continue } if strings.HasPrefix(key, "door") && strings.HasSuffix(key, "Position") { pos := strings.TrimPrefix(key, "door") pos = strings.TrimSuffix(pos, "Position") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if door, ok := v.Doors[pos]; ok { door.Status = child.Data().(string) door.Updated = time.Now() } else { door.Status = child.Data().(string) door.Updated = time.Now() v.Doors[pos] = Door{ Position: submatchall[0], Status: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if d, ok := v.Doors[pos]; ok { d.SubPosition = submatchall[1] v.Doors[pos] = d } } } } if strings.HasPrefix(key, "door") && strings.HasSuffix(key, "LockStatus") { pos := strings.TrimPrefix(key, "door") pos = strings.TrimSuffix(pos, "LockStatus") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if door, ok := v.Doors[pos]; ok { door.Lock = child.Data().(string) door.Updated = time.Now() } else { door.Lock = child.Data().(string) door.Updated = time.Now() v.Doors[pos] = Door{ Position: submatchall[0], Lock: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if d, ok := v.Doors[pos]; ok { d.SubPosition = submatchall[1] v.Doors[pos] = d } } } } if strings.HasPrefix(key, "window") && strings.HasSuffix(key, "Status") { pos := strings.TrimPrefix(key, "window") pos = strings.TrimSuffix(pos, "Status") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if window, ok := v.Windows[pos]; ok { window.Status = child.Data().(string) window.Updated = time.Now() } else { window.Status = child.Data().(string) window.Updated = time.Now() v.Windows[pos] = Window{ Position: submatchall[0], Status: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if w, ok := v.Windows[pos]; ok { w.SubPosition = submatchall[1] v.Windows[pos] = w } } } } if strings.HasPrefix(key, "tirePressure") && strings.HasSuffix(key, "Psi") { pos := strings.TrimPrefix(key, "tirePressure") pos = strings.TrimSuffix(pos, "Psi") submatchall := re.FindAllString(pos, -1) v.client.logger.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) if tire, ok := v.Tires[pos]; ok { tire.PressurePsi = child.Data().(string) tire.Updated = time.Now() } else { tire.PressurePsi = child.Data().(string) tire.Updated = time.Now() v.Tires[pos] = Tire{ Position: submatchall[0], PressurePsi: child.Data().(string), Updated: time.Now(), } if len(submatchall) >= 2 { if t, ok := v.Tires[pos]; ok { t.SubPosition = submatchall[1] v.Tires[pos] = t } } } } v.Updated = time.Now() } } // VEHICLE_STATE_TYPE >> IGNITION_OFF // DISTANCE_TO_EMPTY_FUEL >> 241 // AVG_FUEL_CONSUMPTION >> 127 // POSITION_SPEED_KMPH >> 0 // POSITION_HEADING_DEGREE >> 155 // POSITION_TIMESTAMP >> 2022-01-18T20:40:10Z // ODOMETER >> 40223712 (meters) // TYRE_( STATUS | PRESSURE )_( REAR | FRONT )_( LEFT | RIGHT ) >> 2275 | UNKNOWN // SEAT_BELT_STATUS_( ( FRONT | SECOND | THIRD )_( LEFT | MIDDLE | RIGHT ) ) >> NOT_EQUIPPED | UNKNOWN | NOT_BELTED | BELTED // SEAT_OCCUPATION_STATUS_( ( FRONT | SECOND | THIRD )_( LEFT | MIDDLE | RIGHT ) ) >> // DOOR_( ( FRONT | REAR )_( LEFT | RIGHT ) | ENGINE_HOOD | BOOT )_LOCK_STATUS >> UNKNOWN // DOOR_( ( FRONT | REAR )_( LEFT | RIGHT ) | ENGINE_HOOD | BOOT )_POSITION >> CLOSED // WINDOW_( BACK | SUNROOF | ( FRONT | REAR )_( LEFT | RIGHT ) )_STATUS >> UNKNOWN | CLOSE } // GetVehicleHealth . func (v *Vehicle) GetVehicleHealth() { v.selectVehicle() params := map[string]string{ "vin": v.Vin, "_": timestamp()} reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_HEALTH"] resp := v.client.execute(reqURL, GET, params, "", false) if v.client.isResponseSuccessfull(resp) { _, err := gabs.ParseJSON(resp) if err != nil { v.client.logger.Error("error while parsing json", "request", "GetVehicleHealth", "error", err.Error()) } // TODO: } } // GetClimateSettings . func (v *Vehicle) GetClimateSettings() { v.selectVehicle() reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_CLIMATE_SETTINGS"] v.client.execute(reqURL, GET, map[string]string{}, "", false) // TODO } // GetFeaturesList . func (v *Vehicle) GetFeaturesList() { for i, f := range v.Features { if _, ok := features[f]; ok { fmt.Printf("%d >> %s // %s\n", i+1, f, features[f]) } else { fmt.Printf("%d >> %s\n", i+1, f) } } } // selectVehicle . func (v *Vehicle) selectVehicle() { if v.client.currentVin != v.Vin { vData := (*v.client).SelectVehicle(v.Vin) 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 contains(v.Features, FEATURE_G1_TELEMATICS) { return "g1" } if contains(v.Features, FEATURE_G2_TELEMATICS) { return "g2" } if 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 VIN is an Electric Vehicle. func (v *Vehicle) isEV() bool { return contains(v.Features, FEATURE_PHEV) } // getRemoteOptionsStatus . // Get whether the specified VIN has remote locks/horn/light service available func (v *Vehicle) getRemoteOptionsStatus() bool { return contains(v.SubscriptionFeatures, FEATURE_REMOTE) } // getRemoteStartStatus . // Get whether the specified VIN has remote engine start service available. func (v *Vehicle) getRemoteStartStatus() bool { return 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 contains(v.SubscriptionFeatures, FEATURE_SAFETY) } // getSubscriptionStatus . // Get whether the specified VIN has an active service plan. func (v *Vehicle) getSubscriptionStatus() bool { return 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() {} // func fetch() {} // "vhsId": 914631252, // "odometerValue": 23865, // "odometerValueKilometers": 38399, // "tirePressureFrontLeft": "2344", // "tirePressureFrontRight": "2344", // "tirePressureRearLeft": "2413", // "tirePressureRearRight": "2344", // "tirePressureFrontLeftPsi": "34", // "tirePressureFrontRightPsi": "34", // "tirePressureRearLeftPsi": "35", // "tirePressureRearRightPsi": "34", // TireStatusFrontLeft string `json:"tyreStatusFrontLeft"` // "UNKNOWN" // TireStatusFrontRight string `json:"tyreStatusFrontRight"` // "UNKNOWN" // TireStatusRearLeft string `json:"tyreStatusRearLeft"` // "UNKNOWN" // TireStatusRearRight string `json:"tyreStatusRearRight"` // "UNKNOWN" // WindowFrontLeftStatus string `json:"windowFrontLeftStatus"` // "CLOSE" // WindowFrontRightStatus string `json:"windowFrontRightStatus"` // "CLOSE" // WindowRearLeftStatus string `json:"windowRearLeftStatus"` // "CLOSE" // WindowRearRightStatus string `json:"windowRearRightStatus"` // "CLOSE" // WindowSunroofStatus string `json:"windowSunroofStatus"` // "UNKNOWN"