1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 12:59:34 +00:00

Bump go2rtc to 1.9.12 and go2rtc-client to 0.3.0 (#156948)

This commit is contained in:
Robert Resch
2025-11-20 19:41:40 +01:00
committed by GitHub
parent 0327b0e1ec
commit 126fd217e7
13 changed files with 134 additions and 68 deletions

2
Dockerfile generated
View File

@@ -25,7 +25,7 @@ RUN \
"armv7") go2rtc_suffix='arm' ;; \ "armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \ *) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \ 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 \ && chmod +x /bin/go2rtc \
# Verify go2rtc can be executed # Verify go2rtc can be executed
&& go2rtc --version && go2rtc --version

View File

@@ -60,35 +60,6 @@ from .server import Server
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_FFMPEG = "ffmpeg" _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( CONFIG_SCHEMA = vol.Schema(
{ {
@@ -197,6 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bo
return False return False
provider = entry.runtime_data = WebRTCProvider(hass, url, session, client) provider = entry.runtime_data = WebRTCProvider(hass, url, session, client)
await provider.initialize()
entry.async_on_unload(async_register_webrtc_provider(hass, provider)) entry.async_on_unload(async_register_webrtc_provider(hass, provider))
return True return True
@@ -228,16 +200,21 @@ class WebRTCProvider(CameraWebRTCProvider):
self._session = session self._session = session
self._rest_client = rest_client self._rest_client = rest_client
self._sessions: dict[str, Go2RtcWsClient] = {} self._sessions: dict[str, Go2RtcWsClient] = {}
self._supported_schemes: set[str] = set()
@property @property
def domain(self) -> str: def domain(self) -> str:
"""Return the integration domain of the provider.""" """Return the integration domain of the provider."""
return DOMAIN return DOMAIN
async def initialize(self) -> None:
"""Initialize the provider."""
self._supported_schemes = await self._rest_client.schemes.list()
@callback @callback
def async_is_supported(self, stream_source: str) -> bool: def async_is_supported(self, stream_source: str) -> bool:
"""Return if this provider is supports the Camera as source.""" """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( async def async_handle_async_webrtc_offer(
self, self,

View File

@@ -6,4 +6,4 @@ CONF_DEBUG_UI = "debug_ui"
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
HA_MANAGED_API_PORT = 11984 HA_MANAGED_API_PORT = 11984
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
RECOMMENDED_VERSION = "1.9.11" RECOMMENDED_VERSION = "1.9.12"

View File

@@ -8,6 +8,6 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["go2rtc-client==0.2.1"], "requirements": ["go2rtc-client==0.3.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

@@ -29,8 +29,18 @@ _RESPAWN_COOLDOWN = 1
_GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant _GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant
# Do not edit it manually # Do not edit it manually
app:
modules: {app_modules}
api: api:
listen: "{api_ip}:{api_port}" 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: rtsp:
listen: "127.0.0.1:18554" listen: "127.0.0.1:18554"
@@ -40,6 +50,43 @@ webrtc:
ice_servers: [] 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 = { _LOG_LEVEL_MAP = {
"TRC": logging.DEBUG, "TRC": logging.DEBUG,
"DBG": logging.DEBUG, "DBG": logging.DEBUG,
@@ -61,14 +108,34 @@ class Go2RTCWatchdogError(HomeAssistantError):
"""Raised on watchdog error.""" """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.""" """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 # 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 # Linux is clearing tmp folder on reboot, so no need to delete it manually
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
file.write( file.write(
_GO2RTC_CONFIG_FORMAT.format( _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() ).encode()
) )
return file.name return file.name
@@ -86,10 +153,7 @@ class Server:
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE) self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
self._process: asyncio.subprocess.Process | None = None self._process: asyncio.subprocess.Process | None = None
self._startup_complete = asyncio.Event() self._startup_complete = asyncio.Event()
self._api_ip = _LOCALHOST_IP self._enable_ui = enable_ui
if enable_ui:
# Listen on all interfaces for allowing access from all ips
self._api_ip = ""
self._watchdog_task: asyncio.Task | None = None self._watchdog_task: asyncio.Task | None = None
self._watchdog_tasks: list[asyncio.Task] = [] self._watchdog_tasks: list[asyncio.Task] = []
@@ -104,7 +168,7 @@ class Server:
"""Start the server.""" """Start the server."""
_LOGGER.debug("Starting go2rtc server") _LOGGER.debug("Starting go2rtc server")
config_file = await self._hass.async_add_executor_job( 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() self._startup_complete.clear()

View File

@@ -33,7 +33,7 @@ cryptography==46.0.2
dbus-fast==3.0.0 dbus-fast==3.0.0
file-read-backwards==2.0.0 file-read-backwards==2.0.0
fnv-hash-fast==1.6.0 fnv-hash-fast==1.6.0
go2rtc-client==0.2.1 go2rtc-client==0.3.0
ha-ffmpeg==3.2.2 ha-ffmpeg==3.2.2
habluetooth==5.7.0 habluetooth==5.7.0
hass-nabucasa==1.5.1 hass-nabucasa==1.5.1

2
requirements_all.txt generated
View File

@@ -1049,7 +1049,7 @@ gitterpy==0.1.7
glances-api==0.8.0 glances-api==0.8.0
# homeassistant.components.go2rtc # homeassistant.components.go2rtc
go2rtc-client==0.2.1 go2rtc-client==0.3.0
# homeassistant.components.goalzero # homeassistant.components.goalzero
goalzero==0.2.2 goalzero==0.2.2

View File

@@ -10,7 +10,7 @@
astroid==4.0.1 astroid==4.0.1
coverage==7.10.6 coverage==7.10.6
freezegun==1.5.2 freezegun==1.5.2
go2rtc-client==0.2.1 go2rtc-client==0.3.0
# librt is an internal mypy dependency # librt is an internal mypy dependency
librt==0.2.1 librt==0.2.1
license-expression==30.4.3 license-expression==30.4.3

View File

@@ -916,7 +916,7 @@ gios==6.1.2
glances-api==0.8.0 glances-api==0.8.0
# homeassistant.components.go2rtc # homeassistant.components.go2rtc
go2rtc-client==0.2.1 go2rtc-client==0.3.0
# homeassistant.components.goalzero # homeassistant.components.goalzero
goalzero==0.2.2 goalzero==0.2.2

View File

@@ -29,7 +29,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.9.6,source=/uv,target=/bin/uv \
tqdm==4.67.1 \ tqdm==4.67.1 \
ruff==0.13.0 \ ruff==0.13.0 \
PyTurboJPEG==1.8.0 \ PyTurboJPEG==1.8.0 \
go2rtc-client==0.2.1 \ go2rtc-client==0.3.0 \
ha-ffmpeg==3.2.2 \ ha-ffmpeg==3.2.2 \
hassil==3.4.0 \ hassil==3.4.0 \
home-assistant-intents==2025.11.7 \ home-assistant-intents==2025.11.7 \

View File

@@ -4,7 +4,7 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from go2rtc_client.rest import _StreamClient, _WebRTCClient from go2rtc_client.rest import _SchemesClient, _StreamClient, _WebRTCClient
import pytest import pytest
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN 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), patch("homeassistant.components.go2rtc.server.Go2RtcRestClient", mock_client),
): ):
client = mock_client.return_value 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) client.streams = streams = Mock(spec_set=_StreamClient)
streams.list.return_value = {} streams.list.return_value = {}
client.validate_server_version = AsyncMock( client.validate_server_version = AsyncMock(

View File

@@ -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({
}),
),
])
# ---

