Add cloud ICE server registration (#128942)
* Add cloud ICE server registration * Add ice_servers to prefs, fix registration flow * Add support for list of ICE servers * Add ICE server cleanup on cloud logout, create tests * Fix RTCIceServer types * Update homeassistant/components/cloud/client.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Improve tests based on PR reviews * Improve tests * Use set_cloud_prefs fixture --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Robert Resch <robert@resch.dev>pull/129454/head^2
parent
96ba5c3983
commit
a1e2d79613
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
@ -11,12 +12,14 @@ from typing import Any, Literal
|
|||
|
||||
import aiohttp
|
||||
from hass_nabucasa.client import CloudClient as Interface, RemoteActivationNotAllowed
|
||||
from webrtc_models import RTCIceServer
|
||||
|
||||
from homeassistant.components import google_assistant, persistent_notification, webhook
|
||||
from homeassistant.components.alexa import (
|
||||
errors as alexa_errors,
|
||||
smart_home as alexa_smart_home,
|
||||
)
|
||||
from homeassistant.components.camera.webrtc import async_register_ice_servers
|
||||
from homeassistant.components.google_assistant import smart_home as ga
|
||||
from homeassistant.const import __version__ as HA_VERSION
|
||||
from homeassistant.core import Context, HassJob, HomeAssistant, callback
|
||||
|
@ -27,7 +30,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
|||
from homeassistant.util.aiohttp import MockRequest, serialize_response
|
||||
|
||||
from . import alexa_config, google_config
|
||||
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
|
||||
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN, PREF_ENABLE_CLOUD_ICE_SERVERS
|
||||
from .prefs import CloudPreferences
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -60,6 +63,7 @@ class CloudClient(Interface):
|
|||
self._alexa_config_init_lock = asyncio.Lock()
|
||||
self._google_config_init_lock = asyncio.Lock()
|
||||
self._relayer_region: str | None = None
|
||||
self._cloud_ice_servers_listener: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def base_path(self) -> Path:
|
||||
|
@ -187,6 +191,49 @@ class CloudClient(Interface):
|
|||
if is_new_user:
|
||||
await gconf.async_sync_entities(gconf.agent_user_id)
|
||||
|
||||
async def setup_cloud_ice_servers(_: datetime) -> None:
|
||||
async def register_cloud_ice_server(
|
||||
ice_servers: list[RTCIceServer],
|
||||
) -> Callable[[], None]:
|
||||
"""Register cloud ice server."""
|
||||
|
||||
def get_ice_servers() -> list[RTCIceServer]:
|
||||
return ice_servers
|
||||
|
||||
return async_register_ice_servers(self._hass, get_ice_servers)
|
||||
|
||||
async def async_register_cloud_ice_servers_listener(
|
||||
prefs: CloudPreferences,
|
||||
) -> None:
|
||||
is_cloud_ice_servers_enabled = (
|
||||
self.cloud.is_logged_in
|
||||
and not self.cloud.subscription_expired
|
||||
and prefs.cloud_ice_servers_enabled
|
||||
)
|
||||
if is_cloud_ice_servers_enabled:
|
||||
if self._cloud_ice_servers_listener is None:
|
||||
self._cloud_ice_servers_listener = await self.cloud.ice_servers.async_register_ice_servers_listener(
|
||||
register_cloud_ice_server
|
||||
)
|
||||
elif self._cloud_ice_servers_listener:
|
||||
self._cloud_ice_servers_listener()
|
||||
self._cloud_ice_servers_listener = None
|
||||
|
||||
async def async_prefs_updated(prefs: CloudPreferences) -> None:
|
||||
updated_prefs = prefs.last_updated
|
||||
|
||||
if (
|
||||
updated_prefs is None
|
||||
or PREF_ENABLE_CLOUD_ICE_SERVERS not in updated_prefs
|
||||
):
|
||||
return
|
||||
|
||||
await async_register_cloud_ice_servers_listener(prefs)
|
||||
|
||||
await async_register_cloud_ice_servers_listener(self._prefs)
|
||||
|
||||
self._prefs.async_listen_updates(async_prefs_updated)
|
||||
|
||||
tasks = []
|
||||
|
||||
if self._prefs.alexa_enabled and self._prefs.alexa_report_state:
|
||||
|
@ -195,6 +242,8 @@ class CloudClient(Interface):
|
|||
if self._prefs.google_enabled:
|
||||
tasks.append(enable_google)
|
||||
|
||||
tasks.append(setup_cloud_ice_servers)
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*(task(None) for task in tasks))
|
||||
|
||||
|
@ -222,6 +271,10 @@ class CloudClient(Interface):
|
|||
self._google_config.async_deinitialize()
|
||||
self._google_config = None
|
||||
|
||||
if self._cloud_ice_servers_listener:
|
||||
self._cloud_ice_servers_listener()
|
||||
self._cloud_ice_servers_listener = None
|
||||
|
||||
@callback
|
||||
def user_message(self, identifier: str, title: str, message: str) -> None:
|
||||
"""Create a message for user to UI."""
|
||||
|
|
|
@ -43,6 +43,7 @@ PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
|
|||
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
|
||||
PREF_GOOGLE_CONNECTED = "google_connected"
|
||||
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
|
||||
PREF_ENABLE_CLOUD_ICE_SERVERS = "cloud_ice_servers_enabled"
|
||||
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural")
|
||||
DEFAULT_DISABLE_2FA = False
|
||||
DEFAULT_ALEXA_REPORT_STATE = True
|
||||
|
|
|
@ -42,6 +42,7 @@ from .const import (
|
|||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_DISABLE_2FA,
|
||||
PREF_ENABLE_ALEXA,
|
||||
PREF_ENABLE_CLOUD_ICE_SERVERS,
|
||||
PREF_ENABLE_GOOGLE,
|
||||
PREF_GOOGLE_REPORT_STATE,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||
|
@ -448,6 +449,7 @@ def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
|
|||
vol.Coerce(tuple), validate_language_voice
|
||||
),
|
||||
vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
|
||||
vol.Optional(PREF_ENABLE_CLOUD_ICE_SERVERS): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
|
|
|
@ -32,6 +32,7 @@ from .const import (
|
|||
PREF_CLOUD_USER,
|
||||
PREF_CLOUDHOOKS,
|
||||
PREF_ENABLE_ALEXA,
|
||||
PREF_ENABLE_CLOUD_ICE_SERVERS,
|
||||
PREF_ENABLE_GOOGLE,
|
||||
PREF_ENABLE_REMOTE,
|
||||
PREF_GOOGLE_CONNECTED,
|
||||
|
@ -176,6 +177,7 @@ class CloudPreferences:
|
|||
google_settings_version: int | UndefinedType = UNDEFINED,
|
||||
google_connected: bool | UndefinedType = UNDEFINED,
|
||||
remote_allow_remote_enable: bool | UndefinedType = UNDEFINED,
|
||||
cloud_ice_servers_enabled: bool | UndefinedType = UNDEFINED,
|
||||
) -> None:
|
||||
"""Update user preferences."""
|
||||
prefs = {**self._prefs}
|
||||
|
@ -198,6 +200,7 @@ class CloudPreferences:
|
|||
(PREF_REMOTE_DOMAIN, remote_domain),
|
||||
(PREF_GOOGLE_CONNECTED, google_connected),
|
||||
(PREF_REMOTE_ALLOW_REMOTE_ENABLE, remote_allow_remote_enable),
|
||||
(PREF_ENABLE_CLOUD_ICE_SERVERS, cloud_ice_servers_enabled),
|
||||
)
|
||||
if value is not UNDEFINED
|
||||
}
|
||||
|
@ -246,6 +249,7 @@ class CloudPreferences:
|
|||
PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
|
||||
PREF_REMOTE_ALLOW_REMOTE_ENABLE: self.remote_allow_remote_enable,
|
||||
PREF_TTS_DEFAULT_VOICE: self.tts_default_voice,
|
||||
PREF_ENABLE_CLOUD_ICE_SERVERS: self.cloud_ice_servers_enabled,
|
||||
}
|
||||
|
||||
@property
|
||||
|
@ -362,6 +366,14 @@ class CloudPreferences:
|
|||
"""
|
||||
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return]
|
||||
|
||||
@property
|
||||
def cloud_ice_servers_enabled(self) -> bool:
|
||||
"""Return if cloud ICE servers are enabled."""
|
||||
cloud_ice_servers_enabled: bool = self._prefs.get(
|
||||
PREF_ENABLE_CLOUD_ICE_SERVERS, True
|
||||
)
|
||||
return cloud_ice_servers_enabled
|
||||
|
||||
async def get_cloud_user(self) -> str:
|
||||
"""Return ID of Home Assistant Cloud system user."""
|
||||
user = await self._load_cloud_user()
|
||||
|
@ -409,6 +421,7 @@ class CloudPreferences:
|
|||
PREF_ENABLE_ALEXA: True,
|
||||
PREF_ENABLE_GOOGLE: True,
|
||||
PREF_ENABLE_REMOTE: False,
|
||||
PREF_ENABLE_CLOUD_ICE_SERVERS: True,
|
||||
PREF_GOOGLE_CONNECTED: False,
|
||||
PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
||||
PREF_GOOGLE_ENTITY_CONFIGS: {},
|
||||
|
|
|
@ -33,6 +33,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
|||
data["remote_connected"] = cloud.remote.is_connected
|
||||
data["alexa_enabled"] = client.prefs.alexa_enabled
|
||||
data["google_enabled"] = client.prefs.google_enabled
|
||||
data["cloud_ice_servers_enabled"] = client.prefs.cloud_ice_servers_enabled
|
||||
data["remote_server"] = cloud.remote.snitun_server
|
||||
data["certificate_status"] = cloud.remote.certificate_status
|
||||
data["instance_id"] = client.prefs.instance_id
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
from collections.abc import AsyncGenerator, Callable, Coroutine, Generator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import DEFAULT, MagicMock, PropertyMock, patch
|
||||
from unittest.mock import DEFAULT, AsyncMock, MagicMock, PropertyMock, patch
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
from hass_nabucasa.auth import CognitoAuth
|
||||
from hass_nabucasa.cloudhooks import Cloudhooks
|
||||
from hass_nabucasa.const import DEFAULT_SERVERS, DEFAULT_VALUES, STATE_CONNECTED
|
||||
from hass_nabucasa.google_report_state import GoogleReportState
|
||||
from hass_nabucasa.ice_servers import IceServers
|
||||
from hass_nabucasa.iot import CloudIoT
|
||||
from hass_nabucasa.remote import RemoteUI
|
||||
from hass_nabucasa.voice import Voice
|
||||
|
@ -68,6 +69,12 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock]:
|
|||
)
|
||||
mock_cloud.voice = MagicMock(spec=Voice)
|
||||
mock_cloud.started = None
|
||||
mock_cloud.ice_servers = MagicMock(
|
||||
spec=IceServers,
|
||||
async_register_ice_servers_listener=AsyncMock(
|
||||
return_value=lambda: "mock-unregister"
|
||||
),
|
||||
)
|
||||
|
||||
def set_up_mock_cloud(
|
||||
cloud_client: CloudClient, mode: str, **kwargs: Any
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Test the cloud.iot module."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
|
||||
|
@ -183,6 +184,59 @@ async def test_handler_google_actions_disabled(
|
|||
assert resp["payload"] == response_payload
|
||||
|
||||
|
||||
async def test_handler_ice_servers(
|
||||
hass: HomeAssistant,
|
||||
cloud: MagicMock,
|
||||
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
||||
) -> None:
|
||||
"""Test handler ICE servers."""
|
||||
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
||||
await hass.async_block_till_done()
|
||||
# make sure that preferences will not be reset
|
||||
await cloud.client.prefs.async_set_username(cloud.username)
|
||||
await set_cloud_prefs(
|
||||
{
|
||||
"alexa_enabled": False,
|
||||
"google_enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
await cloud.login("test-user", "test-pass")
|
||||
await cloud.client.cloud_connected()
|
||||
|
||||
assert cloud.client._cloud_ice_servers_listener is not None
|
||||
assert cloud.client._cloud_ice_servers_listener() == "mock-unregister"
|
||||
|
||||
|
||||
async def test_handler_ice_servers_disabled(
|
||||
hass: HomeAssistant,
|
||||
cloud: MagicMock,
|
||||
set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
|
||||
) -> None:
|
||||
"""Test handler ICE servers when user has disabled it."""
|
||||
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
||||
await hass.async_block_till_done()
|
||||
# make sure that preferences will not be reset
|
||||
await cloud.client.prefs.async_set_username(cloud.username)
|
||||
await set_cloud_prefs(
|
||||
{
|
||||
"alexa_enabled": False,
|
||||
"google_enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
await cloud.login("test-user", "test-pass")
|
||||
await cloud.client.cloud_connected()
|
||||
|
||||
await set_cloud_prefs(
|
||||
{
|
||||
"cloud_ice_servers_enabled": False,
|
||||
}
|
||||
)
|
||||
|
||||
assert cloud.client._cloud_ice_servers_listener is None
|
||||
|
||||
|
||||
async def test_webhook_msg(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
|
@ -475,13 +529,16 @@ async def test_logged_out(
|
|||
await cloud.client.cloud_connected()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert cloud.client._cloud_ice_servers_listener is not None
|
||||
|
||||
# Simulate logged out
|
||||
await cloud.logout()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check we clean up Alexa and Google
|
||||
# Check we clean up Alexa, Google and ICE servers
|
||||
assert cloud.client._alexa_config is None
|
||||
assert cloud.client._google_config is None
|
||||
assert cloud.client._cloud_ice_servers_listener is None
|
||||
google_config_mock.async_deinitialize.assert_called_once_with()
|
||||
alexa_config_mock.async_deinitialize.assert_called_once_with()
|
||||
|
||||
|
|
|
@ -784,6 +784,7 @@ async def test_websocket_status(
|
|||
"google_report_state": True,
|
||||
"remote_allow_remote_enable": True,
|
||||
"remote_enabled": False,
|
||||
"cloud_ice_servers_enabled": True,
|
||||
"tts_default_voice": ["en-US", "JennyNeural"],
|
||||
},
|
||||
"alexa_entities": {
|
||||
|
@ -903,6 +904,7 @@ async def test_websocket_update_preferences(
|
|||
assert cloud.client.prefs.alexa_enabled
|
||||
assert cloud.client.prefs.google_secure_devices_pin is None
|
||||
assert cloud.client.prefs.remote_allow_remote_enable is True
|
||||
assert cloud.client.prefs.cloud_ice_servers_enabled is True
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
|
@ -914,6 +916,7 @@ async def test_websocket_update_preferences(
|
|||
"google_secure_devices_pin": "1234",
|
||||
"tts_default_voice": ["en-GB", "RyanNeural"],
|
||||
"remote_allow_remote_enable": False,
|
||||
"cloud_ice_servers_enabled": False,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
@ -923,6 +926,7 @@ async def test_websocket_update_preferences(
|
|||
assert not cloud.client.prefs.alexa_enabled
|
||||
assert cloud.client.prefs.google_secure_devices_pin == "1234"
|
||||
assert cloud.client.prefs.remote_allow_remote_enable is False
|
||||
assert cloud.client.prefs.cloud_ice_servers_enabled is False
|
||||
assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural")
|
||||
|
||||
|
||||
|
|
|
@ -50,7 +50,12 @@ async def test_cloud_system_health(
|
|||
|
||||
await cloud.client.async_system_message({"region": "xx-earth-616"})
|
||||
await set_cloud_prefs(
|
||||
{"alexa_enabled": True, "google_enabled": False, "remote_enabled": True}
|
||||
{
|
||||
"alexa_enabled": True,
|
||||
"google_enabled": False,
|
||||
"remote_enabled": True,
|
||||
"cloud_ice_servers_enabled": True,
|
||||
}
|
||||
)
|
||||
|
||||
info = await get_system_health_info(hass, "cloud")
|
||||
|
@ -70,6 +75,7 @@ async def test_cloud_system_health(
|
|||
"remote_server": "us-west-1",
|
||||
"alexa_enabled": True,
|
||||
"google_enabled": False,
|
||||
"cloud_ice_servers_enabled": True,
|
||||
"can_reach_cert_server": "ok",
|
||||
"can_reach_cloud_auth": {"type": "failed", "error": "unreachable"},
|
||||
"can_reach_cloud": "ok",
|
||||
|
|
Loading…
Reference in New Issue