Deprecate camera frontend_stream_type (#130932)

This commit is contained in:
Robert Resch 2024-11-22 13:42:33 +01:00 committed by GitHub
parent 4b5a8bf9fe
commit 154282ff5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 143 additions and 65 deletions

View File

@ -60,6 +60,7 @@ from homeassistant.helpers.deprecation import (
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_time_interval 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.network import get_url
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, VolDictType from homeassistant.helpers.typing import ConfigType, VolDictType
@ -466,6 +467,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Entity Properties # Entity Properties
_attr_brand: str | None = None _attr_brand: str | None = None
_attr_frame_interval: float = MIN_STREAM_INTERVAL _attr_frame_interval: float = MIN_STREAM_INTERVAL
# Deprecated in 2024.12. Remove in 2025.6
_attr_frontend_stream_type: StreamType | None _attr_frontend_stream_type: StreamType | None
_attr_is_on: bool = True _attr_is_on: bool = True
_attr_is_recording: bool = False _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 type(self).async_handle_async_webrtc_offer
!= Camera.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 @cached_property
def entity_picture(self) -> str: 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 frontend which camera attributes and player to use. The default type
is to use HLS, and components can override to change the 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 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 return self._attr_frontend_stream_type
if CameraEntityFeature.STREAM not in self.supported_features_compat: if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None 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.WEB_RTC
return StreamType.HLS 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 Async means that it could take some time to process the offer and responses/message
will be sent with the send_message callback. 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. An integration overriding this method must also implement async_on_webrtc_candidate.
Integrations can override with a native WebRTC implementation. Integrations can override with a native WebRTC implementation.

View File

@ -95,14 +95,16 @@ class CameraMediaSource(MediaSource):
can_stream_hls = "stream" in self.hass.config.components can_stream_hls = "stream" in self.hass.config.components
async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None: async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None:
stream_type = camera.frontend_stream_type stream_types = camera.camera_capabilities.frontend_stream_types
if stream_type is None: if not stream_types:
return _media_source_for_camera(self.hass, camera, camera.content_type) return _media_source_for_camera(self.hass, camera, camera.content_type)
if not can_stream_hls: if not can_stream_hls:
return None return None
content_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] 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 None
return _media_source_for_camera(self.hass, camera, content_type) return _media_source_for_camera(self.hass, camera, content_type)

View File

@ -24,7 +24,6 @@ from webrtc_models import RTCIceCandidateInit
from homeassistant.components.camera import ( from homeassistant.components.camera import (
Camera, Camera,
CameraEntityFeature, CameraEntityFeature,
StreamType,
WebRTCAnswer, WebRTCAnswer,
WebRTCClientConfiguration, WebRTCClientConfiguration,
WebRTCSendMessage, WebRTCSendMessage,
@ -254,11 +253,6 @@ class NestWebRTCEntity(NestCameraBaseEntity):
self._webrtc_sessions: dict[str, WebRtcStream] = {} self._webrtc_sessions: dict[str, WebRtcStream] = {}
self._refresh_unsub: dict[str, Callable[[], None]] = {} 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: async def _async_refresh_stream(self, session_id: str) -> datetime.datetime | None:
"""Refresh stream to extend expiration time.""" """Refresh stream to extend expiration time."""
if not (webrtc_stream := self._webrtc_sessions.get(session_id)): if not (webrtc_stream := self._webrtc_sessions.get(session_id)):

View File

@ -62,32 +62,17 @@ async def mock_camera_fixture(hass: HomeAssistant) -> AsyncGenerator[None]:
def mock_camera_hls_fixture(mock_camera: None) -> Generator[None]: def mock_camera_hls_fixture(mock_camera: None) -> Generator[None]:
"""Initialize a demo camera platform with HLS.""" """Initialize a demo camera platform with HLS."""
with patch( with patch(
"homeassistant.components.camera.Camera.frontend_stream_type", "homeassistant.components.camera.Camera.camera_capabilities",
new_callable=PropertyMock(return_value=StreamType.HLS), new_callable=PropertyMock(
): return_value=camera.CameraCapabilities({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),
): ):
yield yield
@pytest.fixture @pytest.fixture
async def mock_camera_webrtc( async def mock_camera_webrtc(
mock_camera_webrtc_frontendtype_only: None, mock_camera: None,
) -> AsyncGenerator[None]: ) -> AsyncGenerator[None]:
"""Initialize a demo camera platform with WebRTC.""" """Initialize a demo camera platform with WebRTC."""
@ -96,9 +81,17 @@ async def mock_camera_webrtc(
) -> None: ) -> None:
send_message(WebRTCAnswer(WEBRTC_ANSWER)) send_message(WebRTCAnswer(WEBRTC_ANSWER))
with patch( with (
patch(
"homeassistant.components.camera.Camera.async_handle_async_webrtc_offer", "homeassistant.components.camera.Camera.async_handle_async_webrtc_offer",
side_effect=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 yield
@ -168,7 +161,6 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
_attr_supported_features: camera.CameraEntityFeature = ( _attr_supported_features: camera.CameraEntityFeature = (
camera.CameraEntityFeature.STREAM camera.CameraEntityFeature.STREAM
) )
_attr_frontend_stream_type: camera.StreamType = camera.StreamType.WEB_RTC
async def stream_source(self) -> str | None: async def stream_source(self) -> str | None:
return STREAM_SOURCE return STREAM_SOURCE

View File

@ -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.components.websocket_api import TYPE_RESULT
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
CONF_PLATFORM,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
@ -1054,3 +1055,27 @@ async def test_camera_capabilities_changing_native_support(
await hass.async_block_till_done() await hass.async_block_till_done()
await _test_capabilities(hass, hass_ws_client, cam.entity_id, set(), set()) 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

View File

@ -92,7 +92,7 @@ async def test_browsing_webrtc(hass: HomeAssistant) -> None:
assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] 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: async def test_resolving(hass: HomeAssistant) -> None:
"""Test resolving.""" """Test resolving."""
# Adding stream enables HLS camera # Adding stream enables HLS camera
@ -110,7 +110,7 @@ async def test_resolving(hass: HomeAssistant) -> None:
assert item.mime_type == FORMAT_CONTENT_TYPE["hls"] 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: async def test_resolving_errors(hass: HomeAssistant) -> None:
"""Test resolving.""" """Test resolving."""

View File

@ -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( async def test_ws_get_client_config_no_rtc_camera(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None: ) -> None:

View File

@ -176,16 +176,6 @@ async def async_get_image(
return image.content 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( async def async_frontend_stream_types(
client: MockHAClientWebSocket, entity_id: str client: MockHAClientWebSocket, entity_id: str
) -> list[str] | None: ) -> list[str] | None:
@ -268,9 +258,9 @@ async def test_camera_stream(
await setup_platform() await setup_platform()
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
assert ( cam = hass.states.get("camera.my_camera")
get_frontend_stream_type_attribute(hass, "camera.my_camera") == StreamType.HLS assert cam is not None
) assert cam.state == CameraState.STREAMING
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
frontend_stream_types = await async_frontend_stream_types( frontend_stream_types = await async_frontend_stream_types(
client, "camera.my_camera" client, "camera.my_camera"
@ -294,10 +284,9 @@ async def test_camera_ws_stream(
await setup_platform() await setup_platform()
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
assert ( cam = hass.states.get("camera.my_camera")
get_frontend_stream_type_attribute(hass, "camera.my_camera") == StreamType.HLS assert cam is not None
) assert cam.state == CameraState.STREAMING
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
frontend_stream_types = await async_frontend_stream_types( frontend_stream_types = await async_frontend_stream_types(
client, "camera.my_camera" client, "camera.my_camera"
@ -671,7 +660,10 @@ async def test_camera_web_rtc(
cam = hass.states.get("camera.my_camera") cam = hass.states.get("camera.my_camera")
assert cam is not None assert cam is not None
assert cam.state == CameraState.STREAMING 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) client = await hass_ws_client(hass)
await client.send_json_auto_id( await client.send_json_auto_id(
@ -720,17 +712,11 @@ async def test_camera_web_rtc_unsupported(
cam = hass.states.get("camera.my_camera") cam = hass.states.get("camera.my_camera")
assert cam is not None assert cam is not None
assert cam.state == CameraState.STREAMING assert cam.state == CameraState.STREAMING
assert cam.attributes["frontend_stream_type"] == StreamType.HLS
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
await client.send_json_auto_id( assert await async_frontend_stream_types(client, "camera.my_camera") == [
{"type": "camera/capabilities", "entity_id": "camera.my_camera"} StreamType.HLS
) ]
msg = await client.receive_json()
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"] == {"frontend_stream_types": ["hls"]}
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
@ -844,6 +830,10 @@ async def test_camera_multiple_streams(
assert cam.state == CameraState.STREAMING assert cam.state == CameraState.STREAMING
# Prefer WebRTC over RTSP/HLS # Prefer WebRTC over RTSP/HLS
assert cam.attributes["frontend_stream_type"] == StreamType.WEB_RTC 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 # RTSP stream is not supported
stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") 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 is not None
assert cam.state == CameraState.STREAMING assert cam.state == CameraState.STREAMING
assert cam.attributes["frontend_stream_type"] == StreamType.WEB_RTC 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) client = await hass_ws_client(hass)
await client.send_json_auto_id( await client.send_json_auto_id(

View File

@ -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