From 62bc8df9640ddbadc6b9da0c2713b87ee4b2028b Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Tue, 11 Apr 2023 11:13:52 +0200 Subject: [PATCH] Fall back to polling if webhook cannot be registered on Nuki (#91013) fix(nuki): throw warning if webhook cannot be created --- homeassistant/components/nuki/__init__.py | 159 ++++++++++++---------- homeassistant/components/nuki/helpers.py | 4 + 2 files changed, 89 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index ef168374bd8..b0bfe18614e 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -25,7 +25,6 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( device_registry as dr, entity_registry as er, @@ -47,7 +46,7 @@ from .const import ( DOMAIN, ERROR_STATES, ) -from .helpers import parse_id +from .helpers import NukiWebhookException, parse_id _NukiDeviceT = TypeVar("_NukiDeviceT", bound=NukiDevice) @@ -61,6 +60,87 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp return bridge.locks, bridge.openers +async def _create_webhook( + hass: HomeAssistant, entry: ConfigEntry, bridge: NukiBridge +) -> None: + # Create HomeAssistant webhook + async def handle_webhook( + hass: HomeAssistant, webhook_id: str, request: web.Request + ) -> web.Response: + """Handle webhook callback.""" + try: + data = await request.json() + except ValueError: + return web.Response(status=HTTPStatus.BAD_REQUEST) + + locks = hass.data[DOMAIN][entry.entry_id][DATA_LOCKS] + openers = hass.data[DOMAIN][entry.entry_id][DATA_OPENERS] + + devices = [x for x in locks + openers if x.nuki_id == data["nukiId"]] + if len(devices) == 1: + devices[0].update_from_callback(data) + + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator.async_set_updated_data(None) + + return web.Response(status=HTTPStatus.OK) + + webhook.async_register( + hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=True + ) + + webhook_url = webhook.async_generate_path(entry.entry_id) + + try: + hass_url = get_url( + hass, + allow_cloud=False, + allow_external=False, + allow_ip=True, + require_ssl=False, + ) + except NoURLAvailableError: + webhook.async_unregister(hass, entry.entry_id) + raise NukiWebhookException( + f"Error registering URL for webhook {entry.entry_id}: " + "HomeAssistant URL is not available" + ) from None + + url = f"{hass_url}{webhook_url}" + + if hass_url.startswith("https"): + ir.async_create_issue( + hass, + DOMAIN, + "https_webhook", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="https_webhook", + translation_placeholders={ + "base_url": hass_url, + "network_link": "https://my.home-assistant.io/redirect/network/", + }, + ) + else: + ir.async_delete_issue(hass, DOMAIN, "https_webhook") + + try: + async with async_timeout.timeout(10): + await hass.async_add_executor_job( + _register_webhook, bridge, entry.entry_id, url + ) + except InvalidCredentialsException as err: + webhook.async_unregister(hass, entry.entry_id) + raise NukiWebhookException( + f"Invalid credentials for Bridge: {err}" + ) from err + except RequestException as err: + webhook.async_unregister(hass, entry.entry_id) + raise NukiWebhookException( + f"Error communicating with Bridge: {err}" + ) from err + + def _register_webhook(bridge: NukiBridge, entry_id: str, url: str) -> bool: # Register HA URL as webhook if not already callbacks = bridge.callback_list() @@ -126,79 +206,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=info["versions"]["firmwareVersion"], ) - async def handle_webhook( - hass: HomeAssistant, webhook_id: str, request: web.Request - ) -> web.Response: - """Handle webhook callback.""" - try: - data = await request.json() - except ValueError: - return web.Response(status=HTTPStatus.BAD_REQUEST) - - locks = hass.data[DOMAIN][entry.entry_id][DATA_LOCKS] - openers = hass.data[DOMAIN][entry.entry_id][DATA_OPENERS] - - devices = [x for x in locks + openers if x.nuki_id == data["nukiId"]] - if len(devices) == 1: - devices[0].update_from_callback(data) - - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - coordinator.async_set_updated_data(None) - - return web.Response(status=HTTPStatus.OK) - - webhook.async_register( - hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=True - ) - - webhook_url = webhook.async_generate_path(entry.entry_id) - try: - hass_url = get_url( - hass, - allow_cloud=False, - allow_external=False, - allow_ip=True, - require_ssl=False, - ) - except NoURLAvailableError: - webhook.async_unregister(hass, entry.entry_id) - raise ConfigEntryNotReady( - f"Error registering URL for webhook {entry.entry_id}: " - "HomeAssistant URL is not available" - ) from None - - url = f"{hass_url}{webhook_url}" - - if hass_url.startswith("https"): - ir.async_create_issue( - hass, - DOMAIN, - "https_webhook", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="https_webhook", - translation_placeholders={ - "base_url": hass_url, - "network_link": "https://my.home-assistant.io/redirect/network/", - }, - ) - else: - ir.async_delete_issue(hass, DOMAIN, "https_webhook") - - try: - async with async_timeout.timeout(10): - await hass.async_add_executor_job( - _register_webhook, bridge, entry.entry_id, url - ) - except InvalidCredentialsException as err: - webhook.async_unregister(hass, entry.entry_id) - raise ConfigEntryNotReady(f"Invalid credentials for Bridge: {err}") from err - except RequestException as err: - webhook.async_unregister(hass, entry.entry_id) - raise ConfigEntryNotReady( - f"Error communicating with Bridge: {err}" - ) from err + await _create_webhook(hass, entry, bridge) + except NukiWebhookException as err: + _LOGGER.warning("Error registering HomeAssistant webhook: %s", err) async def _stop_nuki(_: Event): """Stop and remove the Nuki webhook.""" diff --git a/homeassistant/components/nuki/helpers.py b/homeassistant/components/nuki/helpers.py index 45b7420754a..1ba8e393f54 100644 --- a/homeassistant/components/nuki/helpers.py +++ b/homeassistant/components/nuki/helpers.py @@ -13,3 +13,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class NukiWebhookException(exceptions.HomeAssistantError): + """Error to indicate there was an issue with the webhook."""