From bfed21acd8731dd1ec77d887ee1ec301cbea416e Mon Sep 17 00:00:00 2001 From: Alex Savin Date: Tue, 20 May 2025 15:54:30 -0400 Subject: [PATCH] first commit --- .gitignore | 34 + README.md | 35 + client.go | 882 +++++++++++++++++++++++++ config/config.go | 72 ++ consts.go | 357 ++++++++++ example/config.sample.yaml | 15 + example/example.go | 129 ++++ example/go.mod | 31 + go.mod | 38 ++ main.go | 129 ++++ mysubaru.go | 296 +++++++++ utils.go | 143 ++++ vehicle.go | 1283 ++++++++++++++++++++++++++++++++++++ 13 files changed, 3444 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 client.go create mode 100644 config/config.go create mode 100644 consts.go create mode 100644 example/config.sample.yaml create mode 100644 example/example.go create mode 100644 example/go.mod create mode 100644 go.mod create mode 100644 main.go create mode 100644 mysubaru.go create mode 100644 utils.go create mode 100644 vehicle.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a135b5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +coverage.out +coverage.txt +go.sum + +.tmp + +config.yaml + +go-mysubaru.code-workspace \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dadd820 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# MySubaru!GO +Is a simple API client to interact with My Subaru service (https://www.mysubaru.com/) via HTTP, written in Go + +## News + * v0.0.0-dev First public release on April 11, 2023 + +## Features + * Simple and chainable methods for settings and request + +## Installation +```bash +# Go Modules +go get github.com/alex-savin/go-mysubaru +``` + +## Usage +The following samples will assist you to become as comfortable as possible with mysubaru library. +```go +// Import hassky into your code and refer it as `mysubaru`. +import "github.com/alex-savin/go-mysubaru" +``` + +#### Create a new MySubaru connection and get a car by VIN +```go +// Create a MySubaru Client +mysubaru, _ := New() +outback := mysubaru.GetVehicleByVIN("VIN-CODE-HERE") +``` + +#### Start/Stop Lights request +```go +outback.LightsStart() +time.Sleep(30 * time.Second) +outback.LightsStop() +``` \ No newline at end of file diff --git a/client.go b/client.go new file mode 100644 index 0000000..d3f7a90 --- /dev/null +++ b/client.go @@ -0,0 +1,882 @@ +package mysubaru + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "sync" + "time" + + "github.com/Jeffail/gabs/v2" + "github.com/alex-savin/go-mysubaru/config" + "github.com/go-resty/resty/v2" +) + +// credentials . +type credentials struct { + username string + password string + pin string + deviceId string + deviceName string +} + +// Client . +type Client struct { + baseURL string + credentials credentials + httpClient *resty.Client + cookies []*http.Cookie + country string // + updateInterval int // 7200 + fetchInterval int // 360 + currentVin string + listOfVins []string + isAuthenticated bool + isRegistered bool + log *slog.Logger + sync.RWMutex +} + +// Option . +type Option func(*Client) error + +// BaseURL allows overriding of API client baseURL for testing +func BaseURL(baseURL string) Option { + return func(c *Client) error { + c.baseURL = baseURL + return nil + } +} + +// Username . +func Username(username string) Option { + return func(c *Client) error { + c.credentials.username = username + return nil + } +} + +// Password . +func Password(password string) Option { + return func(c *Client) error { + c.credentials.password = password + return nil + } +} + +// auth . +func (c *Client) auth() []byte { + // { + // "success": true, + // "errorCode": null, + // "dataName": "sessionData", + // "data": { + // "sessionChanged": false, + // "vehicleInactivated": false, + // "account": { + // "marketId": 1, + // "createdDate": 1476984644000, + // "firstName": "Tatiana", + // "lastName": "Savin", + // "zipCode": "07974", + // "accountKey": 765268, + // "lastLoginDate": 1640539132000, + // "zipCode5": "07974" + // }, + // "resetPassword": false, + // "deviceId": "JddMBQXvAkgutSmEP6uFsThbq4QgEBBQ", + // "sessionId": "010A2C0C25A5D35AC3776DB6B7900A3B", + // "deviceRegistered": true, + // "passwordToken": null, + // "vehicles": [ + // { + // "customer": { + // "sessionCustomer": null, + // "email": null, + // "firstName": null, + // "lastName": null, + // "zip": null, + // "oemCustId": null, + // "phone": null + // }, + // "stolenVehicle": false, + // "vehicleName": "Subaru Outback LXT", + // "features": null, + // "vin": "4S4BTGND8L3137058", + // "modelYear": null, + // "modelCode": null, + // "engineSize": null, + // "nickname": "Subaru Outback LXT", + // "vehicleKey": 3832950, + // "active": true, + // "licensePlate": "", + // "licensePlateState": "", + // "email": null, + // "firstName": null, + // "lastName": null, + // "subscriptionFeatures": null, + // "accessLevel": -1, + // "zip": null, + // "oemCustId": "CRM-631-HQN48K", + // "vehicleMileage": null, + // "phone": null, + // "userOemCustId": "CRM-631-HQN48K", + // "subscriptionStatus": null, + // "authorizedVehicle": false, + // "preferredDealer": null, + // "cachedStateCode": "NJ", + // "subscriptionPlans": [], + // "needMileagePrompt": false, + // "phev": null, + // "remoteServicePinExist": true, + // "needEmergencyContactPrompt": false, + // "vehicleGeoPosition": null, + // "extDescrip": null, + // "intDescrip": null, + // "modelName": null, + // "transCode": null, + // "provisioned": true, + // "timeZone": "America/New_York" + // } + // ], + // "currentVehicleIndex": 0, + // "handoffToken": "$2a$08$99me3RRpB00MNMSFxrG7AOg1T5BaDVacqXhbdTii0eRXoEoeFvEPy$1640540934344", + // "enableXtime": true, + // "termsAndConditionsAccepted": true, + // "digitalGlobeConnectId": "0572e32b-2fcf-4bc8-abe0-1e3da8767132", + // "digitalGlobeImageTileService": "https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/DigitalGlobe:ImageryTileService@EPSG:3857@png/{z}/{x}/{y}.png?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132", + // "digitalGlobeTransparentTileService": "https://earthwatch.digitalglobe.com/earthservice/tmsaccess/tms/1.0.0/Digitalglobe:OSMTransparentTMSTileService@EPSG:3857@png/{z}/{x}/{-y}.png/?connectId=0572e32b-2fcf-4bc8-abe0-1e3da8767132", + // "tomtomKey": "DHH9SwEQ4MW55Hj2TfqMeldbsDjTdgAs", + // "satelliteViewEnabled": true + // } + // } + params := map[string]string{ + "env": "cloudprod", + "deviceType": "android", + "loginUsername": c.credentials.username, + "password": c.credentials.password, + "deviceId": c.credentials.deviceId, + "passwordToken": "", + "selectedVin": "", + "pushToken": ""} + reqURL := MOBILE_API_VERSION + apiURLs["API_LOGIN"] + resp := c.execute(reqURL, POST, params, "", false) + + c.log.Debug("AUTH HTTP OUTPUT", "body", string([]byte(resp))) + + return resp +} + +// GET +// https://www.mysubaru.com/profile/verifyDeviceName.json?clientId=2574212&deviceName=Alex%20Google%20Pixel%204%20XL +// RESP: true/false + +// POST +// https://www.mysubaru.com/profile/editDeviceName.json?clientId=2574212&deviceName=Alex%20Google%20Pixel%204%20XL +// clientId: 2574212 +// deviceName: Alex Google Pixel 4 XL +// RESP: true/false + +// GET +// https://www.mysubaru.com/listMyDevices.json +// {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]} +// {"success":true,"dataName":"authorizedDevices","data":[{"telematicsClientDeviceKey":2574212,"deviceType":"android","deviceName":"Alex Google Pixel 4 XL","createdDate":"2019-11-29T20:32:21.000+0000","modifiedDate":"2020-06-08T17:48:22.000+0000"},{"telematicsClientDeviceKey":4847533,"deviceName":"Home Assistant: Added 2021-03-03","createdDate":"2021-03-03T20:53:44.000+0000","modifiedDate":"2021-03-03T20:53:47.000+0000"},{"telematicsClientDeviceKey":7222995,"deviceType":"android","deviceName":"Alex Google Pixel 6 Pro","createdDate":"2021-10-28T15:27:36.000+0000","modifiedDate":"2021-10-28T15:27:58.000+0000"},{"telematicsClientDeviceKey":8210723,"deviceName":"Hassio Golang Integration","createdDate":"2021-12-22T01:38:43.000+0000","modifiedDate":"2021-12-22T01:38:43.000+0000"},{"telematicsClientDeviceKey":8207130,"deviceName":"Mac/iOS Chrome","createdDate":"2021-12-21T21:19:40.000+0000","modifiedDate":"2021-12-21T21:19:40.000+0000"}]} + +// registerDevice . +func (c *Client) registerDevice() bool { + // c.httpClient. + // SetBaseURL(WEB_API_SERVER[c.country]). + // R(). + // SetFormData(map[string]string{ + // "username": c.credentials.username, + // "password": c.credentials.password, + // "deviceId": c.credentials.deviceId, + // }). + // Post(apiURLs["WEB_API_LOGIN"]) + params := map[string]string{ + "username": c.credentials.username, + "password": c.credentials.password, + "deviceId": c.credentials.deviceId} + reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_LOGIN"] + c.execute(reqURL, POST, params, "", true) + + // Authorizing device via web API + // c.httpClient. + // SetBaseURL(WEB_API_SERVER[c.country]). + // R(). + // SetQueryParams(map[string]string{ + // "deviceId": c.credentials.deviceId, + // }). + // Get(apiURLs["WEB_API_AUTHORIZE_DEVICE"]) + params = map[string]string{ + "deviceId": c.credentials.deviceId} + reqURL = WEB_API_SERVER[c.country] + apiURLs["WEB_API_AUTHORIZE_DEVICE"] + c.execute(reqURL, GET, params, "", false) + + return c.setDeviceName() +} + +// setDeviceName . +func (c *Client) setDeviceName() bool { + params := map[string]string{ + "deviceId": c.credentials.deviceId, + "deviceName": c.credentials.deviceName} + reqURL := WEB_API_SERVER[c.country] + apiURLs["WEB_API_NAME_DEVICE"] + c.execute(reqURL, GET, params, "", false) + + return true +} + +// listDevices . +func (c *Client) listDevices() { + // Accept: application/json, text/javascript, */*; q=0.01 + // Accept-Encoding: gzip, deflate, br + // Accept-Language: en-US,en;q=0.9,ru;q=0.8 + // Connection: keep-alive + // Cookie: ORA_OTD_JROUTE=ozLwELf5jS-NHQ2CKZorOFfRgb8uo6lL; soa-visitor=12212021VHWnkqERZYThWe87TLUhr2Db; AMCVS_94001C8B532957140A490D4D%40AdobeOrg=1; mys-referringCodes=7~direct~; s_cc=true; AMCVS_subarucom%40AdobeOrg=1; style=null; s_pv=login.html; AMCV_subarucom%40AdobeOrg=-1124106680%7CMCIDTS%7C18988%7CMCMID%7C81535064704660726005836131001032500276%7CMCAID%7CNONE%7CMCOPTOUT-1640567559s%7CNONE%7CvVersion%7C5.2.0; AMCV_94001C8B532957140A490D4D%40AdobeOrg=-1124106680%7CMCIDTS%7C18988%7CMCMID%7C76913534164341455390435376071204508177%7CMCAID%7CNONE%7CMCOPTOUT-1640567559s%7CNONE%7CvVersion%7C5.2.0; s_sq=subarumysubarucwpprod%3D%2526c.%2526a.%2526activitymap.%2526page%253Dlogin.html%2526link%253DLog%252520In%2526region%253DloginForm%2526pageIDType%253D1%2526.activitymap%2526.a%2526.c%2526pid%253Dlogin.html%2526pidt%253D1%2526oid%253DLog%252520In%2526oidt%253D3%2526ot%253DSUBMIT; JSESSIONID=9685CFEB7888A0E6E25239D559E3B580; X-Oracle-BMC-LBS-Route=89e3283ece707e8a0ba4850e1a622122e039fd3d27da03a11a2ff120e313e9b656c62fd8a7c42ae8061a49ad6e1caf63a49d7befe4ad2a0194b0aeca + // Host: www.mysubaru.com + // Referer: https://www.mysubaru.com/profile/authorizedDevices.html + // User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 + // X-Requested-With: XMLHttpRequest + resp, err := c.httpClient. + SetBaseURL(WEB_API_SERVER[c.country]). + R(). + EnableTrace(). + Get(apiURLs["WEB_API_LIST_DEVICES"]) + + // Explore response object + fmt.Println("Response Info:") + fmt.Println(" Error :", err) + fmt.Println(" Status Code:", resp.StatusCode()) + fmt.Println(" Status :", resp.Status()) + fmt.Println(" Proto :", resp.Proto()) + fmt.Println(" Time :", resp.Time()) + fmt.Println(" Received At:", resp.ReceivedAt()) + // fmt.Println(" Body :\n", resp) + fmt.Println() + + // Explore trace info + fmt.Println("Request Trace Info:") + ti := resp.Request.TraceInfo() + fmt.Println(" DNSLookup :", ti.DNSLookup) + fmt.Println(" ConnTime :", ti.ConnTime) + fmt.Println(" TCPConnTime :", ti.TCPConnTime) + fmt.Println(" TLSHandshake :", ti.TLSHandshake) + fmt.Println(" ServerTime :", ti.ServerTime) + fmt.Println(" ResponseTime :", ti.ResponseTime) + fmt.Println(" TotalTime :", ti.TotalTime) + fmt.Println(" IsConnReused :", ti.IsConnReused) + fmt.Println(" IsConnWasIdle :", ti.IsConnWasIdle) + fmt.Println(" ConnIdleTime :", ti.ConnIdleTime) + fmt.Println(" RequestAttempt:", ti.RequestAttempt) + fmt.Println(" RemoteAddr :", ti.RemoteAddr.String()) + + // c.log.Debug("LIST DEVICES OUTPUT", "body", string([]byte(resp.Body()))) + + // c.httpClient.SetBaseURL(WEB_API_SERVER[c.country]).SetCookies(c.cookies) + // reqURL := apiURLs["WEB_API_LIST_DEVICES"] + // resp := c.execute(reqURL, GET, map[string]string{}, "", false) + + // if isResponseSuccessfull(resp) { + // log.Debugf("LIST DEVICES OUTPUT >> %v\n", string(resp)) + // } +} + +// validateSession . +func (c *Client) validateSession() bool { + // { + // "success": true, + // "errorCode": null, + // "dataName": null, + // "data": null + // } + reqURL := MOBILE_API_VERSION + apiURLs["API_VALIDATE_SESSION"] + resp := c.execute(reqURL, GET, map[string]string{}, "", false) + + if !c.isResponseSuccessfull(resp) { + return false + } + // result = False + // js_resp = await self.__open(API_VALIDATE_SESSION, GET) + // _LOGGER.debug(pprint.pformat(js_resp)) + // if js_resp["success"]: + // if vin != self._current_vin: + // # API call for VIN that is not the current remote context. + // _LOGGER.debug("Switching Subaru API vehicle context to: %s", vin) + // if await self._select_vehicle(vin): + // result = True + // else: + // result = True + + // if result is False: + // await self._authenticate(vin) + // # New session cookie. Must call selectVehicle.json before any other API call. + // if await self._select_vehicle(vin): + // result = True + + c.log.Debug("[DEBUG] Validate Session", "body", string([]byte(resp))) + + return true +} + +func (c *Client) SelectVehicle(vin string) VehicleData { + // { + // "success": true, + // "errorCode": null, + // "dataName": "vehicle", + // "data": { + // "customer": { + // "sessionCustomer": null, + // "email": null, + // "firstName": null, + // "lastName": null, + // "zip": null, + // "oemCustId": null, + // "phone": null + // }, + // "stolenVehicle": false, + // "vehicleName": "Subaru Outback LXT", + // "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" + // ], + // "vin": "4S4BTGND8L3137058", + // "modelYear": "2020", + // "modelCode": "LDJ", + // "engineSize": 2.4, + // "nickname": "Subaru Outback LXT", + // "vehicleKey": 3832950, + // "active": true, + // "licensePlate": "8KV8", + // "licensePlateState": "NJ", + // "email": null, + // "firstName": null, + // "lastName": null, + // "subscriptionFeatures": [ + // "REMOTE", + // "SAFETY", + // "Retail" + // ], + // "accessLevel": -1, + // "zip": null, + // "oemCustId": "CRM-631-HQN48K", + // "vehicleMileage": null, + // "phone": null, + // "userOemCustId": "CRM-631-HQN48K", + // "subscriptionStatus": "ACTIVE", + // "authorizedVehicle": false, + // "preferredDealer": null, + // "cachedStateCode": "NJ", + // "subscriptionPlans": [], + // "needMileagePrompt": false, + // "phev": null, + // "remoteServicePinExist": true, + // "needEmergencyContactPrompt": false, + // "vehicleGeoPosition": { + // "latitude": 40.70019, + // "longitude": -74.401375, + // "speed": null, + // "heading": null, + // "timestamp": 1640494569000 + // }, + // "extDescrip": "Abyss Blue Pearl", + // "intDescrip": "Gray", + // "modelName": "Outback", + // "transCode": "CVT", + // "provisioned": true, + // "timeZone": "America/New_York" + // } + // } + + if vin == "" { + vin = c.currentVin + } + + vinCheck(vin) + + params := map[string]string{ + "vin": vin, + "_": timestamp()} + reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] + resp := c.execute(reqURL, GET, params, "", false) + + var vData VehicleData + respParsed, err := gabs.ParseJSON([]byte(resp)) + if err != nil { + panic(err) + } + + vdString := respParsed.Path("data").String() + json.Unmarshal([]byte(vdString), &vData) + + // resp := c.execute(reqURL, GET, params, "", false) + // log.Debugf("SELECT VEHICLE OUTPUT >> %v\n", string([]byte(resp))) + + // ERRORS + // {"success":false,"errorCode":"vehicleNotInAccount","dataName":null,"data":null} + + // """Select active vehicle for accounts with multiple VINs.""" + // params = {"vin": vin, "_": int(time.time())} + // js_resp = await self.get(API_SELECT_VEHICLE, params=params) + // _LOGGER.debug(pprint.pformat(js_resp)) + // if js_resp.get("success"): + // self._current_vin = vin + // _LOGGER.debug("Current vehicle: vin=%s", js_resp["data"]["vin"]) + // return js_resp["data"] + // if not js_resp.get("success") and js_resp.get("errorCode") == "VEHICLESETUPERROR": + // # Occasionally happens every few hours. Resetting the session seems to deal with it. + // _LOGGER.warning("VEHICLESETUPERROR received. Resetting session.") + // self.reset_session() + // return False + // _LOGGER.debug("Failed to switch vehicle errorCode=%s", js_resp.get("errorCode")) + // # Something else is probably wrong with the backend server context - try resetting + // self.reset_session() + // raise SubaruException("Failed to switch vehicle %s - resetting session." % js_resp.get("errorCode")) + return vData +} + +// New function creates a New MySubaru client +func New(logger *slog.Logger, config *config.MySubaru) (*Client, error) { + + credentials := credentials{ + username: config.Credentials.Username, + password: config.Credentials.Password, + pin: config.Credentials.PIN, + deviceId: config.Credentials.DeviceID, + deviceName: config.Credentials.DeviceName, + } + + client := &Client{ + credentials: credentials, + country: config.Region, + updateInterval: 7200, + fetchInterval: 360, + log: logger, + } + + httpClient := resty.New() + httpClient. + SetBaseURL(MOBILE_API_SERVER[client.country]). + SetHeaders(map[string]string{ + "User-Agent": "Mozilla/5.0 (Linux; Android 10; Android SDK built for x86 Build/QSR1.191030.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.185 Mobile Safari/537.36", + "Origin": "file://", + "X-Requested-With": MOBILE_APP[client.country], + "Accept-Language": "en-US,en;q=0.9", + "Accept-Encoding": "gzip, deflate", + "Accept": "*/*"}) + + client.httpClient = httpClient + + resp := client.auth() + + respParsed, err := gabs.ParseJSON(resp) + if err != nil { + panic(err) + } + + if client.isResponseSuccessfull(resp) { + logger.Debug("Client authentication successful", "isRegistered", respParsed.Path("data.deviceRegistered").Data().(bool)) + client.isAuthenticated = true + client.isRegistered = respParsed.Path("data.deviceRegistered").Data().(bool) + } else { + error, _ := respParsed.Path("errorCode").Data().(string) + switch { + case error == apiErrors["ERROR_INVALID_ACCOUNT"]: + logger.Debug("Invalid account") + case error == apiErrors["ERROR_INVALID_CREDENTIALS"]: + logger.Debug("Client authentication failed") + case error == apiErrors["ERROR_PASSWORD_WARNING"]: + logger.Debug("Multiple Password Failures.") + default: + logger.Debug("Uknown error") + } + } + + if !client.isRegistered { + client.registerDevice() + } + + // TODO + fmt.Printf("Parcing cars: %d\n", len(respParsed.Path("data.vehicles").Children())) + for _, vehicle := range respParsed.Path("data.vehicles").Children() { + fmt.Printf("CAR: %s\n", vehicle.Path("vin").Data().(string)) + client.listOfVins = append(client.listOfVins, vehicle.Path("vin").Data().(string)) + } + client.currentVin = respParsed.Path("data.vehicles.0.vin").Data().(string) + + return client, nil +} + +// GetVehicles . +func (c *Client) GetVehicles() []*Vehicle { + var vehicles []*Vehicle + for _, vin := range c.listOfVins { + params := map[string]string{ + "vin": vin, + "_": timestamp()} + reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] + resp := c.execute(reqURL, GET, params, "", false) + + respParsed, err := gabs.ParseJSON([]byte(resp)) + if err != nil { + panic(err) + } + + vData := VehicleData{} + vdString := respParsed.Path("data").String() + + json.Unmarshal([]byte(vdString), &vData) + + // fmt.Printf("VEHICLE STRING: %+v\n\n", vdString) + // fmt.Printf("VEHICLE DATA: %+v\n\n", vData) + + vehicle := &Vehicle{ + Vin: vin, + CarName: vData.VehicleName, + CarNickname: vData.Nickname, + ModelName: vData.ModelName, + ModelYear: vData.ModelYear, + ModelCode: vData.ModelCode, + ExtDescrip: vData.ExtDescrip, + IntDescrip: vData.IntDescrip, + TransCode: vData.TransCode, + EngineSize: vData.EngineSize, + VehicleKey: vData.VehicleKey, + LicensePlate: vData.LicensePlate, + LicensePlateState: vData.LicensePlateState, + Features: vData.Features, + SubscriptionFeatures: vData.SubscriptionFeatures, + client: c, + } + vehicle.GetVehicleStatus() + vehicle.GetVehicleCondition() + vehicle.GetClimatePresets() + vehicle.GetClimateUserPresets() + + vehicles = append(vehicles, vehicle) + } + + return vehicles +} + +// GetVehicleByVIN . +func (c *Client) GetVehicleByVIN(vin string) *Vehicle { + var vehicle *Vehicle + if contains(c.listOfVins, vin) { + params := map[string]string{ + "vin": vin, + "_": timestamp()} + reqURL := MOBILE_API_VERSION + apiURLs["API_SELECT_VEHICLE"] + resp := c.execute(reqURL, GET, params, "", false) + + respParsed, err := gabs.ParseJSON([]byte(resp)) + if err != nil { + panic(err) + } + + vData := VehicleData{} + vdString := respParsed.Path("data").String() + json.Unmarshal([]byte(vdString), &vData) + + // fmt.Printf("VEHICLE DATA: %+v\n\n", vData) + + vehicle = &Vehicle{ + Vin: vin, + CarName: vData.VehicleName, + CarNickname: vData.Nickname, + ModelName: vData.ModelName, + ModelYear: vData.ModelYear, + ModelCode: vData.ModelCode, + ExtDescrip: vData.ExtDescrip, + IntDescrip: vData.IntDescrip, + TransCode: vData.TransCode, + EngineSize: vData.EngineSize, + VehicleKey: vData.VehicleKey, + LicensePlate: vData.LicensePlate, + LicensePlateState: vData.LicensePlateState, + Features: vData.Features, + SubscriptionFeatures: vData.SubscriptionFeatures, + client: c, + } + vehicle.GetVehicleStatus() + vehicle.GetVehicleCondition() + vehicle.GetClimatePresets() + vehicle.GetClimateUserPresets() + } + + return vehicle +} + +// GetVehicleStatus . +func (c *Client) GetVehicleStatus() { + // { + // "dataName": null, + // "errorCode": null, + // "success": true, + // "data": { + // "avgFuelConsumptionLitersPer100Kilometers": 12.5, + // "avgFuelConsumptionMpg": 18.8, + // "distanceToEmptyFuelKilometers": 563, + // "distanceToEmptyFuelKilometers10s": 560, + // "distanceToEmptyFuelMiles": 349.83, + // "distanceToEmptyFuelMiles10s": 350, + // "evDistanceToEmptyByStateKilometers": null, + // "evDistanceToEmptyByStateMiles": null, + // "evDistanceToEmptyKilometers": null, + // "evDistanceToEmptyMiles": null, + // "evStateOfChargePercent": null, + // "eventDate": 1640494569000, + // "eventDateStr": "2021-12-26T04:56+0000", + // "latitude": 40.700192, + // "longitude": -74.401377, + // "odometerValue": 24065, + // "odometerValueKilometers": 38721, + // "positionHeadingDegree": "150", + // "tirePressureFrontLeft": "2413", + // "tirePressureFrontLeftPsi": "35", + // "tirePressureFrontRight": "2413", + // "tirePressureFrontRightPsi": "35", + // "tirePressureRearLeft": "2551", + // "tirePressureRearLeftPsi": "37", + // "tirePressureRearRight": "2482", + // "tirePressureRearRightPsi": "36", + // "vehicleStateType": "IGNITION_OFF", + // "vhsId": 923920223 + // } + // } + + reqURL := MOBILE_API_VERSION + apiURLs["API_VEHICLE_STATUS"] + resp := c.execute(reqURL, GET, map[string]string{}, "", false) + + respParsed, err := gabs.ParseJSON(resp) + if err != nil { + panic(err) + } + c.log.Debug("GET VEHICLE STATUS OUTPUT", "body", respParsed) + + success, ok := respParsed.Path("success").Data().(bool) + // value == string, ok == false + if !ok { + // TODO: Work with errorCode + panic(success) + } +} + +// GetVehicleStatus . +func (c *Client) GetClimateSettings() { + // { + // "success": true, + // "errorCode": null, + // "dataName": null, + // "data": { + // "climateZoneFrontTemp": "70", + // "runTimeMinutes": "10", + // "climateZoneFrontAirMode": "WINDOW", + // "heatedSeatFrontLeft": "LOW_HEAT", + // "heatedSeatFrontRight": "LOW_HEAT", + // "heatedRearWindowActive": "true", + // "climateZoneFrontAirVolume": "6", + // "outerAirCirculation": "outsideAir", + // "airConditionOn": "false", + // "startConfiguration": "START_ENGINE_ALLOW_KEY_IN_IGNITION" + // } + // } + reqURL := MOBILE_API_VERSION + apiURLs["API_G2_FETCH_CLIMATE_SETTINGS"] + resp := c.execute(reqURL, GET, map[string]string{}, "", false) + + respParsed, err := gabs.ParseJSON(resp) + if err != nil { + panic(err) + } + c.log.Debug("CLIMATE SETTINGS OUTPUT", "response", respParsed) + + // 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) + } + c.log.Debug("CLIMATE SETTINGS OUTPUT", "body", data) +} + +// func isPINRequired() {} +// func getVehicles() {} +// func getEVStatus() {} +// func getRemoteOptionsStatus() {} +// func getRemoteStartStatus() {} +// func getSafetyStatus() {} +// func getSubscriptionStatus() {} +// func getAPIGen() {} +// func getClimateData() {} +// func saveClimateSettings() {} +// func getVehicleName() {} +// func fetch() {} + +// Exec method executes a Client instance with the API URL +func (c *Client) execute(requestUrl string, method string, params map[string]string, pollingUrl string, json bool) []byte { + defer timeTrack("[TIMETRK] Executing Get Request") + + // if !isNil(resp.Cookies()) { + // log.Debugf("AUTH COOKIES OUTPUT >> %v\n", resp.Cookies()) + // c.cookies = resp.Cookies() + // } + + var resp *resty.Response + // GET Requests + if method == "GET" { + resp, _ = c.httpClient. + // SetBaseURL(MOBILE_API_SERVER[c.country]). + R(). + SetQueryParams(params). + Get(requestUrl) + } + + // POST Requests + if method == "POST" { + if json { + // POST > JSON Body + resp, _ = c.httpClient.R(). + SetBody(params). + Post(requestUrl) + } else { + // POST > Form Data + resp, _ = c.httpClient.R(). + SetFormData(params). + Post(requestUrl) + } + } + + respParsed, err := gabs.ParseJSON([]byte(resp.Body())) + if err != nil { + panic(err) + } + + c.log.Debug("HTTP OUTPUT", "body", string(resp.Body())) + + _, ok := respParsed.Path("success").Data().(bool) + // value == string, ok == false + if !ok { + // TODO: Work with errorCode + // panic(success) + fmt.Printf("ERROR: %+v", string([]byte(resp.Body()))) + } + + if pollingUrl != "" { + serviceRequestId, _ := respParsed.Path("data.serviceRequestId").Data().(string) + + time.Sleep(3 * time.Second) + + attempts := 20 + poolingLoop: + for attempts > 0 { + resp, _ = c.httpClient. + SetBaseURL(MOBILE_API_SERVER[c.country]). + R(). + SetQueryParams(map[string]string{ + "serviceRequestId": serviceRequestId, + }). + Get(pollingUrl) + + c.log.Debug("POLLING HTTP OUTPUT", "body", string([]byte(resp.Body()))) + // {"success":false,"errorCode":"404-soa-unableToParseResponseBody","dataName":"errorResponse","data":{"errorLabel":"404-soa-unableToParseResponseBody","errorDescription":null}} + + respParsed, err := gabs.ParseJSON(resp.Body()) + if err != nil { + panic(err) + } + + success, ok := respParsed.Path("success").Data().(bool) + if !ok { + panic(success) + } + if success { + status, _ := respParsed.Path("data.remoteServiceState").Data().(string) + switch { + case status == "finished": + c.log.Debug("Remote service request completed successfully", "request id", serviceRequestId) + break poolingLoop + case status == "started": + c.log.Debug("Subaru API reports remote service request is in progress", "request id", serviceRequestId) + } + } else { + c.log.Debug("Backend session expired, please try again") + break poolingLoop + } + attempts-- + time.Sleep(3 * time.Second) + } + } + + // fmt.Printf("[DEBUG] HTTP OUTPUT >> %v\n", string([]byte(resp.Body()))) + + return resp.Body() +} + +// isResponseSuccessfull . +func (c *Client) isResponseSuccessfull(resp []byte) bool { + respParsed, err := gabs.ParseJSON(resp) + if err != nil { + c.log.Debug("error while parsing json response", "error", err) + } + + success, ok := respParsed.Path("success").Data().(bool) + if !ok { + c.log.Debug("response is not successful", "error", resp) + } + + // ERRORS FROM CLIENT CREATION AFTER AUTH + // error, _ := respParsed.Path("errorCode").Data().(string) + // switch { + // case error == apiErrors["ERROR_INVALID_ACCOUNT"]: + // fmt.Println("Invalid account") + // case error == apiErrors["ERROR_INVALID_CREDENTIALS"]: + // {"success":false,"errorCode":"InvalidCredentials","dataName":"remoteServiceStatus","data":{"serviceRequestId":null,"success":false,"cancelled":false,"remoteServiceType":null,"remoteServiceState":null,"subState":null,"errorCode":null,"result":null,"updateTime":null,"vin":null,"errorDescription":"The credentials supplied are invalid, tries left 2"}} + // fmt.Println("Client authentication failed") + // case error == apiErrors["ERROR_PASSWORD_WARNING"]: + // fmt.Println("Multiple Password Failures.") + // default: + // fmt.Println("Uknown error") + // } + + return success +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..7a62d51 --- /dev/null +++ b/config/config.go @@ -0,0 +1,72 @@ +package config + +import ( + "log/slog" + "os" + + "github.com/spf13/viper" +) + +// Config . +type Config struct { + MySubaru MySubaru `json:"mysubaru" yaml:"mysubaru"` + Timezone string `json:"timezone" yaml:"timezone"` + Logging *Logging `json:"logging" yaml:"logging"` +} + +// Emporia . +type MySubaru struct { + Credentials Credentials `json:"credentials" yaml:"credentials"` + Region string `json:"region" yaml:"region"` + AutoReconnect bool `json:"auto_reconnect" yaml:"auto_reconnect"` +} + +// Credentials . +type Credentials struct { + Username string `json:"username" yaml:"username"` + Password string `json:"password" yaml:"password"` + PIN string `json:"pin" yaml:"pin"` + DeviceID string `json:"deviceid" yaml:"deviceid"` + DeviceName string `json:"devicename" yaml:"devicename"` +} + +// Logging . +type Logging struct { + Level string `json:"level" yaml:"level"` + Output string `json:"output" yaml:"output"` + Source bool `json:"source,omitempty" yaml:"source,omitempty"` +} + +func New() (*Config, error) { + viper.SetConfigName("config") // name of config file (without extension) + viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name + viper.AddConfigPath(".") // optionally look for config in the working directory + viper.AddConfigPath("/etc/mysubaru") // optionally look for config in the working directory + + viper.AutomaticEnv() + + viper.SetDefault("Timezone", "America/New_York") + viper.SetDefault("MySubaru.AutoReconnect", true) + viper.SetDefault("Logging.Level", "INFO") + viper.SetDefault("Logging.Output", "TEXT") + viper.SetDefault("Logging.Source", "false") + + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + slog.Error("cannot open config file", "error", err.Error()) + os.Exit(1) + } + + // viper.WatchConfig() + // viper.OnConfigChange(func(e fsnotify.Event) { + // log.Println("Config file changed:", e.Name) + // }) + + var config Config + if err := viper.Unmarshal(&config); err != nil { + slog.Error("cannot unmarshal config file", "error", err.Error()) + os.Exit(1) + } + + return &config, nil +} diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..e027cd3 --- /dev/null +++ b/consts.go @@ -0,0 +1,357 @@ +package mysubaru + +// Lastest version /g2v27 +var MOBILE_API_VERSION = "/g2v28" + +var MOBILE_API_SERVER = map[string]string{ + "USA": "https://mobileapi.prod.subarucs.com", + "CAN": "https://mobileapi.ca.prod.subarucs.com", +} + +var MOBILE_APP = map[string]string{ + "USA": "com.subaru.telematics.app.remote", + "CAN": "ca.subaru.telematics.remote", +} + +var WEB_API_SERVER = map[string]string{ + "USA": "https://www.mysubaru.com", + "CAN": "https://www.mysubaru.ca", +} + +var apiURLs = map[string]string{ + "WEB_API_LOGIN": "/login", + "WEB_API_LIST_DEVICES": "/listMyDevices.json", // TODO + "WEB_API_AUTHORIZE_DEVICE": "/profile/updateDeviceEntry.json", + "WEB_API_NAME_DEVICE": "/profile/addDeviceName.json", + "WEB_API_EDIT_NAME_DEVICE": "/profile/editDeviceName.json", + "WEB_API_VERIFY_NAME_DEVICE": "/profile/verifyDeviceName.json", + "API_LOGIN": "/login.json", // Same API for g1 and g2 + "API_REFRESH_VEHICLES": "/refreshVehicles.json", + "API_SELECT_VEHICLE": "/selectVehicle.json", + "API_VALIDATE_SESSION": "/validateSession.json", + "API_VEHICLE_STATUS": "/vehicleStatus.json", + "API_AUTHORIZE_DEVICE": "/authenticateDevice.json", + "API_NAME_DEVICE": "/nameThisDevice.json", + "API_VEHICLE_HEALTH": "/vehicleHealth.json", + "API_LOCK": "/service/api_gen/lock/execute.json", // Similar API for g1 and g2 -- controller should replace 'api_gen' with either 'g1' or 'g2' + "API_LOCK_CANCEL": "/service/api_gen/lock/cancel.json", + "API_UNLOCK": "/service/api_gen/unlock/execute.json", + "API_UNLOCK_CANCEL": "/service/api_gen/unlock/cancel.json", + "API_HORN_LIGHTS": "/service/api_gen/hornLights/execute.json", + "API_HORN_LIGHTS_CANCEL": "/service/api_gen/hornLights/cancel.json", + "API_HORN_LIGHTS_STOP": "/service/api_gen/hornLights/stop.json", + "API_LIGHTS": "/service/api_gen/lightsOnly/execute.json", + "API_LIGHTS_CANCEL": "/service/api_gen/lightsOnly/cancel.json", + "API_LIGHTS_STOP": "/service/api_gen/lightsOnly/stop.json", + "API_CONDITION": "/service/api_gen/condition/execute.json", + "API_LOCATE": "/service/api_gen/locate/execute.json", // Get the last location the vehicle has reported to Subaru + "API_REMOTE_SVC_STATUS": "/service/g2/remoteService/status.json", + "API_G1_LOCATE_UPDATE": "/service/g1/vehicleLocate/execute.json", // Different API for g1 and g2 + "API_G1_LOCATE_STATUS": "/service/g1/vehicleLocate/status.json", + "API_G1_HORN_LIGHTS_STATUS": "/service/g1/hornLights/status.json", // g1-Only API + "API_G2_LOCATE_UPDATE": "/service/g2/vehicleStatus/execute.json", + "API_G2_LOCATE_STATUS": "/service/g2/vehicleStatus/locationStatus.json", + "API_G2_SEND_POI": "/service/g2/sendPoi/execute.json", // g2-Only API + "API_G2_SPEEDFENCE": "/service/g2/speedFence/execute.json", + "API_G2_GEOFENCE": "/service/g2/geoFence/execute.json", + "API_G2_CURFEW": "/service/g2/curfew/execute.json", + "API_G2_REMOTE_ENGINE_START": "/service/g2/engineStart/execute.json", + "API_G2_REMOTE_ENGINE_START_CANCEL": "/service/g2/engineStart/cancel.json", + "API_G2_REMOTE_ENGINE_STOP": "/service/g2/engineStop/execute.json", + + "API_G2_FETCH_CLIMATE_SETTINGS": "/service/g2/remoteEngineStart/fetch.json", + "API_G2_SAVE_CLIMATE_SETTINGS": "/service/g2/remoteEngineStart/save.json", + + "API_G2_FETCH_RES_QUICK_START_SETTINGS": "/service/g2/remoteEngineQuickStartSettings/fetch.json", + "API_G2_FETCH_RES_USER_PRESETS": "/service/g2/remoteEngineStartSettings/fetch.json", + "API_G2_FETCH_RES_SUBARU_PRESETS": "/service/g2/climatePresetSettings/fetch.json", + "API_G2_SAVE_RES_SETTINGS": "/service/g2/remoteEngineStartSettings/save.json", + "API_G2_SAVE_RES_QUICK_START_SETTINGS": "/service/g2/remoteEngineQuickStartSettings/save.json", + + "API_EV_CHARGE_NOW": "/service/g2/phevChargeNow/execute.json", // EV-Only API + "API_EV_FETCH_CHARGE_SETTINGS": "/service/g2/phevGetTimerSettings/execute.json", + "API_EV_SAVE_CHARGE_SETTINGS": "/service/g2/phevSendTimerSetting/execute.json", + "API_EV_DELETE_CHARGE_SCHEDULE": "/service/g2/phevDeleteTimerSetting/execute.json", +} + +// API_VEHICLE_FEATURES items that determine available functionality +// 11.6MMAN ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL NAV_TOMTOM OPL_MIL RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA SRS_MIL TEL_MIL TIF_35 TIR_33 TPMS_MIL VDC_MIL WASH_MIL g2 +// ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL OPL_MIL PANPM-TUIRWAOC PWAAADWWAP RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA RTGU RVFS SRH_MIL SRS_MIL TEL_MIL TIF_35 TIR_33 TLD TPMS_MIL VALET VDC_MIL WASH_MIL g3 +var apiFeatures = map[string]string{ + "API_FEATURE_PHEV": "PHEV", + "API_FEATURE_REMOTE_START": "RES", + "API_FEATURE_REMOTE": "REMOTE", + "API_FEATURE_SAFETY": "SAFETY", + "API_FEATURE_ACTIVE": "ACTIVE", + "API_FEATURE_MOONROOF_PANORAMIC": "PANPM-DG2G", + "API_FEATURE_MOONROOF_POWER": "PANPM-TUIRWAOC", + "API_FEATURE_POWER_WINDOWS": "PWAAADWWAP", + "API_FEATURE_FRONT_TIRE_RECOMMENDED_PRESSURE_PREFIX": "TIF_", + "API_FEATURE_REAR_TIRE_RECOMMENDED_PRESSURE_PREFIX": "TIR_", + "API_FEATURE_G1_TELEMATICS": "g1", + "API_FEATURE_G2_TELEMATICS": "g2", + "API_FEATURE_G3_TELEMATICS": "g3", +} + +var apiErrors = map[string]string{ + "ERROR_SOA_403": "403-soa-unableToParseResponseBody", // G2 Error Codes + "ERROR_INVALID_CREDENTIALS": "InvalidCredentials", + "ERROR_SERVICE_ALREADY_STARTED": "ServiceAlreadyStarted", + "ERROR_INVALID_ACCOUNT": "invalidAccount", + "ERROR_PASSWORD_WARNING": "passwordWarning", + "ERROR_ACCOUNT_LOCKED": "accountLocked", + "ERROR_NO_VEHICLES": "noVehiclesOnAccount", + "ERROR_NO_ACCOUNT": "accountNotFound", + "ERROR_TOO_MANY_ATTEMPTS": "tooManyAttempts", + "ERROR_VEHICLE_NOT_IN_ACCOUNT": "vehicleNotInAccount", + "ERROR_G1_NO_SUBSCRIPTION": "SXM40004", // G1 Error Codes + "ERROR_G1_STOLEN_VEHICLE": "SXM40005", + "ERROR_G1_INVALID_PIN": "SXM40006", + "ERROR_G1_SERVICE_ALREADY_STARTED": "SXM40009", + "ERROR_G1_PIN_LOCKED": "SXM40017", +} + +// ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL OPL_MIL PANPM-TUIRWAOC PWAAADWWAP RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA RTGU RVFS SRH_MIL SRS_MIL TEL_MIL TIF_35 TIR_33 TLD TPMS_MIL VALET VDC_MIL WASH_MIL g3 +// 11.6MMAN ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL NAV_TOMTOM OPL_MIL RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA SRS_MIL TEL_MIL TIF_35 TIR_33 TPMS_MIL VDC_MIL WASH_MIL g2 +var features = map[string]string{ + "g1": "Generation #1", + "g2": "Generation #2", + "g3": "Generation #3", + "11.6MMAN": "11.6-inch Infotainment System", + "EYESIGHT": "EyeSight Exclusive Advanced Driver-Assist System", + "NAV_TOMTOM": "TomTom Navigation", + "PWAAADWWAP": "Power Windows", + "PANPM-TUIRWAOC": "Power Moonroof", + "PANPM-DG2G": "Panoramic Moonroof", + "PHEV": "Electric Vehicle", + "RES": "Remote Start", + "TIF_35": "Tire Pressure Front 35", + "TIR_33": "Tire Pressure Rear 35", + "VALET": "Valet Parking", +} + +var errors = map[string]string{ + "ABS_MIL": "Anti-Lock Braking System", + "AHBL_MIL": "Automatic Headlight Beam Leveler", + "ATF_MIL": "Automatic Transmission Oil Temperature", + "AWD_MIL": "Symmetrical Full-Time AWD", + "BSDRCT_MIL": "Blind-Spot Detection", + "CEL_MIL": "Check Engine Light", + "EBD_MIL": "Electronic Brake Force Distribution", + "EOL_MIL": "Engine Oil Level", + "EPAS_MIL": "Electric Power Assisted Steering", + "EPB_MIL": "Parking Brake", + "ESS_MIL": "EyeSight Exclusive Advanced Driver-Assist System", + "ISS_MIL": "iss", + "OPL_MIL": "Oil Pressure", + "RAB_MIL": "Reverse Auto Braking", + "SRH_MIL": "Steering Responsive Headlights", + "SRS_MIL": "Airbag System", + "TEL_MIL": "telematics", + "TPMS_MIL": "tpms", + "VDC_MIL": "Vehicle Dynamics Control", + "WASH_MIL": "Windshield Washer Fluid Level", +} +var modelCodes = map[string]string{ + "PDL": "Subaru Outback Touring XT", + "LDJ": "Subaru Outback Limited XT", + "KDF": "Outback 2.5i Limited", + "LDD": "Outback Premium", + "PDG": "Outback Touring", + "KFB": "Forester Base Model", + "HFJ": "Forester 2.5i Touring", + "KFJ": "Forester Touring", + "KAF": "Legacy 2.5i Limited", + "KLF": "Impreza 2.0i Sport", + "LRD": "Crosstrek Premium", + "KRD": "Crosstrek 2.0i Premium", + "PCL": "Ascent Limited 7-Passenger", +} + +const ( + GET = "GET" + POST = "POST" + + SERVICE_REQ_ID = "serviceRequestId" + TEMP_F = "climateZoneFrontTemp" // Remote start constants + TEMP_F_MAX = 85 + TEMP_F_MIN = 60 + TEMP_C = "climateZoneFrontTempCelsius" + TEMP_C_MAX = 30 + TEMP_C_MIN = 15 + CLIMATE = "climateSettings" + CLIMATE_DEFAULT = "climateSettings" + RUNTIME = "runTimeMinutes" + RUNTIME_DEFAULT = "10" + MODE = "climateZoneFrontAirMode" + MODE_DEFROST = "WINDOW" + MODE_FEET_DEFROST = "FEET_WINDOW" + MODE_FACE = "FACE" + MODE_FEET = "FEET" + MODE_SPLIT = "FEET_FACE_BALANCED" + MODE_AUTO = "AUTO" + HEAT_SEAT_LEFT = "heatedSeatFrontLeft" + HEAT_SEAT_RIGHT = "heatedSeatFrontRight" + HEAT_SEAT_HI = "HIGH_HEAT" + HEAT_SEAT_MED = "MEDIUM_HEAT" + HEAT_SEAT_LOW = "LOW_HEAT" + HEAT_SEAT_OFF = "OFF" + REAR_DEFROST = "heatedRearWindowActive" + REAR_DEFROST_ON = "true" + REAR_DEFROST_OFF = "false" + FAN_SPEED = "climateZoneFrontAirVolume" + FAN_SPEED_LOW = "2" + FAN_SPEED_MED = "4" + FAN_SPEED_HI = "7" + FAN_SPEED_AUTO = "AUTO" + RECIRCULATE = "outerAirCirculation" + RECIRCULATE_OFF = "outsideAir" + RECIRCULATE_ON = "recirculation" + REAR_AC = "airConditionOn" + REAR_AC_ON = "true" + REAR_AC_OFF = "false" + START_CONFIG = "startConfiguration" + START_CONFIG_DEFAULT_EV = "start_Climate_Control_only_allow_key_in_ignition" + START_CONFIG_DEFAULT_RES = "START_ENGINE_ALLOW_KEY_IN_IGNITION" + WHICH_DOOR = "unlockDoorType" // Unlock doors constants + ALL_DOORS = "ALL_DOORS_CMD" + DRIVERS_DOOR = "FRONT_LEFT_DOOR_CMD" + HEADING = "heading" // Location data constants + LATITUDE = "latitude" + LONGITUDE = "longitude" + LOCATION_TIME = "locationTimestamp" + SPEED = "speed" + BAD_LATITUDE = 90.0 + BAD_LONGITUDE = 180.0 + AVG_FUEL_CONSUMPTION = "AVG_FUEL_CONSUMPTION" // Vehicle status constants + BATTERY_VOLTAGE = "BATTERY_VOLTAGE" // NO LONGER + DIST_TO_EMPTY = "DISTANCE_TO_EMPTY_FUEL" + DOOR_BOOT_POSITION = "DOOR_BOOT_POSITION" + DOOR_ENGINE_HOOD_POSITION = "DOOR_ENGINE_HOOD_POSITION" + DOOR_FRONT_LEFT_POSITION = "DOOR_FRONT_LEFT_POSITION" + DOOR_FRONT_RIGHT_POSITION = "DOOR_FRONT_RIGHT_POSITION" + DOOR_REAR_LEFT_POSITION = "DOOR_REAR_LEFT_POSITION" + DOOR_REAR_RIGHT_POSITION = "DOOR_REAR_RIGHT_POSITION" + EV_CHARGER_STATE_TYPE = "EV_CHARGER_STATE_TYPE" + EV_CHARGE_SETTING_AMPERE_TYPE = "EV_CHARGE_SETTING_AMPERE_TYPE" + EV_CHARGE_VOLT_TYPE = "EV_CHARGE_VOLT_TYPE" + EV_DISTANCE_TO_EMPTY = "EV_DISTANCE_TO_EMPTY" + EV_IS_PLUGGED_IN = "EV_IS_PLUGGED_IN" + EV_STATE_OF_CHARGE_MODE = "EV_STATE_OF_CHARGE_MODE" + EV_STATE_OF_CHARGE_PERCENT = "EV_STATE_OF_CHARGE_PERCENT" + EV_TIME_TO_FULLY_CHARGED = "EV_TIME_TO_FULLY_CHARGED" + EV_TIME_TO_FULLY_CHARGED_UTC = "EV_TIME_TO_FULLY_CHARGED_UTC" + EXTERNAL_TEMP = "EXT_EXTERNAL_TEMP" + ODOMETER = "ODOMETER" + POSITION_TIMESTAMP = "POSITION_TIMESTAMP" + TIMESTAMP = "TIMESTAMP" + TIRE_PRESSURE_FL = "TYRE_PRESSURE_FRONT_LEFT" + TIRE_PRESSURE_FR = "TYRE_PRESSURE_FRONT_RIGHT" + TIRE_PRESSURE_RL = "TYRE_PRESSURE_REAR_LEFT" + TIRE_PRESSURE_RR = "TYRE_PRESSURE_REAR_RIGHT" + VEHICLE_STATE = "VEHICLE_STATE_TYPE" + WINDOW_FRONT_LEFT_STATUS = "WINDOW_FRONT_LEFT_STATUS" + WINDOW_FRONT_RIGHT_STATUS = "WINDOW_FRONT_RIGHT_STATUS" + WINDOW_REAR_LEFT_STATUS = "WINDOW_REAR_LEFT_STATUS" + WINDOW_REAR_RIGHT_STATUS = "WINDOW_REAR_RIGHT_STATUS" + CHARGING = "CHARGING" + LOCKED_CONNECTED = "LOCKED_CONNECTED" + UNLOCKED_CONNECTED = "UNLOCKED_CONNECTED" + DOOR_OPEN = "OPEN" + DOOR_CLOSED = "CLOSED" + WINDOW_OPEN = "OPEN" + WINDOW_CLOSED = "CLOSE" + IGNITION_ON = "IGNITION_ON" + NOT_EQUIPPED = "NOT_EQUIPPED" + VS_AVG_FUEL_CONSUMPTION = "avgFuelConsumptionLitersPer100Kilometers" // vehicleStatus.json keys + VS_DIST_TO_EMPTY = "distanceToEmptyFuelKilometers" + VS_TIMESTAMP = "eventDate" + VS_LATITUDE = "latitude" + VS_LONGITUDE = "longitude" + VS_HEADING = "positionHeadingDegree" + VS_ODOMETER = "odometerValueKilometers" + VS_VEHICLE_STATE = "vehicleStateType" + VS_TIRE_PRESSURE_FL = "tirePressureFrontLeft" + VS_TIRE_PRESSURE_FR = "tirePressureFrontRight" + VS_TIRE_PRESSURE_RL = "tirePressureRearLeft" + VS_TIRE_PRESSURE_RR = "tirePressureRearRight" + BAD_AVG_FUEL_CONSUMPTION = "16383" // Erroneous Values + BAD_DISTANCE_TO_EMPTY_FUEL = "16383" + BAD_EV_TIME_TO_FULLY_CHARGED = "65535" + BAD_TIRE_PRESSURE = "32767" + BAD_ODOMETER = "None" + BAD_EXTERNAL_TEMP = "-64.0" + UNKNOWN = "UNKNOWN" + VENTED = "VENTED" + LOCATION_VALID = "location_valid" + TIMESTAMP_FMT = "%Y-%m-%dT%H:%M:%S%z" // "2020-04-25T23:35:55+0000" // Timestamp Formats + POSITION_TIMESTAMP_FMT = "%Y-%m-%dT%H:%M:%SZ" // "2020-04-25T23:35:55Z" + ERROR_SOA_403 = "403-soa-unableToParseResponseBody" // G2 Error Codes + ERROR_SOA_404 = "404-soa-unableToParseResponseBody" // Bad request body + ERROR_INVALID_CREDENTIALS = "InvalidCredentials" + ERROR_SERVICE_ALREADY_STARTED = "ServiceAlreadyStarted" + ERROR_INVALID_ACCOUNT = "invalidAccount" + ERROR_PASSWORD_WARNING = "passwordWarning" + ERROR_ACCOUNT_LOCKED = "accountLocked" + ERROR_NO_VEHICLES = "noVehiclesOnAccount" + ERROR_NO_ACCOUNT = "accountNotFound" + ERROR_TOO_MANY_ATTEMPTS = "tooManyAttempts" + ERROR_VEHICLE_NOT_IN_ACCOUNT = "vehicleNotInAccount" + ERROR_G1_NO_SUBSCRIPTION = "SXM40004" // G1 Error Codes + ERROR_G1_STOLEN_VEHICLE = "SXM40005" + ERROR_G1_INVALID_PIN = "SXM40006" + ERROR_G1_SERVICE_ALREADY_STARTED = "SXM40009" + ERROR_G1_PIN_LOCKED = "SXM40017" + VEHICLE_ATTRIBUTES = "attributes" //Controller Vehicle Data Dict Keys + VEHICLE_STATUS = "status" + VEHICLE_ID = "id" + VEHICLE_NAME = "nickname" + VEHICLE_API_GEN = "api_gen" + VEHICLE_LOCK = "lock" + VEHICLE_LAST_UPDATE = "last_update_time" + VEHICLE_LAST_FETCH = "last_fetch_time" + VEHICLE_FEATURES = "features" + VEHICLE_SUBSCRIPTION_FEATURES = "subscriptionFeatures" + VEHICLE_SUBSCRIPTION_STATUS = "subscriptionStatus" + FEATURE_PHEV = "PHEV" + FEATURE_REMOTE_START = "RES" + FEATURE_G1_TELEMATICS = "g1" // Vehicle has 2016-2018 telematics version. + FEATURE_G2_TELEMATICS = "g2" // Vehicle has 2019+ telematics version. + FEATURE_G3_TELEMATICS = "g3" // Vehicle has 2019+ telematics version. + FEATURE_REMOTE = "REMOTE" + FEATURE_SAFETY = "SAFETY" + FEATURE_ACTIVE = "ACTIVE" + DEFAULT_UPDATE_INTERVAL = 7200 + DEFAULT_FETCH_INTERVAL = 300 + // VALID_CLIMATE_OPTIONS = { + // CLIMATE: [CLIMATE_DEFAULT], + // TEMP_C: [str(_) for _ in range(TEMP_C_MIN, TEMP_C_MAX + 1)], + // TEMP_F: [str(_) for _ in range(TEMP_F_MIN, TEMP_F_MAX + 1)], + // FAN_SPEED: [FAN_SPEED_AUTO, FAN_SPEED_LOW, FAN_SPEED_MED, FAN_SPEED_HI], + // HEAT_SEAT_LEFT: [HEAT_SEAT_OFF, HEAT_SEAT_LOW, HEAT_SEAT_MED, HEAT_SEAT_HI], + // HEAT_SEAT_RIGHT: [HEAT_SEAT_OFF, HEAT_SEAT_LOW, HEAT_SEAT_MED, HEAT_SEAT_HI], + // MODE: [ + // MODE_DEFROST, + // MODE_FEET_DEFROST, + // MODE_FACE, + // MODE_FEET, + // MODE_SPLIT, + // MODE_AUTO, + // ], + // RECIRCULATE: [RECIRCULATE_OFF, RECIRCULATE_ON], + // REAR_AC: [REAR_AC_OFF, REAR_AC_ON], + // REAR_DEFROST: [REAR_DEFROST_OFF, REAR_DEFROST_ON], + // START_CONFIG: [START_CONFIG_DEFAULT_EV, START_CONFIG_DEFAULT_RES], + // RUNTIME: [str(RUNTIME_DEFAULT)], + // } + + // BAD_SENSOR_VALUES = [ + // BAD_AVG_FUEL_CONSUMPTION, + // BAD_DISTANCE_TO_EMPTY_FUEL, + // BAD_EV_TIME_TO_FULLY_CHARGED, + // BAD_TIRE_PRESSURE, + // BAD_ODOMETER, + // BAD_EXTERNAL_TEMP, + // ] + // BAD_BINARY_SENSOR_VALUES = [UNKNOWN, VENTED, NOT_EQUIPPED] +) diff --git a/example/config.sample.yaml b/example/config.sample.yaml new file mode 100644 index 0000000..a2dd2ec --- /dev/null +++ b/example/config.sample.yaml @@ -0,0 +1,15 @@ +credentials: + username: user@email.com + password: "Secr#TPassW0rd" + pin: "PIN" + deviceId: GENERATE-DEVICE-ID + deviceName: Golang Integration +mysubaru: + region: USA +mqtt: + client_id: mysubaru + username: mysubaru + password: + host: mqtt.hostname.com + port: 1883 +log: info diff --git a/example/example.go b/example/example.go new file mode 100644 index 0000000..156cef3 --- /dev/null +++ b/example/example.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + + mysubaru "github.com/alex-savin/go-mysubaru" + "github.com/alex-savin/go-mysubaru/config" +) + +// var log = *slog.Logger +var cfg = &config.Config{} + +const ( + LoggingOutputJson = "JSON" + LoggingOutputText = "TEXT" +) + +func configureLogging(config *config.Logging) *slog.Logger { //nolint:unparam + if config == nil { + return nil + } + + var level slog.Level + if err := level.UnmarshalText([]byte(config.Level)); err != nil { + slog.Warn(err.Error()) + slog.Warn(fmt.Sprintf("logging level not recognized, defaulting to level %s", slog.LevelInfo.String())) + level = slog.LevelInfo + } + + var handler slog.Handler + switch config.Output { + case LoggingOutputJson: + handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + case LoggingOutputText: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + default: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + } + + return slog.New(handler) +} + +func main() { + + var err error + cfg, err = config.New() + if err != nil { // Handle errors reading the config file + slog.Error("Fatal error config file", "error", err.Error()) + os.Exit(1) + } + + logger := configureLogging(cfg.Logging) + + logger.Debug("printting config", "config", cfg) + + ms, _ := mysubaru.New(logger, &cfg.MySubaru) + // mysubaru.selectVehicle("4S4BTGPD0P3199198") + + // 11.6MMAN ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL NAV_TOMTOM OPL_MIL RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA SRS_MIL TEL_MIL TIF_35 TIR_33 TPMS_MIL VDC_MIL WASH_MIL g2 + // subaru1 := mysubaru.GetVehicleByVIN("4S4BTGND8L3137058") + // fmt.Printf("SUBARU #2 (Vehicle Status):\n") + // subaru1.GetVehicleStatus() + // fmt.Printf("SUBARU #2 (Vehicle Condition):\n") + // subaru1.GetVehicleCondition() + // fmt.Printf("SUBARU #1: %+v\n", subaru1) + // fmt.Printf("GEN #1: %+v\n\n", subaru1.getAPIGen()) + + // ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL OPL_MIL PANPM-TUIRWAOC PWAAADWWAP RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA RTGU RVFS SRH_MIL SRS_MIL TEL_MIL TIF_35 TIR_33 TLD TPMS_MIL VALET VDC_MIL WASH_MIL g3 + subaru := ms.GetVehicleByVIN("4S4BTGPD0P3199198") + subaru.GetLocation(true) + // subaru.EngineStart() + fmt.Printf("SUBARU #1 (Vehicle Status):\n") + subaru.GetVehicleStatus() + // fmt.Printf("SUBARU #1 (Vehicle Condition):\n") + // subaru.GetVehicleCondition() + // fmt.Printf("SUBARU #1: %+v\n", subaru) + // subaru.GetClimatePresets() + // subaru.GetClimateUserPresets() + // fmt.Printf("SUBARU #2: %+v\n", subaru) + // subaru.GetVehicleHealth() + // subaru.GetFeaturesList() + + // subaru.LightsStart() + // time.Sleep(10 * time.Second) + // subaru.LightsStop() + + // subaru := mysubaru.GetVehicles()[0] + + // fmt.Printf("SUBARU: %+v\n", subaru) + + // fmt.Printf("Subaru Gen: %+v\n\n", subaru.getAPIGen()) + + // subaru.EngineOn() + // subaru.GetLocation(false) + + // subaru.GetVehicleStatus() + // subaru.GetVehicleCondition() + // fmt.Printf("SUBARU: %+v\n", subaru) + + // subaru.GetClimateQuickPresets() + // subaru.GetClimatePresets() + // subaru.GetClimateUserPresets() + + // subaru.LightsStart() + // time.Sleep(15 * time.Second) + // subaru.LightsStop() + + // subaru.listDevices() + + // subaru.GetVehicleStatus() + + // subaru.GetClimateSettings() + + // subaru.EngineStart() + // time.Sleep(15 * time.Second) + // subaru.EngineStop() + + // subaru.Unlock() + // time.Sleep(20 * time.Second) + // subaru.Lock() + + // subaru.LightsStart() + // time.Sleep(15 * time.Second) + // subaru.LightsStop() + + // subaru.GetLocation() +} diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..2586c8a --- /dev/null +++ b/example/go.mod @@ -0,0 +1,31 @@ +module example + +go 1.21 + +require github.com/alex-savin/go-mysubaru v0.0.0-20231207172256-23b354b0f2c1 + +require ( + github.com/Jeffail/gabs/v2 v2.7.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-resty/resty/v2 v2.10.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6289dbf --- /dev/null +++ b/go.mod @@ -0,0 +1,38 @@ +module github.com/alex-savin/go-mysubaru + +go 1.24 + +require ( + github.com/Jeffail/gabs v1.4.0 + github.com/Jeffail/gabs/v2 v2.7.0 + github.com/go-resty/resty/v2 v2.16.5 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/viper v1.20.1 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/cast v1.8.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..744e7cc --- /dev/null +++ b/main.go @@ -0,0 +1,129 @@ +package mysubaru + +import ( + "fmt" + "log/slog" + "os" + + mysubaru "github.com/alex-savin/go-mysubaru" + "github.com/alex-savin/go-mysubaru/config" +) + +// var log = *slog.Logger +var cfg = &config.Config{} + +const ( + LoggingOutputJson = "JSON" + LoggingOutputText = "TEXT" +) + +func configureLogging(config *config.Logging) *slog.Logger { //nolint:unparam + if config == nil { + return nil + } + + var level slog.Level + if err := level.UnmarshalText([]byte(config.Level)); err != nil { + slog.Warn(err.Error()) + slog.Warn(fmt.Sprintf("logging level not recognized, defaulting to level %s", slog.LevelInfo.String())) + level = slog.LevelInfo + } + + var handler slog.Handler + switch config.Output { + case LoggingOutputJson: + handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + case LoggingOutputText: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + default: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + } + + return slog.New(handler) +} + +func main() { + + var err error + cfg, err = config.New() + if err != nil { // Handle errors reading the config file + slog.Error("Fatal error config file", "error", err.Error()) + os.Exit(1) + } + + logger := configureLogging(cfg.Logging) + + logger.Debug("printting config", "config", cfg) + + ms, _ := mysubaru.New(logger, &cfg.MySubaru) + // mysubaru.selectVehicle("4S4BTGPD0P3199198") + + // 11.6MMAN ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL NAV_TOMTOM OPL_MIL RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA SRS_MIL TEL_MIL TIF_35 TIR_33 TPMS_MIL VDC_MIL WASH_MIL g2 + // subaru1 := mysubaru.GetVehicleByVIN("4S4BTGND8L3137058") + // fmt.Printf("SUBARU #2 (Vehicle Status):\n") + // subaru1.GetVehicleStatus() + // fmt.Printf("SUBARU #2 (Vehicle Condition):\n") + // subaru1.GetVehicleCondition() + // fmt.Printf("SUBARU #1: %+v\n", subaru1) + // fmt.Printf("GEN #1: %+v\n\n", subaru1.getAPIGen()) + + // ABS_MIL ACCS AHBL_MIL ATF_MIL AWD_MIL BSD BSDRCT_MIL CEL_MIL EBD_MIL EOL_MIL EPAS_MIL EPB_MIL ESS_MIL EYESIGHT ISS_MIL OPL_MIL PANPM-TUIRWAOC PWAAADWWAP RAB_MIL RCC REARBRK RES RESCC RHSF RPOI RPOIA RTGU RVFS SRH_MIL SRS_MIL TEL_MIL TIF_35 TIR_33 TLD TPMS_MIL VALET VDC_MIL WASH_MIL g3 + subaru := ms.GetVehicleByVIN("4S4BTGPD0P3199198") + subaru.GetLocation(true) + // subaru.EngineStart() + fmt.Printf("SUBARU #1 (Vehicle Status):\n") + subaru.GetVehicleStatus() + // fmt.Printf("SUBARU #1 (Vehicle Condition):\n") + // subaru.GetVehicleCondition() + // fmt.Printf("SUBARU #1: %+v\n", subaru) + // subaru.GetClimatePresets() + // subaru.GetClimateUserPresets() + // fmt.Printf("SUBARU #2: %+v\n", subaru) + // subaru.GetVehicleHealth() + // subaru.GetFeaturesList() + + // subaru.LightsStart() + // time.Sleep(10 * time.Second) + // subaru.LightsStop() + + // subaru := mysubaru.GetVehicles()[0] + + // fmt.Printf("SUBARU: %+v\n", subaru) + + // fmt.Printf("Subaru Gen: %+v\n\n", subaru.getAPIGen()) + + // subaru.EngineOn() + // subaru.GetLocation(false) + + // subaru.GetVehicleStatus() + // subaru.GetVehicleCondition() + // fmt.Printf("SUBARU: %+v\n", subaru) + + // subaru.GetClimateQuickPresets() + // subaru.GetClimatePresets() + // subaru.GetClimateUserPresets() + + // subaru.LightsStart() + // time.Sleep(15 * time.Second) + // subaru.LightsStop() + + // subaru.listDevices() + + // subaru.GetVehicleStatus() + + // subaru.GetClimateSettings() + + // subaru.EngineStart() + // time.Sleep(15 * time.Second) + // subaru.EngineStop() + + // subaru.Unlock() + // time.Sleep(20 * time.Second) + // subaru.Lock() + + // subaru.LightsStart() + // time.Sleep(15 * time.Second) + // subaru.LightsStop() + + // subaru.GetLocation() +} diff --git a/mysubaru.go b/mysubaru.go new file mode 100644 index 0000000..c949e3e --- /dev/null +++ b/mysubaru.go @@ -0,0 +1,296 @@ +package mysubaru + +import ( + "time" + + "github.com/Jeffail/gabs/v2" +) + +// Response . +type Response struct { + Success bool `json:"success"` // true | false + ErrorCode string `json:"errorCode,omitempty"` // string | Error message if Success is false + DataName string `json:"dataName"` // string | Describes the structure which is incleded in Data field + Data interface{} `json:"data"` // Data struct +} + +// Account . +type Account struct { + MarketID int `json:"marketId"` + CreatedDate int64 `json:"createdDate"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + ZipCode string `json:"zipCode"` + AccountKey int `json:"accountKey"` + LastLoginDate time.Time `json:"lastLoginDate"` + ZipCode5 string `json:"zipCode5"` +} + +// Customer . +type Customer struct { + SessionCustomer string `json:"sessionCustomer"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Zip string `json:"zip"` + OemCustID string `json:"oemCustId"` + Phone string `json:"phone"` +} + +// SessionData . +// "dataName": "sessionData" +type SessionData struct { + SessionChanged bool `json:"sessionChanged"` + VehicleInactivated bool `json:"vehicleInactivated"` + Account Account `json:"account"` + ResetPassword bool `json:"resetPassword"` + DeviceID string `json:"deviceId"` + SessionID string `json:"sessionId"` + DeviceRegistered bool `json:"deviceRegistered"` + PasswordToken string `json:"passwordToken"` + Vehicles []VehicleData `json:"vehicles"` + RightToRepairEnabled bool `json:"rightToRepairEnabled"` + RightToRepairStates string `json:"rightToRepairStates"` + CurrentVehicleIndex int `json:"currentVehicleIndex"` + HandoffToken string `json:"handoffToken"` + EnableXtime bool `json:"enableXtime"` + TermsAndConditionsAccepted bool `json:"termsAndConditionsAccepted"` + RightToRepairStartYear int `json:"rightToRepairStartYear"` + DigitalGlobeConnectID string `json:"digitalGlobeConnectId"` + DigitalGlobeImageTileService string `json:"digitalGlobeImageTileService"` + DigitalGlobeTransparentTileService string `json:"digitalGlobeTransparentTileService"` + TomtomKey string `json:"tomtomKey"` + SatelliteViewEnabled bool `json:"satelliteViewEnabled"` + RegisteredDevicePermanent bool `json:"registeredDevicePermanent"` +} + +// Vehicle . +// "dataName": "vehicle" +type VehicleData struct { + Customer Customer `json:"customer"` // Customer struct + UserOemCustID string `json:"userOemCustId"` // CRM-631-HQN48K + OemCustID string `json:"oemCustId"` // CRM-631-HQN48K + Active bool `json:"active"` // true | false + Email string `json:"email"` // null | email@address.com + FirstName string `json:"firstName"` // null | First Name + LastName string `json:"lastName"` // null | Last Name + Zip string `json:"zip"` // 12345 + Phone string `json:"phone"` // null | 123-456-7890 + StolenVehicle bool `json:"stolenVehicle"` // true | false + VehicleName string `json:"vehicleName"` // Subaru Outback LXT + Features []string `json:"features"` // "11.6MMAN", "ABS_MIL", "ACCS", "AHBL_MIL", "ATF_MIL", "AWD_MIL", "BSD", "BSDRCT_MIL", "CEL_MIL", "EBD_MIL", "EOL_MIL", "EPAS_MIL", "EPB_MIL", "ESS_MIL", "EYESIGHT", "ISS_MIL", "NAV_TOMTOM", "OPL_MIL", "RAB_MIL", "RCC", "REARBRK", "RES", "RESCC", "RHSF", "RPOI", "RPOIA", "SRH_MIL", "SRS_MIL", "TEL_MIL", "TPMS_MIL", "VDC_MIL", "WASH_MIL", "g2" + Vin string `json:"vin"` // 4Y1SL65848Z411439 + VehicleKey int64 `json:"vehicleKey"` // 3832950 + Nickname string `json:"nickname"` // Subaru Outback LXT + ModelName string `json:"modelName"` // Outback + ModelYear string `json:"modelYear"` // 2020 + ModelCode string `json:"modelCode"` // LDJ + ExtDescrip string `json:"extDescrip"` // Abyss Blue Pearl (ext color) + IntDescrip string `json:"intDescrip"` // Gray (int color) + TransCode string `json:"transCode"` // CVT + EngineSize float64 `json:"engineSize"` // 2.4 + Phev bool `json:"phev"` // null + CachedStateCode string `json:"cachedStateCode"` // NJ + LicensePlate string `json:"licensePlate"` // NJ + LicensePlateState string `json:"licensePlateState"` // ABCDEF + SubscriptionStatus string `json:"subscriptionStatus"` // ACTIVE + SubscriptionFeatures []string `json:"subscriptionFeatures"` // "REMOTE", "SAFETY", "Retail" + SubscriptionPlans []string `json:"subscriptionPlans"` // [] + VehicleGeoPosition GeoPosition `json:"vehicleGeoPosition"` // GeoPosition struct + AccessLevel int `json:"accessLevel"` // -1 + VehicleMileage int `json:"vehicleMileage"` // null + CrmRightToRepair bool `json:"crmRightToRepair"` // true | false + AuthorizedVehicle bool `json:"authorizedVehicle"` // false | true + NeedMileagePrompt bool `json:"needMileagePrompt"` // false | true + RemoteServicePinExist bool `json:"remoteServicePinExist"` // true | false + NeedEmergencyContactPrompt bool `json:"needEmergencyContactPrompt"` // false | true + Show3GSunsetBanner bool `json:"show3gSunsetBanner"` // false | true + Provisioned bool `json:"provisioned"` // true | false + TimeZone time.Location `json:"timeZone"` // America/New_York + SunsetUpgraded bool `json:"sunsetUpgraded"` // true | false + PreferredDealer string `json:"preferredDealer"` // null | +} + +// GeoPosition . +type GeoPosition struct { + Latitude float64 `json:"latitude"` // 40.700184 + Longitude float64 `json:"longitude"` // -74.401375 + Speed float64 `json:"speed,omitempty"` // 62 + Heading int `json:"heading,omitempty"` // 155 + Timestamp string `json:"timestamp,string"` // "2021-12-22T13:14:47" +} + +// type GeoPositionTime time.Time + +// func (g *GeoPositionTime) UnmarshalJSON(b []byte) error { +// s := strings.Trim(string(b), "\"") +// t, err := time.Parse("2006-01-02T15:04:05", s) +// if err != nil { +// panic(err) +// } +// *g = GeoPositionTime(t) + +// return nil +// } + +// VehicleStatus . +// "dataName":null +// parts = []{"door", "tire", "tyre", "window"} +// prefix = []{"pressure", "status"} +// suffix = []{"status", "position", "unit", "psi"} +// positions = []{"front", "rear", "sunroof", "boot", "enginehood"} +// subPositions []{"left", "right"} + +type VehicleStatus struct { + VehicleId int64 `json:"vhsId"` // + 9969776690 5198812434 + OdometerValue int `json:"odometerValue"` // + 23787 + OdometerValueKm int `json:"odometerValueKilometers"` // + 38273 + EventDate int64 `json:"eventDate"` // + 1701896993000 + EventDateStr string `json:"eventDateStr"` // + 2023-12-06T21:09+0000 + EventDateCarUser int64 `json:"eventDateCarUser"` // + 1701896993000 + EventDateStrCarUser string `json:"eventDateStrCarUser"` // + 2023-12-06T21:09+0000 + Latitude float64 `json:"latitude"` // + 40.700183 + Longitude float64 `json:"longitude"` // + -74.401372 + Heading string `json:"positionHeadingDegree"` // + "154" + DistanceToEmptyFuelMiles float64 `json:"distanceToEmptyFuelMiles"` // + 209.4 + DistanceToEmptyFuelKilometers int `json:"distanceToEmptyFuelKilometers"` // + 337 + DistanceToEmptyFuelMiles10s int `json:"distanceToEmptyFuelMiles10s"` // + 210 + DistanceToEmptyFuelKilometers10s int `json:"distanceToEmptyFuelKilometers10s"` // + 340 + AvgFuelConsumptionMpg float64 `json:"avgFuelConsumptionMpg"` // + 18.4 + AvgFuelConsumptionLitersPer100Kilometers float64 `json:"avgFuelConsumptionLitersPer100Kilometers"` // + 12.8 + RemainingFuelPercent int `json:"remainingFuelPercent"` // + 82 + TirePressureFrontLeft string `json:"tirePressureFrontLeft"` // + "2275" + TirePressureFrontRight string `json:"tirePressureFrontRight"` // + "2344" + TirePressureRearLeft string `json:"tirePressureRearLeft"` // + "2413" + TirePressureRearRight string `json:"tirePressureRearRight"` // + "2344" + TirePressureFrontLeftPsi string `json:"tirePressureFrontLeftPsi"` // + "33" + TirePressureFrontRightPsi string `json:"tirePressureFrontRightPsi"` // + "34" + TirePressureRearLeftPsi string `json:"tirePressureRearLeftPsi"` // + "35" + TirePressureRearRightPsi string `json:"tirePressureRearRightPsi"` // + "34" + TyreStatusFrontLeft string `json:"tyreStatusFrontLeft"` // + "UNKNOWN" + TyreStatusFrontRight string `json:"tyreStatusFrontRight"` // + "UNKNOWN" + TyreStatusRearLeft string `json:"tyreStatusRearLeft"` // + "UNKNOWN" + TyreStatusRearRight string `json:"tyreStatusRearRight"` // + "UNKNOWN" + EvStateOfChargePercent float64 `json:"evStateOfChargePercent,omitempty"` // + null + EvDistanceToEmptyMiles int `json:"evDistanceToEmptyMiles,omitempty"` // + null + EvDistanceToEmptyKilometers int `json:"evDistanceToEmptyKilometers,omitempty"` // + null + EvDistanceToEmptyByStateMiles int `json:"evDistanceToEmptyByStateMiles,omitempty"` // + null + EvDistanceToEmptyByStateKilometers int `json:"evDistanceToEmptyByStateKilometers,omitempty"` // + null + VehicleStateType string `json:"vehicleStateType"` // + "IGNITION_OFF | IGNITION_ON" + 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" + DoorBootPosition string `json:"doorBootPosition"` // CLOSED + DoorEngineHoodPosition string `json:"doorEngineHoodPosition"` // CLOSED + DoorFrontLeftPosition string `json:"doorFrontLeftPosition"` // CLOSED + DoorFrontRightPosition string `json:"doorFrontRightPosition"` // CLOSED + DoorRearLeftPosition string `json:"doorRearLeftPosition"` // CLOSED + DoorRearRightPosition string `json:"doorRearRightPosition"` // CLOSED + DoorBootLockStatus string `json:"doorBootLockStatus"` // LOCKED + DoorFrontLeftLockStatus string `json:"doorFrontLeftLockStatus"` // LOCKED + DoorFrontRightLockStatus string `json:"doorFrontRightLockStatus"` // LOCKED + DoorRearLeftLockStatus string `json:"doorRearLeftLockStatus"` // LOCKED + DoorRearRightLockStatus string `json:"doorRearRightLockStatus"` // LOCKED +} + +// VehicleCondition . +// "dataName":"remoteServiceStatus" +// "remoteServiceType":"condition" +type VehicleCondition struct { + VehicleStateType string `json:"vehicleStateType"` // "IGNITION_OFF", + AvgFuelConsumption float64 `json:"avgFuelConsumption"` // null, + AvgFuelConsumptionUnit string `json:"avgFuelConsumptionUnit"` // "MPG", + DistanceToEmptyFuel int `json:"distanceToEmptyFuel"` // null, + DistanceToEmptyFuelUnit string `json:"distanceToEmptyFuelUnit"` // "MILES", + RemainingFuelPercent int `json:"remainingFuelPercent"` // "66", + Odometer int `json:"odometer"` // 92, + OdometerUnit string `json:"odometerUnit"` // "MILES", + TirePressureFrontLeft float64 `json:"tirePressureFrontLeft"` // null, + TirePressureFrontLeftUnit string `json:"tirePressureFrontLeftUnit"` // "PSI", + TirePressureFrontRight float64 `json:"tirePressureFrontRight"` // null, + TirePressureFrontRightUnit string `json:"tirePressureFrontRightUnit"` // "PSI", + TirePressureRearLeft float64 `json:"tirePressureRearLeft"` // null, + TirePressureRearLeftUnit string `json:"tirePressureRearLeftUnit"` // "PSI", + TirePressureRearRight float64 `json:"tirePressureRearRight"` // null, + TirePressureRearRightUnit string `json:"tirePressureRearRightUnit"` // "PSI", + DoorBootPosition string `json:"doorBootPosition"` // "CLOSED", + DoorEngineHoodPosition string `json:"doorEngineHoodPosition"` // "CLOSED", + DoorFrontLeftPosition string `json:"doorFrontLeftPosition"` // "CLOSED", + DoorFrontRightPosition string `json:"doorFrontRightPosition"` // "CLOSED", + DoorRearLeftPosition string `json:"doorRearLeftPosition"` // "CLOSED", + DoorRearRightPosition string `json:"doorRearRightPosition"` // "CLOSED" + WindowFrontLeftStatus string `json:"windowFrontLeftStatus"` // "CLOSE", + WindowFrontRightStatus string `json:"windowFrontRightStatus"` // "CLOSE", + WindowRearLeftStatus string `json:"windowRearLeftStatus"` // "CLOSE", + WindowRearRightStatus string `json:"windowRearRightStatus"` // "CLOSE", + WindowSunroofStatus string `json:"windowSunroofStatus"` // "CLOSE", + EvDistanceToEmpty int `json:"evDistanceToEmpty"` // null, + EvDistanceToEmptyUnit string `json:"evDistanceToEmptyUnit"` // null, + EvChargerStateType string `json:"evChargerStateType"` // null, + EvIsPluggedIn bool `json:"evIsPluggedIn"` // null, + EvStateOfChargeMode string `json:"evStateOfChargeMode"` // null, + EvTimeToFullyCharged string `json:"evTimeToFullyCharged"` // null, + EvStateOfChargePercent int `json:"evStateOfChargePercent"` // null, + LastUpdatedTime string `json:"lastUpdatedTime,string"` // "2023-04-10T17:50:54+0000", +} + +// ClimateSettings . +// "dataName":null +// type ClimateSettings struct { +// RunTimeMinutes string `json:"runTimeMinutes"` +// StartConfiguration string `json:"startConfiguration"` +// AirConditionOn string `json:"airConditionOn"` +// OuterAirCirculation string `json:"outerAirCirculation"` +// ClimateZoneFrontAirMode string `json:"climateZoneFrontAirMode"` +// ClimateZoneFrontTemp string `json:"climateZoneFrontTemp"` +// ClimateZoneFrontAirVolume string `json:"climateZoneFrontAirVolume"` +// HeatedSeatFrontLeft string `json:"heatedSeatFrontLeft"` +// HeatedSeatFrontRight string `json:"heatedSeatFrontRight"` +// HeatedRearWindowActive string `json:"heatedRearWindowActive"` +// } + +// ServiceRequest . +// "dataName": "remoteServiceStatus" +type ServiceRequest struct { + ServiceRequestID *string `json:"serviceRequestId,omitempty"` // 4S4BTGND8L3137058_1640294426029_19_@NGTP + Success bool `json:"success"` // false | true + Cancelled bool `json:"cancelled"` // false | true + RemoteServiceType string `json:"remoteServiceType"` // unlock | lock | locate | vehicleStatus | lightsOnly | engineStart | engineStop | phevChargeNow | condition + RemoteServiceState string `json:"remoteServiceState"` // started | finished | stopping + SubState *string `json:"subState,omitempty"` // null + ErrorCode *string `json:"errorCode,omitempty"` // null:null + Result *gabs.Container `json:"result,omitempty"` // null + UpdateTime *time.Time `json:"updateTime,omitempty"` // timestamp + Vin string `json:"vin"` // 4S4BTGND8L3137058 +} + +// ErrorResponse . +// "dataName":"errorResponse" +type ErrorResponse struct { + ErrorLabel string `json:"errorLabel"` // "404-soa-unableToParseResponseBody" + ErrorDescription *string `json:"errorDescription,omitempty"` //null +} + +// climateSettings: [ climateSettings ] +// climateZoneFrontTempCelsius: [for _ in range(15, 30 + 1)] +// climateZoneFrontTemp: [for _ in range(60, 85 + 1)] +// climateZoneFrontAirVolume: [ AUTO | 2 | 4 | 7 ] +// heatedSeatFrontLeft: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ] +// heatedSeatFrontRight: [ OFF | LOW_HEAT | MEDIUM_HEAT | HIGH_HEAT ] +// climateZoneFrontAirMode: [ WINDOW | FEET_WINDOW | FACE | FEET | FEET_FACE_BALANCED | AUTO ] +// outerAirCirculation: [ outsideAir, recirculation ] +// airConditionOn: [ false | true ] +// heatedRearWindowActive: [ false | true ] +// startConfiguration: [ start_Climate_Control_only_allow_key_in_ignition | START_ENGINE_ALLOW_KEY_IN_IGNITION ] +// runTimeMinutes: [ 10 ], + +type VehicleHealthItem struct { + B2cCode string `json:"b2cCode"` + FeatureCode string `json:"featureCode"` + IsTrouble bool `json:"isTrouble"` + OnDaiID int `json:"onDaiId"` + OnDates []string `json:"onDates"` + WarningCode int `json:"warningCode"` +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..1b55f41 --- /dev/null +++ b/utils.go @@ -0,0 +1,143 @@ +package mysubaru + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" + "time" +) + +// VinCheck - Vehicle Identification Number check digit validation +// Parameter: string - 17 digit VIN +// Return: +// +// 1- boolean - Validity flag. Set to true if VIN check digit is correct, false otherwise. +// 2- string - Valid VIN. Same VIN passed as parameter but with the correct check digit on it. +func vinCheck(vin string) (bool, string) { + var valid = false + vin = strings.ToUpper(vin) + var retVin = vin + + if len(vin) == 17 { + traSum := transcodeDigits(vin) + checkNum := math.Mod(float64(traSum), 11) + var checkDigit byte + if checkNum == 10 { + checkDigit = byte('X') + } else { + checkDigitTemp := strconv.Itoa(int(checkNum)) + checkDigit = checkDigitTemp[len(checkDigitTemp)-1] + } + if retVin[8] == checkDigit { + valid = true + } + retVin = retVin[:8] + string(checkDigit) + retVin[9:] + } else { + valid = false + retVin = "" + } + + return valid, retVin +} + +func transcodeDigits(vin string) int { + var digitSum = 0 + var code int + for i, chr := range vin { + code = 0 + + switch chr { + case 'A', 'J', '1': + code = 1 + case 'B', 'K', 'S', '2': + code = 2 + case 'C', 'L', 'T', '3': + code = 3 + case 'D', 'M', 'U', '4': + code = 4 + case 'E', 'N', 'V', '5': + code = 5 + case 'F', 'W', '6': + code = 6 + case 'G', 'P', 'X', '7': + code = 7 + case 'H', 'Y', '8': + code = 8 + case 'R', 'Z', '9': + code = 9 + case 'I', 'O', 'Q': + code = 0 + } + switch i + 1 { + case 1, 11: + digitSum += code * 8 + case 2, 12: + digitSum += code * 7 + case 3, 13: + digitSum += code * 6 + case 4, 14: + digitSum += code * 5 + case 5, 15: + digitSum += code * 4 + case 6, 16: + digitSum += code * 3 + case 7, 17: + digitSum += code * 2 + case 8: + digitSum += code * 10 + case 9: + digitSum += code * 0 + case 10: + digitSum += code * 9 + } + } + + return digitSum +} + +// isNilFixed . +// func isNil(i interface{}) bool { +// if i == nil { +// return true +// } +// switch reflect.TypeOf(i).Kind() { +// case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: +// return reflect.ValueOf(i).IsNil() +// } +// return false +// } + +// timeTrack . +func timeTrack(name string) { + start := time.Now() + fmt.Printf("%s took %v\n", name, time.Since(start)) +} + +// timestamp is a function +func timestamp() string { + return strconv.FormatInt(time.Now().UnixNano()/1000000, 10) +} + +// contains . +func contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + return false +} + +// urlToGen . +func urlToGen(url string, gen string) string { + var re = regexp.MustCompile(`api_gen`) + // dirty trick for current G3 + if gen == "g3" { + gen = "g2" + } + url = re.ReplaceAllString(url, gen) + + return url +} diff --git a/vehicle.go b/vehicle.go new file mode 100644 index 0000000..a03de7a --- /dev/null +++ b/vehicle.go @@ -0,0 +1,1283 @@ +package mysubaru + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/Jeffail/gabs/v2" +) + +var parts = map[string]map[string][]string{ + "door": map[string][]string{ + "suffix": []string{"position", "status"}, + "position1": []string{"front", "rear", "boot", "enginehood"}, + "position2": []string{"right", "left"}, + }, + "window": map[string][]string{ + "suffix": []string{"status"}, + "position1": []string{"front", "rear", "sunroof"}, + "position2": []string{"right", "left"}, + }, + "tire": map[string][]string{ + "prefix": []string{"status"}, + "position1": []string{"front", "rear"}, + "position2": []string{"right", "left"}, + }, + "tyre": map[string][]string{ + "prefix": []string{"pressure"}, + "suffix": []string{"psi", "unit"}, + "position1": []string{"front", "rear"}, + "position2": []string{"right", "left"}, + }, +} + +// { +// "success": true, +// "errorCode": null, +// "dataName": "vehicle", +// "data": { +// "customer": { +// "sessionCustomer": null, +// "email": null, +// "firstName": null, +// "lastName": null, +// "zip": null, +// "oemCustId": null, +// "phone": null +// }, +// "stolenVehicle": false, +// "vehicleName": "Subaru Outback LXT", +// "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"], +// "vin": "4S4BTGND8L3137058", +// "modelYear": "2020", +// "modelCode": "LDJ", +// "engineSize": 2.4, +// "nickname": "Subaru Outback LXT", +// "vehicleKey": 3832950, +// "active": true, +// "licensePlate": "8KV8", +// "licensePlateState": "NJ", +// "email": null, +// "firstName": null, +// "lastName": null, +// "subscriptionFeatures": ["REMOTE","SAFETY","Retail"], +// "accessLevel": -1, +// "zip": null, +// "oemCustId": "CRM-631-HQN48K", +// "vehicleMileage": null, +// "phone": null, +// "userOemCustId": "CRM-631-HQN48K", +// "subscriptionStatus": "ACTIVE", +// "authorizedVehicle": false, +// "preferredDealer": null, +// "cachedStateCode": "NJ", +// "subscriptionPlans": [], +// "needMileagePrompt": false, +// "phev": null, +// "remoteServicePinExist": true, +// "needEmergencyContactPrompt": false, +// "vehicleGeoPosition": { +// "latitude": 40.70018, +// "longitude": -74.40139, +// "speed": null, +// "heading": null, +// "timestamp": 1642538410000 +// }, +// "extDescrip": "Abyss Blue Pearl", +// "intDescrip": "Gray", +// "modelName": "Outback", +// "transCode": "CVT", +// "provisioned": true, +// "timeZone": "America/New_York" +// } +// } + +// 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 + } + FuelConsumptionAvg struct { + MPG float64 // STATUS REQUEST > "avgFuelConsumptionMpg": 18.5 + LP100Km float64 // STATUS REQUEST > "avgFuelConsumptionLitersPer100Kilometers": 12.7 + } + ClimateProfiles []*ClimateProfile + Doors []*Door // CONDITION REQUEST > + Windows []*Window // CONDITION REQUEST > + Tires []*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 + SubPosition string + Status string + 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 int + 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 CONSUMPTION =====================\n" + vString += "MPG: " + fmt.Sprintf("%v", v.FuelConsumptionAvg.MPG) + "\n" + vString += "Litres per 100 km: " + fmt.Sprintf("%v", v.FuelConsumptionAvg.LP100Km) + "\n" + + vString += "=== WINDOWS =====================\n" + for i, w := range v.Windows { + vString += fmt.Sprintf("%d >> %+v\n", i+1, w) + // fmt.Printf("%d >> %+v\n", i+1, w) + } + + vString += "=== DOORS =====================\n" + for i, d := range v.Doors { + vString += fmt.Sprintf("%d >> %+v\n", i+1, d) + // fmt.Printf("%d >> %+v\n", i+1, d) + } + + 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]) + // fmt.Printf("%d >> %s // %s\n", i+1, f, features[f]) + } else { + vString += fmt.Sprintf("%d >> %+v\n", i+1, f) + // fmt.Printf("%d >> %s\n", i+1, f) + } + } + } + return vString +} + +// Lock . +// Send command to lock doors. +func (v *Vehicle) Lock() { + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640454085449_20_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "lock", + // "remoteServiceState": "started", + // "subState": null, + // "errorCode": null, + // "result": null, + // "updateTime": 1640454085000, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640454085449_20_@NGTP", + // "success": true, + // "cancelled": false, + // "remoteServiceType": "lock", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": "null:null", + // "result": null, + // "updateTime": 1640454091000, + // "vin": "4S4BTGND8L3137058" + // } + // } + + 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.log.Error("active STARLINK Security Plus subscription required") + } +} + +// Unlock . +// Send command to unlock doors. +func (v *Vehicle) Unlock() { + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640539133289_19_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "unlock", + // "remoteServiceState": "started", + // "subState": null, + // "errorCode": null, + // "result": null, + // "updateTime": 1640539133000, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640539133289_19_@NGTP", + // "success": true, + // "cancelled": false, + // "remoteServiceType": "unlock", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": "null:null", + // "result": null, + // "updateTime": 1640539140000, + // "vin": "4S4BTGND8L3137058" + // } + // } + + 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.log.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) +} + +// EngineOn . +// Send command to start engine and set climate control. +func (v *Vehicle) EngineStart() { + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640456287656_22_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "engineStart", + // "remoteServiceState": "started", + // "subState": null, + // "errorCode": null, + // "result": null, + // "updateTime": 1640456287000, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640456287656_22_@NGTP", + // "success": true, + // "cancelled": false, + // "remoteServiceType": "engineStart", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": "null:null", + // "result": null, + // "updateTime": 1640456302000, + // "vin": "4S4BTGND8L3137058" + // } + // } + + 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.log.Error("active STARLINK Security Plus subscription required") + } +} + +// EngineOff . +// Send command to stop engine. +func (v *Vehicle) EngineStop() { + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640456318773_23_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "engineStop", + // "remoteServiceState": "started", + // "subState": null, + // "errorCode": null, + // "result": null, + // "updateTime": null, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640456318773_23_@NGTP", + // "success": true, + // "cancelled": false, + // "remoteServiceType": "engineStop", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": "null:null", + // "result": null, + // "updateTime": 1640456321000, + // "vin": "4S4BTGND8L3137058" + // } + // } + + 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.log.Error("Active STARLINK Security Plus subscription required") + } +} + +// LightsStart . +// Send command to flash lights. +func (v *Vehicle) LightsStart() { + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640457256003_21_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "lightsOnly", + // "remoteServiceState": "started", + // "subState": null, + // "errorCode": null, + // "result": null, + // "updateTime": 1640457256000, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640457256003_21_@NGTP", + // "success": true, + // "cancelled": false, + // "remoteServiceType": "lightsOnly", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": "null:null", + // "result": null, + // "updateTime": 1640457262000, + // "vin": "4S4BTGND8L3137058" + // } + // } + + 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.log.Error("active STARLINK Security Plus subscription required") + } +} + +// LightsStop . +// Send command to stop flash lights. +func (v *Vehicle) LightsStop() { + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640457280857_47_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "lightsOnly", + // "remoteServiceState": "stopping", + // "subState": null, + // "errorCode": "null:null", + // "result": null, + // "updateTime": 1640457262000, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640457280857_47_@NGTP", + // "success": true, + // "cancelled": false, + // "remoteServiceType": "lightsOnly", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": "null:null", + // "result": null, + // "updateTime": 1640457280000, + // "vin": "4S4BTGND8L3137058" + // } + // } + + 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.log.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.log.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.log.Error("Active STARLINK Security Plus subscription required") + } +} + +// ChargeStart . +func (v *Vehicle) ChargeOn() { + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640542818241_31_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "phevChargeNow", + // "remoteServiceState": "started", + // "subState": null, + // "errorCode": null, + // "result": null, + // "updateTime": null, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640542818241_31_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "phevChargeNow", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": "NegativeAcknowledge:unknown", + // "result": null, + // "updateTime": 1640542824000, + // "vin": "4S4BTGND8L3137058" + // } + // } + + 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 + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": "4S4BTGND8L3137058_1640454589498_11_@NGTP", + // "success": false, + // "cancelled": false, + // "remoteServiceType": "vehicleStatus", + // "remoteServiceState": "started", + // "subState": null, + // "errorCode": null, + // "result": null, + // "updateTime": 1640454589000, + // "vin": "4S4BTGND8L3137058" + // } + // } + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": null, + // "success": true, + // "cancelled": false, + // "remoteServiceType": "locate", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": null, + // "result": { + // "latitude": 40.700294, + // "longitude": -74.401344, + // "speed": 0, + // "heading": 189, + // "timestamp": 1640454600000 + // }, + // "updateTime": null, + // "vin": "4S4BTGND8L3137058" + // } + // } + 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 + // { + // "success": true, + // "errorCode": null, + // "dataName": "remoteServiceStatus", + // "data": { + // "serviceRequestId": null, + // "success": true, + // "cancelled": false, + // "remoteServiceType": "locate", + // "remoteServiceState": "finished", + // "subState": null, + // "errorCode": null, + // "result": { + // "latitude": 40.700203, + // "longitude": -74.40142, + // "speed": 0, + // "heading": 156, + // "timestamp": 1642526388000 + // }, + // "updateTime": null, + // "vin": "4S4BTGND8L3137058" + // } + // } + 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() { + // params := map[string]string{ + // "vin": v.Vin, + // "pin": v.Client.credentials.pin} + 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 { + panic(err) + } + v.client.log.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) + // 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) + } + v.client.log.Debug("PRESETS", "output", data) + } else { + v.client.log.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 { + panic(err) + } + + // ONLY FOR THAT REQUEST BECAUSE OF API SENDS BACK ESCAPED DATA IN DATA FIELD + for _, child := range respParsed.S("data").Children() { + // log.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.log.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 { + panic(err) + } + v.client.log.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() { + // log.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) + // } + // log.Debugf("PRESETS: %+v\n", data) + } else { + v.client.log.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) + + if v.client.isResponseSuccessfull(resp) { + respParsed, err := gabs.ParseJSON(resp) + if err != nil { + v.client.log.Error("error", err) + } + + 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.FuelConsumptionAvg.MPG = float64(vSta.AvgFuelConsumptionMpg) + v.FuelConsumptionAvg.LP100Km = float64(vSta.AvgFuelConsumptionLitersPer100Kilometers) + + v.GeoLocation.Latitude = float64(vSta.Latitude) + v.GeoLocation.Longitude = float64(vSta.Longitude) + + 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") { + submatchall := re.FindAllString(key, -1) + v.client.log.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) + for _, element := range submatchall { + fmt.Println(element) + } + door := Door{} + door.Position = submatchall[0] + if len(submatchall) >= 3 { + door.SubPosition = submatchall[1] + } + door.Status = child.Data().(string) + door.Updated = time.Now() + v.Doors = append(v.Doors, &door) + } + if strings.HasPrefix(key, "door") && strings.HasSuffix(key, "Status") { + submatchall := re.FindAllString(key, -1) + v.client.log.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) + for _, element := range submatchall { + fmt.Println(element) + } + door := Door{} + door.Position = submatchall[0] + if len(submatchall) >= 3 { + door.SubPosition = submatchall[1] + } + door.Status = child.Data().(string) + door.Updated = time.Now() + v.Doors = append(v.Doors, &door) + } + if strings.HasPrefix(key, "window") && strings.HasSuffix(key, "Status") { + submatchall := re.FindAllString(key, -1) + v.client.log.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) + for _, element := range submatchall { + fmt.Println(element) + } + window := Window{} + window.Position = submatchall[0] + if len(submatchall) >= 3 { + window.SubPosition = submatchall[1] + } + window.Status = child.Data().(string) + window.Updated = time.Now() + v.Windows = append(v.Windows, &window) + } + if strings.HasPrefix(key, "tire") && !strings.HasSuffix(key, "Psi") { + v.client.log.Debug("VEHICLE COND", "key", key, "data", child.Data()) + submatchall := re.FindAllString(key, -1) + for _, element := range submatchall { + fmt.Println(element) + } + } + } + + 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 + // } + // } +} + +// GetVehicleStatus . +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.log.Error("error", err) + } + + 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 { + fmt.Println("Skipping") + continue + } + if strings.HasPrefix(key, "door") && strings.HasSuffix(key, "Position") { + submatchall := re.FindAllString(key, -1) + v.client.log.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) + for _, element := range submatchall { + fmt.Println(element) + } + door := Door{} + door.Position = submatchall[0] + if len(submatchall) >= 3 { + door.SubPosition = submatchall[1] + } + door.Status = child.Data().(string) + door.Updated = time.Now() + v.Doors = append(v.Doors, &door) + + } + if strings.HasPrefix(key, "window") && strings.HasSuffix(key, "Status") { + submatchall := re.FindAllString(key, -1) + v.client.log.Debug("VEHICLE COND", "key", key, "data", child.Data(), "number", len(submatchall)) + for _, element := range submatchall { + fmt.Println(element) + } + window := Window{} + window.Position = submatchall[0] + if len(submatchall) >= 3 { + window.SubPosition = submatchall[1] + } + window.Status = child.Data().(string) + window.Updated = time.Now() + v.Windows = append(v.Windows, &window) + } + if strings.HasPrefix(key, "tire") && !strings.HasSuffix(key, "Unit") { + v.client.log.Debug("VEHICLE COND", "key", key, "data", child.Data()) + submatchall := re.FindAllString(key, -1) + for _, element := range submatchall { + fmt.Println(element) + } + } + v.Updated = time.Now() + } + + // vCon := VehicleCondition{} + // vcString := respParsed.Path("data.result").String() + // json.Unmarshal([]byte(vcString), &vCon) + + // for _, elem := range vCon.VehicleStatus { + // if elem.Value == "NOT_EQUIPPED" || elem.Value == "UNKNOWN" || elem.Value == "16383" || elem.Value == "65535" || elem.Value == "None" || elem.Value == "-64.0" { + // continue + // } + // if strings.HasPrefix(elem.Key, "door") { + // log.Debugf("VEHICLE COND: %v > %v\n", elem.Key, elem.Value) + // } + // } + // for _, elem := range vCon.VehicleStatus { + // if elem.Value == "NOT_EQUIPPED" || elem.Value == "UNKNOWN" || elem.Value == "16383" || elem.Value == "65535" || elem.Value == "None" || elem.Value == "-64.0" { + // continue + // } + // if strings.HasPrefix(elem.Key, "window") { + // log.Debugf("VEHICLE COND: %v > %v\n", elem.Key, elem.Value) + // } + // } + // for _, elem := range vCon.VehicleStatus { + // if elem.Value == "NOT_EQUIPPED" || elem.Value == "UNKNOWN" || elem.Value == "16383" || elem.Value == "65535" || elem.Value == "None" || elem.Value == "-64.0" { + // continue + // } + // if strings.HasPrefix(elem.Key, "tire") { + // log.Debugf("VEHICLE COND: %v > %v\n", elem.Key, elem.Value) + // } + // } + // log.Debugf("VEHICLE COND REQUEST: %+v", vCon) + } + + // 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.log.Error("error", err) + } + // 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"