mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add option to set a stun server for RTSPtoWebRTC (#72574)
This commit is contained in:
parent
3c07d40fe7
commit
90637a721c
@ -24,10 +24,11 @@ import async_timeout
|
|||||||
from rtsp_to_webrtc.client import get_adaptive_client
|
from rtsp_to_webrtc.client import get_adaptive_client
|
||||||
from rtsp_to_webrtc.exceptions import ClientError, ResponseError
|
from rtsp_to_webrtc.exceptions import ClientError, ResponseError
|
||||||
from rtsp_to_webrtc.interface import WebRTCClientInterface
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ DOMAIN = "rtsp_to_webrtc"
|
|||||||
DATA_SERVER_URL = "server_url"
|
DATA_SERVER_URL = "server_url"
|
||||||
DATA_UNSUB = "unsub"
|
DATA_UNSUB = "unsub"
|
||||||
TIMEOUT = 10
|
TIMEOUT = 10
|
||||||
|
CONF_STUN_SERVER = "stun_server"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
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:
|
except (TimeoutError, ClientError) as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
|
hass.data[DOMAIN][CONF_STUN_SERVER] = entry.options.get(CONF_STUN_SERVER, "")
|
||||||
|
|
||||||
async def async_offer_for_stream_source(
|
async def async_offer_for_stream_source(
|
||||||
stream_source: str,
|
stream_source: str,
|
||||||
offer_sdp: 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
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
|
if DOMAIN in hass.data:
|
||||||
|
del hass.data[DOMAIN]
|
||||||
return True
|
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 import config_entries
|
||||||
from homeassistant.components.hassio import HassioServiceInfo
|
from homeassistant.components.hassio import HassioServiceInfo
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -104,3 +105,42 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
title=self._hassio_discovery["addon"],
|
title=self._hassio_discovery["addon"],
|
||||||
data={DATA_SERVER_URL: url},
|
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_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.",
|
||||||
"server_unreachable": "Unable to communicate with RTSPtoWebRTC server. 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"
|
"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
|
@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."""
|
"""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
|
@pytest.fixture
|
||||||
|
@ -9,8 +9,11 @@ import rtsp_to_webrtc
|
|||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.hassio import HassioServiceInfo
|
from homeassistant.components.hassio import HassioServiceInfo
|
||||||
from homeassistant.components.rtsp_to_webrtc import DOMAIN
|
from homeassistant.components.rtsp_to_webrtc import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .conftest import ComponentSetup
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
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"], {})
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
assert result.get("type") == "abort"
|
assert result.get("type") == "abort"
|
||||||
assert result.get("reason") == "server_failure"
|
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 pytest
|
||||||
import rtsp_to_webrtc
|
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.components.websocket_api.const import TYPE_RESULT
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup
|
from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
# The webrtc component does not inspect the details of the offer and answer,
|
# 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 response["error"].get("code") == "web_rtc_offer_failed"
|
||||||
assert "message" in response["error"]
|
assert "message" in response["error"]
|
||||||
assert "RTSPtoWebRTC server communication failure" in response["error"]["message"]
|
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…
x
Reference in New Issue
Block a user