From 1f57f170da9aee9e15bfc0a595d30c4f565e4a86 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Sep 2026 04:42:10 -0400 Subject: [PATCH] Use live remote domain for the cloud remote UI URL (#183284) Co-authored-by: Claude --- homeassistant/components/cloud/__init__.py | 13 ++++++-- homeassistant/components/cloud/client.py | 9 ++++++ tests/components/cloud/test_client.py | 29 +++++++++++++++++- tests/components/cloud/test_init.py | 35 +++++++++++++++++++++- 4 files changed, 81 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 0b990c7fbf73..950492d3031e 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -287,10 +287,16 @@ def async_remote_ui_url(hass: HomeAssistant) -> str: if not async_is_logged_in(hass): raise CloudNotAvailable - if not hass.data[DATA_CLOUD].client.prefs.remote_enabled: + cloud = hass.data[DATA_CLOUD] + if not cloud.client.prefs.remote_enabled: raise CloudNotAvailable - if not (remote_domain := hass.data[DATA_CLOUD].client.prefs.remote_domain): + # Fall back to the domain in the preferences while the remote backend is + # not loaded. + if not ( + remote_domain := cloud.remote.instance_domain + or cloud.client.prefs.remote_domain + ): raise CloudNotAvailable return f"https://{remote_domain}" @@ -375,7 +381,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _on_initialized() -> None: """Update preferences.""" - await prefs.async_update(remote_domain=cloud.remote.instance_domain) + if (remote_domain := cloud.remote.instance_domain) != prefs.remote_domain: + await prefs.async_update(remote_domain=remote_domain) hass.data[DATA_PENDING_AUTO_LOGIN] = None diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 90bef942403c..199205eed24a 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -10,6 +10,7 @@ from typing import Any, Literal, override import aiohttp from hass_nabucasa.client import CloudClient as Interface, RemoteActivationNotAllowed +from hass_nabucasa.const import DISPATCH_REMOTE_BACKEND_UP from webrtc_models import RTCIceServer from homeassistant.components import google_assistant, persistent_notification, webhook @@ -309,6 +310,14 @@ class CloudClient(Interface): @override def dispatcher_message(self, identifier: str, data: Any = None) -> None: """Match cloud notification to dispatcher.""" + if ( + identifier == DISPATCH_REMOTE_BACKEND_UP + and (remote_domain := self.cloud.remote.instance_domain) + != self._prefs.remote_domain + ): + self._hass.async_create_task( + self._prefs.async_update(remote_domain=remote_domain) + ) if identifier.startswith("remote_"): async_dispatcher_send(self._hass, DISPATCHER_REMOTE_UPDATE, data) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 88d20493b570..ba0b2ac3f94d 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Coroutine from datetime import timedelta from typing import Any -from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, call, patch import aiohttp from aiohttp import web @@ -634,6 +634,33 @@ async def test_remote_enable(hass: HomeAssistant) -> None: prefs.async_update.assert_called_once_with(remote_enabled=True) +@pytest.mark.parametrize( + ("stored_domain", "expected_calls"), + [ + pytest.param(None, 1, id="changed"), + pytest.param("example.ui.nabu.casa", 0, id="unchanged"), + ], +) +async def test_remote_backend_up_updates_remote_domain( + hass: HomeAssistant, stored_domain: str | None, expected_calls: int +) -> None: + """Test the remote domain preference is synced when the backend is up.""" + prefs = MagicMock( + async_update=AsyncMock(return_value=None), remote_domain=stored_domain + ) + client = CloudClient(hass, prefs, None, {}, {}) + client.cloud = MagicMock() + client.cloud.remote.instance_domain = "example.ui.nabu.casa" + + client.dispatcher_message("remote_backend_up") + await hass.async_block_till_done() + + assert ( + prefs.async_update.call_args_list + == [call(remote_domain="example.ui.nabu.casa")] * expected_calls + ) + + async def test_remote_enable_not_allowed(hass: HomeAssistant) -> None: """Test enabling remote UI.""" prefs = MagicMock( diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 9df1c7a18243..79251da3b0be 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -2,7 +2,7 @@ from collections.abc import Callable, Coroutine from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch from hass_nabucasa import ( AutoLoginController, @@ -278,6 +278,39 @@ async def test_remote_ui_url(hass: HomeAssistant) -> None: assert async_remote_ui_url(hass) == "https://example.com" + # Remote backend loaded after a login, stored domain not refreshed + cl.client.prefs._prefs["remote_domain"] = None + with patch.object(cl.remote, "_instance_domain", "live.example.com"): + assert async_remote_ui_url(hass) == "https://live.example.com" + + +@pytest.mark.parametrize( + ("stored_domain", "expected_calls"), + [ + pytest.param(None, 1, id="changed"), + pytest.param("example.ui.nabu.casa", 0, id="unchanged"), + ], +) +@pytest.mark.usefixtures("mock_cloud_fixture") +async def test_on_initialized_updates_remote_domain( + hass: HomeAssistant, stored_domain: str | None, expected_calls: int +) -> None: + """Test the remote domain preference is synced when cloud is initialized.""" + cl = hass.data[DATA_CLOUD] + cl.client.prefs._prefs["remote_domain"] = stored_domain + (on_initialized,) = cl._on_initialized + + with ( + patch.object(cl.remote, "_instance_domain", "example.ui.nabu.casa"), + patch.object(cl.client.prefs, "async_update") as mock_update, + ): + await on_initialized() + + assert ( + mock_update.call_args_list + == [call(remote_domain="example.ui.nabu.casa")] * expected_calls + ) + async def test_async_get_or_create_cloudhook( hass: HomeAssistant,