Esempio n. 1
0
def validate_webhook_requirements(hass: HomeAssistantType) -> bool:
    """Ensure HASS is setup properly to receive webhooks."""
    if cloud.async_active_subscription(hass):
        return True
    if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None:
        return True
    return get_webhook_url(hass).lower().startswith('https://')
Esempio n. 2
0
def validate_webhook_requirements(hass: HomeAssistant) -> bool:
    """Ensure Home Assistant is setup properly to receive webhooks."""
    if cloud.async_active_subscription(hass):
        return True
    if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None:
        return True
    return get_webhook_url(hass).lower().startswith("https://")
Esempio n. 3
0
def get_webhook_url(hass: HomeAssistantType) -> str:
    """
    Get the URL of the webhook.

    Return the cloudhook if available, otherwise local webhook.
    """
    cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL]
    if cloud.async_active_subscription(hass) and cloudhook_url is not None:
        return cloudhook_url
    return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID])
Esempio n. 4
0
def get_webhook_url(hass: HomeAssistant) -> str:
    """
    Get the URL of the webhook.

    Return the cloudhook if available, otherwise local webhook.
    """
    cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL]
    if cloud.async_active_subscription(hass) and cloudhook_url is not None:
        return cloudhook_url
    return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID])
Esempio n. 5
0
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
    """Cleanup when entry is removed."""
    if CONF_WEBHOOK_ID in entry.data and cloud.async_active_subscription(hass):
        try:
            _LOGGER.debug(
                "Removing Netatmo cloudhook (%s)", entry.data[CONF_WEBHOOK_ID]
            )
            await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
        except cloud.CloudNotAvailable:
            pass
Esempio n. 6
0
    async def _get_webhook_id(self):
        """Generate webhook ID."""
        webhook_id = webhook.async_generate_id()
        if cloud.async_active_subscription(self.hass):
            webhook_url = await cloud.async_create_cloudhook(
                self.hass, webhook_id)
            cloudhook = True
        else:
            webhook_url = webhook.async_generate_url(self.hass, webhook_id)
            cloudhook = False

        return webhook_id, webhook_url, cloudhook
Esempio n. 7
0
    async def register_webhook(self, event: Event | None = None) -> None:
        """Register a webhook with Toon to get live updates."""
        if CONF_WEBHOOK_ID not in self.entry.data:
            data = {**self.entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
            self.hass.config_entries.async_update_entry(self.entry, data=data)

        if cloud.async_active_subscription(self.hass):

            if CONF_CLOUDHOOK_URL not in self.entry.data:
                try:
                    webhook_url = await cloud.async_create_cloudhook(
                        self.hass, self.entry.data[CONF_WEBHOOK_ID]
                    )
                except cloud.CloudNotConnected:
                    webhook_url = webhook.async_generate_url(
                        self.hass, self.entry.data[CONF_WEBHOOK_ID]
                    )
                else:
                    data = {**self.entry.data, CONF_CLOUDHOOK_URL: webhook_url}
                    self.hass.config_entries.async_update_entry(self.entry, data=data)
            else:
                webhook_url = self.entry.data[CONF_CLOUDHOOK_URL]
        else:
            webhook_url = webhook.async_generate_url(
                self.hass, self.entry.data[CONF_WEBHOOK_ID]
            )

        # Ensure the webhook is not registered already
        webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])

        webhook_register(
            self.hass,
            DOMAIN,
            "Toon",
            self.entry.data[CONF_WEBHOOK_ID],
            self.handle_webhook,
        )

        try:
            await self.toon.subscribe_webhook(
                application_id=self.entry.entry_id, url=webhook_url
            )
            _LOGGER.info("Registered Toon webhook: %s", webhook_url)
        except ToonError as err:
            _LOGGER.error("Error during webhook registration - %s", err)

        self.hass.bus.async_listen_once(
            EVENT_HOMEASSISTANT_STOP, self.unregister_webhook
        )
