Skip to content
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
2 changes: 1 addition & 1 deletion Dockerfile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 7 additions & 30 deletions homeassistant/components/go2rtc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,35 +60,6 @@
_LOGGER = logging.getLogger(__name__)

_FFMPEG = "ffmpeg"
_SUPPORTED_STREAMS = frozenset(
(
"bubble",
"dvrip",
"expr",
_FFMPEG,
"gopro",
"homekit",
"http",
"https",
"httpx",
"isapi",
"ivideon",
"kasa",
"nest",
"onvif",
"roborock",
"rtmp",
"rtmps",
"rtmpx",
"rtsp",
"rtsps",
"rtspx",
"tapo",
"tcp",
"webrtc",
"webtorrent",
)
)

CONFIG_SCHEMA = vol.Schema(
{
Expand Down Expand Up @@ -197,6 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bo
return False

provider = entry.runtime_data = WebRTCProvider(hass, url, session, client)
await provider.initialize()
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provider.initialize() call is not wrapped in error handling. If the schemes.list() API call fails, it will raise an unhandled exception and cause the config entry setup to fail without a proper error message or ConfigEntryNotReady exception.

Consider wrapping this call in a try-except block similar to the pattern used above for client.validate_server_version():

try:
    await provider.initialize()
except Go2RtcClientError as err:
    if isinstance(err.__cause__, _RETRYABLE_ERRORS):
        raise ConfigEntryNotReady(
            f"Could not connect to go2rtc instance on {url}"
        ) from err
    _LOGGER.warning("Could not initialize go2rtc provider on %s (%s)", url, err)
    return False
except Exception as err:  # noqa: BLE001
    _LOGGER.warning("Could not initialize go2rtc provider on %s (%s)", url, err)
    return False
Suggested change
await provider.initialize()
try:
await provider.initialize()
except Go2RtcClientError as err:
if isinstance(err.__cause__, _RETRYABLE_ERRORS):
raise ConfigEntryNotReady(
f"Could not connect to go2rtc instance on {url}"
) from err
_LOGGER.warning("Could not initialize go2rtc provider on %s (%s)", url, err)
return False
except Exception as err: # noqa: BLE001
_LOGGER.warning("Could not initialize go2rtc provider on %s (%s)", url, err)
return False

Copilot uses AI. Check for mistakes.
entry.async_on_unload(async_register_webrtc_provider(hass, provider))
return True

Expand Down Expand Up @@ -228,16 +200,21 @@ def __init__(
self._session = session
self._rest_client = rest_client
self._sessions: dict[str, Go2RtcWsClient] = {}
self._supported_schemes: set[str] = set()

@property
def domain(self) -> str:
"""Return the integration domain of the provider."""
return DOMAIN

async def initialize(self) -> None:
"""Initialize the provider."""
self._supported_schemes = await self._rest_client.schemes.list()

@callback
def async_is_supported(self, stream_source: str) -> bool:
"""Return if this provider is supports the Camera as source."""
return stream_source.partition(":")[0] in _SUPPORTED_STREAMS
return stream_source.partition(":")[0] in self._supported_schemes

async def async_handle_async_webrtc_offer(
self,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/go2rtc/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
RECOMMENDED_VERSION = "1.9.11"
RECOMMENDED_VERSION = "1.9.12"
2 changes: 1 addition & 1 deletion homeassistant/components/go2rtc/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"integration_type": "system",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["go2rtc-client==0.2.1"],
"requirements": ["go2rtc-client==0.3.0"],
"single_config_entry": true
}
78 changes: 71 additions & 7 deletions homeassistant/components/go2rtc/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,18 @@
_GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant
# Do not edit it manually

app:
modules: {app_modules}

api:
listen: "{api_ip}:{api_port}"
allow_paths: {api_allow_paths}

# ffmpeg needs the exec module
# Restrict execution to only ffmpeg binary
exec:
allow_paths:
- ffmpeg

rtsp:
listen: "127.0.0.1:18554"
Expand All @@ -40,6 +50,43 @@
ice_servers: []
"""

_APP_MODULES = (
"api",
"exec", # Execution module for ffmpeg
"ffmpeg",
"http",
"mjpeg",
"onvif",
"rtmp",
"rtsp",
"srtp",
"webrtc",
"ws",
)

_API_ALLOW_PATHS = (
"/", # UI static page and version control
"/api", # Main API path
"/api/frame.jpeg", # Snapshot functionality
"/api/schemes", # Supported stream schemes
"/api/streams", # Stream management
"/api/webrtc", # Webrtc functionality
"/api/ws", # Websocket functionality (e.g. webrtc candidates)
)

# Additional modules when UI is enabled
_UI_APP_MODULES = (
*_APP_MODULES,
"debug",
)
# Additional api paths when UI is enabled
_UI_API_ALLOW_PATHS = (
*_API_ALLOW_PATHS,
"/api/config", # UI config view
"/api/log", # UI log view
"/api/streams.dot", # UI network view
)

_LOG_LEVEL_MAP = {
"TRC": logging.DEBUG,
"DBG": logging.DEBUG,
Expand All @@ -61,14 +108,34 @@ class Go2RTCWatchdogError(HomeAssistantError):
"""Raised on watchdog error."""


def _create_temp_file(api_ip: str) -> str:
def _format_list_for_yaml(items: tuple[str, ...]) -> str:
"""Format a list of strings for yaml config."""
if not items:
return "[]"
formatted_items = ",".join(f'"{item}"' for item in items)
return f"[{formatted_items}]"


def _create_temp_file(enable_ui: bool) -> str:
"""Create temporary config file."""
app_modules: tuple[str, ...] = _APP_MODULES
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
api_ip = _LOCALHOST_IP
if enable_ui:
app_modules = _UI_APP_MODULES
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could make a new tuple as a local variable here and add the tuple constants into the new tuple and only make the constants contain the separate features instead of making the constants contain all needed features.

api_paths = _UI_API_ALLOW_PATHS
# Listen on all interfaces for allowing access from all ips
api_ip = ""

# Set delete=False to prevent the file from being deleted when the file is closed
# Linux is clearing tmp folder on reboot, so no need to delete it manually
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write(
_GO2RTC_CONFIG_FORMAT.format(
api_ip=api_ip, api_port=HA_MANAGED_API_PORT
api_ip=api_ip,
api_port=HA_MANAGED_API_PORT,
app_modules=_format_list_for_yaml(app_modules),
api_allow_paths=_format_list_for_yaml(api_paths),
).encode()
)
return file.name
Expand All @@ -86,10 +153,7 @@ def __init__(
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
self._process: asyncio.subprocess.Process | None = None
self._startup_complete = asyncio.Event()
self._api_ip = _LOCALHOST_IP
if enable_ui:
# Listen on all interfaces for allowing access from all ips
self._api_ip = ""
self._enable_ui = enable_ui
self._watchdog_task: asyncio.Task | None = None
self._watchdog_tasks: list[asyncio.Task] = []

Expand All @@ -104,7 +168,7 @@ async def _start(self) -> None:
"""Start the server."""
_LOGGER.debug("Starting go2rtc server")
config_file = await self._hass.async_add_executor_job(
_create_temp_file, self._api_ip
_create_temp_file, self._enable_ui
)

self._startup_complete.clear()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ cryptography==46.0.2
dbus-fast==3.0.0
file-read-backwards==2.0.0
fnv-hash-fast==1.6.0
go2rtc-client==0.2.1
go2rtc-client==0.3.0
ha-ffmpeg==3.2.2
habluetooth==5.7.0
hass-nabucasa==1.5.1
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
astroid==4.0.1
coverage==7.10.6
freezegun==1.5.2
go2rtc-client==0.2.1
go2rtc-client==0.3.0
# librt is an internal mypy dependency
librt==0.2.1
license-expression==30.4.3
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion script/hassfest/docker/Dockerfile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion tests/components/go2rtc/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from unittest.mock import AsyncMock, Mock, patch

from awesomeversion import AwesomeVersion
from go2rtc_client.rest import _StreamClient, _WebRTCClient
from go2rtc_client.rest import _SchemesClient, _StreamClient, _WebRTCClient
import pytest

from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
Expand Down Expand Up @@ -39,6 +39,23 @@ def rest_client() -> Generator[AsyncMock]:
patch("homeassistant.components.go2rtc.server.Go2RtcRestClient", mock_client),
):
client = mock_client.return_value
client.schemes = schemes = Mock(spec_set=_SchemesClient)
schemes.list.return_value = {
"onvif",
"exec",
"http",
"rtmps",
"https",
"rtmpx",
"httpx",
"rtsps",
"webrtc",
"rtmp",
"tcp",
"rtsp",
"rtspx",
"ffmpeg",
}
client.streams = streams = Mock(spec_set=_StreamClient)
streams.list.return_value = {}
client.validate_server_version = AsyncMock(
Expand Down
23 changes: 23 additions & 0 deletions tests/components/go2rtc/snapshots/test_server.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# serializer version: 1
# name: test_server_run_success[False]
_CallList([
_Call(
tuple(
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws"]\n\napi:\n listen: "127.0.0.1:11984"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
),
dict({
}),
),
])
# ---
# name: test_server_run_success[True]
_CallList([
_Call(
tuple(
b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws","debug"]\n\napi:\n listen: ":11984"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n',
),
dict({
}),
),
])
# ---
29 changes: 7 additions & 22 deletions tests/components/go2rtc/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.go2rtc.server import Server
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -75,20 +76,17 @@ def assert_server_output_not_logged(


@pytest.mark.parametrize(
("enable_ui", "api_ip"),
[
(True, ""),
(False, "127.0.0.1"),
],
"enable_ui",
[True, False],
)
@pytest.mark.usefixtures("rest_client")
async def test_server_run_success(
mock_create_subprocess: AsyncMock,
rest_client: AsyncMock,
server_stdout: list[str],
server: Server,
caplog: pytest.LogCaptureFixture,
mock_tempfile: Mock,
api_ip: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test that the server runs successfully."""
await server.start()
Expand All @@ -104,21 +102,8 @@ async def test_server_run_success(
)

# Verify that the config file was written
mock_tempfile.write.assert_called_once_with(
f"""# This file is managed by Home Assistant
# Do not edit it manually

api:
listen: "{api_ip}:11984"

rtsp:
listen: "127.0.0.1:18554"

webrtc:
listen: ":18555/tcp"
ice_servers: []
""".encode()
)
calls = mock_tempfile.write.call_args_list
assert calls == snapshot()

# Verify go2rtc binary stdout was logged with debug level
assert_server_output_logged(server_stdout, caplog, logging.DEBUG)
Expand Down
Loading