示例#1
0
async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Handle migration of a previous version config entry.

    A config entry created under a previous version must go through the
    integration setup again so we can properly retrieve the needed data
    elements. Force this by removing the entry and triggering a new flow.
    """
    from pysmartthings import SmartThings

    # Remove the installed_app, which if already removed raises a 403 error.
    api = SmartThings(async_get_clientsession(hass),
                      entry.data[CONF_ACCESS_TOKEN])
    installed_app_id = entry.data[CONF_INSTALLED_APP_ID]
    try:
        await api.delete_installed_app(installed_app_id)
    except ClientResponseError as ex:
        if ex.status == 403:
            _LOGGER.exception("Installed app %s has already been removed",
                              installed_app_id)
        else:
            raise
    _LOGGER.debug("Removed installed app %s", installed_app_id)

    # Delete the entry
    hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
    # only create new flow if there isn't a pending one for SmartThings.
    flows = hass.config_entries.flow.async_progress()
    if not [flow for flow in flows if flow['handler'] == DOMAIN]:
        hass.async_create_task(
            hass.config_entries.flow.async_init(DOMAIN,
                                                context={'source': 'import'}))

    # Return False because it could not be migrated.
    return False
示例#2
0
async def smartapp_sync_subscriptions(
        hass: HomeAssistantType, auth_token: str, location_id: str,
        installed_app_id: str, devices):
    """Synchronize subscriptions of an installed up."""
    from pysmartthings import (
        CAPABILITIES, SmartThings, SourceType, Subscription,
        SubscriptionEntity
    )

    api = SmartThings(async_get_clientsession(hass), auth_token)
    tasks = []

    async def create_subscription(target: str):
        sub = Subscription()
        sub.installed_app_id = installed_app_id
        sub.location_id = location_id
        sub.source_type = SourceType.CAPABILITY
        sub.capability = target
        try:
            await api.create_subscription(sub)
            _LOGGER.debug("Created subscription for '%s' under app '%s'",
                          target, installed_app_id)
        except Exception:  # pylint:disable=broad-except
            _LOGGER.exception("Failed to create subscription for '%s' under "
                              "app '%s'", target, installed_app_id)

    async def delete_subscription(sub: SubscriptionEntity):
        try:
            await api.delete_subscription(
                installed_app_id, sub.subscription_id)
            _LOGGER.debug("Removed subscription for '%s' under app '%s' "
                          "because it was no longer needed",
                          sub.capability, installed_app_id)
        except Exception:  # pylint:disable=broad-except
            _LOGGER.exception("Failed to remove subscription for '%s' under "
                              "app '%s'", sub.capability, installed_app_id)

    # Build set of capabilities and prune unsupported ones
    capabilities = set()
    for device in devices:
        capabilities.update(device.capabilities)
    capabilities.intersection_update(CAPABILITIES)

    # Get current subscriptions and find differences
    subscriptions = await api.subscriptions(installed_app_id)
    for subscription in subscriptions:
        if subscription.capability in capabilities:
            capabilities.remove(subscription.capability)
        else:
            # Delete the subscription
            tasks.append(delete_subscription(subscription))

    # Remaining capabilities need subscriptions created
    tasks.extend([create_subscription(c) for c in capabilities])

    if tasks:
        await asyncio.gather(*tasks)
    else:
        _LOGGER.debug("Subscriptions for app '%s' are up-to-date",
                      installed_app_id)
示例#3
0
async def smartapp_install(hass: HomeAssistantType, req, resp, app):
    """
    Handle when a SmartApp is installed by the user into a location.

    Setup subscriptions using the access token SmartThings provided in the
    event. An explicit subscription is required for each 'capability' in order
    to receive the related attribute updates.  Finally, create a config entry
    representing the installation if this is not the first installation under
    the account.
    """
    from pysmartthings import SmartThings, Subscription, SourceType

    # This access token is a temporary 'SmartApp token' that expires in 5 min
    # and is used to create subscriptions only.
    api = SmartThings(async_get_clientsession(hass), req.auth_token)

    async def create_subscription(target):
        sub = Subscription()
        sub.installed_app_id = req.installed_app_id
        sub.location_id = req.location_id
        sub.source_type = SourceType.CAPABILITY
        sub.capability = target
        try:
            await api.create_subscription(sub)
            _LOGGER.debug("Created subscription for '%s' under app '%s'",
                          target, req.installed_app_id)
        except Exception:  # pylint:disable=broad-except
            _LOGGER.exception(
                "Failed to create subscription for '%s' under "
                "app '%s'", target, req.installed_app_id)

    tasks = [create_subscription(c) for c in SUPPORTED_CAPABILITIES]
    await asyncio.gather(*tasks)
    _LOGGER.debug("SmartApp '%s' under parent app '%s' was installed",
                  req.installed_app_id, app.app_id)

    # The permanent access token is copied from another config flow with the
    # same parent app_id.  If one is not found, that means the user is within
    # the initial config flow and the entry at the conclusion.
    access_token = next((entry.data.get(CONF_ACCESS_TOKEN)
                         for entry in hass.config_entries.async_entries(DOMAIN)
                         if entry.data[CONF_APP_ID] == app.app_id), None)
    if access_token:
        # Add as job not needed because the current coroutine was invoked
        # from the dispatcher and is not being awaited.
        await hass.config_entries.flow.async_init(
            DOMAIN,
            context={'source': 'install'},
            data={
                CONF_APP_ID: app.app_id,
                CONF_INSTALLED_APP_ID: req.installed_app_id,
                CONF_LOCATION_ID: req.location_id,
                CONF_ACCESS_TOKEN: access_token
            })
    async def async_step_user(self, user_input=None):
        """Get access token and validate it."""
        from pysmartthings import SmartThings

        errors = {}
        if not self.hass.config.api.base_url.lower().startswith('https://'):
            errors['base'] = "base_url_not_https"
            return self._show_step_user(errors)

        if user_input is None or CONF_ACCESS_TOKEN not in user_input:
            return self._show_step_user(errors)

        self.access_token = user_input.get(CONF_ACCESS_TOKEN, '')
        self.api = SmartThings(async_get_clientsession(self.hass),
                               self.access_token)

        # Ensure token is a UUID
        if not VAL_UID_MATCHER.match(self.access_token):
            errors[CONF_ACCESS_TOKEN] = "token_invalid_format"
            return self._show_step_user(errors)
        # Check not already setup in another entry
        if any(
                entry.data.get(CONF_ACCESS_TOKEN) == self.access_token
                for entry in self.hass.config_entries.async_entries(DOMAIN)):
            errors[CONF_ACCESS_TOKEN] = "token_already_setup"
            return self._show_step_user(errors)

        # Setup end-point
        await setup_smartapp_endpoint(self.hass)

        try:
            app = await find_app(self.hass, self.api)
            if app:
                await app.refresh()  # load all attributes
                await update_app(self.hass, app)
            else:
                app = await create_app(self.hass, self.api)
            setup_smartapp(self.hass, app)
            self.app_id = app.app_id
        except ClientResponseError as ex:
            if ex.status == 401:
                errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
            elif ex.status == 403:
                errors[CONF_ACCESS_TOKEN] = "token_forbidden"
            else:
                errors['base'] = "app_setup_error"
            return self._show_step_user(errors)
        except Exception:  # pylint:disable=broad-except
            errors['base'] = "app_setup_error"
            _LOGGER.exception("Unexpected error setting up the SmartApp")
            return self._show_step_user(errors)

        return await self.async_step_wait_install()
示例#5
0
    async def async_step_install(self, data=None):
        """
        Create a config entry at completion of a flow.

        Launched when the user completes the flow or when the SmartApp
        is installed into an additional location.
        """
        if not self.api:
            # Launched from the SmartApp install event handler
            self.api = SmartThings(async_get_clientsession(self.hass),
                                   data[CONF_ACCESS_TOKEN])

        location = await self.api.location(data[CONF_LOCATION_ID])
        return self.async_create_entry(title=location.name, data=data)
示例#6
0
async def async_remove_entry(hass: HomeAssistantType,
                             entry: ConfigEntry) -> None:
    """Perform clean-up when entry is being removed."""
    api = SmartThings(async_get_clientsession(hass),
                      entry.data[CONF_ACCESS_TOKEN])

    # Remove the installed_app, which if already removed raises a 403 error.
    installed_app_id = entry.data[CONF_INSTALLED_APP_ID]
    try:
        await api.delete_installed_app(installed_app_id)
    except ClientResponseError as ex:
        if ex.status == 403:
            _LOGGER.debug(
                "Installed app %s has already been removed",
                installed_app_id,
                exc_info=True,
            )
        else:
            raise
    _LOGGER.debug("Removed installed app %s", installed_app_id)

    # Remove the app if not referenced by other entries, which if already
    # removed raises a 403 error.
    all_entries = hass.config_entries.async_entries(DOMAIN)
    app_id = entry.data[CONF_APP_ID]
    app_count = sum(1 for entry in all_entries
                    if entry.data[CONF_APP_ID] == app_id)
    if app_count > 1:
        _LOGGER.debug(
            "App %s was not removed because it is in use by other"
            "config entries",
            app_id,
        )
        return
    # Remove the app
    try:
        await api.delete_app(app_id)
    except ClientResponseError as ex:
        if ex.status == 403:
            _LOGGER.debug("App %s has already been removed",
                          app_id,
                          exc_info=True)
        else:
            raise
    _LOGGER.debug("Removed app %s", app_id)

    if len(all_entries) == 1:
        await unload_smartapp_endpoint(hass)
async def remove_apps(token: str):
    """Remove Home Assistant apps and installed apps."""
    async with aiohttp.ClientSession() as session:
        api = SmartThings(session, token)

        apps = await api.apps()
        installed_apps = await api.installed_apps()

        for app in apps:
            if not app.app_name.startswith('homeassistant.'):
                continue
            # Remove installed apps first
            for installed_app in installed_apps:
                if installed_app.app_id == app.app_id:
                    await api.delete_installed_app(
                        installed_app.installed_app_id)
                    print("Removed installed app '{}' ({})".format(
                        installed_app.display_name,
                        installed_app.installed_app_id))
            # Remove the app itself
            await api.delete_app(app.app_id)
            print("Removed app '{}' ({})".format(app.app_name, app.app_id))
示例#8
0
async def smartapp_sync_subscriptions(
        hass: HomeAssistantType, auth_token: str, location_id: str,
        installed_app_id: str, *, skip_delete=False):
    """Synchronize subscriptions of an installed up."""
    from pysmartthings import (
        CAPABILITIES, SmartThings, SourceType, Subscription)

    api = SmartThings(async_get_clientsession(hass), auth_token)
    devices = await api.devices(location_ids=[location_id])

    # Build set of capabilities and prune unsupported ones
    capabilities = set()
    for device in devices:
        capabilities.update(device.capabilities)
    capabilities.intersection_update(CAPABILITIES)

    # Remove all (except for installs)
    if not skip_delete:
        await api.delete_subscriptions(installed_app_id)

    # Create for each capability
    async def create_subscription(target):
        sub = Subscription()
        sub.installed_app_id = installed_app_id
        sub.location_id = location_id
        sub.source_type = SourceType.CAPABILITY
        sub.capability = target
        try:
            await api.create_subscription(sub)
            _LOGGER.debug("Created subscription for '%s' under app '%s'",
                          target, installed_app_id)
        except Exception:  # pylint:disable=broad-except
            _LOGGER.exception("Failed to create subscription for '%s' under "
                              "app '%s'", target, installed_app_id)

    tasks = [create_subscription(c) for c in capabilities]
    await asyncio.gather(*tasks)
async def smartapp_sync_subscriptions(
    hass: HomeAssistant,
    auth_token: str,
    location_id: str,
    installed_app_id: str,
    devices,
):
    """Synchronize subscriptions of an installed up."""
    api = SmartThings(async_get_clientsession(hass), auth_token)
    tasks = []

    async def create_subscription(target: str):
        sub = Subscription()
        sub.installed_app_id = installed_app_id
        sub.location_id = location_id
        sub.source_type = SourceType.CAPABILITY
        sub.capability = target
        try:
            await api.create_subscription(sub)
            _LOGGER.debug("Created subscription for '%s' under app '%s'",
                          target, installed_app_id)
        except Exception as error:  # pylint:disable=broad-except
            _LOGGER.error(
                "Failed to create subscription for '%s' under app '%s': %s",
                target,
                installed_app_id,
                error,
            )

    async def delete_subscription(sub: SubscriptionEntity):
        try:
            await api.delete_subscription(installed_app_id,
                                          sub.subscription_id)
            _LOGGER.debug(
                "Removed subscription for '%s' under app '%s' because it was no longer needed",
                sub.capability,
                installed_app_id,
            )
        except Exception as error:  # pylint:disable=broad-except
            _LOGGER.error(
                "Failed to remove subscription for '%s' under app '%s': %s",
                sub.capability,
                installed_app_id,
                error,
            )

    # Build set of capabilities and prune unsupported ones
    capabilities = set()
    for device in devices:
        capabilities.update(device.capabilities)
    # Remove items not defined in the library
    capabilities.intersection_update(CAPABILITIES)
    # Remove unused capabilities
    capabilities.difference_update(IGNORED_CAPABILITIES)
    capability_count = len(capabilities)
    if capability_count > SUBSCRIPTION_WARNING_LIMIT:
        _LOGGER.warning(
            "Some device attributes may not receive push updates and there may be subscription "
            "creation failures under app '%s' because %s subscriptions are required but "
            "there is a limit of %s per app",
            installed_app_id,
            capability_count,
            SUBSCRIPTION_WARNING_LIMIT,
        )
    _LOGGER.debug(
        "Synchronizing subscriptions for %s capabilities under app '%s': %s",
        capability_count,
        installed_app_id,
        capabilities,
    )

    # Get current subscriptions and find differences
    subscriptions = await api.subscriptions(installed_app_id)
    for subscription in subscriptions:
        if subscription.capability in capabilities:
            capabilities.remove(subscription.capability)
        else:
            # Delete the subscription
            tasks.append(delete_subscription(subscription))

    # Remaining capabilities need subscriptions created
    tasks.extend([create_subscription(c) for c in capabilities])

    if tasks:
        await asyncio.gather(*tasks)
    else:
        _LOGGER.debug("Subscriptions for app '%s' are up-to-date",
                      installed_app_id)
示例#10
0
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Initialize config entry which represents an installed SmartApp."""
    from pysmartthings import SmartThings

    if not hass.config.api.base_url.lower().startswith('https://'):
        _LOGGER.warning("The 'base_url' of the 'http' component must be "
                        "configured and start with 'https://'")
        return False

    api = SmartThings(async_get_clientsession(hass),
                      entry.data[CONF_ACCESS_TOKEN])

    remove_entry = False
    try:
        # See if the app is already setup. This occurs when there are
        # installs in multiple SmartThings locations (valid use-case)
        manager = hass.data[DOMAIN][DATA_MANAGER]
        smart_app = manager.smartapps.get(entry.data[CONF_APP_ID])
        if not smart_app:
            # Validate and setup the app.
            app = await api.app(entry.data[CONF_APP_ID])
            smart_app = setup_smartapp(hass, app)

        # Validate and retrieve the installed app.
        installed_app = await validate_installed_app(
            api, entry.data[CONF_INSTALLED_APP_ID])

        # Get devices and their current status
        devices = await api.devices(
            location_ids=[installed_app.location_id])

        async def retrieve_device_status(device):
            try:
                await device.status.refresh()
            except ClientResponseError:
                _LOGGER.debug("Unable to update status for device: %s (%s), "
                              "the device will be ignored",
                              device.label, device.device_id, exc_info=True)
                devices.remove(device)

        await asyncio.gather(*[retrieve_device_status(d)
                               for d in devices.copy()])

        # Setup device broker
        broker = DeviceBroker(hass, devices,
                              installed_app.installed_app_id)
        broker.event_handler_disconnect = \
            smart_app.connect_event(broker.event_handler)
        hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker

    except ClientResponseError as ex:
        if ex.status in (401, 403):
            _LOGGER.exception("Unable to setup config entry '%s' - please "
                              "reconfigure the integration", entry.title)
            remove_entry = True
        else:
            _LOGGER.debug(ex, exc_info=True)
            raise ConfigEntryNotReady
    except (ClientConnectionError, RuntimeWarning) as ex:
        _LOGGER.debug(ex, exc_info=True)
        raise ConfigEntryNotReady

    if remove_entry:
        hass.async_create_task(
            hass.config_entries.async_remove(entry.entry_id))
        # only create new flow if there isn't a pending one for SmartThings.
        flows = hass.config_entries.flow.async_progress()
        if not [flow for flow in flows if flow['handler'] == DOMAIN]:
            hass.async_create_task(
                hass.config_entries.flow.async_init(
                    DOMAIN, context={'source': 'import'}))
        return False

    for component in SUPPORTED_PLATFORMS:
        hass.async_create_task(hass.config_entries.async_forward_entry_setup(
            entry, component))
    return True
