Files
mysubaru/vehicle.go
Alex Savin c2128f278e
All checks were successful
Golan Testing / testing (1.24.x, ubuntu-latest) (push) Successful in 24s
More changes
2025-06-03 10:41:45 -04:00

1052 lines
40 KiB
Go

package mysubaru
import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"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 >
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 |
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()
// 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"]
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)
}
}
// 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() {
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)
var r Response
err := json.Unmarshal(resp, &r)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetClimatePresets", "error", err.Error())
}
re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.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 {
if v.isEV() && cp.VehicleType == "phev" {
if _, ok := v.ClimateProfiles[cp.PresetType+cp.Name]; ok {
v.ClimateProfiles[cp.PresetType+cp.Name] = cp
} else {
if _, ok := v.ClimateProfiles[cp.PresetType+cp.Name]; ok {
v.ClimateProfiles[cp.PresetType+cp.Name] = cp
} else {
v.ClimateProfiles[cp.PresetType+cp.Name] = cp
}
}
}
if !v.isEV() && cp.VehicleType == "gas" {
if _, ok := v.ClimateProfiles[cp.PresetType+cp.Name]; ok {
v.ClimateProfiles[cp.PresetType+cp.Name] = cp
} else {
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.Debug("didn't find any climate quick presets")
}
v.Updated = time.Now()
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
}
// GetClimateQuickPresets
// Used while user uses "quick start engine" button in the app
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)
// v.client.logger.Debug("http request output", "request", "GetClimateQuickPresets", "body", resp)
var r Response
err := json.Unmarshal(resp, &r)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetClimateQuickPresets", "error", err.Error())
}
re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.Data), "")
re2 := regexp.MustCompile(`\\`)
result = re2.ReplaceAllString(result, `"`) // \u0022
var cProfile ClimateProfile
err = json.Unmarshal([]byte(result), &cProfile)
if err != nil {
v.client.logger.Error("error while parsing climate quick presets json", "request", "GetClimateQuickPresets", "error", err.Error())
}
if _, ok := v.ClimateProfiles[cProfile.PresetType+cProfile.Name]; ok {
v.ClimateProfiles[cProfile.PresetType+cProfile.Name] = cProfile
} else {
v.ClimateProfiles[cProfile.PresetType+cProfile.Name] = cProfile
}
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)
var r Response
err := json.Unmarshal(resp, &r)
if err != nil {
v.client.logger.Error("error while parsing json", "request", "GetClimateUserPresets", "error", err.Error())
}
re1 := regexp.MustCompile(`\"`)
result := re1.ReplaceAllString(string(r.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 {
if _, ok := v.ClimateProfiles[cp.PresetType+cp.Name]; ok {
v.ClimateProfiles[cp.PresetType+cp.Name] = cp
} else {
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.Debug("didn't find any user climate presets")
}
v.Updated = time.Now()
} 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 r, ok := v.client.parseResponse(resp); ok {
var vs VehicleStatus
err := json.Unmarshal(r.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 >= 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
// re := regexp.MustCompile(`[A-Z][^A-Z]*`)
val := reflect.ValueOf(vs)
typeOfS := val.Type()
for i := 0; i < val.NumField(); i++ {
if val.Field(i).Interface() == "NOT_EQUIPPED" ||
val.Field(i).Interface() == "UNKNOWN" ||
val.Field(i).Interface() == "None" ||
val.Field(i).Interface() == "16383" ||
val.Field(i).Interface() == "65535" ||
val.Field(i).Interface() == "-64" ||
val.Field(i).Interface() == "" ||
val.Field(i).Interface() == 0 ||
val.Field(i).Interface() == float64(0) ||
val.Field(i).Interface() == nil {
continue
}
fmt.Printf("Field: %s, Value: %v, Type: %v\n", typeOfS.Field(i).Name, val.Field(i).Interface(), val.Field(i).Type())
}
// v := reflect.ValueOf(vSta)
// values := make([]interface{}, v.NumField())
// for i := 0; i < v.NumField(); i++ {
// values[i] = v.Field(i).Interface()
// }
// 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()
}
}
// 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)
// v.client.logger.Debug("http request output", "request", "GetVehicleCondition", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var sr ServiceRequest
err := json.Unmarshal(r.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 := 0; i < val.NumField(); i++ {
if val.Field(i).Interface() == "NOT_EQUIPPED" ||
val.Field(i).Interface() == "UNKNOWN" ||
val.Field(i).Interface() == "None" ||
val.Field(i).Interface() == "16383" ||
val.Field(i).Interface() == "65535" ||
val.Field(i).Interface() == "-64" ||
val.Field(i).Interface() == "" ||
val.Field(i).Interface() == 0 ||
val.Field(i).Interface() == float64(0) ||
val.Field(i).Interface() == nil {
continue
}
fmt.Printf("Field: %s, Value: %v, Type: %v\n", typeOfS.Field(i).Name, val.Field(i).Interface(), val.Field(i).Type())
}
// 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)
// v.client.logger.Debug("http request output", "request", "GetVehicleHealth", "body", resp)
if r, ok := v.client.parseResponse(resp); ok {
var vh VehicleHealth
err := json.Unmarshal(r.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)
// TODO: Loop over all the Vehicle Health Items
for i, vhi := range vh.VehicleHealthItems {
v.client.logger.Debug("vehicle health item", "id", i, "item", vhi)
if vhi.IsTrouble {
}
}
} else {
v.client.logger.Error("active STARLINK Security Plus subscription required")
}
}
// 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"