Add option to set a stun server for RTSPtoWebRTC (#72574)

This commit is contained in:
Allen Porter 2022-10-03 18:10:28 -07:00 committed by GitHub
parent 3c07d40fe7
commit 90637a721c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 219 additions and 6 deletions

View file

@ -24,10 +24,11 @@ import async_timeout
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 @@ DOMAIN = "rtsp_to_webrtc"
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_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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, "")},
)

View file

@ -11,10 +11,11 @@ import voluptuous as vol
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 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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,
}
),
)

View file

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

View file

@ -23,5 +23,14 @@
"title": "Configure RTSPtoWebRTC"
}
}
},
"options": {
"step": {
"init": {
"data": {
"stun_server": "Stun server address (host:port)"
}
}
}
}
}

View file

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

View file

@ -9,8 +9,11 @@ import rtsp_to_webrtc
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 == {}

View file

@ -11,13 +11,14 @@ import aiohttp
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") == ""