mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add config entry to go2rtc (#129436)
* Add config entry to go2rtc * Address review comments * Remove config entry if go2rtc is not configured * Allow importing default_config * Address review comment
This commit is contained in:
parent
24829bc44f
commit
db81edfb2b
@ -3,7 +3,9 @@
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from go2rtc_client import Go2RtcRestClient
|
||||
from go2rtc_client.exceptions import Go2RtcClientError
|
||||
from go2rtc_client.ws import (
|
||||
Go2RtcWsClient,
|
||||
ReceiveMessages,
|
||||
@ -24,11 +26,15 @@ from homeassistant.components.camera import (
|
||||
WebRTCSendMessage,
|
||||
async_register_webrtc_provider,
|
||||
)
|
||||
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.package import is_docker_env
|
||||
|
||||
from .const import DOMAIN
|
||||
@ -72,15 +78,24 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
|
||||
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up WebRTC."""
|
||||
url: str | None = None
|
||||
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
|
||||
await _remove_go2rtc_entries(hass)
|
||||
return True
|
||||
|
||||
if not (configured_by_user := DOMAIN in config) or not (
|
||||
url := config[DOMAIN].get(CONF_URL)
|
||||
):
|
||||
if not is_docker_env():
|
||||
if not configured_by_user:
|
||||
# Remove config entry if it exists
|
||||
await _remove_go2rtc_entries(hass)
|
||||
return True
|
||||
_LOGGER.warning("Go2rtc URL required in non-docker installs")
|
||||
return False
|
||||
@ -99,12 +114,36 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
url = "http://localhost:1984/"
|
||||
|
||||
hass.data[_DATA_GO2RTC] = url
|
||||
discovery_flow.async_create_flow(
|
||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
|
||||
"""Remove go2rtc config entries, if any."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up go2rtc from a config entry."""
|
||||
url = hass.data[_DATA_GO2RTC]
|
||||
|
||||
# Validate the server URL
|
||||
try:
|
||||
client = Go2RtcRestClient(async_get_clientsession(hass), url)
|
||||
await client.streams.list()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.warning("Could not connect to go2rtc instance on %s", url)
|
||||
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 connect to go2rtc instance on %s (%s)", url, err)
|
||||
return False
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
||||
return False
|
||||
|
||||
provider = WebRTCProvider(hass, url)
|
||||
@ -112,6 +151,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a go2rtc config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def _get_binary(hass: HomeAssistant) -> str | None:
|
||||
"""Return the binary path if found."""
|
||||
return await hass.async_add_executor_job(shutil.which, "go2rtc")
|
||||
|
21
homeassistant/components/go2rtc/config_flow.py
Normal file
21
homeassistant/components/go2rtc/config_flow.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Config flow for the go2rtc integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class CloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for the go2rtc integration."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_system(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the system step."""
|
||||
return self.async_create_entry(title="go2rtc", data={})
|
@ -7,5 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/go2rtc",
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["go2rtc-client==0.0.1b3"]
|
||||
"requirements": ["go2rtc-client==0.0.1b3"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -121,6 +121,7 @@ ALLOWED_USED_COMPONENTS = {
|
||||
"alert",
|
||||
"automation",
|
||||
"conversation",
|
||||
"default_config",
|
||||
"device_automation",
|
||||
"frontend",
|
||||
"group",
|
||||
|
45
tests/components/go2rtc/test_config_flow.py
Normal file
45
tests/components/go2rtc/test_config_flow.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Test the Home Assistant Cloud config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.go2rtc.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_config_flow(hass: HomeAssistant) -> None:
|
||||
"""Test create cloud entry."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.go2rtc.async_setup", return_value=True
|
||||
) as mock_setup,
|
||||
patch(
|
||||
"homeassistant.components.go2rtc.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "system"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "go2rtc"
|
||||
assert result["data"] == {}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_multiple_entries(hass: HomeAssistant) -> None:
|
||||
"""Test creating multiple cloud entries."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "system"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
@ -5,7 +5,9 @@ import logging
|
||||
from typing import NamedTuple
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||
from go2rtc_client import Stream
|
||||
from go2rtc_client.exceptions import Go2RtcClientError
|
||||
from go2rtc_client.models import Producer
|
||||
from go2rtc_client.ws import (
|
||||
ReceiveMessages,
|
||||
@ -27,9 +29,10 @@ from homeassistant.components.camera import (
|
||||
WebRTCMessage,
|
||||
WebRTCSendMessage,
|
||||
)
|
||||
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
|
||||
from homeassistant.components.go2rtc import WebRTCProvider
|
||||
from homeassistant.components.go2rtc.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@ -100,6 +103,21 @@ def mock_get_binary(go2rtc_binary) -> Generator[Mock]:
|
||||
yield mock_which
|
||||
|
||||
|
||||
@pytest.fixture(name="has_go2rtc_entry")
|
||||
def has_go2rtc_entry_fixture() -> bool:
|
||||
"""Fixture to control if a go2rtc config entry should be created."""
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_go2rtc_entry(hass: HomeAssistant, has_go2rtc_entry: bool) -> None:
|
||||
"""Mock a go2rtc onfig entry."""
|
||||
if not has_go2rtc_entry:
|
||||
return
|
||||
config_entry = MockConfigEntry(domain=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
|
||||
@pytest.fixture(name="is_docker_env")
|
||||
def is_docker_env_fixture() -> bool:
|
||||
"""Fixture to provide is_docker_env return value."""
|
||||
@ -187,7 +205,10 @@ async def _test_setup_and_signaling(
|
||||
assert camera.frontend_stream_type == StreamType.HLS
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(config_entries) == 1
|
||||
assert config_entries[0].state == ConfigEntryState.LOADED
|
||||
after_setup_fn()
|
||||
|
||||
receive_message_callback = Mock(spec_set=WebRTCSendMessage)
|
||||
@ -239,8 +260,13 @@ async def _test_setup_and_signaling(
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"init_test_integration", "mock_get_binary", "mock_is_docker_env"
|
||||
"init_test_integration",
|
||||
"mock_get_binary",
|
||||
"mock_is_docker_env",
|
||||
"mock_go2rtc_entry",
|
||||
)
|
||||
@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}])
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
async def test_setup_go_binary(
|
||||
hass: HomeAssistant,
|
||||
rest_client: AsyncMock,
|
||||
@ -249,21 +275,25 @@ async def test_setup_go_binary(
|
||||
server_start: Mock,
|
||||
server_stop: Mock,
|
||||
init_test_integration: MockCamera,
|
||||
has_go2rtc_entry: bool,
|
||||
config: ConfigType,
|
||||
) -> None:
|
||||
"""Test the go2rtc config entry with binary."""
|
||||
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
|
||||
|
||||
def after_setup() -> None:
|
||||
server.assert_called_once_with(hass, "/usr/bin/go2rtc")
|
||||
server_start.assert_called_once()
|
||||
|
||||
await _test_setup_and_signaling(
|
||||
hass, rest_client, ws_client, {DOMAIN: {}}, after_setup, init_test_integration
|
||||
hass, rest_client, ws_client, config, after_setup, init_test_integration
|
||||
)
|
||||
|
||||
await hass.async_stop()
|
||||
server_stop.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_go2rtc_entry")
|
||||
@pytest.mark.parametrize(
|
||||
("go2rtc_binary", "is_docker_env"),
|
||||
[
|
||||
@ -271,6 +301,7 @@ async def test_setup_go_binary(
|
||||
(None, False),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
async def test_setup_go(
|
||||
hass: HomeAssistant,
|
||||
rest_client: AsyncMock,
|
||||
@ -279,8 +310,11 @@ async def test_setup_go(
|
||||
init_test_integration: MockCamera,
|
||||
mock_get_binary: Mock,
|
||||
mock_is_docker_env: Mock,
|
||||
has_go2rtc_entry: bool,
|
||||
) -> None:
|
||||
"""Test the go2rtc config entry without binary."""
|
||||
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
|
||||
|
||||
config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}}
|
||||
|
||||
def after_setup() -> None:
|
||||
@ -431,6 +465,9 @@ async def test_close_session(
|
||||
|
||||
ERR_BINARY_NOT_FOUND = "Could not find go2rtc docker binary"
|
||||
ERR_CONNECT = "Could not connect to go2rtc instance"
|
||||
ERR_CONNECT_RETRY = (
|
||||
"Could not connect to go2rtc instance on http://localhost:1984/; Retrying"
|
||||
)
|
||||
ERR_INVALID_URL = "Invalid config for 'go2rtc': invalid url"
|
||||
ERR_URL_REQUIRED = "Go2rtc URL required in non-docker installs"
|
||||
|
||||
@ -441,7 +478,10 @@ ERR_URL_REQUIRED = "Go2rtc URL required in non-docker installs"
|
||||
({}, None, False),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_get_binary", "mock_is_docker_env", "server")
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server"
|
||||
)
|
||||
async def test_non_user_setup_with_error(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
@ -450,28 +490,105 @@ async def test_non_user_setup_with_error(
|
||||
"""Test setup integration does not fail if not setup by user."""
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert not hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "go2rtc_binary", "is_docker_env", "expected_log_message"),
|
||||
[
|
||||
({}, None, True, ERR_BINARY_NOT_FOUND),
|
||||
({}, "/usr/bin/go2rtc", True, ERR_CONNECT),
|
||||
({DEFAULT_CONFIG_DOMAIN: {}}, None, True, ERR_BINARY_NOT_FOUND),
|
||||
({DOMAIN: {}}, None, False, ERR_URL_REQUIRED),
|
||||
({DOMAIN: {}}, None, True, ERR_BINARY_NOT_FOUND),
|
||||
({DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT),
|
||||
({DOMAIN: {CONF_URL: "invalid"}}, None, True, ERR_INVALID_URL),
|
||||
({DOMAIN: {CONF_URL: "http://localhost:1984/"}}, None, True, ERR_CONNECT),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_get_binary", "mock_is_docker_env", "server")
|
||||
async def test_setup_with_error(
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server"
|
||||
)
|
||||
async def test_setup_with_setup_error(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
has_go2rtc_entry: bool,
|
||||
expected_log_message: str,
|
||||
) -> None:
|
||||
"""Test setup integration fails."""
|
||||
|
||||
assert not await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert bool(hass.config_entries.async_entries(DOMAIN)) == has_go2rtc_entry
|
||||
assert expected_log_message in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "go2rtc_binary", "is_docker_env", "expected_log_message"),
|
||||
[
|
||||
({DEFAULT_CONFIG_DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT),
|
||||
({DOMAIN: {}}, "/usr/bin/go2rtc", True, ERR_CONNECT),
|
||||
({DOMAIN: {CONF_URL: "http://localhost:1984/"}}, None, True, ERR_CONNECT),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server"
|
||||
)
|
||||
async def test_setup_with_setup_entry_error(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
expected_log_message: str,
|
||||
) -> None:
|
||||
"""Test setup integration entry fails."""
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(config_entries) == 1
|
||||
assert config_entries[0].state == ConfigEntryState.SETUP_ERROR
|
||||
assert expected_log_message in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [{DOMAIN: {}}, {DEFAULT_CONFIG_DOMAIN: {}}])
|
||||
@pytest.mark.parametrize(
|
||||
("cause", "expected_config_entry_state", "expected_log_message"),
|
||||
[
|
||||
(ClientConnectionError(), ConfigEntryState.SETUP_RETRY, ERR_CONNECT_RETRY),
|
||||
(ServerConnectionError(), ConfigEntryState.SETUP_RETRY, ERR_CONNECT_RETRY),
|
||||
(None, ConfigEntryState.SETUP_ERROR, ERR_CONNECT),
|
||||
(Exception(), ConfigEntryState.SETUP_ERROR, ERR_CONNECT),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("has_go2rtc_entry", [True, False])
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_get_binary", "mock_go2rtc_entry", "mock_is_docker_env", "server"
|
||||
)
|
||||
async def test_setup_with_retryable_setup_entry_error(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
rest_client: AsyncMock,
|
||||
config: ConfigType,
|
||||
cause: Exception,
|
||||
expected_config_entry_state: ConfigEntryState,
|
||||
expected_log_message: str,
|
||||
) -> None:
|
||||
"""Test setup integration entry fails."""
|
||||
go2rtc_error = Go2RtcClientError()
|
||||
go2rtc_error.__cause__ = cause
|
||||
rest_client.streams.list.side_effect = go2rtc_error
|
||||
assert await async_setup_component(hass, DOMAIN, config)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(config_entries) == 1
|
||||
assert config_entries[0].state == expected_config_entry_state
|
||||
assert expected_log_message in caplog.text
|
||||
|
||||
|
||||
async def test_config_entry_remove(hass: HomeAssistant) -> None:
|
||||
"""Test config entry removed when neither default_config nor go2rtc is in config."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert not await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
|
||||
|
Loading…
x
Reference in New Issue
Block a user