예제 #1
0
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)
예제 #2
0
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Initialize config entry which represents an installed SmartApp."""
    # For backwards compat
    if entry.unique_id is None:
        hass.config_entries.async_update_entry(
            entry,
            unique_id=format_unique_id(
                entry.data[CONF_APP_ID], entry.data[CONF_LOCATION_ID]
            ),
        )

    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_CLIENT_ID],
            entry.data[CONF_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 (HTTPStatus.UNAUTHORIZED, HTTPStatus.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 from ex
    except (ClientConnectionError, RuntimeWarning) as ex:
        _LOGGER.debug(ex, exc_info=True)
        raise ConfigEntryNotReady from ex

    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.
        if not hass.config_entries.flow.async_progress_by_handler(DOMAIN):
            hass.async_create_task(
                hass.config_entries.flow.async_init(
                    DOMAIN, context={"source": SOURCE_IMPORT}
                )
            )
        return False

    hass.config_entries.async_setup_platforms(entry, PLATFORMS)
    return True
예제 #3
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()
예제 #4
0
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
    """Initialize config entry which represents an installed SmartApp."""
    from pysmartthings import SmartThings

    if not validate_webhook_requirements(hass):
        _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 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])
        entry.data[CONF_REFRESH_TOKEN] = token.refresh_token
        hass.config_entries.async_update_entry(entry)

        # 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, 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
예제 #5
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."""
    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)
    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)
예제 #6
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()