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:
2
Dockerfile
generated
2
Dockerfile
generated
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
2
requirements_all.txt
generated
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -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
|
||||||
|
|||||||
2
script/hassfest/docker/Dockerfile
generated
2
script/hassfest/docker/Dockerfile
generated
@@ -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 \
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
23
tests/components/go2rtc/snapshots/test_server.ambr
Normal file
23
tests/components/go2rtc/snapshots/test_server.ambr
Normal 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({
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user