From 154282ff5c47dc117ea0aac7bb9b6a6b08a4dd18 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 22 Nov 2024 13:42:33 +0100 Subject: [PATCH] Deprecate camera frontend_stream_type (#130932) --- homeassistant/components/camera/__init__.py | 34 ++++++++++++- .../components/camera/media_source.py | 8 ++-- homeassistant/components/nest/camera.py | 6 --- tests/components/camera/conftest.py | 40 +++++++--------- tests/components/camera/test_init.py | 25 ++++++++++ tests/components/camera/test_media_source.py | 4 +- tests/components/camera/test_webrtc.py | 2 +- tests/components/nest/test_camera.py | 48 ++++++++----------- .../custom_components/test/camera.py | 41 ++++++++++++++++ 9 files changed, 143 insertions(+), 65 deletions(-) create mode 100644 tests/testing_config/custom_components/test/camera.py diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 64a4480d9d3..176cba8ae8b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -60,6 +60,7 @@ from homeassistant.helpers.deprecation import ( from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.frame import ReportBehavior, report_usage from homeassistant.helpers.network import get_url from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, VolDictType @@ -466,6 +467,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): # Entity Properties _attr_brand: str | None = None _attr_frame_interval: float = MIN_STREAM_INTERVAL + # Deprecated in 2024.12. Remove in 2025.6 _attr_frontend_stream_type: StreamType | None _attr_is_on: bool = True _attr_is_recording: bool = False @@ -497,6 +499,16 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): type(self).async_handle_async_webrtc_offer != Camera.async_handle_async_webrtc_offer ) + self._deprecate_attr_frontend_stream_type_logged = False + if type(self).frontend_stream_type != Camera.frontend_stream_type: + report_usage( + ( + f"is overwriting the 'frontend_stream_type' property in the {type(self).__name__} class," + " which is deprecated and will be removed in Home Assistant 2025.6, " + ), + core_integration_behavior=ReportBehavior.ERROR, + exclude_integrations={DOMAIN}, + ) @cached_property def entity_picture(self) -> str: @@ -566,11 +578,29 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): frontend which camera attributes and player to use. The default type is to use HLS, and components can override to change the type. """ + # Deprecated in 2024.12. Remove in 2025.6 + # Use the camera_capabilities instead if hasattr(self, "_attr_frontend_stream_type"): + if not self._deprecate_attr_frontend_stream_type_logged: + report_usage( + ( + f"is setting the '_attr_frontend_stream_type' attribute in the {type(self).__name__} class," + " which is deprecated and will be removed in Home Assistant 2025.6, " + ), + core_integration_behavior=ReportBehavior.ERROR, + exclude_integrations={DOMAIN}, + ) + + self._deprecate_attr_frontend_stream_type_logged = True return self._attr_frontend_stream_type if CameraEntityFeature.STREAM not in self.supported_features_compat: return None - if self._webrtc_provider or self._legacy_webrtc_provider: + if ( + self._webrtc_provider + or self._legacy_webrtc_provider + or self._supports_native_sync_webrtc + or self._supports_native_async_webrtc + ): return StreamType.WEB_RTC return StreamType.HLS @@ -628,7 +658,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): Async means that it could take some time to process the offer and responses/message will be sent with the send_message callback. - This method is used by cameras with CameraEntityFeature.STREAM and StreamType.WEB_RTC. + This method is used by cameras with CameraEntityFeature.STREAM. An integration overriding this method must also implement async_on_webrtc_candidate. Integrations can override with a native WebRTC implementation. diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index 222c95ff998..701457afc3e 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -95,14 +95,16 @@ class CameraMediaSource(MediaSource): can_stream_hls = "stream" in self.hass.config.components async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None: - stream_type = camera.frontend_stream_type - if stream_type is None: + stream_types = camera.camera_capabilities.frontend_stream_types + if not stream_types: return _media_source_for_camera(self.hass, camera, camera.content_type) if not can_stream_hls: return None content_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] - if stream_type != StreamType.HLS and not (await camera.stream_source()): + if StreamType.HLS not in stream_types and not ( + await camera.stream_source() + ): return None return _media_source_for_camera(self.hass, camera, content_type) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 281e6b0bb28..b7e0f210741 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -24,7 +24,6 @@ from webrtc_models import RTCIceCandidateInit from homeassistant.components.camera import ( Camera, CameraEntityFeature, - StreamType, WebRTCAnswer, WebRTCClientConfiguration, WebRTCSendMessage, @@ -254,11 +253,6 @@ class NestWebRTCEntity(NestCameraBaseEntity): self._webrtc_sessions: dict[str, WebRtcStream] = {} self._refresh_unsub: dict[str, Callable[[], None]] = {} - @property - def frontend_stream_type(self) -> StreamType | None: - """Return the type of stream supported by this camera.""" - return StreamType.WEB_RTC - async def _async_refresh_stream(self, session_id: str) -> datetime.datetime | None: """Refresh stream to extend expiration time.""" if not (webrtc_stream := self._webrtc_sessions.get(session_id)): diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py index cb25b366029..b529ee3e9b9 100644 --- a/tests/components/camera/conftest.py +++ b/tests/components/camera/conftest.py @@ -62,32 +62,17 @@ async def mock_camera_fixture(hass: HomeAssistant) -> AsyncGenerator[None]: def mock_camera_hls_fixture(mock_camera: None) -> Generator[None]: """Initialize a demo camera platform with HLS.""" with patch( - "homeassistant.components.camera.Camera.frontend_stream_type", - new_callable=PropertyMock(return_value=StreamType.HLS), - ): - yield - - -@pytest.fixture -async def mock_camera_webrtc_frontendtype_only( - hass: HomeAssistant, -) -> AsyncGenerator[None]: - """Initialize a demo camera platform with WebRTC.""" - assert await async_setup_component( - hass, "camera", {camera.DOMAIN: {"platform": "demo"}} - ) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.camera.Camera.frontend_stream_type", - new_callable=PropertyMock(return_value=StreamType.WEB_RTC), + "homeassistant.components.camera.Camera.camera_capabilities", + new_callable=PropertyMock( + return_value=camera.CameraCapabilities({StreamType.HLS}) + ), ): yield @pytest.fixture async def mock_camera_webrtc( - mock_camera_webrtc_frontendtype_only: None, + mock_camera: None, ) -> AsyncGenerator[None]: """Initialize a demo camera platform with WebRTC.""" @@ -96,9 +81,17 @@ async def mock_camera_webrtc( ) -> None: send_message(WebRTCAnswer(WEBRTC_ANSWER)) - with patch( - "homeassistant.components.camera.Camera.async_handle_async_webrtc_offer", - side_effect=async_handle_async_webrtc_offer, + with ( + patch( + "homeassistant.components.camera.Camera.async_handle_async_webrtc_offer", + side_effect=async_handle_async_webrtc_offer, + ), + patch( + "homeassistant.components.camera.Camera.camera_capabilities", + new_callable=PropertyMock( + return_value=camera.CameraCapabilities({StreamType.WEB_RTC}) + ), + ), ): yield @@ -168,7 +161,6 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None: _attr_supported_features: camera.CameraEntityFeature = ( camera.CameraEntityFeature.STREAM ) - _attr_frontend_stream_type: camera.StreamType = camera.StreamType.WEB_RTC async def stream_source(self) -> str | None: return STREAM_SOURCE diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index af8c220bbe4..f9d30c240db 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -27,6 +27,7 @@ from homeassistant.components.camera.helper import get_camera_from_entity_id from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_PLATFORM, EVENT_HOMEASSISTANT_STARTED, STATE_UNAVAILABLE, ) @@ -1054,3 +1055,27 @@ async def test_camera_capabilities_changing_native_support( await hass.async_block_till_done() await _test_capabilities(hass, hass_ws_client, cam.entity_id, set(), set()) + + +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_deprecated_frontend_stream_type_logs( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test using (_attr_)frontend_stream_type will log.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + for entity_id in ( + "camera.property_frontend_stream_type", + "camera.attr_frontend_stream_type", + ): + camera_obj = get_camera_from_entity_id(hass, entity_id) + assert camera_obj.frontend_stream_type == StreamType.WEB_RTC + + assert ( + "Detected that custom integration 'test' is overwriting the 'frontend_stream_type' property in the PropertyFrontendStreamTypeCamera class, which is deprecated and will be removed in Home Assistant 2025.6," + ) in caplog.text + assert ( + "Detected that custom integration 'test' is setting the '_attr_frontend_stream_type' attribute in the AttrFrontendStreamTypeCamera class, which is deprecated and will be removed in Home Assistant 2025.6," + ) in caplog.text diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 3b75b58c53f..bd92010d242 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -92,7 +92,7 @@ async def test_browsing_webrtc(hass: HomeAssistant) -> None: assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] -@pytest.mark.usefixtures("mock_camera_hls") +@pytest.mark.usefixtures("mock_camera") async def test_resolving(hass: HomeAssistant) -> None: """Test resolving.""" # Adding stream enables HLS camera @@ -110,7 +110,7 @@ async def test_resolving(hass: HomeAssistant) -> None: assert item.mime_type == FORMAT_CONTENT_TYPE["hls"] -@pytest.mark.usefixtures("mock_camera_hls") +@pytest.mark.usefixtures("mock_camera") async def test_resolving_errors(hass: HomeAssistant) -> None: """Test resolving.""" diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py index 89bd74be301..04cafbf4ae5 100644 --- a/tests/components/camera/test_webrtc.py +++ b/tests/components/camera/test_webrtc.py @@ -399,7 +399,7 @@ async def test_ws_get_client_config_custom_config( } -@pytest.mark.usefixtures("mock_camera_hls") +@pytest.mark.usefixtures("mock_camera") async def test_ws_get_client_config_no_rtc_camera( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index eb15b998507..698e9b7a274 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -176,16 +176,6 @@ async def async_get_image( return image.content -def get_frontend_stream_type_attribute( - hass: HomeAssistant, entity_id: str -) -> StreamType: - """Get the frontend_stream_type camera attribute.""" - cam = hass.states.get(entity_id) - assert cam is not None - assert cam.state == CameraState.STREAMING - return cam.attributes.get("frontend_stream_type") - - async def async_frontend_stream_types( client: MockHAClientWebSocket, entity_id: str ) -> list[str] | None: @@ -268,9 +258,9 @@ async def test_camera_stream( await setup_platform() assert len(hass.states.async_all()) == 1 - assert ( - get_frontend_stream_type_attribute(hass, "camera.my_camera") == StreamType.HLS - ) + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == CameraState.STREAMING client = await hass_ws_client(hass) frontend_stream_types = await async_frontend_stream_types( client, "camera.my_camera" @@ -294,10 +284,9 @@ async def test_camera_ws_stream( await setup_platform() assert len(hass.states.async_all()) == 1 - assert ( - get_frontend_stream_type_attribute(hass, "camera.my_camera") == StreamType.HLS - ) - + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == CameraState.STREAMING client = await hass_ws_client(hass) frontend_stream_types = await async_frontend_stream_types( client, "camera.my_camera" @@ -671,7 +660,10 @@ async def test_camera_web_rtc( cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == CameraState.STREAMING - assert cam.attributes["frontend_stream_type"] == StreamType.WEB_RTC + client = await hass_ws_client(hass) + assert await async_frontend_stream_types(client, "camera.my_camera") == [ + StreamType.WEB_RTC + ] client = await hass_ws_client(hass) await client.send_json_auto_id( @@ -720,17 +712,11 @@ async def test_camera_web_rtc_unsupported( cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == CameraState.STREAMING - assert cam.attributes["frontend_stream_type"] == StreamType.HLS client = await hass_ws_client(hass) - await client.send_json_auto_id( - {"type": "camera/capabilities", "entity_id": "camera.my_camera"} - ) - msg = await client.receive_json() - - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"] == {"frontend_stream_types": ["hls"]} + assert await async_frontend_stream_types(client, "camera.my_camera") == [ + StreamType.HLS + ] await client.send_json_auto_id( { @@ -844,6 +830,10 @@ async def test_camera_multiple_streams( assert cam.state == CameraState.STREAMING # Prefer WebRTC over RTSP/HLS assert cam.attributes["frontend_stream_type"] == StreamType.WEB_RTC + client = await hass_ws_client(hass) + assert await async_frontend_stream_types(client, "camera.my_camera") == [ + StreamType.WEB_RTC + ] # RTSP stream is not supported stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") @@ -919,6 +909,10 @@ async def test_webrtc_refresh_expired_stream( assert cam is not None assert cam.state == CameraState.STREAMING assert cam.attributes["frontend_stream_type"] == StreamType.WEB_RTC + client = await hass_ws_client(hass) + assert await async_frontend_stream_types(client, "camera.my_camera") == [ + StreamType.WEB_RTC + ] client = await hass_ws_client(hass) await client.send_json_auto_id( diff --git a/tests/testing_config/custom_components/test/camera.py b/tests/testing_config/custom_components/test/camera.py new file mode 100644 index 00000000000..b2aa1bbc53b --- /dev/null +++ b/tests/testing_config/custom_components/test/camera.py @@ -0,0 +1,41 @@ +"""Provide a mock remote platform. + +Call init before using it in your tests to ensure clean test data. +""" + +from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities_callback: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Return mock entities.""" + async_add_entities_callback( + [AttrFrontendStreamTypeCamera(), PropertyFrontendStreamTypeCamera()] + ) + + +class AttrFrontendStreamTypeCamera(Camera): + """attr frontend stream type Camera.""" + + _attr_name = "attr frontend stream type" + _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM + _attr_frontend_stream_type: StreamType = StreamType.WEB_RTC + + +class PropertyFrontendStreamTypeCamera(Camera): + """property frontend stream type Camera.""" + + _attr_name = "property frontend stream type" + _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM + + @property + def frontend_stream_type(self) -> StreamType | None: + """Return the stream type of the camera.""" + return StreamType.WEB_RTC