Esempio n. 8
0
    async def post(self, request: Request, data: dict) -> Response:
        """Handle the POST request for registration."""
        hass = request.app["hass"]

        webhook_id = secrets.token_hex()

        if cloud.async_active_subscription(hass):
            data[CONF_CLOUDHOOK_URL] = await cloud.async_create_cloudhook(
                hass, webhook_id)

        data[CONF_WEBHOOK_ID] = webhook_id

        if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption():
            data[CONF_SECRET] = secrets.token_hex(SecretBox.KEY_SIZE)

        data[CONF_USER_ID] = request["hass_user"].id

        if slugify(data[ATTR_DEVICE_NAME], separator=""):
            # if slug is not empty and would not only be underscores
            # use DEVICE_NAME
            pass
        elif emoji.emoji_count(data[ATTR_DEVICE_NAME]):
            # If otherwise empty string contains emoji
            # use descriptive name of the first emoji
            data[ATTR_DEVICE_NAME] = emoji.demojize(
                emoji.emoji_lis(data[ATTR_DEVICE_NAME])[0]["emoji"]).replace(
                    ":", "")
        else:
            # Fallback to DEVICE_ID
            data[ATTR_DEVICE_NAME] = data[ATTR_DEVICE_ID]

        await hass.async_create_task(
            hass.config_entries.flow.async_init(
                DOMAIN, data=data, context={"source": "registration"}))

        remote_ui_url = None
        with suppress(hass.components.cloud.CloudNotAvailable):
            remote_ui_url = cloud.async_remote_ui_url(hass)

        return self.json(
            {
                CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL),
                CONF_REMOTE_UI_URL: remote_ui_url,
                CONF_SECRET: data.get(CONF_SECRET),
                CONF_WEBHOOK_ID: data[CONF_WEBHOOK_ID],
            },
            status_code=HTTPStatus.CREATED,
        )
Esempio n. 9
0
    async def post(self, request: Request, data: dict) -> Response:
        """Handle the POST request for registration."""
        hass = request.app["hass"]

        webhook_id = secrets.token_hex()

        if cloud.async_active_subscription(hass):
            data[CONF_CLOUDHOOK_URL] = await cloud.async_create_cloudhook(
                hass, webhook_id)

        data[CONF_WEBHOOK_ID] = webhook_id

        if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption():
            data[CONF_SECRET] = secrets.token_hex(SecretBox.KEY_SIZE)

        data[CONF_USER_ID] = request["hass_user"].id

        # Fallback to DEVICE_ID if slug is empty.
        if not slugify(data[ATTR_DEVICE_NAME], separator=""):
            data[ATTR_DEVICE_NAME] = data[ATTR_DEVICE_ID]

        await hass.async_create_task(
            hass.config_entries.flow.async_init(
                DOMAIN, data=data, context={"source": "registration"}))

        remote_ui_url = None
        with suppress(hass.components.cloud.CloudNotAvailable):
            remote_ui_url = cloud.async_remote_ui_url(hass)

        return self.json(
            {
                CONF_CLOUDHOOK_URL: data.get(CONF_CLOUDHOOK_URL),
                CONF_REMOTE_UI_URL: remote_ui_url,
                CONF_SECRET: data.get(CONF_SECRET),
                CONF_WEBHOOK_ID: data[CONF_WEBHOOK_ID],
            },
            status_code=HTTPStatus.CREATED,
        )
Esempio n. 10
0
    async def register_webhook(
        call_or_event_or_dt: ServiceCall | Event | datetime | None,
    ) -> None:
        if CONF_WEBHOOK_ID not in entry.data:
            data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
            hass.config_entries.async_update_entry(entry, data=data)

        if cloud.async_active_subscription(hass):
            webhook_url = await async_cloudhook_generate_url(hass, entry)
        else:
            webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])

        if entry.data[
            "auth_implementation"
        ] == cloud.DOMAIN and not webhook_url.startswith("https://"):
            _LOGGER.warning(
                "Webhook not registered - "
                "https and port 443 is required to register the webhook"
            )
            return

        webhook_register(
            hass,
            DOMAIN,
            "Netatmo",
            entry.data[CONF_WEBHOOK_ID],
            async_handle_webhook,
        )

        try:
            await hass.data[DOMAIN][entry.entry_id][AUTH].async_addwebhook(webhook_url)
            _LOGGER.info("Register Netatmo webhook: %s", webhook_url)
        except pyatmo.ApiError as err:
            _LOGGER.error("Error during webhook registration - %s", err)

        entry.async_on_unload(
            hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
        )
Esempio n. 11
0
def validate_webhook_requirements(hass: HomeAssistantType) -> bool:
    """Ensure HASS is setup properly to receive webhooks."""
    if cloud.async_active_subscription(hass):
        return True
    return get_webhook_url(hass).lower().startswith('https://')
