Пример #1
0
    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
Пример #2
0
    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")
Пример #3
0
    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
Пример #4
0
    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
Пример #5
0
    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")
Пример #6
0
 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."
     )
Пример #7
0
    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")
Пример #8
0
    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
Пример #9
0
    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
Пример #10
0
    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")
Пример #11
0
 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.")
Пример #12
0
    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")
Пример #13
0
 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.")
Пример #14
0
 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)
Пример #15
0
 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"))
Пример #16
0
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"
Пример #17
0
 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}")
Пример #18
0
 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)
Пример #19
0
 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)
Пример #20
0
 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"]))
Пример #21
0
 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"))
Пример #22
0
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"
Пример #23
0
 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
Пример #24
0
    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.")
Пример #25
0
    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