diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 9ba4809e90d..1c0f97b6a2d 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -54,7 +54,9 @@ class ReolinkHost: ) self.webhook_id: str | None = None - self._webhook_url: str | None = None + self._base_url: str = "" + self._webhook_url: str = "" + self._webhook_reachable: asyncio.Event = asyncio.Event() self._lost_subscription: bool = False @property @@ -138,6 +140,32 @@ class ReolinkHost: await self.subscribe() + _LOGGER.debug( + "Waiting for initial ONVIF state on webhook '%s'", self._webhook_url + ) + try: + await asyncio.wait_for(self._webhook_reachable.wait(), timeout=15) + except asyncio.TimeoutError: + _LOGGER.debug( + "Did not receive initial ONVIF state on webhook '%s' after 15 seconds", + self._webhook_url, + ) + ir.async_create_issue( + self._hass, + DOMAIN, + "webhook_url", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="webhook_url", + translation_placeholders={ + "name": self._api.nvr_name, + "base_url": self._base_url, + "network_link": "https://my.home-assistant.io/redirect/network/", + }, + ) + else: + ir.async_delete_issue(self._hass, DOMAIN, "webhook_url") + if self._api.sw_version_update_required: ir.async_create_issue( self._hass, @@ -287,10 +315,10 @@ class ReolinkHost: ) try: - base_url = get_url(self._hass, prefer_external=False) + self._base_url = get_url(self._hass, prefer_external=False) except NoURLAvailableError: try: - base_url = get_url(self._hass, prefer_external=True) + self._base_url = get_url(self._hass, prefer_external=True) except NoURLAvailableError as err: self.unregister_webhook() raise ReolinkWebhookException( @@ -299,9 +327,9 @@ class ReolinkHost: ) from err webhook_path = webhook.async_generate_path(event_id) - self._webhook_url = f"{base_url}{webhook_path}" + self._webhook_url = f"{self._base_url}{webhook_path}" - if base_url.startswith("https"): + if self._base_url.startswith("https"): ir.async_create_issue( self._hass, DOMAIN, @@ -310,7 +338,7 @@ class ReolinkHost: severity=ir.IssueSeverity.WARNING, translation_key="https_webhook", translation_placeholders={ - "base_url": base_url, + "base_url": self._base_url, "network_link": "https://my.home-assistant.io/redirect/network/", }, ) @@ -337,6 +365,8 @@ class ReolinkHost: """Handle incoming webhook from Reolink for inbound messages and calls.""" _LOGGER.debug("Webhook '%s' called", webhook_id) + if not self._webhook_reachable.is_set(): + self._webhook_reachable.set() if not request.body_exists: _LOGGER.debug("Webhook '%s' triggered without payload", webhook_id) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 74759c12f98..50c561530e5 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -41,7 +41,11 @@ "issues": { "https_webhook": { "title": "Reolink webhook URL uses HTTPS (SSL)", - "description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`" + "description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device" + }, + "webhook_url": { + "title": "Reolink webhook URL unreachable", + "description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL." }, "enable_port": { "title": "Reolink port not enabled", diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 941a1ca7c87..be748ef2c40 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -39,6 +39,8 @@ def reolink_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None with patch( "homeassistant.components.reolink.host.webhook.async_register", return_value=True, + ), patch( + "homeassistant.components.reolink.host.asyncio.Event.wait", AsyncMock() ), patch( "homeassistant.components.reolink.host.Host", autospec=True ) as host_mock_class: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 8849c7d52d3..57d0dbd7cb7 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -1,6 +1,7 @@ """Test the Reolink init.""" +import asyncio from typing import Any -from unittest.mock import AsyncMock, MagicMock, Mock +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from reolink_aio.exceptions import ReolinkError @@ -99,6 +100,7 @@ async def test_no_repair_issue( issue_registry = ir.async_get(hass) assert (const.DOMAIN, "https_webhook") not in issue_registry.issues + assert (const.DOMAIN, "webhook_url") not in issue_registry.issues assert (const.DOMAIN, "enable_port") not in issue_registry.issues assert (const.DOMAIN, "firmware_update") not in issue_registry.issues @@ -138,6 +140,21 @@ async def test_port_repair_issue( assert (const.DOMAIN, "enable_port") in issue_registry.issues +async def test_webhook_repair_issue( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test repairs issue is raised when the webhook url is unreachable.""" + with patch( + "homeassistant.components.reolink.host.asyncio.Event.wait", + AsyncMock(side_effect=asyncio.TimeoutError()), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + issue_registry = ir.async_get(hass) + assert (const.DOMAIN, "webhook_url") in issue_registry.issues + + async def test_firmware_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock ) -> None: