From 90637a721c0a0890bfd1dbc34294bda19787df0a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 3 Oct 2022 18:10:28 -0700 Subject: [PATCH] Add option to set a stun server for RTSPtoWebRTC (#72574) --- .../components/rtsp_to_webrtc/__init__.py | 35 +++++++++- .../components/rtsp_to_webrtc/config_flow.py | 42 ++++++++++- .../components/rtsp_to_webrtc/strings.json | 9 +++ .../rtsp_to_webrtc/translations/en.json | 9 +++ tests/components/rtsp_to_webrtc/conftest.py | 15 +++- .../rtsp_to_webrtc/test_config_flow.py | 46 +++++++++++++ tests/components/rtsp_to_webrtc/test_init.py | 69 ++++++++++++++++++- 7 files changed, 219 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py index 185cfcb0240188..f0e013fc02f141 100644 --- a/homeassistant/components/rtsp_to_webrtc/__init__.py +++ b/homeassistant/components/rtsp_to_webrtc/__init__.py @@ -24,10 +24,11 @@ from rtsp_to_webrtc.client import get_adaptive_client from rtsp_to_webrtc.exceptions import ClientError, ResponseError from rtsp_to_webrtc.interface import WebRTCClientInterface +import voluptuous as vol -from homeassistant.components import camera +from homeassistant.components import camera, websocket_api from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -37,6 +38,7 @@ DATA_SERVER_URL = "server_url" DATA_UNSUB = "unsub" TIMEOUT = 10 +CONF_STUN_SERVER = "stun_server" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -54,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (TimeoutError, ClientError) as err: raise ConfigEntryNotReady from err + hass.data[DOMAIN][CONF_STUN_SERVER] = entry.options.get(CONF_STUN_SERVER, "") + async def async_offer_for_stream_source( stream_source: str, offer_sdp: str, @@ -78,10 +82,37 @@ async def async_offer_for_stream_source( hass, DOMAIN, async_offer_for_stream_source ) ) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + + websocket_api.async_register_command(hass, ws_get_settings) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if DOMAIN in hass.data: + del hass.data[DOMAIN] return True + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry when options change.""" + if hass.data[DOMAIN][CONF_STUN_SERVER] != entry.options.get(CONF_STUN_SERVER, ""): + await hass.config_entries.async_reload(entry.entry_id) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "rtsp_to_webrtc/get_settings", + } +) +@callback +def ws_get_settings( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle the websocket command.""" + connection.send_result( + msg["id"], + {CONF_STUN_SERVER: hass.data.get(DOMAIN, {}).get(CONF_STUN_SERVER, "")}, + ) diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index 815c5e5db7b101..865a6bafcb6ab2 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -11,10 +11,11 @@ from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import DATA_SERVER_URL, DOMAIN +from . import CONF_STUN_SERVER, DATA_SERVER_URL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -104,3 +105,42 @@ async def async_step_hassio_confirm( title=self._hassio_discovery["addon"], data={DATA_SERVER_URL: url}, ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create an options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """RTSPtoWeb Options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_STUN_SERVER, + description={ + "suggested_value": self.config_entry.options.get( + CONF_STUN_SERVER + ), + }, + ): str, + } + ), + ) diff --git a/homeassistant/components/rtsp_to_webrtc/strings.json b/homeassistant/components/rtsp_to_webrtc/strings.json index 5ef91eaf2060ae..939c30766e278d 100644 --- a/homeassistant/components/rtsp_to_webrtc/strings.json +++ b/homeassistant/components/rtsp_to_webrtc/strings.json @@ -23,5 +23,14 @@ "server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.", "server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information." } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/en.json b/homeassistant/components/rtsp_to_webrtc/translations/en.json index c54983d63d35f3..a519883b764d4d 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/en.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/en.json @@ -23,5 +23,14 @@ "title": "Configure RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } \ No newline at end of file diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index 5e737efc39703e..5a0d6de01df991 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -65,9 +65,20 @@ async def config_entry_data() -> dict[str, Any]: @pytest.fixture -async def config_entry(config_entry_data: dict[str, Any]) -> MockConfigEntry: +def config_entry_options() -> dict[str, Any] | None: + """Fixture to set initial config entry options.""" + return None + + +@pytest.fixture +async def config_entry( + config_entry_data: dict[str, Any], + config_entry_options: dict[str, Any] | None, +) -> MockConfigEntry: """Fixture for MockConfigEntry.""" - return MockConfigEntry(domain=DOMAIN, data=config_entry_data) + return MockConfigEntry( + domain=DOMAIN, data=config_entry_data, options=config_entry_options + ) @pytest.fixture diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index a6cd4d6798f8ff..cca6395c3177dd 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -9,8 +9,11 @@ from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.rtsp_to_webrtc import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from .conftest import ComponentSetup + from tests.common import MockConfigEntry @@ -212,3 +215,46 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result.get("type") == "abort" assert result.get("reason") == "server_failure" + + +async def test_options_flow( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_integration: ComponentSetup, +) -> None: + """Test setting stun server in options flow.""" + with patch( + "homeassistant.components.rtsp_to_webrtc.async_setup_entry", + return_value=True, + ): + await setup_integration() + + assert config_entry.state is ConfigEntryState.LOADED + assert not config_entry.options + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + data_schema = result["data_schema"].schema + assert set(data_schema) == {"stun_server"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "stun_server": "example.com:1234", + }, + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + assert config_entry.options == {"stun_server": "example.com:1234"} + + # Clear the value + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + assert config_entry.options == {} diff --git a/tests/components/rtsp_to_webrtc/test_init.py b/tests/components/rtsp_to_webrtc/test_init.py index 759fea7c813141..afa365a3044e1b 100644 --- a/tests/components/rtsp_to_webrtc/test_init.py +++ b/tests/components/rtsp_to_webrtc/test_init.py @@ -11,13 +11,14 @@ import pytest import rtsp_to_webrtc -from homeassistant.components.rtsp_to_webrtc import DOMAIN +from homeassistant.components.rtsp_to_webrtc import CONF_STUN_SERVER, DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker # The webrtc component does not inspect the details of the offer and answer, @@ -154,3 +155,69 @@ async def test_offer_failure( assert response["error"].get("code") == "web_rtc_offer_failed" assert "message" in response["error"] assert "RTSPtoWebRTC server communication failure" in response["error"]["message"] + + +async def test_no_stun_server( + hass: HomeAssistant, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, + hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 2, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 2 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "" + + +@pytest.mark.parametrize( + "config_entry_options", [{CONF_STUN_SERVER: "example.com:1234"}] +) +async def test_stun_server( + hass: HomeAssistant, + rtsp_to_webrtc_client: Any, + setup_integration: ComponentSetup, + config_entry: MockConfigEntry, + hass_ws_client: Callable[[...], Awaitable[aiohttp.ClientWebSocketResponse]], +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 3, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 3 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == "example.com:1234" + + # Simulate an options flow change, clearing the stun server and verify the change is reflected + hass.config_entries.async_update_entry(config_entry, options={}) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 4, + "type": "rtsp_to_webrtc/get_settings", + } + ) + response = await client.receive_json() + assert response.get("id") == 4 + assert response.get("type") == TYPE_RESULT + assert "result" in response + assert response["result"].get("stun_server") == ""