示例#11
0
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Initialize config entry which represents an installed SmartApp."""
    if not validate_webhook_requirements(hass):
        _LOGGER.warning(
            "The 'base_url' of the 'http' integration must be configured and start with 'https://'"
        )
        return False

    api = SmartThings(async_get_clientsession(hass), entry.data[CONF_ACCESS_TOKEN])

    remove_entry = False
    try:
        # See if the app is already setup. This occurs when there are
        # installs in multiple SmartThings locations (valid use-case)
        manager = hass.data[DOMAIN][DATA_MANAGER]
        smart_app = manager.smartapps.get(entry.data[CONF_APP_ID])
        if not smart_app:
            # Validate and setup the app.
            app = await api.app(entry.data[CONF_APP_ID])
            smart_app = setup_smartapp(hass, app)

        # Validate and retrieve the installed app.
        installed_app = await validate_installed_app(
            api, entry.data[CONF_INSTALLED_APP_ID]
        )

        # Get scenes
        scenes = await async_get_entry_scenes(entry, api)

        # Get SmartApp token to sync subscriptions
        token = await api.generate_tokens(
            entry.data[CONF_OAUTH_CLIENT_ID],
            entry.data[CONF_OAUTH_CLIENT_SECRET],
            entry.data[CONF_REFRESH_TOKEN],
        )
        hass.config_entries.async_update_entry(
            entry, data={**entry.data, CONF_REFRESH_TOKEN: token.refresh_token}
        )

        # Get devices and their current status
        devices = await api.devices(location_ids=[installed_app.location_id])

        async def retrieve_device_status(device):
            try:
                await device.status.refresh()
            except ClientResponseError:
                _LOGGER.debug(
                    "Unable to update status for device: %s (%s), the device will be excluded",
                    device.label,
                    device.device_id,
                    exc_info=True,
                )
                devices.remove(device)

        await asyncio.gather(*(retrieve_device_status(d) for d in devices.copy()))

        # Sync device subscriptions
        await smartapp_sync_subscriptions(
            hass,
            token.access_token,
            installed_app.location_id,
            installed_app.installed_app_id,
            devices,
        )

        # Setup device broker
        broker = DeviceBroker(hass, entry, token, smart_app, devices, scenes)
        broker.connect()
        hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker

    except ClientResponseError as ex:
        if ex.status in (401, HTTP_FORBIDDEN):
            _LOGGER.exception(
                "Unable to setup configuration entry '%s' - please reconfigure the integration",
                entry.title,
            )
            remove_entry = True
        else:
            _LOGGER.debug(ex, exc_info=True)
            raise ConfigEntryNotReady
    except (ClientConnectionError, RuntimeWarning) as ex:
        _LOGGER.debug(ex, exc_info=True)
        raise ConfigEntryNotReady

    if remove_entry:
        hass.async_create_task(hass.config_entries.async_remove(entry.entry_id))
        # only create new flow if there isn't a pending one for SmartThings.
        flows = hass.config_entries.flow.async_progress()
        if not [flow for flow in flows if flow["handler"] == DOMAIN]:
            hass.async_create_task(
                hass.config_entries.flow.async_init(
                    DOMAIN, context={"source": "import"}
                )
            )
        return False

    for component in SUPPORTED_PLATFORMS:
        hass.async_create_task(
            hass.config_entries.async_forward_entry_setup(entry, component)
        )
    return True
示例#12
0
    async def async_step_user(self, user_input=None):
        """Get access token and validate it."""
        errors = {}
        if user_input is None or CONF_ACCESS_TOKEN not in user_input:
            return self._show_step_user(errors)

        self.access_token = user_input.get(CONF_ACCESS_TOKEN, '')
        self.api = SmartThings(async_get_clientsession(self.hass),
                               self.access_token)

        # Ensure token is a UUID
        if not VAL_UID_MATCHER.match(self.access_token):
            errors[CONF_ACCESS_TOKEN] = "token_invalid_format"
            return self._show_step_user(errors)
        # Check not already setup in another entry
        if any(
                entry.data.get(CONF_ACCESS_TOKEN) == self.access_token
                for entry in self.hass.config_entries.async_entries(DOMAIN)):
            errors[CONF_ACCESS_TOKEN] = "token_already_setup"
            return self._show_step_user(errors)

        # Setup end-point
        await setup_smartapp_endpoint(self.hass)

        if not validate_webhook_requirements(self.hass):
            errors['base'] = "base_url_not_https"
            return self._show_step_user(errors)

        try:
            app = await find_app(self.hass, self.api)
            if app:
                await app.refresh()  # load all attributes
                await update_app(self.hass, app)
                # Get oauth client id/secret by regenerating it
                app_oauth = AppOAuth(app.app_id)
                app_oauth.client_name = APP_OAUTH_CLIENT_NAME
                app_oauth.scope.extend(APP_OAUTH_SCOPES)
                client = await self.api.generate_app_oauth(app_oauth)
            else:
                app, client = await create_app(self.hass, self.api)
            setup_smartapp(self.hass, app)
            self.app_id = app.app_id
            self.oauth_client_secret = client.client_secret
            self.oauth_client_id = client.client_id

        except APIResponseError as ex:
            if ex.is_target_error():
                errors['base'] = 'webhook_error'
            else:
                errors['base'] = "app_setup_error"
            _LOGGER.exception("API error setting up the SmartApp: %s",
                              ex.raw_error_response)
            return self._show_step_user(errors)
        except ClientResponseError as ex:
            if ex.status == 401:
                errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
            elif ex.status == 403:
                errors[CONF_ACCESS_TOKEN] = "token_forbidden"
            else:
                errors['base'] = "app_setup_error"
                _LOGGER.exception("Unexpected error setting up the SmartApp")
            return self._show_step_user(errors)
        except Exception:  # pylint:disable=broad-except
            errors['base'] = "app_setup_error"
            _LOGGER.exception("Unexpected error setting up the SmartApp")
            return self._show_step_user(errors)

        return await self.async_step_wait_install()
示例#13
0
    async def async_step_pat(self, user_input=None):
        """Get the Personal Access Token and validate it."""
        errors = {}
        if user_input is None or CONF_ACCESS_TOKEN not in user_input:
            return self._show_step_pat(errors)

        self.access_token = user_input[CONF_ACCESS_TOKEN]

        # Ensure token is a UUID
        if not VAL_UID_MATCHER.match(self.access_token):
            errors[CONF_ACCESS_TOKEN] = "token_invalid_format"
            return self._show_step_pat(errors)

        # Setup end-point
        self.api = SmartThings(async_get_clientsession(self.hass),
                               self.access_token)
        try:
            app = await find_app(self.hass, self.api)
            if app:
                await app.refresh()  # load all attributes
                await update_app(self.hass, app)
                # Find an existing entry to copy the oauth client
                existing = next(
                    (entry for entry in self._async_current_entries()
                     if entry.data[CONF_APP_ID] == app.app_id),
                    None,
                )
                if existing:
                    self.oauth_client_id = existing.data[CONF_CLIENT_ID]
                    self.oauth_client_secret = existing.data[
                        CONF_CLIENT_SECRET]
                else:
                    # Get oauth client id/secret by regenerating it
                    app_oauth = AppOAuth(app.app_id)
                    app_oauth.client_name = APP_OAUTH_CLIENT_NAME
                    app_oauth.scope.extend(APP_OAUTH_SCOPES)
                    client = await self.api.generate_app_oauth(app_oauth)
                    self.oauth_client_secret = client.client_secret
                    self.oauth_client_id = client.client_id
            else:
                app, client = await create_app(self.hass, self.api)
                self.oauth_client_secret = client.client_secret
                self.oauth_client_id = client.client_id
            setup_smartapp(self.hass, app)
            self.app_id = app.app_id

        except APIResponseError as ex:
            if ex.is_target_error():
                errors["base"] = "webhook_error"
            else:
                errors["base"] = "app_setup_error"
            _LOGGER.exception("API error setting up the SmartApp: %s",
                              ex.raw_error_response)
            return self._show_step_pat(errors)
        except ClientResponseError as ex:
            if ex.status == HTTP_UNAUTHORIZED:
                errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
                _LOGGER.debug(
                    "Unauthorized error received setting up SmartApp",
                    exc_info=True)
            elif ex.status == HTTP_FORBIDDEN:
                errors[CONF_ACCESS_TOKEN] = "token_forbidden"
                _LOGGER.debug("Forbidden error received setting up SmartApp",
                              exc_info=True)
            else:
                errors["base"] = "app_setup_error"
                _LOGGER.exception("Unexpected error setting up the SmartApp")
            return self._show_step_pat(errors)
        except Exception:  # pylint:disable=broad-except
            errors["base"] = "app_setup_error"
            _LOGGER.exception("Unexpected error setting up the SmartApp")
            return self._show_step_pat(errors)

        return await self.async_step_select_location()