Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to set a stun server for RTSPtoWebRTC #72574

Merged
merged 6 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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") == ""