Fix ONVIF setup being cancelled by a snapshot that never completes (#182628)

This commit is contained in:
Franck Nijhof
2026-09-29 00:29:25 +02:00
committed by GitHub
parent f83224fb9b
commit 3e98d8595c
3 changed files with 51 additions and 2 deletions
+6 -1
View File
@@ -26,6 +26,7 @@ from .const import (
CONF_SNAPSHOT_AUTH,
DEFAULT_ARGUMENTS,
DEFAULT_ENABLE_WEBHOOKS,
SNAPSHOT_TIMEOUT,
)
from .device import ONVIFConfigEntry, ONVIFDevice
@@ -131,7 +132,11 @@ async def _get_snapshot_auth(device: ONVIFDevice) -> str | None:
for basic_auth in (False, True):
method = HTTP_BASIC_AUTHENTICATION if basic_auth else HTTP_DIGEST_AUTHENTICATION
with suppress(ONVIFError):
if await device.device.get_snapshot(device.profiles[0].token, basic_auth):
if await device.device.get_snapshot(
device.profiles[0].token,
basic_auth,
timeout=SNAPSHOT_TIMEOUT.total_seconds(),
):
return method
return None
+5
View File
@@ -1,6 +1,7 @@
"""Constants for the onvif component."""
import asyncio
from datetime import timedelta
import logging
import aiohttp
@@ -21,6 +22,10 @@ CONF_ENABLE_WEBHOOKS = "enable_webhooks"
CONF_MORE_OPTIONS = "more_options"
DEFAULT_ENABLE_WEBHOOKS = True
# Bounds each snapshot auth probe during setup, tighter than the library's
# default so a camera whose snapshot never completes does not stall setup
SNAPSHOT_TIMEOUT = timedelta(seconds=10)
ATTR_PAN = "pan"
ATTR_TILT = "tilt"
ATTR_ZOOM = "zoom"
+40 -1
View File
@@ -1,8 +1,11 @@
"""Tests for the ONVIF integration __init__ module."""
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from homeassistant.components.onvif.const import DOMAIN
from onvif.exceptions import ONVIFTimeoutError
from homeassistant.components.onvif.const import CONF_SNAPSHOT_AUTH, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_HOST,
@@ -145,3 +148,39 @@ async def test_setup_entry(hass: HomeAssistant) -> None:
mock_onvif_camera_cls.assert_called_once()
host, port, username, password = mock_onvif_camera_cls.call_args.args[:4]
assert (host, port, username, password) == (HOST, PORT, USERNAME, PASSWORD)
async def test_setup_entry_snapshot_probe_timeout(hass: HomeAssistant) -> None:
"""Test a snapshot probe that times out does not hold up setup."""
entry = MockConfigEntry(
domain=DOMAIN,
title=NAME,
unique_id=MAC,
data={
CONF_NAME: NAME,
CONF_HOST: HOST,
CONF_PORT: PORT,
CONF_USERNAME: USERNAME,
CONF_PASSWORD: PASSWORD,
},
)
entry.add_to_hass(hass)
with patch("homeassistant.components.onvif.device.ONVIFCamera") as mock_camera_cls:
setup_mock_onvif_camera(mock_camera_cls, with_full_setup=True)
media_service = mock_camera_cls.create_media_service.return_value
media_service.GetServiceCapabilities.return_value = SimpleNamespace(
SnapshotUri=True
)
mock_camera_cls.get_snapshot.side_effect = ONVIFTimeoutError("Timed out")
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
# both auth methods are probed, each with the tighter setup timeout
assert [
call.kwargs["timeout"] for call in mock_camera_cls.get_snapshot.call_args_list
] == [10, 10]
# nothing learned about the snapshot auth, so setup will probe again next time
assert CONF_SNAPSHOT_AUTH not in entry.data