View File

@@ -7,6 +7,7 @@ import subprocess
from unittest.mock import AsyncMock, MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.go2rtc.server import Server from homeassistant.components.go2rtc.server import Server
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -75,20 +76,17 @@ def assert_server_output_not_logged(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("enable_ui", "api_ip"), "enable_ui",
[ [True, False],
(True, ""),
(False, "127.0.0.1"),
],
) )
@pytest.mark.usefixtures("rest_client")
async def test_server_run_success( async def test_server_run_success(
mock_create_subprocess: AsyncMock, mock_create_subprocess: AsyncMock,
rest_client: AsyncMock,
server_stdout: list[str], server_stdout: list[str],
server: Server, server: Server,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
mock_tempfile: Mock, mock_tempfile: Mock,
api_ip: str, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test that the server runs successfully.""" """Test that the server runs successfully."""
await server.start() await server.start()
@@ -104,21 +102,8 @@ async def test_server_run_success(
) )
# Verify that the config file was written # Verify that the config file was written
mock_tempfile.write.assert_called_once_with( calls = mock_tempfile.write.call_args_list
f"""# This file is managed by Home Assistant assert calls == snapshot()
# Do not edit it manually
api:
listen: "{api_ip}:11984"
rtsp:
listen: "127.0.0.1:18554"
webrtc:
listen: ":18555/tcp"
ice_servers: []
""".encode()
)
# Verify go2rtc binary stdout was logged with debug level # Verify go2rtc binary stdout was logged with debug level
assert_server_output_logged(server_stdout, caplog, logging.DEBUG) assert_server_output_logged(server_stdout, caplog, logging.DEBUG)