From 126fd217e7f1958344f6dfbdbdded470fb387d01 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 20 Nov 2025 19:41:40 +0100 Subject: [PATCH] Bump go2rtc to 1.9.12 and go2rtc-client to 0.3.0 (#156948) --- Dockerfile | 2 +- homeassistant/components/go2rtc/__init__.py | 37 ++------- homeassistant/components/go2rtc/const.py | 2 +- homeassistant/components/go2rtc/manifest.json | 2 +- homeassistant/components/go2rtc/server.py | 78 +++++++++++++++++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- tests/components/go2rtc/conftest.py | 19 ++++- .../go2rtc/snapshots/test_server.ambr | 23 ++++++ tests/components/go2rtc/test_server.py | 29 ++----- 13 files changed, 134 insertions(+), 68 deletions(-) create mode 100644 tests/components/go2rtc/snapshots/test_server.ambr diff --git a/Dockerfile b/Dockerfile index 33e8fbbaff9..aa4de12d3eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN \ "armv7") go2rtc_suffix='arm' ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \ esac \ - && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.11/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ + && curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.12/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \ && chmod +x /bin/go2rtc \ # Verify go2rtc can be executed && go2rtc --version diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 6abb16d36ea..497c03adbd4 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -60,35 +60,6 @@ from .server import Server _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( { @@ -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() entry.async_on_unload(async_register_webrtc_provider(hass, provider)) return True @@ -228,16 +200,21 @@ class WebRTCProvider(CameraWebRTCProvider): 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, diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index c020ad79fde..0d7d666b284 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,4 +6,4 @@ CONF_DEBUG_UI = "debug_ui" 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" diff --git a/homeassistant/components/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json index dd50b4ba076..44363360948 100644 --- a/homeassistant/components/go2rtc/manifest.json +++ b/homeassistant/components/go2rtc/manifest.json @@ -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 } diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py index 6699ee4d8a2..37040742aea 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -29,8 +29,18 @@ _RESPAWN_COOLDOWN = 1 _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" @@ -40,6 +50,43 @@ webrtc: 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, @@ -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 + 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 @@ -86,10 +153,7 @@ class Server: 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] = [] @@ -104,7 +168,7 @@ class Server: """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() diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 792b576c9fd..65807a34023 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index ec8d88827bc..62c9fa379a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1049,7 +1049,7 @@ gitterpy==0.1.7 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.2.1 +go2rtc-client==0.3.0 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test.txt b/requirements_test.txt index ba5502b4ff8..767ebcbd093 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26f247f366f..8e5d6515525 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -916,7 +916,7 @@ gios==6.1.2 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.2.1 +go2rtc-client==0.3.0 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 4fa4c7be131..257a60799b4 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -29,7 +29,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.9.6,source=/uv,target=/bin/uv \ tqdm==4.67.1 \ ruff==0.13.0 \ PyTurboJPEG==1.8.0 \ - go2rtc-client==0.2.1 \ + go2rtc-client==0.3.0 \ ha-ffmpeg==3.2.2 \ hassil==3.4.0 \ home-assistant-intents==2025.11.7 \ diff --git a/tests/components/go2rtc/conftest.py b/tests/components/go2rtc/conftest.py index bd6d3841dad..ff5748bffb1 100644 --- a/tests/components/go2rtc/conftest.py +++ b/tests/components/go2rtc/conftest.py @@ -4,7 +4,7 @@ from collections.abc import Generator 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 @@ -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( diff --git a/tests/components/go2rtc/snapshots/test_server.ambr b/tests/components/go2rtc/snapshots/test_server.ambr new file mode 100644 index 00000000000..9ae2ef96439 --- /dev/null +++ b/tests/components/go2rtc/snapshots/test_server.ambr @@ -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({ + }), + ), + ]) +# --- diff --git a/tests/components/go2rtc/test_server.py b/tests/components/go2rtc/test_server.py index e4fe3993f3c..9ec60d30da4 100644 --- a/tests/components/go2rtc/test_server.py +++ b/tests/components/go2rtc/test_server.py @@ -7,6 +7,7 @@ import subprocess 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 @@ -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() @@ -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)