From b1f2e5f897540967ebef2ccf98026d70009b5c4f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 11 Jun 2022 15:38:43 +1000 Subject: [PATCH] Use create_stream in generic camera config flow (#73237) * Use create_stream in generic camera config flow --- .../components/generic/config_flow.py | 59 ++--- homeassistant/components/generic/strings.json | 10 +- tests/components/generic/conftest.py | 22 +- tests/components/generic/test_camera.py | 218 ++++++++---------- tests/components/generic/test_config_flow.py | 202 +++++++--------- 5 files changed, 220 insertions(+), 291 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 272c7a2d98e..93b34133c63 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations import contextlib from errno import EHOSTUNREACH, EIO -from functools import partial import io import logging from types import MappingProxyType @@ -11,7 +10,6 @@ from typing import Any import PIL from async_timeout import timeout -import av from httpx import HTTPStatusError, RequestError, TimeoutException import voluptuous as vol import yarl @@ -19,9 +17,10 @@ import yarl from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + HLS_PROVIDER, RTSP_TRANSPORTS, SOURCE_TIMEOUT, - convert_stream_options, + create_stream, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( @@ -198,6 +197,11 @@ async def async_test_stream(hass, info) -> dict[str, str]: """Verify that the stream is valid before we create an entity.""" if not (stream_source := info.get(CONF_STREAM_SOURCE)): return {} + # Import from stream.worker as stream cannot reexport from worker + # without forcing the av dependency on default_config + # pylint: disable=import-outside-toplevel + from homeassistant.components.stream.worker import StreamWorkerError + if not isinstance(stream_source, template_helper.Template): stream_source = template_helper.Template(stream_source, hass) try: @@ -205,42 +209,21 @@ async def async_test_stream(hass, info) -> dict[str, str]: except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", stream_source, err) return {CONF_STREAM_SOURCE: "template_error"} + stream_options: dict[str, bool | str] = {} + if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): + stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport + if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True try: - # For RTSP streams, prefer TCP. This code is duplicated from - # homeassistant.components.stream.__init__.py:create_stream() - # It may be possible & better to call create_stream() directly. - stream_options: dict[str, bool | str] = {} - if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): - stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport - if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): - stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True - pyav_options = convert_stream_options(stream_options) - if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": - pyav_options = { - "rtsp_flags": "prefer_tcp", - "stimeout": "5000000", - **pyav_options, - } - _LOGGER.debug("Attempting to open stream %s", stream_source) - container = await hass.async_add_executor_job( - partial( - av.open, - stream_source, - options=pyav_options, - timeout=SOURCE_TIMEOUT, - ) - ) - _ = container.streams.video[0] - except (av.error.FileNotFoundError): # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "stream_file_not_found"} - except (av.error.HTTPNotFoundError): # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "stream_http_not_found"} - except (av.error.TimeoutError): # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "timeout"} - except av.error.HTTPUnauthorizedError: # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "stream_unauthorised"} - except (KeyError, IndexError): - return {CONF_STREAM_SOURCE: "stream_no_video"} + stream = create_stream(hass, stream_source, stream_options, "test_stream") + hls_provider = stream.add_provider(HLS_PROVIDER) + await stream.start() + if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT): + hass.async_create_task(stream.stop()) + return {CONF_STREAM_SOURCE: "timeout"} + await stream.stop() + except StreamWorkerError as err: + return {CONF_STREAM_SOURCE: str(err)} except PermissionError: return {CONF_STREAM_SOURCE: "stream_not_permitted"} except OSError as err: diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 6b73c70cf3d..7cb1135fe56 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -12,9 +12,7 @@ "timeout": "Timeout while loading URL", "stream_no_route_to_host": "Could not find host while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", - "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_no_video": "Stream has no video" + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?" }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", @@ -78,15 +76,11 @@ "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", - "stream_file_not_found": "[%key:component::generic::config::error::stream_file_not_found%]", - "stream_http_not_found": "[%key:component::generic::config::error::stream_http_not_found%]", "template_error": "[%key:component::generic::config::error::template_error%]", "timeout": "[%key:component::generic::config::error::timeout%]", "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", "stream_io_error": "[%key:component::generic::config::error::stream_io_error%]", - "stream_unauthorised": "[%key:component::generic::config::error::stream_unauthorised%]", - "stream_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]", - "stream_no_video": "[%key:component::generic::config::error::stream_no_video%]" + "stream_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]" } } } diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index dc5c545869b..266b848ebe2 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -1,7 +1,7 @@ """Test fixtures for the generic component.""" from io import BytesIO -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from PIL import Image import pytest @@ -59,14 +59,20 @@ def fakeimg_gif(fakeimgbytes_gif): @pytest.fixture(scope="package") -def mock_av_open(): - """Fake container object with .streams.video[0] != None.""" - fake = Mock() - fake.streams.video = ["fakevid"] - return patch( - "homeassistant.components.generic.config_flow.av.open", - return_value=fake, +def mock_create_stream(): + """Mock create stream.""" + mock_stream = Mock() + mock_provider = Mock() + mock_provider.part_recv = AsyncMock() + mock_provider.part_recv.return_value = True + mock_stream.add_provider.return_value = mock_provider + mock_stream.start = AsyncMock() + mock_stream.stop = AsyncMock() + fake_create_stream = patch( + "homeassistant.components.generic.config_flow.create_stream", + return_value=mock_stream, ) + return fake_create_stream @pytest.fixture diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 6e8b804f848..ec0d89eb0eb 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -17,26 +17,25 @@ from tests.common import AsyncMock, Mock @respx.mock -async def test_fetching_url(hass, hass_client, fakeimgbytes_png, mock_av_open): +async def test_fetching_url(hass, hass_client, fakeimgbytes_png): """Test that it fetches the given url.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - with mock_av_open: - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "username": "user", - "password": "pass", - "authentication": "basic", - } - }, - ) - await hass.async_block_till_done() + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "username": "user", + "password": "pass", + "authentication": "basic", + } + }, + ) + await hass.async_block_till_done() client = await hass_client() @@ -179,30 +178,27 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j @respx.mock -async def test_stream_source( - hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open -): +async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png): """Test that the stream source is rendered.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) hass.states.async_set("sensor.temp", "0") - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + }, + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() hass.states.async_set("sensor.temp", "5") @@ -227,29 +223,26 @@ async def test_stream_source( @respx.mock -async def test_stream_source_error( - hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open -): +async def test_stream_source_error(hass, hass_client, hass_ws_client, fakeimgbytes_png): """Test that the stream source has an error.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - # Does not exist - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + # Does not exist + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + }, + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() with patch( "homeassistant.components.camera.Stream.endpoint_url", @@ -275,30 +268,27 @@ async def test_stream_source_error( @respx.mock -async def test_setup_alternative_options( - hass, hass_ws_client, fakeimgbytes_png, mock_av_open -): +async def test_setup_alternative_options(hass, hass_ws_client, fakeimgbytes_png): """Test that the stream source is setup with different config options.""" respx.get("https://example.com").respond(stream=fakeimgbytes_png) - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - "authentication": "digest", - "username": "user", - "password": "pass", - "stream_source": "rtsp://example.com:554/rtsp/", - "rtsp_transport": "udp", - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "https://example.com", + "authentication": "digest", + "username": "user", + "password": "pass", + "stream_source": "rtsp://example.com:554/rtsp/", + "rtsp_transport": "udp", }, - ) - await hass.async_block_till_done() + }, + ) + await hass.async_block_till_done() assert hass.states.get("camera.config_test") @@ -346,7 +336,7 @@ async def test_no_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_ @respx.mock async def test_camera_content_type( - hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg, mock_av_open + hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg ): """Test generic camera with custom content_type.""" urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" @@ -372,20 +362,18 @@ async def test_camera_content_type( "verify_ssl": True, } - with mock_av_open: - result1 = await hass.config_entries.flow.async_init( - "generic", - data=cam_config_jpg, - context={"source": SOURCE_IMPORT, "unique_id": 12345}, - ) - await hass.async_block_till_done() - with mock_av_open: - result2 = await hass.config_entries.flow.async_init( - "generic", - data=cam_config_svg, - context={"source": SOURCE_IMPORT, "unique_id": 54321}, - ) - await hass.async_block_till_done() + result1 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_jpg, + context={"source": SOURCE_IMPORT, "unique_id": 12345}, + ) + await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_svg, + context={"source": SOURCE_IMPORT, "unique_id": 54321}, + ) + await hass.async_block_till_done() assert result1["type"] == "create_entry" assert result2["type"] == "create_entry" @@ -457,21 +445,20 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt assert await resp.read() == fakeimgbytes_png -async def test_no_still_image_url(hass, hass_client, mock_av_open): +async def test_no_still_image_url(hass, hass_client): """Test that the component can grab images from stream with no still_image_url.""" - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", }, - ) - await hass.async_block_till_done() + }, + ) + await hass.async_block_till_done() client = await hass_client() @@ -503,23 +490,22 @@ async def test_no_still_image_url(hass, hass_client, mock_av_open): assert await resp.read() == b"stream_keyframe_image" -async def test_frame_interval_property(hass, mock_av_open): +async def test_frame_interval_property(hass): """Test that the frame interval is calculated and returned correctly.""" - with mock_av_open: - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", - "framerate": 5, - }, + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + "framerate": 5, }, - ) - await hass.async_block_till_done() + }, + ) + await hass.async_block_till_done() request = Mock() with patch( diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index a525619d962..ee12056b191 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -2,9 +2,8 @@ import errno import os.path -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -import av import httpx import pytest import respx @@ -23,6 +22,7 @@ from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) +from homeassistant.components.stream.worker import StreamWorkerError from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -57,10 +57,10 @@ TESTDATA_YAML = { @respx.mock -async def test_form(hass, fakeimg_png, mock_av_open, user_flow): +async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): """Test the form with a normal set of settings.""" - with mock_av_open as mock_setup: + with mock_create_stream as mock_setup: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -211,12 +211,12 @@ async def test_still_template( @respx.mock -async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): +async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): """Test we complete ok if the user enters a stream url.""" - with mock_av_open as mock_setup: - data = TESTDATA.copy() - data[CONF_RTSP_TRANSPORT] = "tcp" - data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" + data = TESTDATA.copy() + data[CONF_RTSP_TRANSPORT] = "tcp" + data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" + with mock_create_stream as mock_setup: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) @@ -240,7 +240,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): assert len(mock_setup.mock_calls) == 1 -async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): +async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): """Test we complete ok if the user wants stream only.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -249,7 +249,7 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): data = TESTDATA.copy() data.pop(CONF_STILL_IMAGE_URL) data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2" - with mock_av_open as mock_setup: + with mock_create_stream as mock_setup: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], data, @@ -294,13 +294,13 @@ async def test_form_still_and_stream_not_provided(hass, user_flow): @respx.mock -async def test_form_image_timeout(hass, mock_av_open, user_flow): +async def test_form_image_timeout(hass, user_flow, mock_create_stream): """Test we handle invalid image timeout.""" respx.get("http://127.0.0.1/testurl/1").side_effect = [ httpx.TimeoutException, ] - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -312,10 +312,10 @@ async def test_form_image_timeout(hass, mock_av_open, user_flow): @respx.mock -async def test_form_stream_invalidimage(hass, mock_av_open, user_flow): +async def test_form_stream_invalidimage(hass, user_flow, mock_create_stream): """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -327,10 +327,10 @@ async def test_form_stream_invalidimage(hass, mock_av_open, user_flow): @respx.mock -async def test_form_stream_invalidimage2(hass, mock_av_open, user_flow): +async def test_form_stream_invalidimage2(hass, user_flow, mock_create_stream): """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(content=None) - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -342,10 +342,10 @@ async def test_form_stream_invalidimage2(hass, mock_av_open, user_flow): @respx.mock -async def test_form_stream_invalidimage3(hass, mock_av_open, user_flow): +async def test_form_stream_invalidimage3(hass, user_flow, mock_create_stream): """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(content=bytes([0xFF])) - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -356,43 +356,17 @@ async def test_form_stream_invalidimage3(hass, mock_av_open, user_flow): assert result2["errors"] == {"still_image_url": "invalid_still_image"} -@respx.mock -async def test_form_stream_file_not_found(hass, fakeimg_png, user_flow): - """Test we handle file not found.""" - with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.FileNotFoundError(0, 0), - ): - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - TESTDATA, - ) - assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_file_not_found"} - - -@respx.mock -async def test_form_stream_http_not_found(hass, fakeimg_png, user_flow): - """Test we handle invalid auth.""" - with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.HTTPNotFoundError(0, 0), - ): - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - TESTDATA, - ) - assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_http_not_found"} - - @respx.mock async def test_form_stream_timeout(hass, fakeimg_png, user_flow): """Test we handle invalid auth.""" with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.TimeoutError(0, 0), - ): + "homeassistant.components.generic.config_flow.create_stream" + ) as create_stream: + create_stream.return_value.start = AsyncMock() + create_stream.return_value.add_provider.return_value.part_recv = AsyncMock() + create_stream.return_value.add_provider.return_value.part_recv.return_value = ( + False + ) result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -402,32 +376,18 @@ async def test_form_stream_timeout(hass, fakeimg_png, user_flow): @respx.mock -async def test_form_stream_unauthorised(hass, fakeimg_png, user_flow): - """Test we handle invalid auth.""" +async def test_form_stream_worker_error(hass, fakeimg_png, user_flow): + """Test we handle a StreamWorkerError and pass the message through.""" with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.HTTPUnauthorizedError(0, 0), + "homeassistant.components.generic.config_flow.create_stream", + side_effect=StreamWorkerError("Some message"), ): result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, ) assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_unauthorised"} - - -@respx.mock -async def test_form_stream_novideo(hass, fakeimg_png, user_flow): - """Test we handle invalid stream.""" - with patch( - "homeassistant.components.generic.config_flow.av.open", side_effect=KeyError() - ): - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - TESTDATA, - ) - assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_no_video"} + assert result2["errors"] == {"stream_source": "Some message"} @respx.mock @@ -435,7 +395,7 @@ async def test_form_stream_permission_error(hass, fakeimgbytes_png, user_flow): """Test we handle permission error.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=PermissionError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -450,7 +410,7 @@ async def test_form_stream_permission_error(hass, fakeimgbytes_png, user_flow): async def test_form_no_route_to_host(hass, fakeimg_png, user_flow): """Test we handle no route to host.""" with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=OSError(errno.EHOSTUNREACH, "No route to host"), ): result2 = await hass.config_entries.flow.async_configure( @@ -465,7 +425,7 @@ async def test_form_no_route_to_host(hass, fakeimg_png, user_flow): async def test_form_stream_io_error(hass, fakeimg_png, user_flow): """Test we handle no io error when setting up stream.""" with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=OSError(errno.EIO, "Input/output error"), ): result2 = await hass.config_entries.flow.async_configure( @@ -480,7 +440,7 @@ async def test_form_stream_io_error(hass, fakeimg_png, user_flow): async def test_form_oserror(hass, fakeimg_png, user_flow): """Test we handle OS error when setting up stream.""" with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=OSError("Some other OSError"), ), pytest.raises(OSError): await hass.config_entries.flow.async_configure( @@ -490,7 +450,7 @@ async def test_form_oserror(hass, fakeimg_png, user_flow): @respx.mock -async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): +async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream): """Test the options flow with a template error.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) @@ -503,18 +463,18 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): options=TESTDATA, ) - with mock_av_open: - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" - # try updating the still image url - data = TESTDATA.copy() - data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2" + # try updating the still image url + data = TESTDATA.copy() + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2" + with mock_create_stream: result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input=data, @@ -541,12 +501,12 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): result4["flow_id"], user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM - assert result5["errors"] == {"stream_source": "template_error"} + assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5["errors"] == {"stream_source": "template_error"} @respx.mock -async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open): +async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): """Test the options flow without a still_image_url.""" respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) data = TESTDATA.copy() @@ -558,36 +518,35 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open): data={}, options=data, ) - with mock_av_open: - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" - # try updating the config options + # try updating the config options + with mock_create_stream: result3 = await hass.config_entries.options.async_configure( result["flow_id"], user_input=data, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" # These below can be deleted after deprecation period is finished. @respx.mock -async def test_import(hass, fakeimg_png, mock_av_open): +async def test_import(hass, fakeimg_png): """Test configuration.yaml import used during migration.""" - with mock_av_open: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML - ) - # duplicate import should be aborted - result2 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + # duplicate import should be aborted + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Yaml Defined Name" await hass.async_block_till_done() @@ -599,7 +558,7 @@ async def test_import(hass, fakeimg_png, mock_av_open): # These above can be deleted after deprecation period is finished. -async def test_unload_entry(hass, fakeimg_png, mock_av_open): +async def test_unload_entry(hass, fakeimg_png): """Test unloading the generic IP Camera entry.""" mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA) mock_entry.add_to_hass(hass) @@ -669,7 +628,9 @@ async def test_migrate_existing_ids(hass) -> None: @respx.mock -async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_open): +async def test_use_wallclock_as_timestamps_option( + hass, fakeimg_png, mock_create_stream +): """Test the use_wallclock_as_timestamps option flow.""" mock_entry = MockConfigEntry( @@ -679,19 +640,18 @@ async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_ope options=TESTDATA, ) - with mock_av_open: - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init( - mock_entry.entry_id, context={"show_advanced_options": True} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init( + mock_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + with mock_create_stream: result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY