From e857abb43f9956779535c87b8438752b51aa2a78 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Dec 2025 16:02:30 -0500 Subject: [PATCH] Allow fetching the Cloud ICE servers (#157774) --- homeassistant/components/cloud/client.py | 14 +++++--- homeassistant/components/cloud/http_api.py | 21 ++++++++++++ tests/components/cloud/test_http_api.py | 37 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index a6f9c7a1a79..04353d875fc 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -71,6 +71,7 @@ class CloudClient(Interface): self._google_config_init_lock = asyncio.Lock() self._relayer_region: str | None = None self._cloud_ice_servers_listener: Callable[[], None] | None = None + self._ice_servers: list[RTCIceServer] = [] @property def base_path(self) -> Path: @@ -117,6 +118,11 @@ class CloudClient(Interface): """Return the connected relayer region.""" return self._relayer_region + @property + def ice_servers(self) -> list[RTCIceServer]: + """Return the current ICE servers.""" + return self._ice_servers + async def get_alexa_config(self) -> alexa_config.CloudAlexaConfig: """Return Alexa config.""" if self._alexa_config is None: @@ -203,11 +209,8 @@ class CloudClient(Interface): ice_servers: list[RTCIceServer], ) -> Callable[[], None]: """Register cloud ice server.""" - - def get_ice_servers() -> list[RTCIceServer]: - return ice_servers - - return async_register_ice_servers(self._hass, get_ice_servers) + self._ice_servers = ice_servers + return async_register_ice_servers(self._hass, lambda: self._ice_servers) async def async_register_cloud_ice_servers_listener( prefs: CloudPreferences, @@ -268,6 +271,7 @@ class CloudClient(Interface): async def logout_cleanups(self) -> None: """Cleanup some stuff after logout.""" + self._ice_servers = [] await self.prefs.async_set_username(None) if self._alexa_config: diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 4a8a569a5a6..07c3b1e7b5a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -99,6 +99,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_hook_delete) websocket_api.async_register_command(hass, websocket_remote_connect) websocket_api.async_register_command(hass, websocket_remote_disconnect) + websocket_api.async_register_command(hass, websocket_webrtc_ice_servers) websocket_api.async_register_command(hass, google_assistant_get) websocket_api.async_register_command(hass, google_assistant_list) @@ -1107,6 +1108,7 @@ async def alexa_sync( @websocket_api.websocket_command({"type": "cloud/tts/info"}) +@callback def tts_info( hass: HomeAssistant, connection: websocket_api.ActiveConnection, @@ -1134,3 +1136,22 @@ def tts_info( ) connection.send_result(msg["id"], {"languages": result}) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "cloud/webrtc/ice_servers", + } +) +@_require_cloud_login +@callback +def websocket_webrtc_ice_servers( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle get WebRTC ICE servers websocket command.""" + connection.send_result( + msg["id"], + [server.to_dict() for server in hass.data[DATA_CLOUD].client.ice_servers], + ) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 149a70406a7..f2844264ef0 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -22,6 +22,7 @@ from hass_nabucasa.payments_api import PaymentsApiError from hass_nabucasa.remote import CertificateStatus import pytest from syrupy.assertion import SnapshotAssertion +from webrtc_models import RTCIceServer from homeassistant.components import system_health from homeassistant.components.alexa import errors as alexa_errors @@ -2186,3 +2187,39 @@ async def test_download_support_package_integration_load_error( req = await cloud_client.get("/api/cloud/support_package") assert req.status == HTTPStatus.OK assert await req.text() == snapshot + + +async def test_websocket_ice_servers( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, +) -> None: + """Test getting ICE servers.""" + cloud.client._ice_servers = [ + RTCIceServer(urls="stun:stun.l.bla.com:19302"), + RTCIceServer( + urls="turn:turn.example.com:3478", username="user", credential="pass" + ), + ] + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "cloud/webrtc/ice_servers"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == [ + {"urls": "stun:stun.l.bla.com:19302"}, + { + "urls": "turn:turn.example.com:3478", + "username": "user", + "credential": "pass", + }, + ] + + cloud.id_token = None + + await client.send_json_auto_id({"type": "cloud/webrtc/ice_servers"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "not_logged_in"