Use create_stream in generic camera config flow (#73237)

* Use create_stream in generic camera config flow
This commit is contained in:
uvjustin 2022-06-11 15:38:43 +10:00 committed by GitHub
parent 21cfbe875e
commit b1f2e5f897
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 220 additions and 291 deletions

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import contextlib import contextlib
from errno import EHOSTUNREACH, EIO from errno import EHOSTUNREACH, EIO
from functools import partial
import io import io
import logging import logging
from types import MappingProxyType from types import MappingProxyType
@ -11,7 +10,6 @@ from typing import Any
import PIL import PIL
from async_timeout import timeout from async_timeout import timeout
import av
from httpx import HTTPStatusError, RequestError, TimeoutException from httpx import HTTPStatusError, RequestError, TimeoutException
import voluptuous as vol import voluptuous as vol
import yarl import yarl
@ -19,9 +17,10 @@ import yarl
from homeassistant.components.stream import ( from homeassistant.components.stream import (
CONF_RTSP_TRANSPORT, CONF_RTSP_TRANSPORT,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS, CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
HLS_PROVIDER,
RTSP_TRANSPORTS, RTSP_TRANSPORTS,
SOURCE_TIMEOUT, SOURCE_TIMEOUT,
convert_stream_options, create_stream,
) )
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import ( 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.""" """Verify that the stream is valid before we create an entity."""
if not (stream_source := info.get(CONF_STREAM_SOURCE)): if not (stream_source := info.get(CONF_STREAM_SOURCE)):
return {} 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): if not isinstance(stream_source, template_helper.Template):
stream_source = template_helper.Template(stream_source, hass) stream_source = template_helper.Template(stream_source, hass)
try: try:
@ -205,42 +209,21 @@ async def async_test_stream(hass, info) -> dict[str, str]:
except TemplateError as err: except TemplateError as err:
_LOGGER.warning("Problem rendering template %s: %s", stream_source, err) _LOGGER.warning("Problem rendering template %s: %s", stream_source, err)
return {CONF_STREAM_SOURCE: "template_error"} 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: try:
# For RTSP streams, prefer TCP. This code is duplicated from stream = create_stream(hass, stream_source, stream_options, "test_stream")
# homeassistant.components.stream.__init__.py:create_stream() hls_provider = stream.add_provider(HLS_PROVIDER)
# It may be possible & better to call create_stream() directly. await stream.start()
stream_options: dict[str, bool | str] = {} if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT):
if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): hass.async_create_task(stream.stop())
stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport return {CONF_STREAM_SOURCE: "timeout"}
if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): await stream.stop()
stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True except StreamWorkerError as err:
pyav_options = convert_stream_options(stream_options) return {CONF_STREAM_SOURCE: str(err)}
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"}
except PermissionError: except PermissionError:
return {CONF_STREAM_SOURCE: "stream_not_permitted"} return {CONF_STREAM_SOURCE: "stream_not_permitted"}
except OSError as err: except OSError as err:

View File

@ -12,9 +12,7 @@
"timeout": "Timeout while loading URL", "timeout": "Timeout while loading URL",
"stream_no_route_to_host": "Could not find host while trying to connect to stream", "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_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_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?",
"stream_no_video": "Stream has no video"
}, },
"abort": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "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%]", "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%]", "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%]", "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%]", "template_error": "[%key:component::generic::config::error::template_error%]",
"timeout": "[%key:component::generic::config::error::timeout%]", "timeout": "[%key:component::generic::config::error::timeout%]",
"stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", "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_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_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]",
"stream_no_video": "[%key:component::generic::config::error::stream_no_video%]"
} }
} }
} }

View File

@ -1,7 +1,7 @@
"""Test fixtures for the generic component.""" """Test fixtures for the generic component."""
from io import BytesIO from io import BytesIO
from unittest.mock import Mock, patch from unittest.mock import AsyncMock, Mock, patch
from PIL import Image from PIL import Image
import pytest import pytest
@ -59,14 +59,20 @@ def fakeimg_gif(fakeimgbytes_gif):
@pytest.fixture(scope="package") @pytest.fixture(scope="package")
def mock_av_open(): def mock_create_stream():
"""Fake container object with .streams.video[0] != None.""" """Mock create stream."""
fake = Mock() mock_stream = Mock()
fake.streams.video = ["fakevid"] mock_provider = Mock()
return patch( mock_provider.part_recv = AsyncMock()
"homeassistant.components.generic.config_flow.av.open", mock_provider.part_recv.return_value = True
return_value=fake, 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 @pytest.fixture

View File

@ -17,26 +17,25 @@ from tests.common import AsyncMock, Mock
@respx.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.""" """Test that it fetches the given url."""
respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com").respond(stream=fakeimgbytes_png)
with mock_av_open: await async_setup_component(
await async_setup_component( hass,
hass, "camera",
"camera", {
{ "camera": {
"camera": { "name": "config_test",
"name": "config_test", "platform": "generic",
"platform": "generic", "still_image_url": "http://example.com",
"still_image_url": "http://example.com", "username": "user",
"username": "user", "password": "pass",
"password": "pass", "authentication": "basic",
"authentication": "basic", }
} },
}, )
) await hass.async_block_till_done()
await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
@ -179,30 +178,27 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j
@respx.mock @respx.mock
async def test_stream_source( async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png):
hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open
):
"""Test that the stream source is rendered.""" """Test that the stream source is rendered."""
respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com").respond(stream=fakeimgbytes_png)
respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png)
hass.states.async_set("sensor.temp", "0") hass.states.async_set("sensor.temp", "0")
with mock_av_open: assert await async_setup_component(
assert await async_setup_component( hass,
hass, "camera",
"camera", {
{ "camera": {
"camera": { "name": "config_test",
"name": "config_test", "platform": "generic",
"platform": "generic", "still_image_url": "http://example.com",
"still_image_url": "http://example.com", "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
"stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', "limit_refetch_to_url_change": True,
"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") hass.states.async_set("sensor.temp", "5")
@ -227,29 +223,26 @@ async def test_stream_source(
@respx.mock @respx.mock
async def test_stream_source_error( async def test_stream_source_error(hass, hass_client, hass_ws_client, fakeimgbytes_png):
hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open
):
"""Test that the stream source has an error.""" """Test that the stream source has an error."""
respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com").respond(stream=fakeimgbytes_png)
with mock_av_open: assert await async_setup_component(
assert await async_setup_component( hass,
hass, "camera",
"camera", {
{ "camera": {
"camera": { "name": "config_test",
"name": "config_test", "platform": "generic",
"platform": "generic", "still_image_url": "http://example.com",
"still_image_url": "http://example.com", # Does not exist
# Does not exist "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}',
"stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', "limit_refetch_to_url_change": True,
"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( with patch(
"homeassistant.components.camera.Stream.endpoint_url", "homeassistant.components.camera.Stream.endpoint_url",
@ -275,30 +268,27 @@ async def test_stream_source_error(
@respx.mock @respx.mock
async def test_setup_alternative_options( async def test_setup_alternative_options(hass, hass_ws_client, fakeimgbytes_png):
hass, hass_ws_client, fakeimgbytes_png, mock_av_open
):
"""Test that the stream source is setup with different config options.""" """Test that the stream source is setup with different config options."""
respx.get("https://example.com").respond(stream=fakeimgbytes_png) respx.get("https://example.com").respond(stream=fakeimgbytes_png)
with mock_av_open: assert await async_setup_component(
assert await async_setup_component( hass,
hass, "camera",
"camera", {
{ "camera": {
"camera": { "name": "config_test",
"name": "config_test", "platform": "generic",
"platform": "generic", "still_image_url": "https://example.com",
"still_image_url": "https://example.com", "authentication": "digest",
"authentication": "digest", "username": "user",
"username": "user", "password": "pass",
"password": "pass", "stream_source": "rtsp://example.com:554/rtsp/",
"stream_source": "rtsp://example.com:554/rtsp/", "rtsp_transport": "udp",
"rtsp_transport": "udp",
},
}, },
) },
await hass.async_block_till_done() )
await hass.async_block_till_done()
assert hass.states.get("camera.config_test") 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 @respx.mock
async def test_camera_content_type( 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.""" """Test generic camera with custom content_type."""
urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" 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, "verify_ssl": True,
} }
with mock_av_open: result1 = await hass.config_entries.flow.async_init(
result1 = await hass.config_entries.flow.async_init( "generic",
"generic", data=cam_config_jpg,
data=cam_config_jpg, context={"source": SOURCE_IMPORT, "unique_id": 12345},
context={"source": SOURCE_IMPORT, "unique_id": 12345}, )
) await hass.async_block_till_done()
await hass.async_block_till_done() result2 = await hass.config_entries.flow.async_init(
with mock_av_open: "generic",
result2 = await hass.config_entries.flow.async_init( data=cam_config_svg,
"generic", context={"source": SOURCE_IMPORT, "unique_id": 54321},
data=cam_config_svg, )
context={"source": SOURCE_IMPORT, "unique_id": 54321}, await hass.async_block_till_done()
)
await hass.async_block_till_done()
assert result1["type"] == "create_entry" assert result1["type"] == "create_entry"
assert result2["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 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.""" """Test that the component can grab images from stream with no still_image_url."""
with mock_av_open: assert await async_setup_component(
assert await async_setup_component( hass,
hass, "camera",
"camera", {
{ "camera": {
"camera": { "name": "config_test",
"name": "config_test", "platform": "generic",
"platform": "generic", "stream_source": "rtsp://example.com:554/rtsp/",
"stream_source": "rtsp://example.com:554/rtsp/",
},
}, },
) },
await hass.async_block_till_done() )
await hass.async_block_till_done()
client = await hass_client() 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" 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.""" """Test that the frame interval is calculated and returned correctly."""
with mock_av_open: await async_setup_component(
await async_setup_component( hass,
hass, "camera",
"camera", {
{ "camera": {
"camera": { "name": "config_test",
"name": "config_test", "platform": "generic",
"platform": "generic", "stream_source": "rtsp://example.com:554/rtsp/",
"stream_source": "rtsp://example.com:554/rtsp/", "framerate": 5,
"framerate": 5,
},
}, },
) },
await hass.async_block_till_done() )
await hass.async_block_till_done()
request = Mock() request = Mock()
with patch( with patch(

View File

@ -2,9 +2,8 @@
import errno import errno
import os.path import os.path
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import av
import httpx import httpx
import pytest import pytest
import respx import respx
@ -23,6 +22,7 @@ from homeassistant.components.stream import (
CONF_RTSP_TRANSPORT, CONF_RTSP_TRANSPORT,
CONF_USE_WALLCLOCK_AS_TIMESTAMPS, CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
) )
from homeassistant.components.stream.worker import StreamWorkerError
from homeassistant.const import ( from homeassistant.const import (
CONF_AUTHENTICATION, CONF_AUTHENTICATION,
CONF_NAME, CONF_NAME,
@ -57,10 +57,10 @@ TESTDATA_YAML = {
@respx.mock @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.""" """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( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], user_flow["flow_id"],
TESTDATA, TESTDATA,
@ -211,12 +211,12 @@ async def test_still_template(
@respx.mock @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.""" """Test we complete ok if the user enters a stream url."""
with mock_av_open as mock_setup: data = TESTDATA.copy()
data = TESTDATA.copy() data[CONF_RTSP_TRANSPORT] = "tcp"
data[CONF_RTSP_TRANSPORT] = "tcp" data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2"
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( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], data 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 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.""" """Test we complete ok if the user wants stream only."""
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init( 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 = TESTDATA.copy()
data.pop(CONF_STILL_IMAGE_URL) data.pop(CONF_STILL_IMAGE_URL)
data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2" 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( result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
data, data,
@ -294,13 +294,13 @@ async def test_form_still_and_stream_not_provided(hass, user_flow):
@respx.mock @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.""" """Test we handle invalid image timeout."""
respx.get("http://127.0.0.1/testurl/1").side_effect = [ respx.get("http://127.0.0.1/testurl/1").side_effect = [
httpx.TimeoutException, httpx.TimeoutException,
] ]
with mock_av_open: with mock_create_stream:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], user_flow["flow_id"],
TESTDATA, TESTDATA,
@ -312,10 +312,10 @@ async def test_form_image_timeout(hass, mock_av_open, user_flow):
@respx.mock @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.""" """Test we handle invalid image when a stream is specified."""
respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") 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( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], user_flow["flow_id"],
TESTDATA, TESTDATA,
@ -327,10 +327,10 @@ async def test_form_stream_invalidimage(hass, mock_av_open, user_flow):
@respx.mock @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.""" """Test we handle invalid image when a stream is specified."""
respx.get("http://127.0.0.1/testurl/1").respond(content=None) 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( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], user_flow["flow_id"],
TESTDATA, TESTDATA,
@ -342,10 +342,10 @@ async def test_form_stream_invalidimage2(hass, mock_av_open, user_flow):
@respx.mock @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.""" """Test we handle invalid image when a stream is specified."""
respx.get("http://127.0.0.1/testurl/1").respond(content=bytes([0xFF])) 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( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], user_flow["flow_id"],
TESTDATA, 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"} 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 @respx.mock
async def test_form_stream_timeout(hass, fakeimg_png, user_flow): async def test_form_stream_timeout(hass, fakeimg_png, user_flow):
"""Test we handle invalid auth.""" """Test we handle invalid auth."""
with patch( with patch(
"homeassistant.components.generic.config_flow.av.open", "homeassistant.components.generic.config_flow.create_stream"
side_effect=av.error.TimeoutError(0, 0), ) 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( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], user_flow["flow_id"],
TESTDATA, TESTDATA,
@ -402,32 +376,18 @@ async def test_form_stream_timeout(hass, fakeimg_png, user_flow):
@respx.mock @respx.mock
async def test_form_stream_unauthorised(hass, fakeimg_png, user_flow): async def test_form_stream_worker_error(hass, fakeimg_png, user_flow):
"""Test we handle invalid auth.""" """Test we handle a StreamWorkerError and pass the message through."""
with patch( with patch(
"homeassistant.components.generic.config_flow.av.open", "homeassistant.components.generic.config_flow.create_stream",
side_effect=av.error.HTTPUnauthorizedError(0, 0), side_effect=StreamWorkerError("Some message"),
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
user_flow["flow_id"], user_flow["flow_id"],
TESTDATA, TESTDATA,
) )
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["errors"] == {"stream_source": "stream_unauthorised"} assert result2["errors"] == {"stream_source": "Some message"}
@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"}
@respx.mock @respx.mock
@ -435,7 +395,7 @@ async def test_form_stream_permission_error(hass, fakeimgbytes_png, user_flow):
"""Test we handle permission error.""" """Test we handle permission error."""
respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png)
with patch( with patch(
"homeassistant.components.generic.config_flow.av.open", "homeassistant.components.generic.config_flow.create_stream",
side_effect=PermissionError(), side_effect=PermissionError(),
): ):
result2 = await hass.config_entries.flow.async_configure( 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): async def test_form_no_route_to_host(hass, fakeimg_png, user_flow):
"""Test we handle no route to host.""" """Test we handle no route to host."""
with patch( 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"), side_effect=OSError(errno.EHOSTUNREACH, "No route to host"),
): ):
result2 = await hass.config_entries.flow.async_configure( 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): async def test_form_stream_io_error(hass, fakeimg_png, user_flow):
"""Test we handle no io error when setting up stream.""" """Test we handle no io error when setting up stream."""
with patch( with patch(
"homeassistant.components.generic.config_flow.av.open", "homeassistant.components.generic.config_flow.create_stream",
side_effect=OSError(errno.EIO, "Input/output error"), side_effect=OSError(errno.EIO, "Input/output error"),
): ):
result2 = await hass.config_entries.flow.async_configure( 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): async def test_form_oserror(hass, fakeimg_png, user_flow):
"""Test we handle OS error when setting up stream.""" """Test we handle OS error when setting up stream."""
with patch( with patch(
"homeassistant.components.generic.config_flow.av.open", "homeassistant.components.generic.config_flow.create_stream",
side_effect=OSError("Some other OSError"), side_effect=OSError("Some other OSError"),
), pytest.raises(OSError): ), pytest.raises(OSError):
await hass.config_entries.flow.async_configure( await hass.config_entries.flow.async_configure(
@ -490,7 +450,7 @@ async def test_form_oserror(hass, fakeimg_png, user_flow):
@respx.mock @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.""" """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/1").respond(stream=fakeimgbytes_png)
respx.get("http://127.0.0.1/testurl/2").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, options=TESTDATA,
) )
with mock_av_open: mock_entry.add_to_hass(hass)
mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done()
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(mock_entry.entry_id) result = await hass.config_entries.options.async_init(mock_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init" assert result["step_id"] == "init"
# try updating the still image url # try updating the still image url
data = TESTDATA.copy() data = TESTDATA.copy()
data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2" data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2"
with mock_create_stream:
result2 = await hass.config_entries.options.async_configure( result2 = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input=data, user_input=data,
@ -541,12 +501,12 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open):
result4["flow_id"], result4["flow_id"],
user_input=data, user_input=data,
) )
assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM
assert result5["errors"] == {"stream_source": "template_error"} assert result5["errors"] == {"stream_source": "template_error"}
@respx.mock @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.""" """Test the options flow without a still_image_url."""
respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png)
data = TESTDATA.copy() data = TESTDATA.copy()
@ -558,36 +518,35 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open):
data={}, data={},
options=data, options=data,
) )
with mock_av_open: mock_entry.add_to_hass(hass)
mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done()
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(mock_entry.entry_id) result = await hass.config_entries.options.async_init(mock_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init" 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( result3 = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input=data, user_input=data,
) )
assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg"
# These below can be deleted after deprecation period is finished. # These below can be deleted after deprecation period is finished.
@respx.mock @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.""" """Test configuration.yaml import used during migration."""
with mock_av_open: result = await hass.config_entries.flow.async_init(
result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML )
) # duplicate import should be aborted
# duplicate import should be aborted result2 = await hass.config_entries.flow.async_init(
result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML )
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Yaml Defined Name" assert result["title"] == "Yaml Defined Name"
await hass.async_block_till_done() 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. # 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.""" """Test unloading the generic IP Camera entry."""
mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA) mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA)
mock_entry.add_to_hass(hass) mock_entry.add_to_hass(hass)
@ -669,7 +628,9 @@ async def test_migrate_existing_ids(hass) -> None:
@respx.mock @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.""" """Test the use_wallclock_as_timestamps option flow."""
mock_entry = MockConfigEntry( mock_entry = MockConfigEntry(
@ -679,19 +640,18 @@ async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_ope
options=TESTDATA, options=TESTDATA,
) )
with mock_av_open: mock_entry.add_to_hass(hass)
mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done()
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"
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( result2 = await hass.config_entries.options.async_configure(
result["flow_id"], result["flow_id"],
user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, 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