diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 1634f01bf06..af479587d4f 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -338,6 +338,7 @@ class Analytics: hass = self._hass supervisor_info = None + addons_info: dict[str, Any] | None = None operating_system_info: dict[str, Any] = {} if self._data.uuid is None: @@ -347,6 +348,7 @@ class Analytics: if self.supervisor: supervisor_info = hassio.get_supervisor_info(hass) operating_system_info = hassio.get_os_info(hass) or {} + addons_info = hassio.get_addons_info(hass) or {} system_info = await async_get_system_info(hass) integrations = [] @@ -419,13 +421,10 @@ class Analytics: integrations.append(integration.domain) - if supervisor_info is not None: + if addons_info is not None: supervisor_client = hassio.get_supervisor_client(hass) installed_addons = await asyncio.gather( - *( - supervisor_client.addons.addon_info(addon[ATTR_SLUG]) - for addon in supervisor_info[ATTR_ADDONS] - ) + *(supervisor_client.addons.addon_info(slug) for slug in addons_info) ) addons.extend( { diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index ed7478422c9..6a86cfd6578 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -9,10 +9,21 @@ import logging import os import re import struct -from typing import Any, NamedTuple +from typing import Any, NamedTuple, cast from aiohasupervisor import SupervisorError -from aiohasupervisor.models import GreenOptions, YellowOptions # noqa: F401 +from aiohasupervisor.models import ( + GreenOptions, + HomeAssistantInfo, + HostInfo, + InstalledAddon, + NetworkInfo, + OSInfo, + RootInfo, + StoreInfo, + SupervisorInfo, + YellowOptions, +) import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN @@ -65,7 +76,7 @@ from . import ( # noqa: F401 system_health, update, ) -from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState # noqa: F401 +from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState from .addon_panel import async_setup_addon_panel from .auth import async_setup_auth_view from .config import HassioConfig @@ -82,7 +93,9 @@ from .const import ( ATTR_INPUT, ATTR_LOCATION, ATTR_PASSWORD, + ATTR_REPOSITORIES, ATTR_SLUG, + DATA_ADDONS_LIST, DATA_COMPONENT, DATA_CONFIG_STORE, DATA_CORE_INFO, @@ -100,18 +113,21 @@ from .const import ( from .coordinator import ( HassioDataUpdateCoordinator, get_addons_info, - get_addons_stats, # noqa: F401 - get_core_info, # noqa: F401 - get_core_stats, # noqa: F401 - get_host_info, # noqa: F401 + get_addons_list, + get_addons_stats, + get_core_info, + get_core_stats, + get_host_info, get_info, - get_issues_info, # noqa: F401 + get_issues_info, + get_network_info, get_os_info, - get_supervisor_info, # noqa: F401 - get_supervisor_stats, # noqa: F401 + get_store, + get_supervisor_info, + get_supervisor_stats, ) from .discovery import async_setup_discovery_view -from .handler import ( # noqa: F401 +from .handler import ( HassIO, HassioAPIError, async_update_diagnostics, @@ -122,6 +138,35 @@ from .ingress import async_setup_ingress_view from .issues import SupervisorIssues from .websocket_api import async_load_websocket_api +# Expose the future safe name now so integrations can use it +# All references to addons will eventually be refactored and deprecated +get_apps_list = get_addons_list +__all__ = [ + "AddonError", + "AddonInfo", + "AddonManager", + "AddonState", + "GreenOptions", + "SupervisorError", + "YellowOptions", + "async_update_diagnostics", + "get_addons_info", + "get_addons_list", + "get_addons_stats", + "get_apps_list", + "get_core_info", + "get_core_stats", + "get_host_info", + "get_info", + "get_issues_info", + "get_network_info", + "get_os_info", + "get_store", + "get_supervisor_client", + "get_supervisor_info", + "get_supervisor_stats", +] + _LOGGER = logging.getLogger(__name__) @@ -504,27 +549,55 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: try: ( - hass.data[DATA_INFO], - hass.data[DATA_HOST_INFO], + root_info, + host_info, store_info, - hass.data[DATA_CORE_INFO], - hass.data[DATA_SUPERVISOR_INFO], - hass.data[DATA_OS_INFO], - hass.data[DATA_NETWORK_INFO], - ) = await asyncio.gather( - create_eager_task(hassio.get_info()), - create_eager_task(hassio.get_host_info()), - create_eager_task(supervisor_client.store.info()), - create_eager_task(hassio.get_core_info()), - create_eager_task(hassio.get_supervisor_info()), - create_eager_task(hassio.get_os_info()), - create_eager_task(hassio.get_network_info()), + homeassistant_info, + supervisor_info, + os_info, + network_info, + addons_list, + ) = cast( + tuple[ + RootInfo, + HostInfo, + StoreInfo, + HomeAssistantInfo, + SupervisorInfo, + OSInfo, + NetworkInfo, + list[InstalledAddon], + ], + await asyncio.gather( + create_eager_task(supervisor_client.info()), + create_eager_task(supervisor_client.host.info()), + create_eager_task(supervisor_client.store.info()), + create_eager_task(supervisor_client.homeassistant.info()), + create_eager_task(supervisor_client.supervisor.info()), + create_eager_task(supervisor_client.os.info()), + create_eager_task(supervisor_client.network.info()), + create_eager_task(supervisor_client.addons.list()), + ), ) - except HassioAPIError as err: + except SupervisorError as err: _LOGGER.warning("Can't read Supervisor data: %s", err) else: + hass.data[DATA_INFO] = root_info.to_dict() + hass.data[DATA_HOST_INFO] = host_info.to_dict() hass.data[DATA_STORE] = store_info.to_dict() + hass.data[DATA_CORE_INFO] = homeassistant_info.to_dict() + hass.data[DATA_SUPERVISOR_INFO] = supervisor_info.to_dict() + hass.data[DATA_OS_INFO] = os_info.to_dict() + hass.data[DATA_NETWORK_INFO] = network_info.to_dict() + hass.data[DATA_ADDONS_LIST] = [addon.to_dict() for addon in addons_list] + + # Deprecated 2026.4.0: Folding repositories and addons.list results into supervisor_info for compatibility + # Can drop this after removal period + hass.data[DATA_SUPERVISOR_INFO]["repositories"] = hass.data[DATA_STORE][ + ATTR_REPOSITORIES + ] + hass.data[DATA_SUPERVISOR_INFO]["addons"] = hass.data[DATA_ADDONS_LIST] async_call_later( hass, diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index d71efb3d09e..4f696983825 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -93,6 +93,7 @@ DATA_SUPERVISOR_INFO = "hassio_supervisor_info" DATA_SUPERVISOR_STATS = "hassio_supervisor_stats" DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" +DATA_ADDONS_LIST = "hassio_addons_list" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) ATTR_AUTO_UPDATE = "auto_update" @@ -106,6 +107,7 @@ ATTR_STATE = "state" ATTR_STARTED = "started" ATTR_URL = "url" ATTR_REPOSITORY = "repository" +ATTR_REPOSITORIES = "repositories" DATA_KEY_ADDONS = "addons" DATA_KEY_OS = "os" diff --git a/homeassistant/components/hassio/coordinator.py b/homeassistant/components/hassio/coordinator.py index e67b76458a6..679614acbec 100644 --- a/homeassistant/components/hassio/coordinator.py +++ b/homeassistant/components/hassio/coordinator.py @@ -4,13 +4,20 @@ from __future__ import annotations import asyncio from collections import defaultdict +from collections.abc import Awaitable from copy import deepcopy import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from aiohasupervisor import SupervisorError, SupervisorNotFoundError -from aiohasupervisor.models import StoreInfo -from aiohasupervisor.models.mounts import CIFSMountResponse, NFSMountResponse +from aiohasupervisor.models import ( + AddonState, + CIFSMountResponse, + InstalledAddon, + NFSMountResponse, + StoreInfo, +) +from aiohasupervisor.models.base import ResponseData from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME @@ -23,16 +30,16 @@ from homeassistant.loader import bind_hass from .const import ( ATTR_AUTO_UPDATE, + ATTR_REPOSITORIES, ATTR_REPOSITORY, ATTR_SLUG, - ATTR_STARTED, - ATTR_STATE, ATTR_URL, ATTR_VERSION, CONTAINER_INFO, CONTAINER_STATS, CORE_CONTAINER, DATA_ADDONS_INFO, + DATA_ADDONS_LIST, DATA_ADDONS_STATS, DATA_COMPONENT, DATA_CORE_INFO, @@ -57,7 +64,7 @@ from .const import ( SUPERVISOR_CONTAINER, SupervisorEntityModel, ) -from .handler import HassioAPIError, get_supervisor_client +from .handler import get_supervisor_client from .jobs import SupervisorJobs if TYPE_CHECKING: @@ -118,7 +125,7 @@ def get_network_info(hass: HomeAssistant) -> dict[str, Any] | None: @callback @bind_hass -def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any]] | None: +def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any] | None] | None: """Return Addons info. Async friendly. @@ -126,9 +133,18 @@ def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any]] | None: return hass.data.get(DATA_ADDONS_INFO) +@callback +def get_addons_list(hass: HomeAssistant) -> list[dict[str, Any]] | None: + """Return list of installed addons and subset of details for each. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_LIST) + + @callback @bind_hass -def get_addons_stats(hass: HomeAssistant) -> dict[str, Any]: +def get_addons_stats(hass: HomeAssistant) -> dict[str, dict[str, Any] | None]: """Return Addons stats. Async friendly. @@ -341,7 +357,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): try: await self.force_data_refresh(is_first_update) - except HassioAPIError as err: + except SupervisorError as err: raise UpdateFailed(f"Error on Supervisor API: {err}") from err new_data: dict[str, Any] = {} @@ -350,6 +366,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): addons_stats = get_addons_stats(self.hass) store_data = get_store(self.hass) mounts_info = await self.supervisor_client.mounts.info() + addons_list = get_addons_list(self.hass) or [] if store_data: repositories = { @@ -360,17 +377,17 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): repositories = {} new_data[DATA_KEY_ADDONS] = { - addon[ATTR_SLUG]: { + (slug := addon[ATTR_SLUG]): { **addon, - **((addons_stats or {}).get(addon[ATTR_SLUG]) or {}), - ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get( + **(addons_stats.get(slug) or {}), + ATTR_AUTO_UPDATE: (addons_info.get(slug) or {}).get( ATTR_AUTO_UPDATE, False ), ATTR_REPOSITORY: repositories.get( - addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") + repo_slug := addon.get(ATTR_REPOSITORY, ""), repo_slug ), } - for addon in supervisor_info.get("addons", []) + for addon in addons_list } if self.is_hass_os: new_data[DATA_KEY_OS] = get_os_info(self.hass) @@ -462,32 +479,48 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): container_updates = self._container_updates data = self.hass.data - hassio = self.hassio - updates = { - DATA_INFO: hassio.get_info(), - DATA_CORE_INFO: hassio.get_core_info(), - DATA_SUPERVISOR_INFO: hassio.get_supervisor_info(), - DATA_OS_INFO: hassio.get_os_info(), + client = self.supervisor_client + + updates: dict[str, Awaitable[ResponseData]] = { + DATA_INFO: client.info(), + DATA_CORE_INFO: client.homeassistant.info(), + DATA_SUPERVISOR_INFO: client.supervisor.info(), + DATA_OS_INFO: client.os.info(), + DATA_STORE: client.store.info(), } if CONTAINER_STATS in container_updates[CORE_CONTAINER]: - updates[DATA_CORE_STATS] = hassio.get_core_stats() + updates[DATA_CORE_STATS] = client.homeassistant.stats() if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]: - updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats() + updates[DATA_SUPERVISOR_STATS] = client.supervisor.stats() - results = await asyncio.gather(*updates.values()) - for key, result in zip(updates, results, strict=False): - data[key] = result + # Pull off addons.list results for further processing before caching + addons_list, *results = await asyncio.gather( + client.addons.list(), *updates.values() + ) + for key, result in zip(updates, cast(list[ResponseData], results), strict=True): + data[key] = result.to_dict() + + installed_addons = cast(list[InstalledAddon], addons_list) + data[DATA_ADDONS_LIST] = [addon.to_dict() for addon in installed_addons] + + # Deprecated 2026.4.0: Folding repositories and addons.list results into supervisor_info for compatibility + # Can drop this after removal period + data[DATA_SUPERVISOR_INFO].update( + { + "repositories": data[DATA_STORE][ATTR_REPOSITORIES], + "addons": [addon.to_dict() for addon in installed_addons], + } + ) + + all_addons = {addon.slug for addon in installed_addons} + started_addons = { + addon.slug + for addon in installed_addons + if addon.state in {AddonState.STARTED, AddonState.STARTUP} + } - _addon_data = data[DATA_SUPERVISOR_INFO].get("addons", []) - all_addons: list[str] = [] - started_addons: list[str] = [] - for addon in _addon_data: - slug = addon[ATTR_SLUG] - all_addons.append(slug) - if addon[ATTR_STATE] == ATTR_STARTED: - started_addons.append(slug) # - # Update add-on info if its the first update or + # Update addon info if its the first update or # there is at least one entity that needs the data. # # When entities are added they call async_enable_container_updates @@ -514,6 +547,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): ), ): container_data: dict[str, Any] = data.setdefault(data_key, {}) + + # Clean up cache + for slug in container_data.keys() - wanted_addons: + del container_data[slug] + + # Update cache from API container_data.update( dict( await asyncio.gather( @@ -540,7 +579,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): return (slug, stats.to_dict()) async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]: - """Return the info for an add-on.""" + """Return the info for an addon.""" try: info = await self.supervisor_client.addons.addon_info(slug) except SupervisorError as err: diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 2fb52034fce..f4055efd8df 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -87,70 +87,6 @@ class HassIO: """Return base url for Supervisor.""" return self._base_url - @api_data - def get_info(self) -> Coroutine: - """Return generic Supervisor information. - - This method returns a coroutine. - """ - return self.send_command("/info", method="get") - - @api_data - def get_host_info(self) -> Coroutine: - """Return data for Host. - - This method returns a coroutine. - """ - return self.send_command("/host/info", method="get") - - @api_data - def get_os_info(self) -> Coroutine: - """Return data for the OS. - - This method returns a coroutine. - """ - return self.send_command("/os/info", method="get") - - @api_data - def get_core_info(self) -> Coroutine: - """Return data for Home Asssistant Core. - - This method returns a coroutine. - """ - return self.send_command("/core/info", method="get") - - @api_data - def get_supervisor_info(self) -> Coroutine: - """Return data for the Supervisor. - - This method returns a coroutine. - """ - return self.send_command("/supervisor/info", method="get") - - @api_data - def get_network_info(self) -> Coroutine: - """Return data for the Host Network. - - This method returns a coroutine. - """ - return self.send_command("/network/info", method="get") - - @api_data - def get_core_stats(self) -> Coroutine: - """Return stats for the core. - - This method returns a coroutine. - """ - return self.send_command("/core/stats", method="get") - - @api_data - def get_supervisor_stats(self) -> Coroutine: - """Return stats for the supervisor. - - This method returns a coroutine. - """ - return self.send_command("/supervisor/stats", method="get") - @api_data def get_ingress_panels(self) -> Coroutine: """Return data for Add-on ingress panels. diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 25b4db9c861..7fc32f175c6 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -17,6 +17,7 @@ from aiohasupervisor.models import ( UnsupportedReason, ) +from homeassistant.const import ATTR_NAME from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_call_later @@ -30,6 +31,7 @@ from .const import ( ADDONS_COORDINATOR, ATTR_DATA, ATTR_HEALTHY, + ATTR_SLUG, ATTR_STARTUP, ATTR_SUPPORTED, ATTR_UNHEALTHY_REASONS, @@ -59,7 +61,7 @@ from .const import ( STARTUP_COMPLETE, UPDATE_KEY_SUPERVISOR, ) -from .coordinator import HassioDataUpdateCoordinator, get_addons_info, get_host_info +from .coordinator import HassioDataUpdateCoordinator, get_addons_list, get_host_info from .handler import HassIO, get_supervisor_client ISSUE_KEY_UNHEALTHY = "unhealthy" @@ -265,23 +267,18 @@ class SupervisorIssues: placeholders[PLACEHOLDER_KEY_ADDON_URL] = ( f"/hassio/addon/{issue.reference}" ) - addons = get_addons_info(self._hass) - if addons and issue.reference in addons: - placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][ - "name" - ] - else: - placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference + addons_list = get_addons_list(self._hass) or [] + placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference + for addon in addons_list: + if addon[ATTR_SLUG] == issue.reference: + placeholders[PLACEHOLDER_KEY_ADDON] = addon[ATTR_NAME] + break elif issue.key == ISSUE_KEY_SYSTEM_FREE_SPACE: host_info = get_host_info(self._hass) - if ( - host_info - and "data" in host_info - and "disk_free" in host_info["data"] - ): + if host_info and "disk_free" in host_info: placeholders[PLACEHOLDER_KEY_FREE_SPACE] = str( - host_info["data"]["disk_free"] + host_info["disk_free"] ) else: placeholders[PLACEHOLDER_KEY_FREE_SPACE] = "<2" diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index de90026be5b..9610b594858 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -11,11 +11,13 @@ from aiohasupervisor.models import ContextType import voluptuous as vol from homeassistant.components.repairs import RepairsFlow +from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from . import get_addons_info, get_issues_info +from . import get_addons_list, get_issues_info from .const import ( + ATTR_SLUG, EXTRA_PLACEHOLDERS, ISSUE_KEY_ADDON_BOOT_FAIL, ISSUE_KEY_ADDON_DEPRECATED, @@ -154,7 +156,7 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow): placeholders = {PLACEHOLDER_KEY_COMPONENTS: ""} supervisor_issues = get_issues_info(self.hass) if supervisor_issues and self.issue: - addons = get_addons_info(self.hass) or {} + addons_list = get_addons_list(self.hass) or [] components: list[str] = [] for issue in supervisor_issues.issues: if issue.key == self.issue.key or issue.type != self.issue.type: @@ -166,9 +168,9 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow): components.append( next( ( - info["name"] - for slug, info in addons.items() - if slug == issue.reference + addon[ATTR_NAME] + for addon in addons_list + if addon[ATTR_SLUG] == issue.reference ), issue.reference or "", ) @@ -187,13 +189,12 @@ class AddonIssueRepairFlow(SupervisorIssueRepairFlow): """Get description placeholders for steps.""" placeholders: dict[str, str] = super().description_placeholders or {} if self.issue and self.issue.reference: - addons = get_addons_info(self.hass) - if addons and self.issue.reference in addons: - placeholders[PLACEHOLDER_KEY_ADDON] = addons[self.issue.reference][ - "name" - ] - else: - placeholders[PLACEHOLDER_KEY_ADDON] = self.issue.reference + addons_list = get_addons_list(self.hass) or [] + placeholders[PLACEHOLDER_KEY_ADDON] = self.issue.reference + for addon in addons_list: + if addon[ATTR_SLUG] == self.issue.reference: + placeholders[PLACEHOLDER_KEY_ADDON] = addon[ATTR_NAME] + break return placeholders or None diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index 0a7e9b51e97..ade621df933 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -9,6 +9,7 @@ from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback from .coordinator import ( + get_addons_list, get_host_info, get_info, get_network_info, @@ -35,6 +36,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: host_info = get_host_info(hass) or {} supervisor_info = get_supervisor_info(hass) network_info = get_network_info(hass) or {} + addons_list = get_addons_list(hass) or [] healthy: bool | dict[str, str] if supervisor_info is not None and supervisor_info.get("healthy"): @@ -84,6 +86,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: os_info = get_os_info(hass) or {} information["board"] = os_info.get("board") + # Not using aiohasupervisor for ping call below intentionally. Given system health + # context, it seems preferable to do this check with minimal dependencies information["supervisor_api"] = system_health.async_check_can_reach_url( hass, SUPERVISOR_PING.format(ip_address=ip_address), @@ -95,8 +99,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: ) information["installed_addons"] = ", ".join( - f"{addon['name']} ({addon['version']})" - for addon in (supervisor_info or {}).get("addons", []) + f"{addon['name']} ({addon['version']})" for addon in addons_list ) return information diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 8f8e9913d3f..534106c4957 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -39,7 +39,7 @@ from .const import ( WS_TYPE_EVENT, WS_TYPE_SUBSCRIBE, ) -from .coordinator import get_supervisor_info +from .coordinator import get_addons_list from .update_helper import update_addon, update_core SCHEMA_WEBSOCKET_EVENT = vol.Schema( @@ -168,8 +168,8 @@ async def websocket_update_addon( """Websocket handler to update an addon.""" addon_name: str | None = None addon_version: str | None = None - addons: list = (get_supervisor_info(hass) or {}).get("addons", []) - for addon in addons: + addons_list: list[dict[str, Any]] = get_addons_list(hass) or [] + for addon in addons_list: if addon[ATTR_SLUG] == msg["addon"]: addon_name = addon[ATTR_NAME] addon_version = addon[ATTR_VERSION] diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index d1c90085cb0..a59a3344c10 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -359,10 +359,13 @@ async def test_send_usage_with_supervisor( "healthy": True, "supported": True, "arch": "amd64", - "addons": [{"slug": "test_addon"}], } ), ), + patch( + "homeassistant.components.hassio.get_addons_info", + side_effect=Mock(return_value={"test_addon": {}}), + ), patch( "homeassistant.components.hassio.get_os_info", side_effect=Mock(return_value={}), @@ -578,10 +581,13 @@ async def test_send_statistics_with_supervisor( "healthy": True, "supported": True, "arch": "amd64", - "addons": [{"slug": "test_addon"}], } ), ), + patch( + "homeassistant.components.hassio.get_addons_info", + side_effect=Mock(return_value={"test_addon": {}}), + ), patch( "homeassistant.components.hassio.get_os_info", side_effect=Mock(return_value={}), diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 28864ac1267..5409914f563 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -7,23 +7,52 @@ from collections.abc import AsyncGenerator, Callable, Coroutine, Generator, Mapp from functools import lru_cache from importlib.util import find_spec import inspect +from ipaddress import IPv4Address, IPv4Network from pathlib import Path import re import string from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch -from aiohasupervisor import SupervisorNotFoundError +from aiohasupervisor import SupervisorClient, SupervisorNotFoundError +from aiohasupervisor.addons import AddonsClient +from aiohasupervisor.backups import BackupsClient +from aiohasupervisor.discovery import DiscoveryClient +from aiohasupervisor.homeassistant import HomeAssistantClient +from aiohasupervisor.host import HostClient +from aiohasupervisor.jobs import JobsClient from aiohasupervisor.models import ( + AddonStage, + AddonState, Discovery, + DockerNetwork, GreenInfo, + HomeAssistantInfo, + HomeAssistantStats, + HostInfo, + InstalledAddon, JobsInfo, + LogLevel, + MountsInfo, + NetworkInfo, + OSInfo, Repository, ResolutionInfo, + RootInfo, StoreAddon, StoreInfo, + SupervisorInfo, + SupervisorState, + SupervisorStats, + UpdateChannel, YellowInfo, ) +from aiohasupervisor.mounts import MountsClient +from aiohasupervisor.network import NetworkClient +from aiohasupervisor.os import OSClient +from aiohasupervisor.resolution import ResolutionClient +from aiohasupervisor.store import StoreClient +from aiohasupervisor.supervisor import SupervisorManagementClient import pytest import voluptuous as vol @@ -538,23 +567,239 @@ def os_green_info_fixture(supervisor_client: AsyncMock) -> AsyncMock: return supervisor_client.os.green_info +@pytest.fixture(name="supervisor_root_info") +def supervisor_root_info_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock root info API from supervisor.""" + supervisor_client.info.return_value = RootInfo( + supervisor="222", + homeassistant="0.110.0", + hassos="1.2.3", + docker="", + hostname=None, + operating_system=None, + features=[], + machine=None, + machine_id=None, + arch="", + state=SupervisorState.RUNNING, + supported_arch=[], + supported=True, + channel=UpdateChannel.STABLE, + logging=LogLevel.INFO, + timezone="Etc/UTC", + ) + return supervisor_client.info + + +@pytest.fixture(name="host_info") +def host_info_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock host info API from supervisor.""" + supervisor_client.host.info.return_value = HostInfo( + agent_version=None, + apparmor_version=None, + chassis="vm", + virtualization=None, + cpe=None, + deployment=None, + disk_free=1.6, + disk_total=100.0, + disk_used=98.4, + disk_life_time=None, + features=[], + hostname=None, + llmnr_hostname=None, + kernel="4.19.0-6-amd64", + operating_system="Debian GNU/Linux 10 (buster)", + timezone=None, + dt_utc=None, + dt_synchronized=None, + use_ntp=None, + startup_time=None, + boot_timestamp=None, + broadcast_llmnr=None, + broadcast_mdns=None, + ) + return supervisor_client.host.info + + +@pytest.fixture(name="homeassistant_info") +def homeassistant_info_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock Home Assistant info API from supervisor.""" + supervisor_client.homeassistant.info.return_value = HomeAssistantInfo( + version="1.0.0", + version_latest="1.0.0", + update_available=False, + machine=None, + ip_address=IPv4Address("172.30.32.1"), + arch=None, + image="homeassistant", + boot=True, + port=8123, + ssl=False, + watchdog=True, + audio_input=None, + audio_output=None, + backups_exclude_database=False, + duplicate_log_file=False, + ) + return supervisor_client.homeassistant.info + + +@pytest.fixture(name="supervisor_info") +def supervisor_info_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock supervisor info API from supervisor.""" + supervisor_client.supervisor.info.return_value = SupervisorInfo( + version="1.0.0", + version_latest="1.0.0", + update_available=False, + channel=UpdateChannel.STABLE, + arch="", + supported=True, + healthy=True, + ip_address=IPv4Address("172.30.32.2"), + timezone=None, + logging=LogLevel.INFO, + debug=False, + debug_block=False, + diagnostics=None, + auto_update=True, + country=None, + detect_blocking_io=False, + ) + return supervisor_client.supervisor.info + + +@pytest.fixture(name="addons_list") +def addons_list_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock addons list API from supervisor.""" + supervisor_client.addons.list.return_value = [ + InstalledAddon( + detached=False, + advanced=False, + available=True, + build=False, + description="", + homeassistant=None, + icon=False, + logo=False, + name="test", + repository="core", + slug="test", + stage=AddonStage.STABLE, + update_available=True, + url="https://github.com/home-assistant/addons/test", + version_latest="2.0.1", + version="2.0.0", + state=AddonState.STARTED, + ), + InstalledAddon( + detached=False, + advanced=False, + available=True, + build=False, + description="", + homeassistant=None, + icon=False, + logo=False, + name="test2", + repository="core", + slug="test2", + stage=AddonStage.STABLE, + update_available=False, + url="https://github.com", + version_latest="3.1.0", + version="3.1.0", + state=AddonState.STOPPED, + ), + ] + return supervisor_client.addons.list + + +@pytest.fixture(name="network_info") +def network_info_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock network info API from supervisor.""" + supervisor_client.network.info.return_value = NetworkInfo( + interfaces=[], + docker=DockerNetwork( + interface="hassio", + address=IPv4Network("172.30.32.0/23"), + gateway=IPv4Address("172.30.32.1"), + dns=IPv4Address("172.30.32.3"), + ), + host_internet=True, + supervisor_internet=True, + ) + return supervisor_client.network.info + + +@pytest.fixture(name="os_info") +def os_info_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock os info API from supervisor.""" + supervisor_client.os.info.return_value = OSInfo( + version="1.0.0", + version_latest="1.0.0", + update_available=False, + board=None, + boot=None, + data_disk=None, + boot_slots={}, + ) + return supervisor_client.os.info + + +@pytest.fixture(name="homeassistant_stats") +def homeassistant_stats_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock Home Assistant stats API from supervisor.""" + supervisor_client.homeassistant.stats.return_value = HomeAssistantStats( + cpu_percent=0.99, + memory_usage=182611968, + memory_limit=3977146368, + memory_percent=4.59, + network_rx=362570232, + network_tx=82374138, + blk_read=46010945536, + blk_write=15051526144, + ) + return supervisor_client.homeassistant.stats + + +@pytest.fixture(name="supervisor_stats") +def supervisor_stats_fixture(supervisor_client: AsyncMock) -> AsyncMock: + """Mock supervisor stats API from supervisor.""" + supervisor_client.supervisor.stats.return_value = SupervisorStats( + cpu_percent=0.99, + memory_usage=182611968, + memory_limit=3977146368, + memory_percent=4.59, + network_rx=362570232, + network_tx=82374138, + blk_read=46010945536, + blk_write=15051526144, + ) + return supervisor_client.supervisor.stats + + @pytest.fixture(name="supervisor_client") def supervisor_client() -> Generator[AsyncMock]: """Mock the supervisor client.""" - mounts_info_mock = AsyncMock(spec_set=["default_backup_mount", "mounts"]) - mounts_info_mock.default_backup_mount = None - mounts_info_mock.mounts = [] - supervisor_client = AsyncMock() - supervisor_client.addons = AsyncMock() - supervisor_client.discovery = AsyncMock() - supervisor_client.homeassistant = AsyncMock() - supervisor_client.host = AsyncMock() - supervisor_client.jobs = AsyncMock() - supervisor_client.jobs.info.return_value = MagicMock() - supervisor_client.mounts.info.return_value = mounts_info_mock - supervisor_client.os = AsyncMock() - supervisor_client.resolution = AsyncMock() - supervisor_client.supervisor = AsyncMock() + supervisor_client = AsyncMock(spec=SupervisorClient) + supervisor_client.addons = AsyncMock(spec=AddonsClient) + supervisor_client.backups = AsyncMock(spec=BackupsClient) + supervisor_client.discovery = AsyncMock(spec=DiscoveryClient) + supervisor_client.homeassistant = AsyncMock(spec=HomeAssistantClient) + supervisor_client.host = AsyncMock(spec=HostClient) + supervisor_client.jobs = AsyncMock(spec=JobsClient) + supervisor_client.jobs.info.return_value = JobsInfo(ignore_conditions=[], jobs=[]) + supervisor_client.mounts = AsyncMock(spec=MountsClient) + supervisor_client.mounts.info.return_value = MagicMock( + spec=MountsInfo, default_backup_mount=None, mounts=[] + ) + supervisor_client.network = AsyncMock(spec=NetworkClient) + supervisor_client.os = AsyncMock(spec=OSClient) + supervisor_client.resolution = AsyncMock(spec=ResolutionClient) + supervisor_client.supervisor = AsyncMock(spec=SupervisorManagementClient) + supervisor_client.store = AsyncMock(spec=StoreClient) + with ( patch( "homeassistant.components.hassio.get_supervisor_client", diff --git a/tests/components/hassio/common.py b/tests/components/hassio/common.py index e3310e1d664..c65faaa4575 100644 --- a/tests/components/hassio/common.py +++ b/tests/components/hassio/common.py @@ -84,6 +84,7 @@ def mock_addon_store_info( supervisor_client.store.addon_info.return_value = addon_info = Mock( spec=StoreAddonComplete, slug="test", + name="test", repository="core", available=True, installed=False, @@ -109,15 +110,18 @@ def mock_addon_info( supervisor_client.addons.addon_info.return_value = addon_info = Mock( spec=InstalledAddonComplete, slug="test", + name="test", repository="core", available=False, hostname="", options={}, state="unknown", update_available=False, - version=None, + version="1.0.0", + version_latest="1.0.0", supervisor_api=False, supervisor_role="default", + icon=False, ) addon_info.name = "test" addon_info.to_dict = MethodType( diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index e29346878f9..82c2909c667 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,11 +1,12 @@ """Fixtures for Hass.io.""" from collections.abc import Generator +from dataclasses import replace import os import re -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch -from aiohasupervisor.models import AddonsStats, AddonState +from aiohasupervisor.models import AddonsStats, AddonState, InstalledAddonComplete from aiohttp.test_utils import TestClient import pytest @@ -80,6 +81,15 @@ def all_setup_requests( addon_changelog: AsyncMock, addon_stats: AsyncMock, jobs_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + homeassistant_stats: AsyncMock, + supervisor_stats: AsyncMock, ) -> None: """Mock all setup requests.""" include_addons = hasattr(request, "param") and request.param.get( @@ -88,87 +98,26 @@ def all_setup_requests( aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": { - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": "1.2.3", - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - "disk_free": 1.6, - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={ - "result": "ok", - "data": { - "version_latest": "1.0.0", - "version": "1.0.0", - "update_available": False, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test", - "slug": "test", - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "core", - "state": "started", - "icon": False, - }, - { - "name": "test2", - "slug": "test2", - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "core", - "state": "started", - "icon": False, - }, - ] - if include_addons - else [], - }, - }, - ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + if include_addons: + addons_list.return_value[0] = replace( + addons_list.return_value[0], + version="1.0.0", + version_latest="1.0.0", + update_available=False, + ) + addons_list.return_value[1] = replace( + addons_list.return_value[1], + version="1.0.0", + version_latest="1.0.0", + state=AddonState.STARTED, + ) + else: + addons_list.return_value = [] + addon_installed.return_value.update_available = False addon_installed.return_value.version = "1.0.0" addon_installed.return_value.version_latest = "1.0.0" @@ -177,56 +126,26 @@ def all_setup_requests( addon_installed.return_value.icon = False def mock_addon_info(slug: str): + addon = Mock( + spec=InstalledAddonComplete, + to_dict=addon_installed.return_value.to_dict, + **addon_installed.return_value.to_dict(), + ) if slug == "test": - addon_installed.return_value.name = "test" - addon_installed.return_value.slug = "test" - addon_installed.return_value.url = ( - "https://github.com/home-assistant/addons/test" - ) - addon_installed.return_value.auto_update = True + addon.name = "test" + addon.slug = "test" + addon.url = "https://github.com/home-assistant/addons/test" + addon.auto_update = True else: - addon_installed.return_value.name = "test2" - addon_installed.return_value.slug = "test2" - addon_installed.return_value.url = "https://github.com" - addon_installed.return_value.auto_update = False + addon.name = "test2" + addon.slug = "test2" + addon.url = "https://github.com" + addon.auto_update = False - return addon_installed.return_value + return addon addon_installed.side_effect = mock_addon_info - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - async def mock_addon_stats(addon: str) -> AddonsStats: """Mock addon stats for test and test2.""" if addon == "test2": @@ -252,16 +171,6 @@ def all_setup_requests( ) addon_stats.side_effect = mock_addon_stats - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, - ) aioclient_mock.get( "http://127.0.0.1/jobs/info", diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index dc7a4d961fc..a33b7ac5ab8 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -4,9 +4,10 @@ from dataclasses import replace from datetime import timedelta import os from pathlib import PurePath -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 +from aiohasupervisor.models import AddonState, InstalledAddonComplete from aiohasupervisor.models.mounts import ( CIFSMountResponse, MountsInfo, @@ -41,133 +42,51 @@ def mock_all( addon_stats: AsyncMock, resolution_info: AsyncMock, jobs_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + homeassistant_stats: AsyncMock, + supervisor_stats: AsyncMock, ) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": { - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": "1.2.3", - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={ - "result": "ok", - "data": { - "version_latest": "1.0.0", - "version": "1.0.0", - "update_available": False, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test", - "state": "started", - "slug": "test", - "installed": True, - "update_available": True, - "version": "2.0.0", - "version_latest": "2.0.1", - "repository": "core", - "url": "https://github.com/home-assistant/addons/test", - "icon": False, - }, - { - "name": "test2", - "state": "stopped", - "slug": "test2", - "installed": True, - "update_available": False, - "version": "3.1.0", - "version_latest": "3.1.0", - "repository": "core", - "url": "https://github.com", - "icon": False, - }, - ], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, - ) + + def mock_addon_info(slug: str): + addon = Mock( + spec=InstalledAddonComplete, + to_dict=addon_installed.return_value.to_dict, + **addon_installed.return_value.to_dict(), + ) + if slug == "test": + addon.name = "test" + addon.slug = "test" + addon.version = "2.0.0" + addon.version_latest = "2.0.1" + addon.update_available = True + addon.state = AddonState.STARTED + addon.url = "https://github.com/home-assistant/addons/test" + addon.auto_update = True + else: + addon.name = "test2" + addon.slug = "test2" + addon.version = "3.1.0" + addon.version_latest = "3.1.0" + addon.update_available = False + addon.state = AddonState.STOPPED + addon.url = "https://github.com" + addon.auto_update = False + + return addon + + addon_installed.side_effect = mock_addon_info @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_config.py b/tests/components/hassio/test_config.py index 4cdea02b087..e61e117a519 100644 --- a/tests/components/hassio/test_config.py +++ b/tests/components/hassio/test_config.py @@ -1,6 +1,7 @@ """Test websocket API.""" from collections.abc import Generator +from dataclasses import replace from typing import Any from unittest.mock import AsyncMock, patch from uuid import UUID, uuid4 @@ -28,77 +29,24 @@ def mock_all( supervisor_is_connected: AsyncMock, resolution_info: AsyncMock, addon_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, ) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test", - "state": "started", - "slug": "test", - "installed": True, - "update_available": True, - "icon": False, - "version": "2.0.0", - "version_latest": "2.0.1", - "repository": "core", - "url": "https://github.com/home-assistant/addons/test", - }, - ], - }, - }, + supervisor_root_info.return_value = replace( + supervisor_root_info.return_value, hassos=None ) + addons_list.return_value.pop(1) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, - ) @pytest.fixture diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index a889e28b7f0..c82ff1f49df 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -1,8 +1,10 @@ """Test Supervisor diagnostics.""" +from dataclasses import replace import os -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch +from aiohasupervisor.models import AddonState, InstalledAddonComplete import pytest from homeassistant.components.hassio import DOMAIN @@ -26,135 +28,68 @@ def mock_all( addon_changelog: AsyncMock, resolution_info: AsyncMock, jobs_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + homeassistant_stats: AsyncMock, + supervisor_stats: AsyncMock, ) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": { - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": "1.2.3", - }, - }, + homeassistant_info.return_value = replace( + homeassistant_info.return_value, + version="1.0.0dev221", + version_latest="1.0.0dev222", + update_available=True, ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, + os_info.return_value = replace( + os_info.return_value, + version="1.0.0dev2221", + version_latest="1.0.0dev2222", + update_available=True, ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={ - "result": "ok", - "data": {"version_latest": "1.0.0dev222", "version": "1.0.0dev221"}, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={ - "result": "ok", - "data": { - "version_latest": "1.0.0dev2222", - "version": "1.0.0dev2221", - "update_available": False, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "version": "1.0.0", - "version_latest": "1.0.1dev222", - "addons": [ - { - "name": "test", - "state": "started", - "slug": "test", - "installed": True, - "update_available": True, - "icon": False, - "version": "2.0.0", - "version_latest": "2.0.1", - "repository": "core", - "url": "https://github.com/home-assistant/addons/test", - }, - { - "name": "test2", - "state": "stopped", - "slug": "test2", - "installed": True, - "update_available": False, - "icon": True, - "version": "3.1.0", - "version_latest": "3.1.0", - "repository": "core", - "url": "https://github.com", - }, - ], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, + supervisor_info.return_value = replace( + supervisor_info.return_value, + version_latest="1.0.1dev222", + update_available=True, ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, - ) + + def mock_addon_info(slug: str): + addon = Mock( + spec=InstalledAddonComplete, + to_dict=addon_installed.return_value.to_dict, + **addon_installed.return_value.to_dict(), + ) + if slug == "test": + addon.name = "test" + addon.slug = "test" + addon.version = "2.0.0" + addon.version_latest = "2.0.1" + addon.update_available = True + addon.state = AddonState.STARTED + addon.url = "https://github.com/home-assistant/addons/test" + addon.auto_update = True + else: + addon.name = "test2" + addon.slug = "test2" + addon.version = "3.1.0" + addon.version_latest = "3.1.0" + addon.update_available = False + addon.state = AddonState.STOPPED + addon.url = "https://github.com" + addon.auto_update = False + + return addon + + addon_installed.side_effect = mock_addon_info async def test_diagnostics( diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index ba6338f84e2..72862e13b2a 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -2,15 +2,15 @@ from collections.abc import Generator from http import HTTPStatus -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, patch from uuid import uuid4 +from aiohasupervisor import SupervisorError from aiohasupervisor.models import Discovery from aiohttp.test_utils import TestClient import pytest from homeassistant import config_entries -from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant @@ -103,6 +103,7 @@ async def test_hassio_discovery_startup_done( mock_mqtt: type[config_entries.ConfigFlow], addon_installed: AsyncMock, get_addon_discovery_info: AsyncMock, + supervisor_root_info: AsyncMock, ) -> None: """Test startup and discovery with hass discovery.""" aioclient_mock.post( @@ -125,15 +126,12 @@ async def test_hassio_discovery_startup_done( ] addon_installed.return_value.name = "Mosquitto Test" + supervisor_root_info.side_effect = SupervisorError() with ( patch( "homeassistant.components.hassio.HassIO.update_hass_api", return_value={"result": "ok"}, ), - patch( - "homeassistant.components.hassio.HassIO.get_info", - Mock(side_effect=HassioAPIError()), - ), ): await hass.async_start() await async_setup_component(hass, "hassio", {}) diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index fc024dfcc44..983c98b525f 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -14,169 +14,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from tests.test_util.aiohttp import AiohttpClientMocker -async def test_api_info( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API generic info.""" - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, - }, - ) - - data = await hassio_handler.get_info() - assert aioclient_mock.call_count == 1 - assert data["hassos"] is None - assert data["homeassistant"] == "0.110.0" - assert data["supervisor"] == "222" - - -async def test_api_info_error( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Home Assistant info error.""" - aioclient_mock.get( - "http://127.0.0.1/info", json={"result": "error", "message": None} - ) - - with pytest.raises(HassioAPIError): - await hassio_handler.get_info() - - assert aioclient_mock.call_count == 1 - - -async def test_api_host_info( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Host info.""" - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - ) - - data = await hassio_handler.get_host_info() - assert aioclient_mock.call_count == 1 - assert data["chassis"] == "vm" - assert data["kernel"] == "4.19.0-6-amd64" - assert data["operating_system"] == "Debian GNU/Linux 10 (buster)" - - -async def test_api_supervisor_info( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Supervisor info.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": {"supported": True, "version": "2020.11.1", "channel": "stable"}, - }, - ) - - data = await hassio_handler.get_supervisor_info() - assert aioclient_mock.call_count == 1 - assert data["supported"] - assert data["version"] == "2020.11.1" - assert data["channel"] == "stable" - - -async def test_api_os_info( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API OS info.""" - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={ - "result": "ok", - "data": {"board": "odroid-n2", "version": "2020.11.1"}, - }, - ) - - data = await hassio_handler.get_os_info() - assert aioclient_mock.call_count == 1 - assert data["board"] == "odroid-n2" - assert data["version"] == "2020.11.1" - - -async def test_api_host_info_error( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Home Assistant info error.""" - aioclient_mock.get( - "http://127.0.0.1/host/info", json={"result": "error", "message": None} - ) - - with pytest.raises(HassioAPIError): - await hassio_handler.get_host_info() - - assert aioclient_mock.call_count == 1 - - -async def test_api_core_info( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Home Assistant Core info.""" - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - - data = await hassio_handler.get_core_info() - assert aioclient_mock.call_count == 1 - assert data["version_latest"] == "1.0.0" - - -async def test_api_core_info_error( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Home Assistant Core info error.""" - aioclient_mock.get( - "http://127.0.0.1/core/info", json={"result": "error", "message": None} - ) - - with pytest.raises(HassioAPIError): - await hassio_handler.get_core_info() - - assert aioclient_mock.call_count == 1 - - -async def test_api_core_stats( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Add-on stats.""" - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={"result": "ok", "data": {"memory_percent": 0.01}}, - ) - - data = await hassio_handler.get_core_stats() - assert data["memory_percent"] == 0.01 - assert aioclient_mock.call_count == 1 - - -async def test_api_supervisor_stats( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Add-on stats.""" - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={"result": "ok", "data": {"memory_percent": 0.01}}, - ) - - data = await hassio_handler.get_supervisor_stats() - assert data["memory_percent"] == 0.01 - assert aioclient_mock.call_count == 1 - - async def test_api_ingress_panels( hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker ) -> None: @@ -207,7 +44,7 @@ async def test_api_ingress_panels( @pytest.mark.parametrize( ("api_call", "method", "payload"), [ - ("get_network_info", "GET", None), + ("get_ingress_panels", "GET", None), ], ) @pytest.mark.usefixtures("socket_enabled") diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 0262fd73ae7..d72e9b8f6a9 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,15 +1,20 @@ """The tests for the hassio component.""" +from dataclasses import replace from datetime import timedelta import os from pathlib import PurePath from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch from aiohasupervisor import SupervisorError -from aiohasupervisor.models import AddonsStats -from aiohasupervisor.models.mounts import ( +from aiohasupervisor.models import ( + AddonsStats, + AddonStage, + AddonState, CIFSMountResponse, + InstalledAddon, + InstalledAddonComplete, MountsInfo, MountState, MountType, @@ -52,135 +57,42 @@ from tests.test_util.aiohttp import AiohttpClientMocker MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} -@pytest.fixture -def extra_os_info(): - """Extra os/info.""" - return {} - - -@pytest.fixture -def os_info(extra_os_info): - """Mock os/info.""" - return { - "json": { - "result": "ok", - "data": {"version_latest": "1.0.0", "version": "1.0.0", **extra_os_info}, - } - } - - @pytest.fixture(autouse=True) def mock_all( aioclient_mock: AiohttpClientMocker, - os_info: AsyncMock, store_info: AsyncMock, addon_info: AsyncMock, addon_stats: AsyncMock, addon_changelog: AsyncMock, resolution_info: AsyncMock, jobs_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + homeassistant_stats: AsyncMock, + supervisor_stats: AsyncMock, + addon_installed: AsyncMock, ) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": { - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": "1.2.3", - }, - }, + addons_list.return_value[0] = replace( + addons_list.return_value[0], + version="1.0.0", + version_latest="1.0.0", + update_available=False, + state=AddonState.STOPPED, ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - **os_info, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "version_latest": "1.0.0", - "version": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test", - "slug": "test", - "state": "stopped", - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "core", - "icon": False, - }, - { - "name": "test2", - "slug": "test2", - "state": "stopped", - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "core", - "icon": False, - }, - ], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, + addons_list.return_value[1] = replace( + addons_list.return_value[1], + version="1.0.0", + version_latest="1.0.0", ) + addon_installed.return_value.state = AddonState.STOPPED async def mock_addon_stats(addon: str) -> AddonsStats: """Mock addon stats for test and test2.""" @@ -209,23 +121,28 @@ def mock_all( addon_stats.side_effect = mock_addon_stats def mock_addon_info(slug: str): - addon_info.return_value.auto_update = slug == "test" - return addon_info.return_value + addon = Mock( + spec=InstalledAddonComplete, + to_dict=addon_installed.return_value.to_dict, + **addon_installed.return_value.to_dict(), + ) + if slug == "test": + addon.name = "test" + addon.slug = "test" + addon.url = "https://github.com/home-assistant/addons/test" + addon.auto_update = True + else: + addon.name = "test2" + addon.slug = "test2" + addon.url = "https://github.com" + addon.auto_update = False + + return addon addon_info.side_effect = mock_addon_info aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, - ) async def test_setup_api_ping( @@ -239,7 +156,7 @@ async def test_setup_api_ping( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert get_core_info(hass)["version_latest"] == "1.0.0" assert is_hassio(hass) @@ -310,7 +227,7 @@ async def test_setup_api_push_api_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 9999 assert "watchdog" not in aioclient_mock.mock_calls[0][2] @@ -331,7 +248,7 @@ async def test_setup_api_push_api_data_server_host( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 9999 assert not aioclient_mock.mock_calls[0][2]["watchdog"] @@ -352,7 +269,7 @@ async def test_setup_api_push_api_data_default( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[0][2]["refresh_token"] @@ -433,7 +350,7 @@ async def test_setup_api_existing_hassio_user( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert not aioclient_mock.mock_calls[0][2]["ssl"] assert aioclient_mock.mock_calls[0][2]["port"] == 8123 assert aioclient_mock.mock_calls[0][2]["refresh_token"] == token.token @@ -452,7 +369,7 @@ async def test_setup_core_push_config( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert aioclient_mock.mock_calls[1][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -476,7 +393,7 @@ async def test_setup_hassio_no_additional_data( await hass.async_block_till_done() assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -536,7 +453,6 @@ async def test_service_calls( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, supervisor_client: AsyncMock, - addon_installed: AsyncMock, supervisor_is_connected: AsyncMock, app_or_addon: str, ) -> None: @@ -576,14 +492,14 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 24 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 27 assert aioclient_mock.mock_calls[-1][2] == "test" await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_reboot", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 26 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 29 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -598,7 +514,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 28 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 31 # API receives "addons" even when we pass "apps" assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", @@ -624,7 +540,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 33 # API receives "addons" even when we pass "apps" assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], @@ -644,7 +560,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 31 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 34 assert aioclient_mock.mock_calls[-1][2] == { "name": "backup_name", "location": "backup_share", @@ -660,7 +576,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 32 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 35 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", "location": None, @@ -679,7 +595,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 34 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 37 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 11:48:00", "location": None, @@ -750,38 +666,37 @@ async def test_service_calls_apps_addons_exclusive( "app_or_addon", ["app", "addon"], ) +@pytest.mark.usefixtures("aioclient_mock") async def test_addon_service_call_with_complex_slug( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, supervisor_is_connected: AsyncMock, app_or_addon: str, + addons_list: AsyncMock, ) -> None: """Addon slugs can have ., - and _, confirm that passes validation.""" - supervisor_mock_data = { - "version_latest": "1.0.0", - "version": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test.a_1-2", - "slug": "test.a_1-2", - "state": "stopped", - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "core", - "icon": False, - }, - ], - } + addons_list.return_value = [ + InstalledAddon( + detached=False, + advanced=False, + available=True, + build=False, + description="", + homeassistant=None, + icon=False, + logo=False, + name="test.a_1-2", + repository="core", + slug="test.a_1-2", + stage=AddonStage.STABLE, + update_available=False, + url="https://github.com", + version_latest="1.0.0", + version="1.0.0", + state=AddonState.STOPPED, + ) + ] supervisor_is_connected.side_effect = SupervisorError - with ( - patch.dict(os.environ, MOCK_ENVIRON), - patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, - ), - ): + with patch.dict(os.environ, MOCK_ENVIRON): assert await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() @@ -806,12 +721,12 @@ async def test_service_calls_core( await hass.services.async_call("homeassistant", "stop") await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 6 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 await hass.services.async_call("homeassistant", "check_config") await hass.async_block_till_done() - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 6 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 with patch( "homeassistant.config.async_check_ha_config_file", return_value=None @@ -820,7 +735,7 @@ async def test_service_calls_core( await hass.async_block_till_done() assert mock_check_config.called - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 7 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 21 @pytest.mark.usefixtures("addon_installed") @@ -850,157 +765,79 @@ async def test_migration_off_hassio(hass: HomeAssistant) -> None: assert hass.config_entries.async_entries(DOMAIN) == [] -@pytest.mark.usefixtures("addon_installed") +@pytest.mark.usefixtures("addon_installed", "supervisor_info") async def test_device_registry_calls( - hass: HomeAssistant, device_registry: dr.DeviceRegistry + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + addons_list: AsyncMock, + os_info: AsyncMock, ) -> None: """Test device registry entries for hassio.""" - supervisor_mock_data = { - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test", - "state": "started", - "slug": "test", - "installed": True, - "icon": False, - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "repository": "test", - "url": "https://github.com/home-assistant/addons/test", - }, - { - "name": "test2", - "state": "started", - "slug": "test2", - "installed": True, - "icon": False, - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "url": "https://github.com", - }, - ], - } - os_mock_data = { - "board": "odroid-n2", - "boot": "A", - "update_available": False, - "version": "5.12", - "version_latest": "5.12", - } + addons_list.return_value[0] = replace( + addons_list.return_value[0], + version="1.0.0", + version_latest="1.0.0", + update_available=False, + ) + addons_list.return_value[1] = replace( + addons_list.return_value[1], + version="1.0.0", + version_latest="1.0.0", + state=AddonState.STARTED, + ) + os_info.return_value = replace( + os_info.return_value, + board="odroid-n2", + boot="A", + version="5.12", + version_latest="5.12", + ) - with ( - patch.dict(os.environ, MOCK_ENVIRON), - patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, - ), - patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value=os_mock_data, - ), - ): + with patch.dict(os.environ, MOCK_ENVIRON): config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done(wait_background_tasks=True) assert len(device_registry.devices) == 6 - supervisor_mock_data = { - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test2", - "state": "started", - "slug": "test2", - "installed": True, - "icon": False, - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "url": "https://github.com", - }, - ], - } + addons_list.return_value.pop(0) # Test that when addon is removed, next update will remove the add-on and subsequent updates won't - with ( - patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, - ), - patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value=os_mock_data, - ), - ): - async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1)) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(device_registry.devices) == 5 + async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1)) + await hass.async_block_till_done(wait_background_tasks=True) + assert len(device_registry.devices) == 5 - async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2)) - await hass.async_block_till_done(wait_background_tasks=True) - assert len(device_registry.devices) == 5 + async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2)) + await hass.async_block_till_done(wait_background_tasks=True) + assert len(device_registry.devices) == 5 - supervisor_mock_data = { - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test2", - "slug": "test2", - "state": "started", - "installed": True, - "icon": False, - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "url": "https://github.com", - }, - { - "name": "test3", - "slug": "test3", - "state": "stopped", - "installed": True, - "icon": False, - "update_available": False, - "version": "1.0.0", - "version_latest": "1.0.0", - "url": "https://github.com", - }, - ], - } + addons_list.return_value.append( + InstalledAddon( + detached=False, + advanced=False, + available=True, + build=False, + description="", + homeassistant=None, + icon=False, + logo=False, + name="test3", + repository="core", + slug="test3", + stage=AddonStage.STABLE, + update_available=False, + url="https://github.com", + version_latest="1.0.0", + version="1.0.0", + state=AddonState.STOPPED, + ) + ) # Test that when addon is added, next update will reload the entry so we register # a new device - with ( - patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, - ), - patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value=os_mock_data, - ), - patch( - "homeassistant.components.hassio.HassIO.get_info", - return_value={ - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": None, - }, - ), - ): - async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) - await hass.async_block_till_done() - assert len(device_registry.devices) == 5 + async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) + await hass.async_block_till_done() + assert len(device_registry.devices) == 5 @pytest.mark.usefixtures("addon_installed") @@ -1137,28 +974,31 @@ async def test_coordinator_updates_stats_entities_enabled( @pytest.mark.parametrize( - ("extra_os_info", "integration"), + ("board", "integration"), [ - ({"board": "green"}, "homeassistant_green"), - ({"board": "odroid-c2"}, "hardkernel"), - ({"board": "odroid-c4"}, "hardkernel"), - ({"board": "odroid-n2"}, "hardkernel"), - ({"board": "odroid-xu4"}, "hardkernel"), - ({"board": "rpi2"}, "raspberry_pi"), - ({"board": "rpi3"}, "raspberry_pi"), - ({"board": "rpi3-64"}, "raspberry_pi"), - ({"board": "rpi4"}, "raspberry_pi"), - ({"board": "rpi4-64"}, "raspberry_pi"), - ({"board": "yellow"}, "homeassistant_yellow"), + ("green", "homeassistant_green"), + ("odroid-c2", "hardkernel"), + ("odroid-c4", "hardkernel"), + ("odroid-n2", "hardkernel"), + ("odroid-xu4", "hardkernel"), + ("rpi2", "raspberry_pi"), + ("rpi3", "raspberry_pi"), + ("rpi3-64", "raspberry_pi"), + ("rpi4", "raspberry_pi"), + ("rpi4-64", "raspberry_pi"), + ("yellow", "homeassistant_yellow"), ], ) async def test_setup_hardware_integration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, supervisor_client: AsyncMock, - integration, + os_info: AsyncMock, + board: str, + integration: str, ) -> None: """Test setup initiates hardware integration.""" + os_info.return_value = replace(os_info.return_value, board=board) with ( patch.dict(os.environ, MOCK_ENVIRON), @@ -1175,7 +1015,7 @@ async def test_setup_hardware_integration( await hass.async_block_till_done(wait_background_tasks=True) assert result - assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 20 + assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index 3bde6ef183e..997f3b4717b 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -1047,13 +1047,16 @@ async def test_supervisor_issues_free_space( ) +@pytest.mark.usefixtures("all_setup_requests") async def test_supervisor_issues_free_space_host_info_fail( hass: HomeAssistant, supervisor_client: AsyncMock, hass_ws_client: WebSocketGenerator, + host_info: AsyncMock, ) -> None: """Test supervisor issue for too little free space remaining without host info.""" mock_resolution_info(supervisor_client) + host_info.side_effect = SupervisorError() result = await async_setup_component(hass, "hassio", {}) assert result diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 21eac41f291..ce490643596 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -1,10 +1,12 @@ """The tests for the hassio sensors.""" +from dataclasses import replace from datetime import timedelta import os -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch from aiohasupervisor import SupervisorError +from aiohasupervisor.models import AddonState, InstalledAddonComplete from freezegun.api import FrozenDateTimeFactory import pytest @@ -35,130 +37,57 @@ def mock_all( addon_changelog: AsyncMock, resolution_info: AsyncMock, jobs_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + homeassistant_stats: AsyncMock, + supervisor_stats: AsyncMock, ) -> None: """Mock all setup requests.""" - _install_default_mocks(aioclient_mock) - - -def _install_default_mocks(aioclient_mock: AiohttpClientMocker): - """Install default mocks.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": { - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": "1.2.3", - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "agent_version": "1.0.0", - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test", - "state": "started", - "slug": "test", - "installed": True, - "update_available": False, - "version": "2.0.0", - "version_latest": "2.0.1", - "repository": "core", - "url": "https://github.com/home-assistant/addons/test", - "icon": False, - }, - { - "name": "test2", - "state": "stopped", - "slug": "test2", - "installed": True, - "update_available": False, - "version": "3.1.0", - "version_latest": "3.2.0", - "repository": "core", - "url": "https://github.com", - "icon": False, - }, - ], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, + + host_info.return_value = replace(host_info.return_value, agent_version="1.0.0") + addons_list.return_value[1] = replace( + addons_list.return_value[1], version_latest="3.2.0", update_available=True ) + def mock_addon_info(slug: str): + addon = Mock( + spec=InstalledAddonComplete, + to_dict=addon_installed.return_value.to_dict, + **addon_installed.return_value.to_dict(), + ) + if slug == "test": + addon.name = "test" + addon.slug = "test" + addon.version = "2.0.0" + addon.version_latest = "2.0.1" + addon.update_available = True + addon.state = AddonState.STARTED + addon.url = "https://github.com/home-assistant/addons/test" + addon.auto_update = True + else: + addon.name = "test2" + addon.slug = "test2" + addon.version = "3.1.0" + addon.version_latest = "3.2.0" + addon.update_available = True + addon.state = AddonState.STOPPED + addon.url = "https://github.com" + addon.auto_update = False + + return addon + + addon_installed.side_effect = mock_addon_info + @pytest.mark.parametrize( ("store_addons", "store_repositories"), [(MOCK_STORE_ADDONS, MOCK_REPOSITORIES)] @@ -256,20 +185,14 @@ async def test_stats_addon_sensor( # Verify that the entity is disabled by default. assert hass.states.get(entity_id) is None - aioclient_mock.clear_requests() - _install_default_mocks(aioclient_mock) addon_stats.side_effect = SupervisorError - freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) assert "Could not fetch stats" not in caplog.text - aioclient_mock.clear_requests() - _install_default_mocks(aioclient_mock) addon_stats.side_effect = None - freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -299,10 +222,7 @@ async def test_stats_addon_sensor( state = hass.states.get(entity_id) assert state.state == expected - aioclient_mock.clear_requests() - _install_default_mocks(aioclient_mock) addon_stats.side_effect = SupervisorError - freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/hassio/test_switch.py b/tests/components/hassio/test_switch.py index 7c203142b8a..7477a56e498 100644 --- a/tests/components/hassio/test_switch.py +++ b/tests/components/hassio/test_switch.py @@ -1,9 +1,11 @@ """The tests for the hassio switch.""" from collections.abc import AsyncGenerator +from dataclasses import replace import os -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch +from aiohasupervisor.models import AddonState, InstalledAddonComplete import pytest from homeassistant.components.hassio import DOMAIN @@ -61,134 +63,55 @@ def mock_all( addon_stats: AsyncMock, resolution_info: AsyncMock, jobs_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + homeassistant_stats: AsyncMock, + supervisor_stats: AsyncMock, ) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": { - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": "1.2.3", - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={ - "result": "ok", - "data": { - "version_latest": "1.0.0", - "version": "1.0.0", - "update_available": False, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "version": "1.0.0", - "version_latest": "1.0.0", - "auto_update": True, - "addons": [ - { - "name": "test", - "state": "started", - "slug": "test", - "installed": True, - "update_available": True, - "icon": False, - "version": "2.0.0", - "version_latest": "2.0.1", - "repository": "core", - "url": "https://github.com/home-assistant/addons/test", - }, - { - "name": "test-two", - "state": "stopped", - "slug": "test-two", - "installed": True, - "update_available": False, - "icon": True, - "version": "3.1.0", - "version_latest": "3.1.0", - "repository": "core", - "url": "https://github.com", - }, - ], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, + addons_list.return_value[1] = replace( + addons_list.return_value[1], name="test-two", slug="test-two" ) + def mock_addon_info(slug: str): + addon = Mock( + spec=InstalledAddonComplete, + to_dict=addon_installed.return_value.to_dict, + **addon_installed.return_value.to_dict(), + ) + if slug == "test": + addon.name = "test" + addon.slug = "test" + addon.version = "2.0.0" + addon.version_latest = "2.0.1" + addon.update_available = True + addon.state = AddonState.STARTED + addon.url = "https://github.com/home-assistant/addons/test" + addon.auto_update = True + else: + addon.name = "test-two" + addon.slug = "test-two" + addon.version = "3.1.0" + addon.version_latest = "3.1.0" + addon.update_available = False + addon.state = AddonState.STOPPED + addon.url = "https://github.com" + addon.auto_update = False + + return addon + + addon_installed.side_effect = mock_addon_info + @pytest.mark.parametrize( ("store_addons", "store_repositories"), [(MOCK_STORE_ADDONS, MOCK_REPOSITORIES)] diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index 4839486810a..48896bb6927 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -5,6 +5,7 @@ import os from unittest.mock import patch from aiohttp import ClientError +import pytest from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -15,18 +16,15 @@ from tests.common import get_system_health_info from tests.test_util.aiohttp import AiohttpClientMocker +@pytest.mark.usefixtures( + "supervisor_root_info", "host_info", "os_info", "supervisor_info" +) async def test_hassio_system_health( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test hassio system health.""" - aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}}) - aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}}) - aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="") aioclient_mock.get("https://version.home-assistant.io/stable.json", text="") - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}} - ) hass.config.components.add("hassio") assert await async_setup_component(hass, "system_health", {}) @@ -50,8 +48,21 @@ async def test_hassio_system_health( hass.data["hassio_supervisor_info"] = { "healthy": True, "supported": True, - "addons": [{"name": "Awesome Addon", "version": "1.0.0"}], } + hass.data["hassio_addons_info"] = { + "test": { + "name": "Awesome Addon", + "slug": "test", + "version": "1.0.0", + } + } + hass.data["hassio_addons_list"] = [ + { + "slug": "test", + "name": "Awesome Addon", + "version": "1.0.0", + } + ] hass.data["hassio_network_info"] = { "host_internet": True, "supervisor_internet": True, @@ -90,18 +101,15 @@ async def test_hassio_system_health( } +@pytest.mark.usefixtures( + "supervisor_root_info", "host_info", "os_info", "supervisor_info" +) async def test_hassio_system_health_with_issues( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test hassio system health.""" - aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}}) - aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}}) - aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="") aioclient_mock.get("https://version.home-assistant.io/stable.json", exc=ClientError) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}} - ) hass.config.components.add("hassio") assert await async_setup_component(hass, "system_health", {}) diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index bdd89be124f..a335154c861 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -1,9 +1,10 @@ """The tests for the hassio update entities.""" +from dataclasses import replace from datetime import datetime, timedelta import os from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from uuid import uuid4 from aiohasupervisor import ( @@ -12,7 +13,9 @@ from aiohasupervisor import ( SupervisorNotFoundError, ) from aiohasupervisor.models import ( + AddonState, HomeAssistantUpdateOptions, + InstalledAddonComplete, Job, JobsInfo, OSUpdate, @@ -48,136 +51,68 @@ def mock_all( addon_changelog: AsyncMock, resolution_info: AsyncMock, jobs_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + homeassistant_stats: AsyncMock, + supervisor_stats: AsyncMock, ) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": { - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": "1.2.3", - }, - }, + homeassistant_info.return_value = replace( + homeassistant_info.return_value, + version="1.0.0dev221", + version_latest="1.0.0dev222", + update_available=True, ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, + os_info.return_value = replace( + os_info.return_value, + version="1.0.0dev2221", + version_latest="1.0.0dev2222", + update_available=True, ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={ - "result": "ok", - "data": {"version_latest": "1.0.0dev222", "version": "1.0.0dev221"}, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={ - "result": "ok", - "data": { - "version_latest": "1.0.0dev2222", - "version": "1.0.0dev2221", - "update_available": False, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "version": "1.0.0", - "version_latest": "1.0.1dev222", - "auto_update": True, - "addons": [ - { - "name": "test", - "state": "started", - "slug": "test", - "installed": True, - "update_available": True, - "icon": False, - "version": "2.0.0", - "version_latest": "2.0.1", - "repository": "core", - "url": "https://github.com/home-assistant/addons/test", - }, - { - "name": "test2", - "state": "stopped", - "slug": "test2", - "installed": True, - "update_available": False, - "icon": True, - "version": "3.1.0", - "version_latest": "3.1.0", - "repository": "core", - "url": "https://github.com", - }, - ], - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.99, - "memory_usage": 182611968, - "memory_limit": 3977146368, - "memory_percent": 4.59, - "network_rx": 362570232, - "network_tx": 82374138, - "blk_read": 46010945536, - "blk_write": 15051526144, - }, - }, + supervisor_info.return_value = replace( + supervisor_info.return_value, + version_latest="1.0.1dev222", + update_available=True, ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, - ) + + def mock_addon_info(slug: str): + addon = Mock( + spec=InstalledAddonComplete, + to_dict=addon_installed.return_value.to_dict, + **addon_installed.return_value.to_dict(), + ) + if slug == "test": + addon.name = "test" + addon.slug = "test" + addon.version = "2.0.0" + addon.version_latest = "2.0.1" + addon.update_available = True + addon.state = AddonState.STARTED + addon.url = "https://github.com/home-assistant/addons/test" + addon.auto_update = True + else: + addon.name = "test2" + addon.slug = "test2" + addon.version = "3.1.0" + addon.version_latest = "3.1.0" + addon.update_available = False + addon.state = AddonState.STOPPED + addon.url = "https://github.com" + addon.auto_update = False + + return addon + + addon_installed.side_effect = mock_addon_info @pytest.mark.parametrize( @@ -1588,19 +1523,14 @@ async def test_not_release_notes( assert result["result"] is None -async def test_no_os_entity(hass: HomeAssistant) -> None: +async def test_no_os_entity( + hass: HomeAssistant, supervisor_root_info: AsyncMock +) -> None: """Test handling where there is no os entity.""" - with ( - patch.dict(os.environ, MOCK_ENVIRON), - patch( - "homeassistant.components.hassio.HassIO.get_info", - return_value={ - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": None, - }, - ), - ): + supervisor_root_info.return_value = replace( + supervisor_root_info.return_value, hassos=None + ) + with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component( hass, "hassio", @@ -1624,9 +1554,7 @@ async def test_setting_up_core_update_when_addon_fails( addon_installed.side_effect = SupervisorBadRequestError("Addon Test does not exist") addon_stats.side_effect = SupervisorBadRequestError("add-on is not running") addon_changelog.side_effect = SupervisorBadRequestError("add-on is not running") - with ( - patch.dict(os.environ, MOCK_ENVIRON), - ): + with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component( hass, "hassio", diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index c3198d7c3fc..cc0d41d8c8a 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -1,5 +1,6 @@ """Test websocket API.""" +from dataclasses import replace import os from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -44,39 +45,31 @@ def mock_all( supervisor_is_connected: AsyncMock, resolution_info: AsyncMock, addon_info: AsyncMock, + host_info: AsyncMock, + supervisor_root_info: AsyncMock, + homeassistant_info: AsyncMock, + supervisor_info: AsyncMock, + addons_list: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, + store_info: AsyncMock, ) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, - }, + supervisor_root_info.return_value = replace( + supervisor_root_info.return_value, hassos=None ) + addons_list.return_value.pop(1) + addon_info.return_value.version = "2.0.0" + addon_info.return_value.version_latest = "2.0.1" + addon_info.return_value.update_available = True aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + + # The websocket API still relies on HassIO.send_command for all Supervisor API calls + # So must keep some aioclient mocks normally covered by aiohasupervisor in component aioclient_mock.get( "http://127.0.0.1/supervisor/info", json={ @@ -102,19 +95,6 @@ def mock_all( }, }, ) - aioclient_mock.get( - "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} - ) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, - ) @pytest.mark.usefixtures("hassio_env") diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 8d9d1a83a04..945bc69dee6 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -245,14 +245,15 @@ async def test_ip_ban_manager_never_started( ) ), ) +@pytest.mark.usefixtures( + "hassio_env", "resolution_info", "os_info", "store_info", "supervisor_info" +) async def test_access_from_supervisor_ip( remote_addr, bans, status, hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, - hassio_env, - resolution_info: AsyncMock, ) -> None: """Test accessing to server from supervisor IP.""" app = web.Application() diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 08acdc94afc..d1af9f984f3 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import AsyncGenerator +from dataclasses import replace from http import HTTPStatus import os from typing import Any @@ -54,15 +55,14 @@ async def rpi_fixture( @pytest.fixture(name="no_rpi") async def no_rpi_fixture( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_supervisor + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + homeassistant_info: AsyncMock, + mock_supervisor, ) -> None: """Mock core info with rpi.""" - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={ - "result": "ok", - "data": {"version_latest": "1.0.0", "machine": "odroid-n2"}, - }, + homeassistant_info.return_value = replace( + homeassistant_info.return_value, machine="odroid-n2" ) assert await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() @@ -74,38 +74,20 @@ async def mock_supervisor_fixture( store_info: AsyncMock, supervisor_is_connected: AsyncMock, resolution_info: AsyncMock, + supervisor_root_info: AsyncMock, + host_info: AsyncMock, + supervisor_info: AsyncMock, + network_info: AsyncMock, + os_info: AsyncMock, ) -> AsyncGenerator[None]: """Mock supervisor.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/network/info", - json={ - "result": "ok", - "data": { - "host_internet": True, - "supervisor_internet": True, - }, - }, + supervisor_info.return_value = replace( + supervisor_info.return_value, diagnostics=True ) with ( patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), - patch( - "homeassistant.components.hassio.HassIO.get_info", - return_value={}, - ), - patch( - "homeassistant.components.hassio.HassIO.get_host_info", - return_value={}, - ), - patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value={"diagnostics": True}, - ), - patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value={}, - ), patch( "homeassistant.components.hassio.HassIO.get_ingress_panels", return_value={"panels": {}}, diff --git a/tests/conftest.py b/tests/conftest.py index 4b88f10f9fa..0ec87b11955 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1986,19 +1986,18 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]: @pytest.fixture -def hassio_env(supervisor_is_connected: AsyncMock) -> Generator[None]: +def hassio_env( + supervisor_is_connected: AsyncMock, supervisor_root_info: AsyncMock +) -> Generator[None]: """Fixture to inject hassio env.""" - from homeassistant.components.hassio import HassioAPIError # noqa: PLC0415 + from aiohasupervisor import SupervisorError # noqa: PLC0415 from .components.hassio import SUPERVISOR_TOKEN # noqa: PLC0415 + supervisor_root_info.side_effect = SupervisorError() with ( patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}), - patch( - "homeassistant.components.hassio.HassIO.get_info", - Mock(side_effect=HassioAPIError()), - ), ): yield @@ -2012,8 +2011,6 @@ async def hassio_stubs( supervisor_client: AsyncMock, ) -> RefreshToken: """Create mock hassio http client.""" - from homeassistant.components.hassio import HassioAPIError # noqa: PLC0415 - with ( patch( "homeassistant.components.hassio.HassIO.update_hass_api", @@ -2023,10 +2020,6 @@ async def hassio_stubs( "homeassistant.components.hassio.HassIO.update_hass_config", return_value={"result": "ok"}, ), - patch( - "homeassistant.components.hassio.HassIO.get_info", - side_effect=HassioAPIError(), - ), patch( "homeassistant.components.hassio.HassIO.get_ingress_panels", return_value={"panels": []},