Esempio n. 12
0
async def setup_smartapp_endpoint(hass: HomeAssistantType):
    """
    Configure the SmartApp webhook in hass.

    SmartApps are an extension point within the SmartThings ecosystem and
    is used to receive push updates (i.e. device updates) from the cloud.
    """
    from pysmartapp import Dispatcher, SmartAppManager

    data = hass.data.get(DOMAIN)
    if data:
        # already setup
        return

    # Get/create config to store a unique id for this hass instance.
    store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
    config = await store.async_load()
    if not config:
        # Create config
        config = {
            CONF_INSTANCE_ID: str(uuid4()),
            CONF_WEBHOOK_ID: webhook.generate_secret(),
            CONF_CLOUDHOOK_URL: None
        }
        await store.async_save(config)

    # Register webhook
    webhook.async_register(hass, DOMAIN, 'SmartApp',
                           config[CONF_WEBHOOK_ID], smartapp_webhook)

    # Create webhook if eligible
    cloudhook_url = config.get(CONF_CLOUDHOOK_URL)
    if cloudhook_url is None \
            and cloud.async_active_subscription(hass) \
            and not hass.config_entries.async_entries(DOMAIN):
        cloudhook_url = await cloud.async_create_cloudhook(
            hass, config[CONF_WEBHOOK_ID])
        config[CONF_CLOUDHOOK_URL] = cloudhook_url
        await store.async_save(config)
        _LOGGER.debug("Created cloudhook '%s'", cloudhook_url)

    # SmartAppManager uses a dispatcher to invoke callbacks when push events
    # occur. Use hass' implementation instead of the built-in one.
    dispatcher = Dispatcher(
        signal_prefix=SIGNAL_SMARTAPP_PREFIX,
        connect=functools.partial(async_dispatcher_connect, hass),
        send=functools.partial(async_dispatcher_send, hass))
    # Path is used in digital signature validation
    path = urlparse(cloudhook_url).path if cloudhook_url else \
        webhook.async_generate_path(config[CONF_WEBHOOK_ID])
    manager = SmartAppManager(path, dispatcher=dispatcher)
    manager.connect_install(functools.partial(smartapp_install, hass))
    manager.connect_update(functools.partial(smartapp_update, hass))
    manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))

    hass.data[DOMAIN] = {
        DATA_MANAGER: manager,
        CONF_INSTANCE_ID: config[CONF_INSTANCE_ID],
        DATA_BROKERS: {},
        CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID],
        # Will not be present if not enabled
        CONF_CLOUDHOOK_URL: config.get(CONF_CLOUDHOOK_URL),
        CONF_INSTALLED_APPS: []
    }
    _LOGGER.debug("Setup endpoint for %s",
                  cloudhook_url if cloudhook_url else
                  webhook.async_generate_url(hass, config[CONF_WEBHOOK_ID]))
Esempio n. 13
0
    if not (config := await store.async_load()):
        # Create config
        config = {
            CONF_INSTANCE_ID: str(uuid4()),
            CONF_WEBHOOK_ID: secrets.token_hex(),
            CONF_CLOUDHOOK_URL: None,
        }
        await store.async_save(config)

    # Register webhook
    webhook.async_register(hass, DOMAIN, "SmartApp", config[CONF_WEBHOOK_ID],
                           smartapp_webhook)

    # Create webhook if eligible
    cloudhook_url = config.get(CONF_CLOUDHOOK_URL)
    if (cloudhook_url is None and cloud.async_active_subscription(hass)
            and not hass.config_entries.async_entries(DOMAIN)):
        cloudhook_url = await cloud.async_create_cloudhook(
            hass, config[CONF_WEBHOOK_ID])
        config[CONF_CLOUDHOOK_URL] = cloudhook_url
        await store.async_save(config)
        _LOGGER.debug("Created cloudhook '%s'", cloudhook_url)

    # SmartAppManager uses a dispatcher to invoke callbacks when push events
    # occur. Use hass' implementation instead of the built-in one.
    dispatcher = Dispatcher(
        signal_prefix=SIGNAL_SMARTAPP_PREFIX,
        connect=functools.partial(async_dispatcher_connect, hass),
        send=functools.partial(async_dispatcher_send, hass),
    )
    # Path is used in digital signature validation
Esempio n. 14
0
                           _async_handle_rachio_webhook)


async def async_get_or_create_registered_webhook_id_and_url(hass, entry):
    """Generate webhook ID."""
    config = entry.data.copy()

    updated_config = False
    webhook_url = None

    if not (webhook_id := config.get(CONF_WEBHOOK_ID)):
        webhook_id = webhook.async_generate_id()
        config[CONF_WEBHOOK_ID] = webhook_id
        updated_config = True

    if cloud.async_active_subscription(hass):
        if not (cloudhook_url := config.get(CONF_CLOUDHOOK_URL)):
            cloudhook_url = await cloud.async_create_cloudhook(
                hass, webhook_id)
            config[CONF_CLOUDHOOK_URL] = cloudhook_url
            updated_config = True
        webhook_url = cloudhook_url

    if not webhook_url:
        webhook_url = webhook.async_generate_url(hass, webhook_id)

    if updated_config:
        hass.config_entries.async_update_entry(entry, data=config)

    return webhook_id, webhook_url
