Add support for attribute caching to the camera platform (#106256)

This commit is contained in:
J. Nick Koston 2023-12-23 13:01:47 -10:00 committed by GitHub
parent 2f72d4f9f0
commit abd3c54cbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 48 additions and 40 deletions

View file

@ -12,7 +12,7 @@ from functools import partial
import logging import logging
import os import os
from random import SystemRandom 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 from aiohttp import hdrs, web
import attr import attr
@ -82,6 +82,11 @@ from .const import ( # noqa: F401
from .img_util import scale_jpeg_camera_image from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 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__) _LOGGER = logging.getLogger(__name__)
SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" 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) 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.""" """The base class for camera entities."""
_entity_component_unrecorded_attributes = frozenset( _entity_component_unrecorded_attributes = frozenset(
@ -501,37 +519,37 @@ class Camera(Entity):
"""Whether or not to use stream to generate stills.""" """Whether or not to use stream to generate stills."""
return False return False
@property @cached_property
def supported_features(self) -> CameraEntityFeature: def supported_features(self) -> CameraEntityFeature:
"""Flag supported features.""" """Flag supported features."""
return self._attr_supported_features return self._attr_supported_features
@property @cached_property
def is_recording(self) -> bool: def is_recording(self) -> bool:
"""Return true if the device is recording.""" """Return true if the device is recording."""
return self._attr_is_recording return self._attr_is_recording
@property @cached_property
def is_streaming(self) -> bool: def is_streaming(self) -> bool:
"""Return true if the device is streaming.""" """Return true if the device is streaming."""
return self._attr_is_streaming return self._attr_is_streaming
@property @cached_property
def brand(self) -> str | None: def brand(self) -> str | None:
"""Return the camera brand.""" """Return the camera brand."""
return self._attr_brand return self._attr_brand
@property @cached_property
def motion_detection_enabled(self) -> bool: def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return self._attr_motion_detection_enabled return self._attr_motion_detection_enabled
@property @cached_property
def model(self) -> str | None: def model(self) -> str | None:
"""Return the camera model.""" """Return the camera model."""
return self._attr_model return self._attr_model
@property @cached_property
def frame_interval(self) -> float: def frame_interval(self) -> float:
"""Return the interval between frames of the mjpeg stream.""" """Return the interval between frames of the mjpeg stream."""
return self._attr_frame_interval return self._attr_frame_interval
@ -649,7 +667,7 @@ class Camera(Entity):
return STATE_STREAMING return STATE_STREAMING
return STATE_IDLE return STATE_IDLE
@property @cached_property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if on.""" """Return true if on."""
return self._attr_is_on return self._attr_is_on

View file

@ -19,6 +19,7 @@ async def async_setup_entry(
[ [
DemoCamera("Demo camera", "image/jpg"), DemoCamera("Demo camera", "image/jpg"),
DemoCamera("Demo camera png", "image/png"), 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_is_streaming = True
_attr_motion_detection_enabled = False _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: def __init__(self, name: str, content_type: str) -> None:
"""Initialize demo camera component.""" """Initialize demo camera component."""
@ -68,3 +69,9 @@ class DemoCamera(Camera):
self._attr_is_streaming = True self._attr_is_streaming = True
self._attr_is_on = True self._attr_is_on = True
self.async_write_ha_state() self.async_write_ha_state()
class DemoCameraWithoutStream(DemoCamera):
"""The representation of a Demo camera without stream."""
_attr_supported_features = CameraEntityFeature.ON_OFF

View file

@ -58,10 +58,7 @@ async def mock_stream_source_fixture():
with patch( with patch(
"homeassistant.components.camera.Camera.stream_source", "homeassistant.components.camera.Camera.stream_source",
return_value=STREAM_SOURCE, return_value=STREAM_SOURCE,
) as mock_stream_source, patch( ) as mock_stream_source:
"homeassistant.components.camera.Camera.supported_features",
return_value=camera.CameraEntityFeature.STREAM,
):
yield mock_stream_source yield mock_stream_source
@ -71,10 +68,7 @@ async def mock_hls_stream_source_fixture():
with patch( with patch(
"homeassistant.components.camera.Camera.stream_source", "homeassistant.components.camera.Camera.stream_source",
return_value=HLS_STREAM_SOURCE, return_value=HLS_STREAM_SOURCE,
) as mock_hls_stream_source, patch( ) as mock_hls_stream_source:
"homeassistant.components.camera.Camera.supported_features",
return_value=camera.CameraEntityFeature.STREAM,
):
yield mock_hls_stream_source yield mock_hls_stream_source
@ -934,19 +928,15 @@ async def test_use_stream_for_stills(
return_value=True, return_value=True,
): ):
# First test when the integration does not support stream should fail # 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() await hass.async_block_till_done()
mock_stream_source.assert_not_called() mock_stream_source.assert_not_called()
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
# Test when the integration does not provide a stream_source should fail # Test when the integration does not provide a stream_source should fail
with patch( resp = await client.get("/api/camera_proxy/camera.demo_camera")
"homeassistant.components.demo.camera.DemoCamera.supported_features", await hass.async_block_till_done()
return_value=camera.CameraEntityFeature.STREAM, 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( with patch(
"homeassistant.components.demo.camera.DemoCamera.stream_source", "homeassistant.components.demo.camera.DemoCamera.stream_source",
@ -954,9 +944,6 @@ async def test_use_stream_for_stills(
) as mock_stream_source, patch( ) as mock_stream_source, patch(
"homeassistant.components.camera.create_stream" "homeassistant.components.camera.create_stream"
) as mock_create_stream, patch( ) 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", "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills",
return_value=True, return_value=True,
): ):

View file

@ -22,14 +22,14 @@ async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None:
assert item is not None assert item is not None
assert item.title == "Camera" assert item.title == "Camera"
assert len(item.children) == 0 assert len(item.children) == 0
assert item.not_shown == 2 assert item.not_shown == 3
# Adding stream enables HLS camera # Adding stream enables HLS camera
hass.config.components.add("stream") hass.config.components.add("stream")
item = await media_source.async_browse_media(hass, "media-source://camera") item = await media_source.async_browse_media(hass, "media-source://camera")
assert item.not_shown == 0 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"] 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") item = await media_source.async_browse_media(hass, "media-source://camera")
assert item is not None assert item is not None
assert item.title == "Camera" assert item.title == "Camera"
assert len(item.children) == 2 assert len(item.children) == 1
assert item.not_shown == 0 assert item.not_shown == 2
assert item.children[0].media_content_type == "image/jpg" 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( 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 is not None
assert item.title == "Camera" assert item.title == "Camera"
assert len(item.children) == 0 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: async def test_resolving(hass: HomeAssistant, mock_camera_hls) -> None:

View file

@ -51,9 +51,6 @@ async def mock_camera(hass) -> AsyncGenerator[None, None]:
), patch( ), patch(
"homeassistant.components.camera.Camera.stream_source", "homeassistant.components.camera.Camera.stream_source",
return_value=STREAM_SOURCE, return_value=STREAM_SOURCE,
), patch(
"homeassistant.components.camera.Camera.supported_features",
return_value=camera.CameraEntityFeature.STREAM,
): ):
yield yield