async def __open( self, url, method=GET, headers=None, data=None, json_data=None, params=None, baseurl="", decode_json=True, ): """Open url.""" if not baseurl: baseurl = f"https://{MOBILE_API_SERVER[self._country]}{MOBILE_API_VERSION}" url: URL = URL(baseurl + url) _LOGGER.debug("%s: %s, params=%s, json_data=%s", method.upper(), url, params, json_data) async with self._lock: try: resp = await getattr(self._websession, method)( url, headers=headers, params=params, data=data, json=json_data ) if resp.status > 299: _LOGGER.error(pprint.pformat(resp.request_info)) _LOGGER.error(pprint.pformat(await resp.text())) raise SubaruException("HTTP %d: %s" % (resp.status, resp)) if decode_json: js_resp = await resp.json() if "success" not in js_resp and "serviceType" not in js_resp: raise SubaruException("Unexpected response: %s" % resp) return js_resp return resp except aiohttp.ClientResponseError as err: raise SubaruException(err.status) from err except aiohttp.ClientConnectionError as err: raise SubaruException("aiohttp.ClientConnectionError") from err
async def remote_start(self, vin, form_data=None): """ Send command to start engine. Args: vin (str): Destination VIN for command. form_data (dict, optional): Climate control settings Returns: bool: `True` upon success. `False` upon failure. Raises: InvalidPIN: if PIN is incorrect. SubaruException: for all other failures. """ vin = vin.upper() if self.get_res_status(vin) or self.get_ev_status(vin): if form_data: if self._validate_remote_start_params(vin, form_data): climate_settings = form_data else: raise SubaruException("Error with climate settings") else: await self.get_climate_settings(vin) climate_settings = self._vehicles[vin]["climate"] if climate_settings: success, _ = await self._actuate(vin, sc.API_G2_REMOTE_ENGINE_START, data=climate_settings) return success raise SubaruException("Remote Start not supported for this vehicle")
async def __open( self, url, method="get", headers=None, data=None, json=None, params=None, baseurl="", ) -> None: """Open url.""" if not baseurl: baseurl = self.baseurl url: URL = URL(baseurl + url) _LOGGER.debug("%s: %s", method.upper(), url) with await self.lock: try: resp = await getattr(self.websession, method)( url, headers=headers, params=params, data=data, json=json ) if resp.status > 299: _LOGGER.debug(pprint.pformat(resp.request_info)) js_resp = await resp.json() _LOGGER.debug(pprint.pformat(js_resp)) raise SubaruException(resp.status) except aiohttp.ClientResponseError as exception: raise SubaruException(exception.status) except aiohttp.ClientConnectionError: raise SubaruException("aiohttp.ClientConnectionError") return resp
async def update_user_climate_presets(self, vin, preset_data): """ Save user defined climate control settings to Subaru. Args: vin (str): The VIN to save climate settings to. preset_data (list): List of Climate settings dicts to save. Returns: bool: `True` upon success. None: If `vin` is invalid or unsupported. Raises: SubaruException: If preset_data is invalid or fails to save. VehicleNotSupported: if vehicle/subscription not supported """ self._validate_remote_capability(vin) if not self._vehicles[vin].get("climate"): await self._fetch_climate_presets(vin) if not isinstance(preset_data, list) and not isinstance( preset_data[0], dict): raise SubaruException( "Preset data must be a list of climate settings dicts") if len(preset_data) > 4: raise SubaruException( "Preset list may have a maximum of 4 entries") for preset in preset_data: self._validate_remote_start_params(vin, preset) await self._connection.validate_session(vin) js_resp = await self._post(sc.API_G2_SAVE_RES_SETTINGS, json_data=preset_data) _LOGGER.debug(js_resp) success = js_resp["success"] await self._fetch_climate_presets(vin) return success
async def remote_start(self, vin, preset_name): """ Send command to start engine and set climate control. Args: vin (str): Destination VIN for command. preset_name (str): Climate control preset name Returns: bool: `True` upon success. Raises: InvalidPIN: if PIN is incorrect PINLockoutProtect: if PIN was previously incorrect and was not updated RemoteServiceFailure: for failure after request submitted VehicleNotSupported: if vehicle/subscription not supported SubaruException: for other failures """ self._validate_remote_capability(vin) preset_data = await self.get_climate_preset_by_name(vin, preset_name) if preset_data: js_resp = await self._post(sc.API_G2_SAVE_RES_QUICK_START_SETTINGS, json_data=preset_data) _LOGGER.debug(pprint.pprint(js_resp)) if js_resp.get("success"): success, _ = await self._actuate(vin, sc.API_G2_REMOTE_ENGINE_START, data=preset_data) return success raise SubaruException( f"Climate preset '{preset_name}' failed: {js_resp}") raise SubaruException(f"Climate preset '{preset_name}' does not exist")
async def _authenticate(self, vin=None) -> bool: """Authenticate to Subaru Remote Services API.""" if self.username and self.password and self.device_id: post_data = { "env": "cloudprod", "loginUsername": self.username, "password": self.password, "deviceId": self.device_id, "passwordToken": None, "selectedVin": vin, "pushToken": None, "deviceType": "android", } resp = await self.__open( "/login.json", "post", data=post_data, headers=self.head ) resp = await resp.json() if resp["success"]: _LOGGER.debug("Client authentication successful") _LOGGER.debug(pprint.pformat(resp)) self.authenticated = True self.authorized = resp["data"]["deviceRegistered"] i = resp["data"]["currentVehicleIndex"] self.current_vin = resp["data"]["vehicles"][i]["vin"] return True if resp["errorCode"]: _LOGGER.error("Client authentication failed") raise SubaruException(resp["errorCode"]) _LOGGER.error("Unknown failure") raise SubaruException(resp) raise IncompleteCredentials( "Connection requires email and password and device id." )
async def unlock(self, vin, door=sc.ALL_DOORS): """ Send command to unlock doors. Args: vin (str): Destination VIN for command. door (str, optional): Specify door to unlock. Returns: bool: `True` upon success. Raises: InvalidPIN: if PIN is incorrect PINLockoutProtect: if PIN was previously incorrect and was not updated RemoteServiceFailure: for failure after request submitted VehicleNotSupported: if vehicle/subscription not supported SubaruException: for other failures """ if door in sc.VALID_DOORS: form_data = {sc.WHICH_DOOR: door} success, _ = await self._actuate(vin.upper(), sc.API_UNLOCK, data=form_data) return success raise SubaruException( f"Invalid door '{door}' specified for unlock command")
async def connect(self, test_login=False) -> bool: """ Connect to Subaru Remote Services API. Args: test_login (Bool, optional): Only check for authorization """ if test_login: response = await self._connection.connect() if response: return True return False _LOGGER.debug("Connecting controller to Subaru Remote Services") cars = await self._connection.connect() if cars is None: raise SubaruException("Connection to Subaru API failed") for car in cars: vin = car["vin"] self._cars.append(vin) self._vin_name_map[vin] = car["display_name"] self._vin_id_map[vin] = car["id"] self._api_gen[vin] = car["api_gen"] self._hasEV[vin] = car["hasEV"] self._hasRES[vin] = car["hasRES"] self._hasRemote[vin] = car["hasRemote"] self._lock[vin] = asyncio.Lock() self._last_update_time[vin] = 0 self._last_fetch_time[vin] = 0 self._car_data[vin] = {} self._update[vin] = True _LOGGER.debug("Subaru Remote Services Ready!") return True
async def connect(self, test_login=False): """ Connect to Subaru Remote Services API. Args: test_login (bool, optional): If `True` then username/password is verified only. Returns: bool: `True` if success, `False` if failure Raises: InvalidCredentials: If login credentials are incorrect. IncompleteCredentials: If login credentials were not provided. SubaruException: If authorization and registration sequence fails for any other reason. """ _LOGGER.debug(f"subarulink {self.version}") _LOGGER.debug("Connecting controller to Subaru Remote Services") vehicle_list = await self._connection.connect(test_login=test_login) if vehicle_list is None: raise SubaruException("Connection to Subaru API failed") if not test_login: for vehicle in vehicle_list: self._parse_vehicle(vehicle) _LOGGER.debug("Subaru Remote Services Ready") return True
async def remote_start(self, vin, form_data=None): """ Send command to start engine. Args: vin (str): Destination VIN for command. form_data (dict, optional): Climate control settings Returns: bool: `True` upon success. Raises: InvalidPIN: if PIN is incorrect PINLockoutProtect: if PIN was previously incorrect and was not updated RemoteServiceFailure: for failure after request submitted VehicleNotSupported: if vehicle/subscription not supported SubaruException: for other failures """ vin = vin.upper() if self.get_res_status(vin) or self.get_ev_status(vin): if form_data: if self._validate_remote_start_params(vin, form_data): climate_settings = form_data else: raise SubaruException("Error with climate settings") else: await self.get_climate_settings(vin) climate_settings = self._vehicles[vin]["climate"] if climate_settings: success, _ = await self._actuate(vin, sc.API_G2_REMOTE_ENGINE_START, data=climate_settings) return success raise VehicleNotSupported("Remote Start not supported for this vehicle")
async def _authenticate(self, vin=None) -> bool: """Authenticate to Subaru Remote Services API.""" if self._username and self._password and self._device_id: post_data = { "env": "cloudprod", "loginUsername": self._username, "password": self._password, "deviceId": self._device_id, "passwordToken": None, "selectedVin": vin, "pushToken": None, "deviceType": "android", } js_resp = await self.__open(API_LOGIN, POST, data=post_data, headers=self._head) if js_resp.get("success"): _LOGGER.debug("Client authentication successful") _LOGGER.debug(pprint.pformat(js_resp)) self._authenticated = True self._registered = js_resp["data"]["deviceRegistered"] return True if js_resp.get("errorCode"): _LOGGER.debug(pprint.pformat(js_resp)) error = js_resp.get("errorCode") if error == ERROR_INVALID_CREDENTIALS: _LOGGER.error("Client authentication failed") raise InvalidCredentials(error) if error == ERROR_PASSWORD_WARNING: _LOGGER.error("Multiple Password Failures.") raise InvalidCredentials(error) raise SubaruException(error) raise IncompleteCredentials( "Connection requires email and password and device id.")
async def delete_climate_preset_by_name(self, vin, preset_name): """ Delete climate control user preset by name. Args: vin (str): The VIN of the vehicle. preset_name (str): Name of climate settings preset. Returns: bool: `True` if successful. Raises: SubaruException: if `preset_name` not found VehicleNotSupported: if vehicle/subscription not supported """ self._validate_remote_capability(vin) preset = await self.get_climate_preset_by_name(vin, preset_name) if preset and preset["presetType"] == "userPreset": user_presets = [ i for i in self._vehicles[vin]["climate"] if i["presetType"] == "userPreset" ] user_presets.remove(preset) return await self.update_user_climate_presets(vin, user_presets) raise SubaruException(f"User preset name '{preset_name}' not found")
async def _actuate(self, vin, cmd, data=None): form_data = {"delay": 0, "vin": vin} if data: form_data.update(data) if self.get_remote_status(vin): return await self._remote_command(vin, cmd, data=form_data) raise SubaruException("Command requires REMOTE subscription.")
async def _remote_query(self, vin, cmd, data=None): await self._connection.validate_session(vin) api_gen = self._api_gen[vin] async with self._lock[vin]: js_resp = await self._get("service/%s/%s/execute.json" % (api_gen, cmd), json=data) _LOGGER.debug(pprint.pformat(js_resp)) if js_resp["success"]: return js_resp raise SubaruException("Remote query failed. Response: %s " % js_resp)
async def _select_vehicle(self, vin): """Select active vehicle for accounts with multiple VINs.""" params = {} params["vin"] = vin params["_"] = 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"] raise SubaruException("Failed to switch vehicle %s" % js_resp.get("errorCode"))
async def test_user_form_cannot_connect(hass, user_form): """Test we handle cannot connect error.""" with patch( MOCK_API_CONNECT, side_effect=SubaruException(None), ) as mock_connect: result = await hass.config_entries.flow.async_configure( user_form["flow_id"], TEST_CREDS, ) assert len(mock_connect.mock_calls) == 1 assert result["type"] == "abort" assert result["reason"] == "cannot_connect"
def _check_error_code(self, js_resp): error = js_resp.get("errorCode") if error == sc.ERROR_SOA_403: _LOGGER.debug("SOA 403 error - clearing session cookie") self._connection.reset_session() elif error == sc.ERROR_INVALID_CREDENTIALS or error == "SXM40006": _LOGGER.error("PIN is not valid for Subaru remote services") self._pin_lockout = True raise InvalidPIN("Invalid PIN! %s" % js_resp) elif error in [sc.ERROR_SERVICE_ALREADY_STARTED, sc.ERROR_G1_SERVICE_ALREADY_STARTED]: pass elif error: _LOGGER.error("Unhandled API error code %s", error) raise SubaruException(f"Unhandled API error: {error} - {js_resp}")
async def _remote_command(self, vin, cmd, data=None, poll_url="/service/api_gen/remoteService/status.json"): await self._connection.validate_session(vin) api_gen = self._api_gen[vin] form_data = {"pin": self._pin} if data: form_data.update(data) req_id = "" async with self._lock[vin]: js_resp = await self._post("service/%s/%s/execute.json" % (api_gen, cmd), json=form_data) _LOGGER.debug(pprint.pformat(js_resp)) if js_resp["success"]: req_id = js_resp["data"][sc.SERVICE_REQ_ID] return await self._wait_request_status(req_id, api_gen, poll_url) if js_resp["errorCode"] == "InvalidCredentials": raise InvalidPIN(js_resp["data"]["errorDescription"]) raise SubaruException("Remote command failed. Response: %s " % js_resp)
async def _remote_query(self, vin, cmd): tries_left = 2 js_resp = None while tries_left > 0: await self._connection.validate_session(vin) api_gen = self.get_api_gen(vin) async with self._vehicles[vin][sc.VEHICLE_LOCK]: js_resp = await self._get(cmd.replace("api_gen", api_gen)) _LOGGER.debug(pprint.pformat(js_resp)) if js_resp["success"]: return js_resp if js_resp["errorCode"] == sc.ERROR_SOA_403: tries_left -= 1 else: tries_left = 0 raise SubaruException("Remote query failed. Response: %s " % js_resp)
def _check_error_code(self, js_resp): error = js_resp.get("errorCode") if error == sc.ERROR_SOA_403: _LOGGER.debug("SOA 403 error - clearing session cookie") self._connection.reset_session() elif error == sc.ERROR_INVALID_CREDENTIALS: _LOGGER.error("PIN is not valid for Subaru remote services") self._pin_lockout = True raise InvalidPIN("Invalid PIN! %s" % js_resp["data"]["errorDescription"]) elif error == sc.ERROR_SERVICE_ALREADY_STARTED: pass elif error: _LOGGER.error("Unhandled API error code %s", error) raise SubaruException("Unhandled API error: {} - {}".format( error, js_resp["data"]["errorDescription"]))
async def _select_vehicle(self, vin): """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"] elif 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.warn("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"))
async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}) with patch( "custom_components.subaru.config_flow.SubaruAPI.connect", side_effect=SubaruException(None), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, CONF_PIN: TEST_PIN, }, ) assert result2["type"] == "abort" assert result2["reason"] == "cannot_connect"
def _validate_remote_start_params(self, vin, form_data): is_valid = True err_msg = None try: for item in form_data: if form_data[item] not in sc.VALID_CLIMATE_OPTIONS[item]: is_valid = False err_msg = f"Invalid value for {item}: {form_data[item]}" break except KeyError as err: is_valid = False err_msg = f"Invalid option: {err}" if not is_valid: raise SubaruException(err_msg) form_data[sc.RUNTIME] = sc.RUNTIME_DEFAULT form_data[sc.CLIMATE] = sc.CLIMATE_DEFAULT if self.get_ev_status(vin): form_data[sc.START_CONFIG] = sc.START_CONFIG_DEFAULT_EV elif self.get_res_status(vin): form_data[sc.START_CONFIG] = sc.START_CONFIG_DEFAULT_RES return is_valid
def _validate_remote_start_params(self, vin, form_data): temp = int(form_data[sc.TEMP]) is_valid = True if temp > sc.TEMP_MAX or temp < sc.TEMP_MIN: is_valid = False if form_data[sc.MODE] not in [ sc.MODE_AUTO, sc.MODE_DEFROST, sc.MODE_FACE, sc.MODE_FEET, sc.MODE_FEET_DEFROST, sc.MODE_SPLIT, ]: is_valid = False if form_data[sc.HEAT_SEAT_LEFT] not in [sc.HEAT_SEAT_OFF, sc.HEAT_SEAT_HI, sc.HEAT_SEAT_MED, sc.HEAT_SEAT_LOW]: is_valid = False if form_data[sc.HEAT_SEAT_RIGHT] not in [sc.HEAT_SEAT_OFF, sc.HEAT_SEAT_HI, sc.HEAT_SEAT_MED, sc.HEAT_SEAT_LOW]: is_valid = False if form_data[sc.REAR_DEFROST] not in [sc.REAR_DEFROST_OFF, sc.REAR_DEFROST_ON]: is_valid = False if form_data[sc.FAN_SPEED] not in [ sc.FAN_SPEED_AUTO, sc.FAN_SPEED_HI, sc.FAN_SPEED_LOW, sc.FAN_SPEED_MED, ]: is_valid = False if form_data[sc.RECIRCULATE] not in [sc.RECIRCULATE_OFF, sc.RECIRCULATE_ON]: is_valid = False if form_data[sc.REAR_AC] not in [sc.REAR_AC_OFF, sc.REAR_AC_ON]: is_valid = False form_data[sc.RUNTIME] = sc.RUNTIME_DEFAULT form_data[sc.CLIMATE] = sc.CLIMATE_DEFAULT if self._hasEV[vin]: form_data[sc.START_CONFIG] = sc.START_CONFIG_DEFAULT_EV elif self._hasRES[vin]: form_data[sc.START_CONFIG] = sc.START_CONFIG_DEFAULT_RES else: raise SubaruException("Vehicle Remote Start not supported.")
def _validate_remote_start_params(self, vin, preset_data): is_valid = True err_msg = None try: for item in preset_data: if preset_data[item] not in sc.VALID_CLIMATE_OPTIONS[item]: if item == "name" and isinstance(preset_data[item], str): continue is_valid = False err_msg = f"Invalid value for {item}: {preset_data[item]}" break except KeyError as err: is_valid = False err_msg = f"Invalid option: {err}" if not is_valid: raise SubaruException(err_msg) if self.get_ev_status(vin): preset_data.update(sc.START_CONFIG_CONSTS_EV) else: preset_data.update(sc.START_CONFIG_CONSTS_RES) return is_valid