Check webhook url is reachable in Reolink (#89585)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
starkillerOG 2023-03-28 22:46:59 +02:00 committed by GitHub
parent 24d0d15f38
commit e9925f6062
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 8 deletions

View File

@ -54,7 +54,9 @@ class ReolinkHost:
) )
self.webhook_id: str | None = None 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 self._lost_subscription: bool = False
@property @property
@ -138,6 +140,32 @@ class ReolinkHost:
await self.subscribe() 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: if self._api.sw_version_update_required:
ir.async_create_issue( ir.async_create_issue(
self._hass, self._hass,
@ -287,10 +315,10 @@ class ReolinkHost:
) )
try: try:
base_url = get_url(self._hass, prefer_external=False) self._base_url = get_url(self._hass, prefer_external=False)
except NoURLAvailableError: except NoURLAvailableError:
try: try:
base_url = get_url(self._hass, prefer_external=True) self._base_url = get_url(self._hass, prefer_external=True)
except NoURLAvailableError as err: except NoURLAvailableError as err:
self.unregister_webhook() self.unregister_webhook()
raise ReolinkWebhookException( raise ReolinkWebhookException(
@ -299,9 +327,9 @@ class ReolinkHost:
) from err ) from err
webhook_path = webhook.async_generate_path(event_id) 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( ir.async_create_issue(
self._hass, self._hass,
DOMAIN, DOMAIN,
@ -310,7 +338,7 @@ class ReolinkHost:
severity=ir.IssueSeverity.WARNING, severity=ir.IssueSeverity.WARNING,
translation_key="https_webhook", translation_key="https_webhook",
translation_placeholders={ translation_placeholders={
"base_url": base_url, "base_url": self._base_url,
"network_link": "https://my.home-assistant.io/redirect/network/", "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.""" """Handle incoming webhook from Reolink for inbound messages and calls."""
_LOGGER.debug("Webhook '%s' called", webhook_id) _LOGGER.debug("Webhook '%s' called", webhook_id)
if not self._webhook_reachable.is_set():
self._webhook_reachable.set()
if not request.body_exists: if not request.body_exists:
_LOGGER.debug("Webhook '%s' triggered without payload", webhook_id) _LOGGER.debug("Webhook '%s' triggered without payload", webhook_id)

View File

@ -41,7 +41,11 @@
"issues": { "issues": {
"https_webhook": { "https_webhook": {
"title": "Reolink webhook URL uses HTTPS (SSL)", "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": { "enable_port": {
"title": "Reolink port not enabled", "title": "Reolink port not enabled",

View File

@ -39,6 +39,8 @@ def reolink_connect(mock_get_source_ip: None) -> Generator[MagicMock, None, None
with patch( with patch(
"homeassistant.components.reolink.host.webhook.async_register", "homeassistant.components.reolink.host.webhook.async_register",
return_value=True, return_value=True,
), patch(
"homeassistant.components.reolink.host.asyncio.Event.wait", AsyncMock()
), patch( ), patch(
"homeassistant.components.reolink.host.Host", autospec=True "homeassistant.components.reolink.host.Host", autospec=True
) as host_mock_class: ) as host_mock_class:

View File

@ -1,6 +1,7 @@
"""Test the Reolink init.""" """Test the Reolink init."""
import asyncio
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest import pytest
from reolink_aio.exceptions import ReolinkError from reolink_aio.exceptions import ReolinkError
@ -99,6 +100,7 @@ async def test_no_repair_issue(
issue_registry = ir.async_get(hass) issue_registry = ir.async_get(hass)
assert (const.DOMAIN, "https_webhook") not in issue_registry.issues 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, "enable_port") not in issue_registry.issues
assert (const.DOMAIN, "firmware_update") 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 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( async def test_firmware_repair_issue(
hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock hass: HomeAssistant, config_entry: MockConfigEntry, reolink_connect: MagicMock
) -> None: ) -> None: