Remove deprecated camera async_handle_web_rtc_offer function (#144561)

This commit is contained in:
Robert Resch 2025-05-12 14:47:49 +02:00 committed by GitHub
parent 4dde314338
commit 7b23f21712
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 27 additions and 342 deletions

View File

@ -55,7 +55,6 @@ from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
all_with_deprecated_constants,
check_if_deprecated_constant,
deprecated_function,
dir_with_deprecated_constants,
)
from homeassistant.helpers.entity import Entity, EntityDescription
@ -86,10 +85,10 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer,
WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401
WebRTCClientConfiguration,
WebRTCError,
WebRTCError, # noqa: F401
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
@ -473,9 +472,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_provider: CameraWebRTCProvider | None = None
self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
)
self._supports_native_async_webrtc = (
type(self).async_handle_async_webrtc_offer
!= Camera.async_handle_async_webrtc_offer
@ -579,15 +575,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""
return None
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
"""Handle the WebRTC offer and return an answer.
This is used by cameras with CameraEntityFeature.STREAM
and StreamType.WEB_RTC.
Integrations can override with a native WebRTC implementation.
"""
async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
@ -600,42 +587,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
Integrations can override with a native WebRTC implementation.
"""
if self._supports_native_sync_webrtc:
try:
answer = await deprecated_function(
"async_handle_async_webrtc_offer",
breaks_in_ha_version="2025.6",
)(self.async_handle_web_rtc_offer)(offer_sdp)
except ValueError as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
send_message(
WebRTCError(
"webrtc_offer_failed",
str(ex),
)
)
except TimeoutError:
# This catch was already here and should stay through the deprecation
_LOGGER.error("Timeout handling WebRTC offer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"Timeout handling WebRTC offer",
)
)
else:
if answer:
send_message(WebRTCAnswer(answer))
else:
_LOGGER.error("Error handling WebRTC offer: No answer")
send_message(
WebRTCError(
"webrtc_offer_failed",
"No answer on WebRTC offer",
)
)
return
if self._webrtc_provider:
await self._webrtc_provider.async_handle_async_webrtc_offer(
self, offer_sdp, session_id, send_message
@ -764,9 +715,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
new_provider = None
# Skip all providers if the camera has a native WebRTC implementation
if not (
self._supports_native_sync_webrtc or self._supports_native_async_webrtc
):
if not self._supports_native_async_webrtc:
# Camera doesn't have a native WebRTC implementation
new_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_provider
@ -798,17 +747,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the WebRTC client configuration and extend it with the registered ice servers."""
config = self._async_get_webrtc_client_configuration()
if not self._supports_native_sync_webrtc:
# Until 2024.11, the frontend was not resolving any ice servers
# The async approach was added 2024.11 and new integrations need to use it
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = self._supports_native_sync_webrtc
ice_servers = [
server
for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
for server in servers()
]
config.configuration.ice_servers.extend(ice_servers)
return config
@ -838,7 +782,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the camera capabilities."""
frontend_stream_types = set()
if CameraEntityFeature.STREAM in self.supported_features_compat:
if self._supports_native_sync_webrtc or self._supports_native_async_webrtc:
if self._supports_native_async_webrtc:
# The camera has a native WebRTC implementation
frontend_stream_types.add(StreamType.WEB_RTC)
else:

View File

@ -111,13 +111,11 @@ class WebRTCClientConfiguration:
configuration: RTCConfiguration = field(default_factory=RTCConfiguration)
data_channel: str | None = None
get_candidates_upfront: bool = False
def to_frontend_dict(self) -> dict[str, Any]:
"""Return a dict that can be used by the frontend."""
data: dict[str, Any] = {
"configuration": self.configuration.to_dict(),
"getCandidatesUpfront": self.get_candidates_upfront,
}
if self.data_channel is not None:
data["dataChannel"] = self.data_channel

View File

@ -165,13 +165,15 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
async def stream_source(self) -> str | None:
return STREAM_SOURCE
class SyncCamera(BaseCamera):
"""Mock Camera with native sync WebRTC support."""
class AsyncNoCandidateCamera(BaseCamera):
"""Mock Camera with native async WebRTC support but not implemented candidate support."""
_attr_name = "Sync"
_attr_name = "Async No Candidate"
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
return WEBRTC_ANSWER
async def async_handle_async_webrtc_offer(
self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
) -> None:
send_message(WebRTCAnswer(WEBRTC_ANSWER))
class AsyncCamera(BaseCamera):
"""Mock Camera with native async WebRTC support."""
@ -221,7 +223,10 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None:
),
)
setup_test_component_platform(
hass, camera.DOMAIN, [SyncCamera(), AsyncCamera()], from_config_entry=True
hass,
camera.DOMAIN,
[AsyncNoCandidateCamera(), AsyncCamera()],
from_config_entry=True,
)
mock_platform(hass, f"{domain}.config_flow", Mock())

View File

@ -968,24 +968,19 @@ async def test_camera_capabilities_webrtc(
"""Test WebRTC camera capabilities."""
await _test_capabilities(
hass, hass_ws_client, "camera.sync", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
hass, hass_ws_client, "camera.async", {StreamType.WEB_RTC}, {StreamType.WEB_RTC}
)
@pytest.mark.parametrize(
("entity_id", "expect_native_async_webrtc"),
[("camera.sync", False), ("camera.async", True)],
)
@pytest.mark.usefixtures("mock_test_webrtc_cameras", "register_test_provider")
async def test_webrtc_provider_not_added_for_native_webrtc(
hass: HomeAssistant, entity_id: str, expect_native_async_webrtc: bool
hass: HomeAssistant,
) -> None:
"""Test that a WebRTC provider is not added to a camera when the camera has native WebRTC support."""
camera_obj = get_camera_from_entity_id(hass, entity_id)
camera_obj = get_camera_from_entity_id(hass, "camera.async")
assert camera_obj
assert camera_obj._webrtc_provider is None
assert camera_obj._supports_native_sync_webrtc is not expect_native_async_webrtc
assert camera_obj._supports_native_async_webrtc is expect_native_async_webrtc
assert camera_obj._supports_native_async_webrtc is True
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
@ -1016,14 +1011,12 @@ async def test_camera_capabilities_changing_non_native_support(
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
@pytest.mark.parametrize(("entity_id"), ["camera.sync", "camera.async"])
async def test_camera_capabilities_changing_native_support(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
entity_id: str,
) -> None:
"""Test WebRTC camera capabilities."""
cam = get_camera_from_entity_id(hass, entity_id)
cam = get_camera_from_entity_id(hass, "camera.async")
assert cam.supported_features == camera.CameraEntityFeature.STREAM
await _test_capabilities(

View File

@ -1,7 +1,6 @@
"""Test camera WebRTC."""
from collections.abc import AsyncGenerator
import logging
from typing import Any
from unittest.mock import AsyncMock, Mock, patch
@ -10,9 +9,7 @@ from webrtc_models import RTCIceCandidate, RTCIceCandidateInit, RTCIceServer
from homeassistant.components.camera import (
DATA_ICE_SERVERS,
DOMAIN as CAMERA_DOMAIN,
Camera,
CameraEntityFeature,
CameraWebRTCProvider,
StreamType,
WebRTCAnswer,
@ -25,22 +22,12 @@ from homeassistant.components.camera import (
get_camera_from_entity_id,
)
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider
from tests.common import (
MockConfigEntry,
MockModule,
mock_config_flow,
mock_integration,
mock_platform,
setup_test_component_platform,
)
from tests.typing import WebSocketGenerator
WEBRTC_OFFER = "v=0\r\n"
@ -57,84 +44,6 @@ class Go2RTCProvider(SomeTestProvider):
return "go2rtc"
class MockCamera(Camera):
"""Mock Camera Entity."""
_attr_name = "Test"
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
def __init__(self) -> None:
"""Initialize the mock entity."""
super().__init__()
self._sync_answer: str | None | Exception = WEBRTC_ANSWER
def set_sync_answer(self, value: str | None | Exception) -> None:
"""Set sync offer answer."""
self._sync_answer = value
async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None:
"""Handle the WebRTC offer and return the answer."""
if isinstance(self._sync_answer, Exception):
raise self._sync_answer
return self._sync_answer
async def stream_source(self) -> str | None:
"""Return the source of the stream.
This is used by cameras with CameraEntityFeature.STREAM
and StreamType.HLS.
"""
return "rtsp://stream"
@pytest.fixture
async def init_test_integration(
hass: HomeAssistant,
) -> MockCamera:
"""Initialize components."""
entry = MockConfigEntry(domain=TEST_INTEGRATION_DOMAIN)
entry.add_to_hass(hass)
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setups(
config_entry, [CAMERA_DOMAIN]
)
return True
async def async_unload_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Unload test config entry."""
await hass.config_entries.async_forward_entry_unload(
config_entry, CAMERA_DOMAIN
)
return True
mock_integration(
hass,
MockModule(
TEST_INTEGRATION_DOMAIN,
async_setup_entry=async_setup_entry_init,
async_unload_entry=async_unload_entry_init,
),
)
test_camera = MockCamera()
setup_test_component_platform(
hass, CAMERA_DOMAIN, [test_camera], from_config_entry=True
)
mock_platform(hass, f"{TEST_INTEGRATION_DOMAIN}.config_flow", Mock())
with mock_config_flow(TEST_INTEGRATION_DOMAIN, ConfigFlow):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return test_camera
@pytest.mark.usefixtures("mock_camera", "mock_stream_source")
async def test_async_register_webrtc_provider(
hass: HomeAssistant,
@ -302,7 +211,6 @@ async def test_ws_get_client_config(
},
],
},
"getCandidatesUpfront": False,
}
@callback
@ -341,30 +249,6 @@ async def test_ws_get_client_config(
},
],
},
"getCandidatesUpfront": False,
}
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
async def test_ws_get_client_config_sync_offer(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test get WebRTC client config, when camera is supporting sync offer."""
await async_setup_component(hass, "camera", {})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "camera/webrtc/get_client_config", "entity_id": "camera.sync"}
)
msg = await client.receive_json()
# Assert WebSocket response
assert msg["type"] == TYPE_RESULT
assert msg["success"]
assert msg["result"] == {
"configuration": {},
"getCandidatesUpfront": True,
}
@ -391,7 +275,6 @@ async def test_ws_get_client_config_custom_config(
assert msg["success"]
assert msg["result"] == {
"configuration": {"iceServers": [{"urls": ["stun:custom_stun_server:3478"]}]},
"getCandidatesUpfront": False,
}
@ -625,144 +508,6 @@ async def test_websocket_webrtc_offer_missing_offer(
assert response["error"]["code"] == "invalid_format"
@pytest.mark.parametrize(
("error", "expected_message"),
[
(ValueError("value error"), "value error"),
(HomeAssistantError("offer failed"), "offer failed"),
(TimeoutError(), "Timeout handling WebRTC offer"),
],
)
async def test_websocket_webrtc_offer_failure(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
init_test_integration: MockCamera,
error: Exception,
expected_message: str,
) -> None:
"""Test WebRTC stream that fails handling the offer."""
client = await hass_ws_client(hass)
init_test_integration.set_sync_answer(error)
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.test",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["type"] == TYPE_RESULT
assert response["success"]
subscription_id = response["id"]
# Session id
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"]["type"] == "session"
# Error
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"] == {
"type": "error",
"code": "webrtc_offer_failed",
"message": expected_message,
}
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
async def test_websocket_webrtc_offer_sync(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test sync WebRTC stream offer."""
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.sync",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert (
"tests.components.camera.conftest",
logging.WARNING,
(
"async_handle_web_rtc_offer was called from camera, this is a deprecated "
"function which will be removed in HA Core 2025.6. Use "
"async_handle_async_webrtc_offer instead"
),
) in caplog.record_tuples
assert response["type"] == TYPE_RESULT
assert response["success"]
subscription_id = response["id"]
# Session id
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"]["type"] == "session"
# Answer
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"] == {"type": "answer", "answer": WEBRTC_ANSWER}
async def test_websocket_webrtc_offer_sync_no_answer(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
init_test_integration: MockCamera,
) -> None:
"""Test sync WebRTC stream offer with no answer."""
client = await hass_ws_client(hass)
init_test_integration.set_sync_answer(None)
await client.send_json_auto_id(
{
"type": "camera/webrtc/offer",
"entity_id": "camera.test",
"offer": WEBRTC_OFFER,
}
)
response = await client.receive_json()
assert response["type"] == TYPE_RESULT
assert response["success"]
subscription_id = response["id"]
# Session id
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"]["type"] == "session"
# Answer
response = await client.receive_json()
assert response["id"] == subscription_id
assert response["type"] == "event"
assert response["event"] == {
"type": "error",
"code": "webrtc_offer_failed",
"message": "No answer on WebRTC offer",
}
assert (
"homeassistant.components.camera",
logging.ERROR,
"Error handling WebRTC offer: No answer",
) in caplog.record_tuples
@pytest.mark.usefixtures("mock_camera")
async def test_websocket_webrtc_offer_invalid_stream_type(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
@ -901,7 +646,7 @@ async def test_ws_webrtc_candidate_not_supported(
await client.send_json_auto_id(
{
"type": "camera/webrtc/candidate",
"entity_id": "camera.sync",
"entity_id": "camera.async_no_candidate",
"session_id": "session_id",
"candidate": {"candidate": "candidate"},
}