diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 07c3b1e7b5a..5438b77c73a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -19,6 +19,7 @@ import attr from hass_nabucasa import AlreadyConnectedError, Cloud, auth from hass_nabucasa.const import STATE_DISCONNECTED from hass_nabucasa.voice_data import TTS_VOICES +import psutil_home_assistant as ha_psutil import voluptuous as vol from homeassistant.components import websocket_api @@ -27,6 +28,7 @@ from homeassistant.components.alexa import ( errors as alexa_errors, ) from homeassistant.components.google_assistant import helpers as google_helpers +from homeassistant.components.hassio import get_addons_stats, get_supervisor_info from homeassistant.components.homeassistant import exposed_entities from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin from homeassistant.components.http.data_validator import RequestDataValidator @@ -37,6 +39,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.hassio import is_hassio from homeassistant.loader import ( async_get_custom_components, async_get_loaded_integration, @@ -571,6 +574,11 @@ class DownloadSupportPackageView(HomeAssistantView): "\n\n" ) + markdown += await self._get_host_resources_markdown(hass) + + if is_hassio(hass): + markdown += await self._get_addon_resources_markdown(hass) + log_handler = hass.data[DATA_CLOUD_LOG_HANDLER] logs = "\n".join(await log_handler.get_logs(hass)) markdown += ( @@ -584,6 +592,103 @@ class DownloadSupportPackageView(HomeAssistantView): return markdown + async def _get_host_resources_markdown(self, hass: HomeAssistant) -> str: + """Get host resource usage markdown using psutil.""" + + def _collect_system_stats() -> dict[str, Any]: + """Collect system stats.""" + psutil_wrapper = ha_psutil.PsutilWrapper() + psutil_mod = psutil_wrapper.psutil + + cpu_percent = psutil_mod.cpu_percent(interval=0.1) + memory = psutil_mod.virtual_memory() + disk = psutil_mod.disk_usage("/") + + return { + "cpu_percent": cpu_percent, + "memory_total": memory.total, + "memory_used": memory.used, + "memory_available": memory.available, + "memory_percent": memory.percent, + "disk_total": disk.total, + "disk_used": disk.used, + "disk_free": disk.free, + "disk_percent": disk.percent, + } + + markdown = "" + try: + stats = await hass.async_add_executor_job(_collect_system_stats) + + markdown += "## Host resource usage\n\n" + markdown += "Resource | Value\n" + markdown += "--- | ---\n" + + markdown += f"CPU usage | {stats['cpu_percent']}%\n" + + memory_total_gb = round(stats["memory_total"] / (1024**3), 2) + memory_used_gb = round(stats["memory_used"] / (1024**3), 2) + memory_available_gb = round(stats["memory_available"] / (1024**3), 2) + markdown += f"Memory total | {memory_total_gb} GB\n" + markdown += ( + f"Memory used | {memory_used_gb} GB ({stats['memory_percent']}%)\n" + ) + markdown += f"Memory available | {memory_available_gb} GB\n" + + disk_total_gb = round(stats["disk_total"] / (1024**3), 2) + disk_used_gb = round(stats["disk_used"] / (1024**3), 2) + disk_free_gb = round(stats["disk_free"] / (1024**3), 2) + markdown += f"Disk total | {disk_total_gb} GB\n" + markdown += f"Disk used | {disk_used_gb} GB ({stats['disk_percent']}%)\n" + markdown += f"Disk free | {disk_free_gb} GB\n" + + markdown += "\n" + except Exception: # noqa: BLE001 + # Broad exception catch for robustness in support package generation + markdown += "## Host resource usage\n\n" + markdown += "Unable to collect host resource information\n\n" + + return markdown + + async def _get_addon_resources_markdown(self, hass: HomeAssistant) -> str: + """Get add-on resource usage markdown for hassio.""" + markdown = "" + try: + supervisor_info = get_supervisor_info(hass) or {} + addons_stats = get_addons_stats(hass) + addons = supervisor_info.get("addons", []) + + if addons: + markdown += "## Add-on resource usage\n\n" + markdown += "
Add-on resources\n\n" + markdown += "Add-on | Version | State | CPU | Memory\n" + markdown += "--- | --- | --- | --- | ---\n" + + for addon in addons: + slug = addon.get("slug", "unknown") + name = addon.get("name", slug) + version = addon.get("version", "unknown") + state = addon.get("state", "unknown") + + addon_stats = addons_stats.get(slug, {}) + cpu = addon_stats.get("cpu_percent") + memory = addon_stats.get("memory_percent") + + cpu_str = f"{cpu}%" if cpu is not None else "N/A" + memory_str = f"{memory}%" if memory is not None else "N/A" + + markdown += ( + f"{name} | {version} | {state} | {cpu_str} | {memory_str}\n" + ) + + markdown += "\n
\n\n" + except Exception: # noqa: BLE001 + # Broad exception catch for robustness in support package generation + markdown += "## Add-on resource usage\n\n" + markdown += "Unable to collect add-on resource information\n\n" + + return markdown + async def get(self, request: web.Request) -> web.Response: """Download support package file.""" diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index add6abf6490..b4e108192d8 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -5,7 +5,8 @@ "alexa", "assist_pipeline", "backup", - "google_assistant" + "google_assistant", + "hassio" ], "codeowners": ["@home-assistant/cloud"], "dependencies": ["auth", "http", "repairs", "webhook", "web_rtc"], @@ -13,6 +14,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==1.9.0"], + "requirements": ["hass-nabucasa==1.9.0", "psutil-home-assistant==0.0.1"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index fe1322de330..0c73371f227 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1776,6 +1776,7 @@ prowlpy==1.1.1 # homeassistant.components.proxmoxve proxmoxer==2.0.1 +# homeassistant.components.cloud # homeassistant.components.hardware # homeassistant.components.recorder # homeassistant.components.systemmonitor diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1205567a287..6de9964a5c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1522,6 +1522,7 @@ prometheus-client==0.21.0 # homeassistant.components.prowl prowlpy==1.1.1 +# homeassistant.components.cloud # homeassistant.components.hardware # homeassistant.components.recorder # homeassistant.components.systemmonitor diff --git a/tests/components/cloud/snapshots/test_http_api.ambr b/tests/components/cloud/snapshots/test_http_api.ambr index 2249cc6f9fe..dae61b6d999 100644 --- a/tests/components/cloud/snapshots/test_http_api.ambr +++ b/tests/components/cloud/snapshots/test_http_api.ambr @@ -87,6 +87,18 @@ + ## Host resource usage + + Resource | Value + --- | --- + CPU usage | 25.5% + Memory total | 16.0 GB + Memory used | 8.0 GB (50.0%) + Memory available | 8.0 GB + Disk total | 500.0 GB + Disk used | 200.0 GB (40.0%) + Disk free | 300.0 GB + ## Full logs
Logs @@ -181,6 +193,18 @@
+ ## Host resource usage + + Resource | Value + --- | --- + CPU usage | 25.5% + Memory total | 16.0 GB + Memory used | 8.0 GB (50.0%) + Memory available | 8.0 GB + Disk total | 500.0 GB + Disk used | 200.0 GB (40.0%) + Disk free | 300.0 GB + ## Full logs
Logs @@ -196,6 +220,252 @@ ''' # --- +# name: test_download_support_package_hassio + ''' + ## System Information + + version | core-2025.2.0 + --- | --- + installation_type | Home Assistant OS + dev | False + hassio | True + docker | True + container_arch | aarch64 + user | root + virtualenv | False + python_version | 3.13.1 + os_name | Linux + os_version | 6.12.9 + arch | aarch64 + timezone | US/Pacific + config_dir | config + + ## Active Integrations + + Built-in integrations: 23 + Custom integrations: 1 + +
Built-in integrations + + Domain | Name + --- | --- + ai_task | AI Task + auth | Auth + binary_sensor | Binary Sensor + cloud | Home Assistant Cloud + cloud.ai_task | Unknown + cloud.binary_sensor | Unknown + cloud.conversation | Unknown + cloud.stt | Unknown + cloud.tts | Unknown + conversation | Conversation + ffmpeg | FFmpeg + hassio | hassio + homeassistant | Home Assistant Core Integration + http | HTTP + intent | Intent + media_source | Media Source + mock_no_info_integration | mock_no_info_integration + repairs | Repairs + stt | Speech-to-text (STT) + system_health | System Health + tts | Text-to-speech (TTS) + web_rtc | WebRTC + webhook | Webhook + +
+ +
Custom integrations + + Domain | Name | Version | Documentation + --- | --- | --- | --- + test | Test Components | 1.2.3 | http://example.com + +
+ +
hassio + + host_os | Home Assistant OS 14.0 + --- | --- + update_channel | stable + supervisor_version | supervisor-2025.01.0 + agent_version | 1.6.0 + docker_version | 27.4.1 + disk_total | 128.5 GB + disk_used | 45.2 GB + healthy | True + supported | True + host_connectivity | True + supervisor_connectivity | True + board | green + supervisor_api | ok + version_api | ok + installed_addons | Mosquitto broker (6.4.1), Samba share (12.3.2), Visual Studio Code (5.21.2) + +
+ +
mock_no_info_integration + + No information available +
+ +
cloud + + logged_in | True + --- | --- + subscription_expiration | 2025-01-17T11:19:31+00:00 + relayer_connected | True + relayer_region | xx-earth-616 + remote_enabled | True + remote_connected | False + alexa_enabled | True + google_enabled | False + cloud_ice_servers_enabled | True + remote_server | us-west-1 + certificate_status | ready + instance_id | 12345678901234567890 + can_reach_cert_server | Exception: Unexpected exception + can_reach_cloud_auth | Failed: unreachable + can_reach_cloud | ok + +
+ + ## Host resource usage + + Resource | Value + --- | --- + CPU usage | 25.5% + Memory total | 16.0 GB + Memory used | 8.0 GB (50.0%) + Memory available | 8.0 GB + Disk total | 500.0 GB + Disk used | 200.0 GB (40.0%) + Disk free | 300.0 GB + + ## Add-on resource usage + +
Add-on resources + + Add-on | Version | State | CPU | Memory + --- | --- | --- | --- | --- + Mosquitto broker | 6.4.1 | started | 0.5% | 1.2% + Samba share | 12.3.2 | started | 0.1% | 0.8% + Visual Studio Code | 5.21.2 | stopped | N/A | N/A + +
+ + ## Full logs + +
Logs + + ```logs + 2025-02-10 12:00:00.000 INFO (MainThread) [hass_nabucasa.iot] Hass nabucasa log + 2025-02-10 12:00:00.000 WARNING (MainThread) [snitun.utils.aiohttp_client] Snitun log + 2025-02-10 12:00:00.000 ERROR (MainThread) [homeassistant.components.cloud.client] Cloud log + ``` + +
+ + ''' +# --- +# name: test_download_support_package_host_resources + ''' + ## System Information + + version | core-2025.2.0 + --- | --- + installation_type | Home Assistant Container + dev | False + hassio | False + docker | True + container_arch | x86_64 + user | root + virtualenv | False + python_version | 3.13.1 + os_name | Linux + os_version | 6.12.9 + arch | x86_64 + timezone | US/Pacific + config_dir | config + + ## Active Integrations + + Built-in integrations: 21 + Custom integrations: 0 + +
Built-in integrations + + Domain | Name + --- | --- + ai_task | AI Task + auth | Auth + binary_sensor | Binary Sensor + cloud | Home Assistant Cloud + cloud.ai_task | Unknown + cloud.binary_sensor | Unknown + cloud.conversation | Unknown + cloud.stt | Unknown + cloud.tts | Unknown + conversation | Conversation + ffmpeg | FFmpeg + homeassistant | Home Assistant Core Integration + http | HTTP + intent | Intent + media_source | Media Source + repairs | Repairs + stt | Speech-to-text (STT) + system_health | System Health + tts | Text-to-speech (TTS) + web_rtc | WebRTC + webhook | Webhook + +
+ +
cloud + + logged_in | True + --- | --- + subscription_expiration | 2025-01-17T11:19:31+00:00 + relayer_connected | True + relayer_region | xx-earth-616 + remote_enabled | True + remote_connected | False + alexa_enabled | True + google_enabled | False + cloud_ice_servers_enabled | True + remote_server | us-west-1 + certificate_status | ready + instance_id | 12345678901234567890 + can_reach_cert_server | Exception: Unexpected exception + can_reach_cloud_auth | Failed: unreachable + can_reach_cloud | ok + +
+ + ## Host resource usage + + Resource | Value + --- | --- + CPU usage | 25.5% + Memory total | 16.0 GB + Memory used | 8.0 GB (50.0%) + Memory available | 8.0 GB + Disk total | 500.0 GB + Disk used | 200.0 GB (40.0%) + Disk free | 300.0 GB + + ## Full logs + +
Logs + + ```logs + 2025-02-10 12:00:00.000 INFO (MainThread) [hass_nabucasa.iot] Hass nabucasa log + ``` + +
+ + ''' +# --- # name: test_download_support_package_integration_load_error ''' ## System Information @@ -246,6 +516,18 @@
+ ## Host resource usage + + Resource | Value + --- | --- + CPU usage | 25.5% + Memory total | 16.0 GB + Memory used | 8.0 GB (50.0%) + Memory available | 8.0 GB + Disk total | 500.0 GB + Disk used | 200.0 GB (40.0%) + Disk free | 300.0 GB + ## Full logs
Logs diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index eaaf42bc9bd..336cf85a54d 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,6 +1,6 @@ """Tests for the HTTP API for the cloud component.""" -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Generator from copy import deepcopy import datetime from http import HTTPStatus @@ -114,6 +114,36 @@ PIPELINE_DATA_OTHER = { SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/payments/subscription_info" +@pytest.fixture +def mock_psutil_wrapper() -> Generator[MagicMock]: + """Fixture to mock psutil for support package tests.""" + mock_memory = MagicMock() + mock_memory.total = 16 * 1024**3 # 16 GB + mock_memory.used = 8 * 1024**3 # 8 GB + mock_memory.available = 8 * 1024**3 # 8 GB + mock_memory.percent = 50.0 + + mock_disk = MagicMock() + mock_disk.total = 500 * 1024**3 # 500 GB + mock_disk.used = 200 * 1024**3 # 200 GB + mock_disk.free = 300 * 1024**3 # 300 GB + mock_disk.percent = 40.0 + + mock_psutil = MagicMock() + mock_psutil.cpu_percent = MagicMock(return_value=25.5) + mock_psutil.virtual_memory = MagicMock(return_value=mock_memory) + mock_psutil.disk_usage = MagicMock(return_value=mock_disk) + + mock_wrapper = MagicMock() + mock_wrapper.psutil = mock_psutil + + with patch( + "homeassistant.components.cloud.http_api.ha_psutil.PsutilWrapper", + return_value=mock_wrapper, + ): + yield mock_wrapper + + @pytest.fixture(name="setup_cloud") async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None: """Fixture that sets up cloud.""" @@ -1846,7 +1876,7 @@ async def test_logout_view_dispatch_event( @patch("homeassistant.components.cloud.helpers.FixedSizeQueueLogHandler.MAX_RECORDS", 3) -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "mock_psutil_wrapper") async def test_download_support_package( hass: HomeAssistant, cloud: MagicMock, @@ -1959,7 +1989,7 @@ async def test_download_support_package( assert await req.text() == snapshot -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "mock_psutil_wrapper") async def test_download_support_package_custom_components_error( hass: HomeAssistant, cloud: MagicMock, @@ -1986,7 +2016,7 @@ async def test_download_support_package_custom_components_error( async def mock_empty_info(hass: HomeAssistant) -> dict[str, Any]: return {} - register.async_register_info(mock_empty_info, "/config/mock_integration") + register.async_register_info(mock_empty_info, "/mock_integration") mock_platform( hass, @@ -2071,7 +2101,7 @@ async def test_download_support_package_custom_components_error( assert await req.text() == snapshot -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "mock_psutil_wrapper") async def test_download_support_package_integration_load_error( hass: HomeAssistant, cloud: MagicMock, @@ -2098,7 +2128,7 @@ async def test_download_support_package_integration_load_error( async def mock_empty_info(hass: HomeAssistant) -> dict[str, Any]: return {} - register.async_register_info(mock_empty_info, "/config/mock_integration") + register.async_register_info(mock_empty_info, "/mock_integration") mock_platform( hass, @@ -2188,6 +2218,277 @@ async def test_download_support_package_integration_load_error( assert await req.text() == snapshot +@patch("homeassistant.components.cloud.helpers.FixedSizeQueueLogHandler.MAX_RECORDS", 3) +@pytest.mark.usefixtures("enable_custom_integrations", "mock_psutil_wrapper") +async def test_download_support_package_hassio( + hass: HomeAssistant, + cloud: MagicMock, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + hass_client: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test downloading a support package file with hassio resources.""" + + aioclient_mock.get("https://cloud.bla.com/status", text="") + aioclient_mock.get( + "https://cert-server/directory", exc=Exception("Unexpected exception") + ) + aioclient_mock.get( + "https://cognito-idp.us-east-1.amazonaws.com/AAAA/.well-known/jwks.json", + exc=aiohttp.ClientError, + ) + + def async_register_hassio_platform( + hass: HomeAssistant, + register: system_health.SystemHealthRegistration, + ) -> None: + async def mock_hassio_info(hass: HomeAssistant) -> dict[str, Any]: + return { + "host_os": "Home Assistant OS 14.0", + "update_channel": "stable", + "supervisor_version": "supervisor-2025.01.0", + "agent_version": "1.6.0", + "docker_version": "27.4.1", + "disk_total": "128.5 GB", + "disk_used": "45.2 GB", + "healthy": True, + "supported": True, + "host_connectivity": True, + "supervisor_connectivity": True, + "board": "green", + "supervisor_api": "ok", + "version_api": "ok", + "installed_addons": "Mosquitto broker (6.4.1), Samba share (12.3.2), Visual Studio Code (5.21.2)", + } + + register.async_register_info(mock_hassio_info, "/hassio/system") + + mock_platform( + hass, + "hassio.system_health", + MagicMock(async_register=async_register_hassio_platform), + ) + hass.config.components.add("hassio") + + def async_register_mock_platform( + hass: HomeAssistant, + register: system_health.SystemHealthRegistration, + ) -> None: + async def mock_empty_info(hass: HomeAssistant) -> dict[str, Any]: + return {} + + register.async_register_info(mock_empty_info, "/config/mock_integration") + + mock_platform( + hass, + "mock_no_info_integration.system_health", + MagicMock(async_register=async_register_mock_platform), + ) + hass.config.components.add("mock_no_info_integration") + hass.config.components.add("test") + + assert await async_setup_component(hass, "system_health", {}) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hexmock: + hexmock.return_value = "12345678901234567890" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "user_pool_id": "AAAA", + "region": "us-east-1", + "acme_server": "cert-server", + "relayer_server": "cloud.bla.com", + }, + }, + ) + await hass.async_block_till_done() + + await cloud.login("test-user", "test-pass") + + cloud.remote.snitun_server = "us-west-1" + cloud.remote.certificate_status = CertificateStatus.READY + cloud.expiration_date = dt_util.parse_datetime("2025-01-17T11:19:31.0+00:00") + + await cloud.client.async_system_message({"region": "xx-earth-616"}) + await set_cloud_prefs( + { + "alexa_enabled": True, + "google_enabled": False, + "remote_enabled": True, + "cloud_ice_servers_enabled": True, + } + ) + + now = dt_util.utcnow() + tz = now.astimezone().tzinfo + freezer.move_to(datetime.datetime(2025, 2, 10, 12, 0, 0, tzinfo=tz)) + logging.getLogger("hass_nabucasa.iot").info( + "This message will be dropped since this test patches MAX_RECORDS" + ) + logging.getLogger("hass_nabucasa.iot").info("Hass nabucasa log") + logging.getLogger("snitun.utils.aiohttp_client").warning("Snitun log") + logging.getLogger("homeassistant.components.cloud.client").error("Cloud log") + freezer.move_to(now) + + cloud_client = await hass_client() + + with ( + patch.object(hass.config, "config_dir", new="config"), + patch( + "homeassistant.components.homeassistant.system_health.system_info.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "version": "2025.2.0", + "dev": False, + "hassio": True, + "virtualenv": False, + "python_version": "3.13.1", + "docker": True, + "container_arch": "aarch64", + "arch": "aarch64", + "timezone": "US/Pacific", + "os_name": "Linux", + "os_version": "6.12.9", + "user": "root", + }, + ), + patch( + "homeassistant.components.cloud.http_api.get_supervisor_info", + return_value={ + "addons": [ + { + "slug": "core_mosquitto", + "name": "Mosquitto broker", + "version": "6.4.1", + "state": "started", + }, + { + "slug": "core_samba", + "name": "Samba share", + "version": "12.3.2", + "state": "started", + }, + { + "slug": "a0d7b954_vscode", + "name": "Visual Studio Code", + "version": "5.21.2", + "state": "stopped", + }, + ], + }, + ), + patch( + "homeassistant.components.cloud.http_api.get_addons_stats", + return_value={ + "core_mosquitto": { + "cpu_percent": 0.5, + "memory_percent": 1.2, + }, + "core_samba": { + "cpu_percent": 0.1, + "memory_percent": 0.8, + }, + # No stats for vscode (stopped) + }, + ), + ): + req = await cloud_client.get("/api/cloud/support_package") + assert req.status == HTTPStatus.OK + assert await req.text() == snapshot + + +@patch("homeassistant.components.cloud.helpers.FixedSizeQueueLogHandler.MAX_RECORDS", 3) +@pytest.mark.usefixtures("mock_psutil_wrapper") +async def test_download_support_package_host_resources( + hass: HomeAssistant, + cloud: MagicMock, + set_cloud_prefs: Callable[[dict[str, Any]], Coroutine[Any, Any, None]], + hass_client: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test downloading a support package file with psutil host resources (non-hassio).""" + aioclient_mock.get("https://cloud.bla.com/status", text="") + aioclient_mock.get( + "https://cert-server/directory", exc=Exception("Unexpected exception") + ) + aioclient_mock.get( + "https://cognito-idp.us-east-1.amazonaws.com/AAAA/.well-known/jwks.json", + exc=aiohttp.ClientError, + ) + + assert await async_setup_component(hass, "system_health", {}) + + with patch("uuid.UUID.hex", new_callable=PropertyMock) as hexmock: + hexmock.return_value = "12345678901234567890" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "user_pool_id": "AAAA", + "region": "us-east-1", + "acme_server": "cert-server", + "relayer_server": "cloud.bla.com", + }, + }, + ) + await hass.async_block_till_done() + + await cloud.login("test-user", "test-pass") + + cloud.remote.snitun_server = "us-west-1" + cloud.remote.certificate_status = CertificateStatus.READY + cloud.expiration_date = dt_util.parse_datetime("2025-01-17T11:19:31.0+00:00") + + await cloud.client.async_system_message({"region": "xx-earth-616"}) + await set_cloud_prefs( + { + "alexa_enabled": True, + "google_enabled": False, + "remote_enabled": True, + "cloud_ice_servers_enabled": True, + } + ) + + now = dt_util.utcnow() + tz = now.astimezone().tzinfo + freezer.move_to(datetime.datetime(2025, 2, 10, 12, 0, 0, tzinfo=tz)) + logging.getLogger("hass_nabucasa.iot").info("Hass nabucasa log") + freezer.move_to(now) + + cloud_client = await hass_client() + with ( + patch.object(hass.config, "config_dir", new="config"), + patch( + "homeassistant.components.homeassistant.system_health.system_info.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Container", + "version": "2025.2.0", + "dev": False, + "hassio": False, + "virtualenv": False, + "python_version": "3.13.1", + "docker": True, + "container_arch": "x86_64", + "arch": "x86_64", + "timezone": "US/Pacific", + "os_name": "Linux", + "os_version": "6.12.9", + "user": "root", + }, + ), + ): + req = await cloud_client.get("/api/cloud/support_package") + assert req.status == HTTPStatus.OK + assert await req.text() == snapshot + + async def test_websocket_ice_servers( hass: HomeAssistant, hass_ws_client: WebSocketGenerator,