G-Two / subarulink

A python package for interacting with Subaru STARLINK remote vehicle services.
Apache License 2.0
86 stars 13 forks source link

Updated to support NonEV Vehicles #1

Closed stboch closed 4 years ago

stboch commented 4 years ago

Updated to support nonev subaru's, reduced api's queries and brought into compliance with the Android API calls.

stboch commented 4 years ago

@g-two not sure if you saw this pull request yet.

G-Two commented 4 years ago

Awesome, thanks! I’ll take a look tonight.

G-Two commented 4 years ago

@stboch Thanks for the contribution. I'd like to have this package support more than just the Crosstrek PHEV, so getting feedback from other owners is definitely helpful. I'm having two issues with the changes, and I'm wondering if it is due to inconsistency with the Subaru API for different models/generations.

  1. refreshVehicles.json does not always provide a list of features. I have a g1 and a g2 PHEV on my account and receive the following:
    
    # g1 Outback (not currently subscribed to Starlink)
    vehicles[0]["features"] = ['BSD', 'REARBRK', 'EYESIGHT', 'g1']  
    vehicles[0]["subscriptionFeatures"] = []

g2 Crosstrek PHEV (active subscription to Starlink)

vehicles[1]["features"] = None
vehicles[1]["subscriptionFeatures"] = None

What generation/model are you using?  

2. I'm receiving errors from the Subaru API if I don't call selectVehicle.json prior to using any of the execute.json API calls.  Are you experiencing this problem?

subarulink.controller - DEBUG - Connecting controller to Subaru Remote Services subarulink.connection - DEBUG - POST: https://mobileapi.prod.subarucs.com/g2v15/login.json subarulink.connection - DEBUG - Client authentication successful subarulink.connection - DEBUG - GET: https://mobileapi.prod.subarucs.com/g2v15/refreshVehicles.json subarulink.controller - DEBUG - Fetching vehicle status from Subaru subarulink.connection - DEBUG - GET: https://mobileapi.prod.subarucs.com/g2v15/validateSession.json subarulink.connection - DEBUG - GET: https://mobileapi.prod.subarucs.com/g2v15/service/g2/condition/execute.json subarulink.connection - DEBUG - {'errorCode': 'error', 'errorMessage': 'java.lang.NullPointerException - null', 'httpCode': 500}


We may need to retain controller._select_vehicle() to prevent the above error as well as support owners with more than one vehicle on their account.  Because the vehicle is not specified with any of the action API calls, it depends on the vehicle context on the server side.  The only way I'm aware of changing the vehicle context is with selectVehicle.json.  Although, I vaguely remember a way to select a vehicle upon login using web API by specifying `lastSelectedVehicleKey` during authentication.  Using that might negate the need for selectVehicle.json for those with only one vehicle.
EDIT:  For item2, if I specify `selectedVin` to equal my VIN at login, then I no longer get the HTTP 500 error.  So we can definitely remove the extraneous calls to selectVehicle.json as you proposed, once the VIN is known by the application.  It should only need to be called in the event someone has multiple vehicles.
G-Two commented 4 years ago

@stboch my theory is that the Subaru API is unreliable for accounts with multiple vehicles. When I specified my g2 PHEV VIN as selectedVin on login, the features were correctly returned for that vehicle, and I received None for the g1 Outback. It seems that refreshVehicles.json only populates all the data for the currently selected vehicle. If an account has more than one vehicle then each vehicle needs to be queried with selectVehicle.json to reliably populate all the fields. With your PR branch checked out, I added the following to controller.py and it seems to fix both of the issues I mentioned above us.

    async def _refresh_vehicles(self):
        resp = await self.__open(
            "/refreshVehicles.json", "get", params={"_": int(time.time())}
        )
        js_resp = await resp.json()
        _LOGGER.debug(pprint.pformat(js_resp))
        vehicles = js_resp["data"]["vehicles"]
        if len(vehicles) > 1:
            vehicles = await self._refresh_multi_vehicle(vehicles)
        for vehicle in vehicles:
            car = {}
            car["vin"] = vehicle["vin"]
            car["id"] = vehicle["vehicleKey"]
            car["display_name"] = vehicle["vehicleName"]
            if "g2" in vehicle["features"]:
                car["api_gen"] = "g2"
            elif "g1" in vehicle["features"]:
                car["api_gen"] = "g1"
            else:
                car["api_gen"] = "g0"
            if "PHEV" in vehicle["features"]:
                car["hasEV"] = True
            else:
                car["hasEV"] = False
            if "RES" in vehicle["features"]:
                car["hasRES"] = True
            else:
                car["hasRES"] = False
            if "REMOTE" in vehicle["subscriptionFeatures"]:
                car["hasRemote"] = True
            else:
                car["hasRemote"] = False                         
            self.vehicles.append(car)

    async def _refresh_multi_vehicle(self, vehicles):
        # refreshVehicles.json returns unreliable data if multiple cars on account
        # use selectVehicle.json to get each car's info
        result = []
        for vehicle in vehicles:
            vin = vehicle["vin"]
            result.append(await self._select_vehicle(vin))
        return result

    async def _select_vehicle(self, vin=None):
        params = {}
        params["vin"] = vin
        params["_"] = int(time.time())
        js_resp = await self.get("/selectVehicle.json", params=params)
        _LOGGER.debug(pprint.pformat(js_resp))
        if js_resp["success"]:
            _LOGGER.debug(
                "Current vehicle: vin=%s", js_resp["data"]["vin"]
            )
            return js_resp["data"]