Esempio n. 15
0
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up Netatmo from a config entry."""
    implementation = (
        await config_entry_oauth2_flow.async_get_config_entry_implementation(
            hass, entry
        )
    )

    # Set unique id if non was set (migration)
    if not entry.unique_id:
        hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)

    session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
    try:
        await session.async_ensure_token_valid()
    except aiohttp.ClientResponseError as ex:
        _LOGGER.debug("API error: %s (%s)", ex.code, ex.message)
        if ex.code in (
            HTTPStatus.BAD_REQUEST,
            HTTPStatus.UNAUTHORIZED,
            HTTPStatus.FORBIDDEN,
        ):
            raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex
        raise ConfigEntryNotReady from ex

    if sorted(session.token["scope"]) != sorted(NETATMO_SCOPES):
        _LOGGER.debug(
            "Scope is invalid: %s != %s", session.token["scope"], NETATMO_SCOPES
        )
        raise ConfigEntryAuthFailed("Token scope not valid, trigger renewal")

    hass.data[DOMAIN][entry.entry_id] = {
        AUTH: api.AsyncConfigEntryNetatmoAuth(
            aiohttp_client.async_get_clientsession(hass), session
        )
    }

    data_handler = NetatmoDataHandler(hass, entry)
    await data_handler.async_setup()
    hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler

    hass.config_entries.async_setup_platforms(entry, PLATFORMS)

    async def unregister_webhook(
        call_or_event_or_dt: ServiceCall | Event | datetime | None,
    ) -> None:
        if CONF_WEBHOOK_ID not in entry.data:
            return
        _LOGGER.debug("Unregister Netatmo webhook (%s)", entry.data[CONF_WEBHOOK_ID])
        async_dispatcher_send(
            hass,
            f"signal-{DOMAIN}-webhook-None",
            {"type": "None", "data": {WEBHOOK_PUSH_TYPE: WEBHOOK_DEACTIVATION}},
        )
        webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
        try:
            await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook()
        except pyatmo.ApiError:
            _LOGGER.debug(
                "No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID]
            )

    async def register_webhook(
        call_or_event_or_dt: ServiceCall | Event | datetime | None,
    ) -> None:
        if CONF_WEBHOOK_ID not in entry.data:
            data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
            hass.config_entries.async_update_entry(entry, data=data)

        if cloud.async_active_subscription(hass):
            webhook_url = await async_cloudhook_generate_url(hass, entry)
        else:
            webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID])

        if entry.data[
            "auth_implementation"
        ] == cloud.DOMAIN and not webhook_url.startswith("https://"):
            _LOGGER.warning(
                "Webhook not registered - "
                "https and port 443 is required to register the webhook"
            )
            return

        webhook_register(
            hass,
            DOMAIN,
            "Netatmo",
            entry.data[CONF_WEBHOOK_ID],
            async_handle_webhook,
        )

        try:
            await hass.data[DOMAIN][entry.entry_id][AUTH].async_addwebhook(webhook_url)
            _LOGGER.info("Register Netatmo webhook: %s", webhook_url)
        except pyatmo.ApiError as err:
            _LOGGER.error("Error during webhook registration - %s", err)

        entry.async_on_unload(
            hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
        )

    async def manage_cloudhook(state: cloud.CloudConnectionState) -> None:
        if state is cloud.CloudConnectionState.CLOUD_CONNECTED:
            await register_webhook(None)

        if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED:
            await unregister_webhook(None)
            async_call_later(hass, 30, register_webhook)

    if cloud.async_active_subscription(hass):
        if cloud.async_is_connected(hass):
            await register_webhook(None)
        cloud.async_listen_connection_change(hass, manage_cloudhook)

    else:
        if hass.state == CoreState.running:
            await register_webhook(None)
        else:
            hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, register_webhook)

    hass.services.async_register(DOMAIN, "register_webhook", register_webhook)
    hass.services.async_register(DOMAIN, "unregister_webhook", unregister_webhook)

    entry.add_update_listener(async_config_entry_updated)

    return True
Esempio n. 16
0
async def setup_smartapp_endpoint(hass: HomeAssistantType):
    """
    Configure the SmartApp webhook in hass.

    SmartApps are an extension point within the SmartThings ecosystem and
    is used to receive push updates (i.e. device updates) from the cloud.
    """
    data = hass.data.get(DOMAIN)
    if data:
        # already setup
        return

    # Get/create config to store a unique id for this hass instance.
    store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
    config = await store.async_load()
    if not config:
        # Create config
        config = {
            CONF_INSTANCE_ID: str(uuid4()),
            CONF_WEBHOOK_ID: webhook.generate_secret(),
            CONF_CLOUDHOOK_URL: None,
        }
        await store.async_save(config)

    # Register webhook
    webhook.async_register(hass, DOMAIN, "SmartApp", config[CONF_WEBHOOK_ID],
                           smartapp_webhook)

    # Create webhook if eligible
    cloudhook_url = config.get(CONF_CLOUDHOOK_URL)
    if (cloudhook_url is None and cloud.async_active_subscription(hass)
            and not hass.config_entries.async_entries(DOMAIN)):
        cloudhook_url = await cloud.async_create_cloudhook(
            hass, config[CONF_WEBHOOK_ID])
        config[CONF_CLOUDHOOK_URL] = cloudhook_url
        await store.async_save(config)
        _LOGGER.debug("Created cloudhook '%s'", cloudhook_url)

    # SmartAppManager uses a dispatcher to invoke callbacks when push events
    # occur. Use hass' implementation instead of the built-in one.
    dispatcher = Dispatcher(
        signal_prefix=SIGNAL_SMARTAPP_PREFIX,
        connect=functools.partial(async_dispatcher_connect, hass),
        send=functools.partial(async_dispatcher_send, hass),
    )
    # Path is used in digital signature validation
    path = (urlparse(cloudhook_url).path if cloudhook_url else
            webhook.async_generate_path(config[CONF_WEBHOOK_ID]))
    manager = SmartAppManager(path, dispatcher=dispatcher)
    manager.connect_install(functools.partial(smartapp_install, hass))
    manager.connect_update(functools.partial(smartapp_update, hass))
    manager.connect_uninstall(functools.partial(smartapp_uninstall, hass))

    hass.data[DOMAIN] = {
        DATA_MANAGER: manager,
        CONF_INSTANCE_ID: config[CONF_INSTANCE_ID],
        DATA_BROKERS: {},
        CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID],
        # Will not be present if not enabled
        CONF_CLOUDHOOK_URL: config.get(CONF_CLOUDHOOK_URL),
        CONF_INSTALLED_APPS: [],
    }
    _LOGGER.debug(
        "Setup endpoint for %s",
        cloudhook_url if cloudhook_url else webhook.async_generate_url(
            hass, config[CONF_WEBHOOK_ID]),
    )
Esempio n. 17
0
    async def register_webhook(
        call_or_event_or_dt: ServiceCall | Event | datetime | None, ) -> None:
        if CONF_WEBHOOK_ID not in entry.data:
            data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
            hass.config_entries.async_update_entry(entry, data=data)

        if cloud.async_active_subscription(hass):
            if CONF_CLOUDHOOK_URL not in entry.data:
                webhook_url = await cloud.async_create_cloudhook(
                    hass, entry.data[CONF_WEBHOOK_ID])
                data = {**entry.data, CONF_CLOUDHOOK_URL: webhook_url}
                hass.config_entries.async_update_entry(entry, data=data)
            else:
                webhook_url = entry.data[CONF_CLOUDHOOK_URL]
        else:
            webhook_url = webhook_generate_url(hass,
                                               entry.data[CONF_WEBHOOK_ID])

        if entry.data[
                "auth_implementation"] == cloud.DOMAIN and not webhook_url.startswith(
                    "https://"):
            _LOGGER.warning(
                "Webhook not registered - "
                "https and port 443 is required to register the webhook")
            return

        try:
            webhook_register(
                hass,
                DOMAIN,
                "Netatmo",
                entry.data[CONF_WEBHOOK_ID],
                async_handle_webhook,
            )

            async def handle_event(event: dict) -> None:
                """Handle webhook events."""
                if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION:
                    if activation_listener is not None:
                        activation_listener()

                    if activation_timeout is not None:
                        activation_timeout()

            activation_listener = async_dispatcher_connect(
                hass,
                f"signal-{DOMAIN}-webhook-None",
                handle_event,
            )

            activation_timeout = async_call_later(hass, 30, unregister_webhook)

            await hass.data[DOMAIN][entry.entry_id
                                    ][AUTH].async_addwebhook(webhook_url)
            _LOGGER.info("Register Netatmo webhook: %s", webhook_url)
        except pyatmo.ApiError as err:
            _LOGGER.error("Error during webhook registration - %s", err)

        entry.async_on_unload(
            hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
                                       unregister_webhook))