Add option to set a stun server for RTSPtoWebRTC (#72574)
This commit is contained in:
parent
3c07d40fe7
commit
90637a721c
7 changed files with 219 additions and 6 deletions
|
@ -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, "")},
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,5 +23,14 @@
|
|||
"title": "Configure RTSPtoWebRTC"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"stun_server": "Stun server address (host:port)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 == {}
|
||||
|
|
|
@ -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") == ""
|
||||
|
|
Loading…
Add table
Reference in a new issue