diff --git a/CODEOWNERS b/CODEOWNERS
index a9a77d12267..5df8a86a876 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1805,6 +1805,8 @@ build.json @home-assistant/supervisor
/tests/components/weatherflow_cloud/ @jeeftor
/homeassistant/components/weatherkit/ @tjhorner
/tests/components/weatherkit/ @tjhorner
+/homeassistant/components/web_rtc/ @home-assistant/core
+/tests/components/web_rtc/ @home-assistant/core
/homeassistant/components/webdav/ @jpbede
/tests/components/webdav/ @jpbede
/homeassistant/components/webhook/ @home-assistant/core
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index 3e91e94db4e..9362faa1093 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -20,7 +20,7 @@ from aiohttp import hdrs, web
import attr
from propcache.api import cached_property, under_cached_property
import voluptuous as vol
-from webrtc_models import RTCIceCandidateInit, RTCIceServer
+from webrtc_models import RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
@@ -37,6 +37,7 @@ from homeassistant.components.stream import (
Stream,
create_stream,
)
+from homeassistant.components.web_rtc import async_get_ice_servers
from homeassistant.components.websocket_api import ActiveConnection
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -84,7 +85,6 @@ from .prefs import (
get_dynamic_camera_stream_settings,
)
from .webrtc import (
- DATA_ICE_SERVERS,
CameraWebRTCProvider,
WebRTCAnswer, # noqa: F401
WebRTCCandidate, # noqa: F401
@@ -93,7 +93,6 @@ from .webrtc import (
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_provider,
- async_register_ice_servers,
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@@ -400,20 +399,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
SERVICE_RECORD, CAMERA_SERVICE_RECORD, async_handle_record_service
)
- @callback
- def get_ice_servers() -> list[RTCIceServer]:
- if hass.config.webrtc.ice_servers:
- return hass.config.webrtc.ice_servers
- return [
- RTCIceServer(
- urls=[
- "stun:stun.home-assistant.io:3478",
- "stun:stun.home-assistant.io:80",
- ]
- ),
- ]
-
- async_register_ice_servers(hass, get_ice_servers)
return True
@@ -731,11 +716,7 @@ 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()
- ice_servers = [
- server
- for servers in self.hass.data.get(DATA_ICE_SERVERS, [])
- for server in servers()
- ]
+ ice_servers = async_get_ice_servers(self.hass)
config.configuration.ice_servers.extend(ice_servers)
return config
diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json
index fa279a9b205..72ccfd5b02e 100644
--- a/homeassistant/components/camera/manifest.json
+++ b/homeassistant/components/camera/manifest.json
@@ -3,7 +3,7 @@
"name": "Camera",
"after_dependencies": ["media_player"],
"codeowners": ["@home-assistant/core"],
- "dependencies": ["http"],
+ "dependencies": ["http", "web_rtc"],
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py
index c2de5eac0a0..796a5160c07 100644
--- a/homeassistant/components/camera/webrtc.py
+++ b/homeassistant/components/camera/webrtc.py
@@ -4,7 +4,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
-from collections.abc import Awaitable, Callable, Iterable
+from collections.abc import Awaitable, Callable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
@@ -12,12 +12,7 @@ from typing import TYPE_CHECKING, Any
from mashumaro import MissingField
import voluptuous as vol
-from webrtc_models import (
- RTCConfiguration,
- RTCIceCandidate,
- RTCIceCandidateInit,
- RTCIceServer,
-)
+from webrtc_models import RTCConfiguration, RTCIceCandidate, RTCIceCandidateInit
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
@@ -38,9 +33,6 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
-DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
- "camera_webrtc_ice_servers"
-)
_WEBRTC = "WebRTC"
@@ -367,21 +359,3 @@ async def async_get_supported_provider(
return provider
return None
-
-
-@callback
-def async_register_ice_servers(
- hass: HomeAssistant,
- get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
-) -> Callable[[], None]:
- """Register a ICE server.
-
- The registering integration is responsible to implement caching if needed.
- """
- servers = hass.data.setdefault(DATA_ICE_SERVERS, [])
-
- def remove() -> None:
- servers.remove(get_ice_server_fn)
-
- servers.append(get_ice_server_fn)
- return remove
diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py
index 04353d875fc..550bb9fc8d7 100644
--- a/homeassistant/components/cloud/client.py
+++ b/homeassistant/components/cloud/client.py
@@ -19,8 +19,8 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
-from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
+from homeassistant.components.web_rtc import async_register_ice_servers
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json
index b148c7957bf..bcf2d015808 100644
--- a/homeassistant/components/cloud/manifest.json
+++ b/homeassistant/components/cloud/manifest.json
@@ -8,7 +8,7 @@
"google_assistant"
],
"codeowners": ["@home-assistant/cloud"],
- "dependencies": ["auth", "http", "repairs", "webhook"],
+ "dependencies": ["auth", "http", "repairs", "webhook", "web_rtc"],
"documentation": "https://www.home-assistant.io/integrations/cloud",
"integration_type": "system",
"iot_class": "cloud_push",
diff --git a/homeassistant/components/web_rtc/__init__.py b/homeassistant/components/web_rtc/__init__.py
new file mode 100644
index 00000000000..8b684cbda3c
--- /dev/null
+++ b/homeassistant/components/web_rtc/__init__.py
@@ -0,0 +1,138 @@
+"""The WebRTC integration."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterable
+from typing import Any
+
+import voluptuous as vol
+from webrtc_models import RTCIceServer
+
+from homeassistant.components import websocket_api
+from homeassistant.const import CONF_URL, CONF_USERNAME
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.core_config import (
+ CONF_CREDENTIAL,
+ CONF_ICE_SERVERS,
+ validate_stun_or_turn_url,
+)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.typing import ConfigType
+from homeassistant.util.hass_dict import HassKey
+
+__all__ = [
+ "async_get_ice_servers",
+ "async_register_ice_servers",
+]
+
+DOMAIN = "web_rtc"
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_ICE_SERVERS): vol.All(
+ cv.ensure_list,
+ [
+ vol.Schema(
+ {
+ vol.Required(CONF_URL): vol.All(
+ cv.ensure_list, [validate_stun_or_turn_url]
+ ),
+ vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_CREDENTIAL): cv.string,
+ }
+ )
+ ],
+ )
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+DATA_ICE_SERVERS_USER: HassKey[Iterable[RTCIceServer]] = HassKey(
+ "web_rtc_ice_servers_user"
+)
+DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
+ "web_rtc_ice_servers"
+)
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up the WebRTC integration."""
+ servers = [
+ RTCIceServer(
+ server[CONF_URL],
+ server.get(CONF_USERNAME),
+ server.get(CONF_CREDENTIAL),
+ )
+ for server in config.get(DOMAIN, {}).get(CONF_ICE_SERVERS, [])
+ ]
+ if servers:
+ hass.data[DATA_ICE_SERVERS_USER] = servers
+
+ hass.data[DATA_ICE_SERVERS] = []
+ websocket_api.async_register_command(hass, ws_ice_servers)
+ return True
+
+
+@callback
+def async_register_ice_servers(
+ hass: HomeAssistant,
+ get_ice_server_fn: Callable[[], Iterable[RTCIceServer]],
+) -> Callable[[], None]:
+ """Register an ICE server.
+
+ The registering integration is responsible to implement caching if needed.
+ """
+ servers = hass.data[DATA_ICE_SERVERS]
+
+ def remove() -> None:
+ servers.remove(get_ice_server_fn)
+
+ servers.append(get_ice_server_fn)
+ return remove
+
+
+@callback
+def async_get_ice_servers(hass: HomeAssistant) -> list[RTCIceServer]:
+ """Return all registered ICE servers."""
+ servers: list[RTCIceServer] = []
+
+ if hass.config.webrtc.ice_servers:
+ servers.extend(hass.config.webrtc.ice_servers)
+
+ if DATA_ICE_SERVERS_USER in hass.data:
+ servers.extend(hass.data[DATA_ICE_SERVERS_USER])
+
+ if not servers:
+ servers = [
+ RTCIceServer(
+ urls=[
+ "stun:stun.home-assistant.io:3478",
+ "stun:stun.home-assistant.io:80",
+ ]
+ ),
+ ]
+
+ for gen_servers in hass.data[DATA_ICE_SERVERS]:
+ servers.extend(gen_servers())
+
+ return servers
+
+
+@websocket_api.websocket_command(
+ {
+ "type": "web_rtc/ice_servers",
+ }
+)
+@callback
+def ws_ice_servers(
+ hass: HomeAssistant,
+ connection: websocket_api.ActiveConnection,
+ msg: dict[str, Any],
+) -> None:
+ """Handle get WebRTC ICE servers websocket command."""
+ ice_servers = [server.to_dict() for server in async_get_ice_servers(hass)]
+ connection.send_result(msg["id"], ice_servers)
diff --git a/homeassistant/components/web_rtc/manifest.json b/homeassistant/components/web_rtc/manifest.json
new file mode 100644
index 00000000000..7944cd7e704
--- /dev/null
+++ b/homeassistant/components/web_rtc/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "web_rtc",
+ "name": "WebRTC",
+ "codeowners": ["@home-assistant/core"],
+ "documentation": "https://www.home-assistant.io/integrations/web_rtc",
+ "integration_type": "system",
+ "quality_scale": "internal"
+}
diff --git a/homeassistant/core_config.py b/homeassistant/core_config.py
index ce28618e455..bb1386d235e 100644
--- a/homeassistant/core_config.py
+++ b/homeassistant/core_config.py
@@ -249,7 +249,7 @@ def _validate_currency(data: Any) -> Any:
raise
-def _validate_stun_or_turn_url(value: Any) -> str:
+def validate_stun_or_turn_url(value: Any) -> str:
"""Validate an URL."""
url_in = str(value)
url = urlparse(url_in)
@@ -331,7 +331,7 @@ CORE_CONFIG_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_URL): vol.All(
- cv.ensure_list, [_validate_stun_or_turn_url]
+ cv.ensure_list, [validate_stun_or_turn_url]
),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CREDENTIAL): cv.string,
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index 5828ed329bc..05f89f250f6 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -115,6 +115,7 @@ NO_IOT_CLASS = [
"tag",
"timer",
"trace",
+ "web_rtc",
"webhook",
"websocket_api",
"zone",
diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py
index e5aca4ef685..cf88614128f 100644
--- a/script/hassfest/quality_scale.py
+++ b/script/hassfest/quality_scale.py
@@ -2196,6 +2196,7 @@ NO_QUALITY_SCALE = [
"timer",
"trace",
"usage_prediction",
+ "web_rtc",
"webhook",
"websocket_api",
"zone",
diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py
index 8ddc03dc628..b43c043fdeb 100644
--- a/tests/components/camera/test_webrtc.py
+++ b/tests/components/camera/test_webrtc.py
@@ -8,7 +8,6 @@ import pytest
from webrtc_models import RTCIceCandidate, RTCIceCandidateInit, RTCIceServer
from homeassistant.components.camera import (
- DATA_ICE_SERVERS,
Camera,
CameraWebRTCProvider,
StreamType,
@@ -17,10 +16,10 @@ from homeassistant.components.camera import (
WebRTCError,
WebRTCMessage,
WebRTCSendMessage,
- async_register_ice_servers,
async_register_webrtc_provider,
get_camera_from_entity_id,
)
+from homeassistant.components.web_rtc import async_register_ice_servers
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config
@@ -101,89 +100,6 @@ async def test_async_register_webrtc_provider_camera_not_loaded(
async_register_webrtc_provider(hass, SomeTestProvider())
-@pytest.mark.usefixtures("mock_test_webrtc_cameras")
-async def test_async_register_ice_server(
- hass: HomeAssistant,
-) -> None:
- """Test registering an ICE server."""
- # Clear any existing ICE servers
- hass.data[DATA_ICE_SERVERS].clear()
-
- called = 0
-
- @callback
- def get_ice_servers() -> list[RTCIceServer]:
- nonlocal called
- called += 1
- return [
- RTCIceServer(urls="stun:example.com"),
- RTCIceServer(urls="turn:example.com"),
- ]
-
- unregister = async_register_ice_servers(hass, get_ice_servers)
- assert not called
-
- camera = get_camera_from_entity_id(hass, "camera.async")
- config = camera.async_get_webrtc_client_configuration()
-
- assert config.configuration.ice_servers == [
- RTCIceServer(urls="stun:example.com"),
- RTCIceServer(urls="turn:example.com"),
- ]
- assert called == 1
-
- # register another ICE server
- called_2 = 0
-
- @callback
- def get_ice_servers_2() -> list[RTCIceServer]:
- nonlocal called_2
- called_2 += 1
- return [
- RTCIceServer(
- urls=["stun:example2.com", "turn:example2.com"],
- username="user",
- credential="pass",
- )
- ]
-
- unregister_2 = async_register_ice_servers(hass, get_ice_servers_2)
-
- config = camera.async_get_webrtc_client_configuration()
- assert config.configuration.ice_servers == [
- RTCIceServer(urls="stun:example.com"),
- RTCIceServer(urls="turn:example.com"),
- RTCIceServer(
- urls=["stun:example2.com", "turn:example2.com"],
- username="user",
- credential="pass",
- ),
- ]
- assert called == 2
- assert called_2 == 1
-
- # unregister the first ICE server
-
- unregister()
-
- config = camera.async_get_webrtc_client_configuration()
- assert config.configuration.ice_servers == [
- RTCIceServer(
- urls=["stun:example2.com", "turn:example2.com"],
- username="user",
- credential="pass",
- ),
- ]
- assert called == 2
- assert called_2 == 2
-
- # unregister the second ICE server
- unregister_2()
-
- config = camera.async_get_webrtc_client_configuration()
- assert config.configuration.ice_servers == []
-
-
@pytest.mark.usefixtures("mock_test_webrtc_cameras")
async def test_ws_get_client_config(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
diff --git a/tests/components/cloud/snapshots/test_http_api.ambr b/tests/components/cloud/snapshots/test_http_api.ambr
index 2876ba20eb8..2249cc6f9fe 100644
--- a/tests/components/cloud/snapshots/test_http_api.ambr
+++ b/tests/components/cloud/snapshots/test_http_api.ambr
@@ -21,7 +21,7 @@
## Active Integrations
- Built-in integrations: 21
+ Built-in integrations: 22
Custom integrations: 1
Built-in integrations
@@ -48,6 +48,7 @@
stt | Speech-to-text (STT)
system_health | System Health
tts | Text-to-speech (TTS)
+ web_rtc | WebRTC
webhook | Webhook
@@ -122,7 +123,7 @@
## Active Integrations
- Built-in integrations: 21
+ Built-in integrations: 22
Custom integrations: 0
Built-in integrations
@@ -149,6 +150,7 @@
stt | Speech-to-text (STT)
system_health | System Health
tts | Text-to-speech (TTS)
+ web_rtc | WebRTC
webhook | Webhook
diff --git a/tests/components/web_rtc/__init__.py b/tests/components/web_rtc/__init__.py
new file mode 100644
index 00000000000..ff0593f07e9
--- /dev/null
+++ b/tests/components/web_rtc/__init__.py
@@ -0,0 +1 @@
+"""Tests for the WebRTC integration."""
diff --git a/tests/components/web_rtc/test_init.py b/tests/components/web_rtc/test_init.py
new file mode 100644
index 00000000000..01f45977854
--- /dev/null
+++ b/tests/components/web_rtc/test_init.py
@@ -0,0 +1,250 @@
+"""Test the WebRTC integration."""
+
+from webrtc_models import RTCIceServer
+
+from homeassistant.components.web_rtc import (
+ async_get_ice_servers,
+ async_register_ice_servers,
+)
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.core_config import async_process_ha_core_config
+from homeassistant.setup import async_setup_component
+
+from tests.typing import WebSocketGenerator
+
+
+async def test_async_setup(hass: HomeAssistant) -> None:
+ """Test setting up the web_rtc integration."""
+ assert await async_setup_component(hass, "web_rtc", {})
+ await hass.async_block_till_done()
+
+ # Verify default ICE servers are registered
+ ice_servers = async_get_ice_servers(hass)
+ assert len(ice_servers) == 1
+ assert ice_servers[0].urls == [
+ "stun:stun.home-assistant.io:3478",
+ "stun:stun.home-assistant.io:80",
+ ]
+
+
+async def test_async_setup_custom_ice_servers_core(hass: HomeAssistant) -> None:
+ """Test setting up web_rtc with custom ICE servers in config."""
+ await async_process_ha_core_config(
+ hass,
+ {"webrtc": {"ice_servers": [{"url": "stun:custom_stun_server:3478"}]}},
+ )
+
+ assert await async_setup_component(hass, "web_rtc", {})
+ await hass.async_block_till_done()
+
+ ice_servers = async_get_ice_servers(hass)
+ assert len(ice_servers) == 1
+ assert ice_servers[0].urls == ["stun:custom_stun_server:3478"]
+
+
+async def test_async_setup_custom_ice_servers_integration(hass: HomeAssistant) -> None:
+ """Test setting up web_rtc with custom ICE servers in config."""
+ assert await async_setup_component(
+ hass,
+ "web_rtc",
+ {
+ "web_rtc": {
+ "ice_servers": [
+ {"url": "stun:custom_stun_server:3478"},
+ {
+ "url": "stun:custom_stun_server:3478",
+ "credential": "mock-credential",
+ },
+ {
+ "url": "stun:custom_stun_server:3478",
+ "username": "mock-username",
+ },
+ {
+ "url": "stun:custom_stun_server:3478",
+ "credential": "mock-credential",
+ "username": "mock-username",
+ },
+ ]
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ ice_servers = async_get_ice_servers(hass)
+ assert ice_servers == [
+ RTCIceServer(
+ urls=["stun:custom_stun_server:3478"],
+ ),
+ RTCIceServer(
+ urls=["stun:custom_stun_server:3478"],
+ credential="mock-credential",
+ ),
+ RTCIceServer(
+ urls=["stun:custom_stun_server:3478"],
+ username="mock-username",
+ ),
+ RTCIceServer(
+ urls=["stun:custom_stun_server:3478"],
+ username="mock-username",
+ credential="mock-credential",
+ ),
+ ]
+
+
+async def test_async_setup_custom_ice_servers_core_and_integration(
+ hass: HomeAssistant,
+) -> None:
+ """Test setting up web_rtc with custom ICE servers in config."""
+ await async_process_ha_core_config(
+ hass,
+ {"webrtc": {"ice_servers": [{"url": "stun:custom_stun_server_core:3478"}]}},
+ )
+
+ assert await async_setup_component(
+ hass,
+ "web_rtc",
+ {
+ "web_rtc": {
+ "ice_servers": [{"url": "stun:custom_stun_server_integration:3478"}]
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ ice_servers = async_get_ice_servers(hass)
+ assert ice_servers == [
+ RTCIceServer(
+ urls=["stun:custom_stun_server_core:3478"],
+ ),
+ RTCIceServer(
+ urls=["stun:custom_stun_server_integration:3478"],
+ ),
+ ]
+
+
+async def test_async_register_ice_servers(hass: HomeAssistant) -> None:
+ """Test registering ICE servers."""
+ assert await async_setup_component(hass, "web_rtc", {})
+ await hass.async_block_till_done()
+ default_servers = async_get_ice_servers(hass)
+
+ called = 0
+
+ @callback
+ def get_ice_servers() -> list[RTCIceServer]:
+ nonlocal called
+ called += 1
+ return [
+ RTCIceServer(urls="stun:example.com"),
+ RTCIceServer(urls="turn:example.com"),
+ ]
+
+ unregister = async_register_ice_servers(hass, get_ice_servers)
+ assert called == 0
+
+ # Getting ice servers should call the callback
+ ice_servers = async_get_ice_servers(hass)
+ assert called == 1
+ assert ice_servers == [
+ *default_servers,
+ RTCIceServer(urls="stun:example.com"),
+ RTCIceServer(urls="turn:example.com"),
+ ]
+
+ # Unregister and verify servers are removed
+ unregister()
+ ice_servers = async_get_ice_servers(hass)
+ assert ice_servers == default_servers
+
+
+async def test_multiple_ice_server_registrations(hass: HomeAssistant) -> None:
+ """Test registering multiple ICE server providers."""
+ assert await async_setup_component(hass, "web_rtc", {})
+ await hass.async_block_till_done()
+ default_servers = async_get_ice_servers(hass)
+
+ @callback
+ def get_ice_servers_1() -> list[RTCIceServer]:
+ return [RTCIceServer(urls="stun:server1.com")]
+
+ @callback
+ def get_ice_servers_2() -> list[RTCIceServer]:
+ return [
+ RTCIceServer(
+ urls=["stun:server2.com", "turn:server2.com"],
+ username="user",
+ credential="pass",
+ )
+ ]
+
+ unregister_1 = async_register_ice_servers(hass, get_ice_servers_1)
+ unregister_2 = async_register_ice_servers(hass, get_ice_servers_2)
+
+ ice_servers = async_get_ice_servers(hass)
+ assert ice_servers == [
+ *default_servers,
+ RTCIceServer(urls="stun:server1.com"),
+ RTCIceServer(
+ urls=["stun:server2.com", "turn:server2.com"],
+ username="user",
+ credential="pass",
+ ),
+ ]
+
+ # Unregister first provider
+ unregister_1()
+ ice_servers = async_get_ice_servers(hass)
+ assert ice_servers == [
+ *default_servers,
+ RTCIceServer(
+ urls=["stun:server2.com", "turn:server2.com"],
+ username="user",
+ credential="pass",
+ ),
+ ]
+
+ # Unregister second provider
+ unregister_2()
+ ice_servers = async_get_ice_servers(hass)
+ assert ice_servers == default_servers
+
+
+async def test_ws_ice_servers_with_registered_servers(
+ hass: HomeAssistant, hass_ws_client: WebSocketGenerator
+) -> None:
+ """Test WebSocket ICE servers endpoint with registered servers."""
+ assert await async_setup_component(hass, "web_rtc", {})
+ await hass.async_block_till_done()
+
+ @callback
+ def get_ice_server() -> list[RTCIceServer]:
+ return [
+ RTCIceServer(
+ urls=["stun:example2.com", "turn:example2.com"],
+ username="user",
+ credential="pass",
+ )
+ ]
+
+ async_register_ice_servers(hass, get_ice_server)
+
+ client = await hass_ws_client(hass)
+ await client.send_json_auto_id({"type": "web_rtc/ice_servers"})
+ msg = await client.receive_json()
+
+ # Assert WebSocket response includes registered ICE servers
+ assert msg["type"] == "result"
+ assert msg["success"]
+ assert msg["result"] == [
+ {
+ "urls": [
+ "stun:stun.home-assistant.io:3478",
+ "stun:stun.home-assistant.io:80",
+ ]
+ },
+ {
+ "urls": ["stun:example2.com", "turn:example2.com"],
+ "username": "user",
+ "credential": "pass",
+ },
+ ]
diff --git a/tests/test_core_config.py b/tests/test_core_config.py
index b20503121fc..ab099cc2aab 100644
--- a/tests/test_core_config.py
+++ b/tests/test_core_config.py
@@ -34,8 +34,8 @@ from homeassistant.core_config import (
DATA_CUSTOMIZE,
Config,
ConfigSource,
- _validate_stun_or_turn_url,
async_process_ha_core_config,
+ validate_stun_or_turn_url,
)
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entity import Entity, EntityPlatformState
@@ -175,8 +175,8 @@ def test_webrtc_schema() -> None:
assert validated["webrtc"] == validated_webrtc
-def test_validate_stun_or_turn_url() -> None:
- """Test _validate_stun_or_turn_url."""
+def testvalidate_stun_or_turn_url() -> None:
+ """Test validate_stun_or_turn_url."""
invalid_urls = (
"custom_stun_server",
"custom_stun_server:3478",
@@ -203,10 +203,10 @@ def test_validate_stun_or_turn_url() -> None:
for url in invalid_urls:
with pytest.raises(Invalid):
- _validate_stun_or_turn_url(url)
+ validate_stun_or_turn_url(url)
for url in valid_urls:
- assert _validate_stun_or_turn_url(url) == url
+ assert validate_stun_or_turn_url(url) == url
def test_customize_glob_is_ordered() -> None: