From 08a3140e6cbffc5677971f98191e24eea7085236 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 26 Dec 2021 15:53:14 +0800 Subject: [PATCH] Allow generic camera conf without still_image_url (#62611) * Allow generic config with no CONF_STILL_IMAGE_URL * Use Stream.async_get_image when no CONF_STILL_IMAGE_URL * Remove GenericCamera.camera_image --- homeassistant/components/generic/camera.py | 24 +++++------ homeassistant/components/stream/__init__.py | 2 + tests/components/generic/test_camera.py | 47 ++++++++++++++++++++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index b6e08ea8582..23fdd4191a3 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -1,7 +1,6 @@ """Support for IP Cameras.""" from __future__ import annotations -import asyncio import logging import httpx @@ -45,8 +44,8 @@ GET_IMAGE_TIMEOUT = 10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_STILL_IMAGE_URL): cv.template, - vol.Optional(CONF_STREAM_SOURCE): cv.template, + vol.Required(vol.Any(CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE)): cv.template, + vol.Optional(vol.Any(CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE)): cv.template, vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -81,9 +80,10 @@ class GenericCamera(Camera): self.hass = hass self._authentication = device_info.get(CONF_AUTHENTICATION) self._name = device_info.get(CONF_NAME) - self._still_image_url = device_info[CONF_STILL_IMAGE_URL] + self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) + if self._still_image_url: + self._still_image_url.hass = hass self._stream_source = device_info.get(CONF_STREAM_SOURCE) - self._still_image_url.hass = hass if self._stream_source is not None: self._stream_source.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] @@ -120,18 +120,16 @@ class GenericCamera(Camera): """Return the interval between frames of the mjpeg stream.""" return self._frame_interval - def camera_image( - self, width: int | None = None, height: int | None = None - ) -> bytes | None: - """Return bytes of camera image.""" - return asyncio.run_coroutine_threadsafe( - self.async_camera_image(), self.hass.loop - ).result() - async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" + if not self._still_image_url: + if not self.stream: + await self.async_create_stream() + if self.stream: + return await self.stream.async_get_image(width, height) + return None try: url = self._still_image_url.async_render(parse_result=False) except TemplateError as err: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 1a4ce3d92e8..7019dbe60b2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -437,6 +437,8 @@ class Stream: hass.add_executor_job underneath the hood. """ + self.add_provider(HLS_PROVIDER) + self.start() return await self._keyframe_converter.async_get_image( width=width, height=height ) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index c52b4bf6e8f..e9b8d886bc3 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -14,7 +14,7 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.common import get_fixture_path +from tests.common import AsyncMock, Mock, get_fixture_path @respx.mock @@ -459,3 +459,48 @@ async def test_timeout_cancelled(hass, hass_client): assert respx.calls.call_count == total_calls assert resp.status == HTTPStatus.OK assert await resp.text() == "hello world" + + +async def test_no_still_image_url(hass, hass_client): + """Test that the component can grab images from stream with no still_image_url.""" + 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() + + client = await hass_client() + + with patch( + "homeassistant.components.generic.camera.GenericCamera.stream_source", + return_value=None, + ) as mock_stream_source: + + # First test when there is no stream_source should fail + resp = await client.get("/api/camera_proxy/camera.config_test") + await hass.async_block_till_done() + mock_stream_source.assert_called_once() + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + + with patch("homeassistant.components.camera.create_stream") as mock_create_stream: + + # Now test when creating the stream succeeds + mock_stream = Mock() + mock_stream.async_get_image = AsyncMock() + mock_stream.async_get_image.return_value = b"stream_keyframe_image" + mock_create_stream.return_value = mock_stream + + # should start the stream and get the image + resp = await client.get("/api/camera_proxy/camera.config_test") + await hass.async_block_till_done() + mock_create_stream.assert_called_once() + mock_stream.async_get_image.assert_called_once() + assert resp.status == HTTPStatus.OK + assert await resp.read() == b"stream_keyframe_image"