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_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.

View File

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

View File

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

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]:
"""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

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

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"]
@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."""

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

View File

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

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