From abd3c54cbe5088ce299a52cae49ad0b8bd3d19e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Dec 2023 13:01:47 -1000 Subject: [PATCH] Add support for attribute caching to the camera platform (#106256) --- homeassistant/components/camera/__init__.py | 38 ++++++++++++++------ homeassistant/components/demo/camera.py | 9 ++++- tests/components/camera/test_init.py | 27 ++++---------- tests/components/camera/test_media_source.py | 11 +++--- tests/components/rtsp_to_webrtc/conftest.py | 3 -- 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f7ce0691efb..f7552e79468 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -12,7 +12,7 @@ from functools import partial import logging import os from random import SystemRandom -from typing import Any, Final, cast, final +from typing import TYPE_CHECKING, Any, Final, cast, final from aiohttp import hdrs, web import attr @@ -82,6 +82,11 @@ from .const import ( # noqa: F401 from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 +if TYPE_CHECKING: + from functools import cached_property +else: + from homeassistant.backports.functools import cached_property + _LOGGER = logging.getLogger(__name__) SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" @@ -458,7 +463,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) -class Camera(Entity): +CACHED_PROPERTIES_WITH_ATTR_ = { + "brand", + "frame_interval", + "frontend_stream_type", + "is_on", + "is_recording", + "is_streaming", + "model", + "motion_detection_enabled", + "supported_features", +} + + +class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """The base class for camera entities.""" _entity_component_unrecorded_attributes = frozenset( @@ -501,37 +519,37 @@ class Camera(Entity): """Whether or not to use stream to generate stills.""" return False - @property + @cached_property def supported_features(self) -> CameraEntityFeature: """Flag supported features.""" return self._attr_supported_features - @property + @cached_property def is_recording(self) -> bool: """Return true if the device is recording.""" return self._attr_is_recording - @property + @cached_property def is_streaming(self) -> bool: """Return true if the device is streaming.""" return self._attr_is_streaming - @property + @cached_property def brand(self) -> str | None: """Return the camera brand.""" return self._attr_brand - @property + @cached_property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return self._attr_motion_detection_enabled - @property + @cached_property def model(self) -> str | None: """Return the camera model.""" return self._attr_model - @property + @cached_property def frame_interval(self) -> float: """Return the interval between frames of the mjpeg stream.""" return self._attr_frame_interval @@ -649,7 +667,7 @@ class Camera(Entity): return STATE_STREAMING return STATE_IDLE - @property + @cached_property def is_on(self) -> bool: """Return true if on.""" return self._attr_is_on diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 722693280a0..502129b5c9d 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -19,6 +19,7 @@ async def async_setup_entry( [ DemoCamera("Demo camera", "image/jpg"), DemoCamera("Demo camera png", "image/png"), + DemoCameraWithoutStream("Demo camera without stream", "image/jpg"), ] ) @@ -28,7 +29,7 @@ class DemoCamera(Camera): _attr_is_streaming = True _attr_motion_detection_enabled = False - _attr_supported_features = CameraEntityFeature.ON_OFF + _attr_supported_features = CameraEntityFeature.ON_OFF | CameraEntityFeature.STREAM def __init__(self, name: str, content_type: str) -> None: """Initialize demo camera component.""" @@ -68,3 +69,9 @@ class DemoCamera(Camera): self._attr_is_streaming = True self._attr_is_on = True self.async_write_ha_state() + + +class DemoCameraWithoutStream(DemoCamera): + """The representation of a Demo camera without stream.""" + + _attr_supported_features = CameraEntityFeature.ON_OFF diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index ca4c0fe9a52..cb9b09a85ab 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -58,10 +58,7 @@ async def mock_stream_source_fixture(): with patch( "homeassistant.components.camera.Camera.stream_source", return_value=STREAM_SOURCE, - ) as mock_stream_source, patch( - "homeassistant.components.camera.Camera.supported_features", - return_value=camera.CameraEntityFeature.STREAM, - ): + ) as mock_stream_source: yield mock_stream_source @@ -71,10 +68,7 @@ async def mock_hls_stream_source_fixture(): with patch( "homeassistant.components.camera.Camera.stream_source", return_value=HLS_STREAM_SOURCE, - ) as mock_hls_stream_source, patch( - "homeassistant.components.camera.Camera.supported_features", - return_value=camera.CameraEntityFeature.STREAM, - ): + ) as mock_hls_stream_source: yield mock_hls_stream_source @@ -934,19 +928,15 @@ async def test_use_stream_for_stills( return_value=True, ): # First test when the integration does not support stream should fail - resp = await client.get("/api/camera_proxy/camera.demo_camera") + resp = await client.get("/api/camera_proxy/camera.demo_camera_without_stream") await hass.async_block_till_done() mock_stream_source.assert_not_called() assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR # Test when the integration does not provide a stream_source should fail - with patch( - "homeassistant.components.demo.camera.DemoCamera.supported_features", - return_value=camera.CameraEntityFeature.STREAM, - ): - resp = await client.get("/api/camera_proxy/camera.demo_camera") - await hass.async_block_till_done() - mock_stream_source.assert_called_once() - assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + resp = await client.get("/api/camera_proxy/camera.demo_camera") + await hass.async_block_till_done() + mock_stream_source.assert_called_once() + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", @@ -954,9 +944,6 @@ async def test_use_stream_for_stills( ) as mock_stream_source, patch( "homeassistant.components.camera.create_stream" ) as mock_create_stream, patch( - "homeassistant.components.demo.camera.DemoCamera.supported_features", - return_value=camera.CameraEntityFeature.STREAM, - ), patch( "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills", return_value=True, ): diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index bbeef35b6f3..7aa41b98efa 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -22,14 +22,14 @@ async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: assert item is not None assert item.title == "Camera" assert len(item.children) == 0 - assert item.not_shown == 2 + assert item.not_shown == 3 # Adding stream enables HLS camera hass.config.components.add("stream") item = await media_source.async_browse_media(hass, "media-source://camera") assert item.not_shown == 0 - assert len(item.children) == 2 + assert len(item.children) == 3 assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] @@ -38,10 +38,9 @@ async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None: item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None assert item.title == "Camera" - assert len(item.children) == 2 - assert item.not_shown == 0 + assert len(item.children) == 1 + assert item.not_shown == 2 assert item.children[0].media_content_type == "image/jpg" - assert item.children[1].media_content_type == "image/png" async def test_browsing_filter_web_rtc( @@ -52,7 +51,7 @@ async def test_browsing_filter_web_rtc( assert item is not None assert item.title == "Camera" assert len(item.children) == 0 - assert item.not_shown == 2 + assert item.not_shown == 3 async def test_resolving(hass: HomeAssistant, mock_camera_hls) -> None: diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index a8ce74624f8..edb8c7c4aca 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -51,9 +51,6 @@ async def mock_camera(hass) -> AsyncGenerator[None, None]: ), patch( "homeassistant.components.camera.Camera.stream_source", return_value=STREAM_SOURCE, - ), patch( - "homeassistant.components.camera.Camera.supported_features", - return_value=camera.CameraEntityFeature.STREAM, ): yield