Skip to content

Commit

Permalink
Add option to set a stun server for RTSPtoWebRTC (#72574)
Browse files Browse the repository at this point in the history
  • Loading branch information
allenporter authored Oct 4, 2022
1 parent 3c07d40 commit 90637a7
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 6 deletions.
35 changes: 33 additions & 2 deletions homeassistant/components/rtsp_to_webrtc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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, "")},
)
42 changes: 41 additions & 1 deletion homeassistant/components/rtsp_to_webrtc/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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,
}
),
)
9 changes: 9 additions & 0 deletions homeassistant/components/rtsp_to_webrtc/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
}
}
}
}
9 changes: 9 additions & 0 deletions homeassistant/components/rtsp_to_webrtc/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,14 @@
"title": "Configure RTSPtoWebRTC"
}
}
},
"options": {
"step": {
"init": {
"data": {
"stun_server": "Stun server address (host:port)"
}
}
}
}
}
15 changes: 13 additions & 2 deletions tests/components/rtsp_to_webrtc/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions tests/components/rtsp_to_webrtc/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 == {}
69 changes: 68 additions & 1 deletion tests/components/rtsp_to_webrtc/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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") == ""

0 comments on commit 90637a7

Please sign in to comment.