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,