diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 01c8bf22787..e5f203f346d 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -177,6 +177,7 @@ class Analytics: hass = self.hass supervisor_info = None operating_system_info: dict[str, Any] = {} + supervisor_client = hassio.get_supervisor_client(hass) if not self.onboarded or not self.preferences.get(ATTR_BASE, False): LOGGER.debug("Nothing to submit") @@ -263,16 +264,16 @@ class Analytics: if supervisor_info is not None: installed_addons = await asyncio.gather( *( - hassio.async_get_addon_info(hass, addon[ATTR_SLUG]) + supervisor_client.addons.addon_info(addon[ATTR_SLUG]) for addon in supervisor_info[ATTR_ADDONS] ) ) addons.extend( { - ATTR_SLUG: addon[ATTR_SLUG], - ATTR_PROTECTED: addon[ATTR_PROTECTED], - ATTR_VERSION: addon[ATTR_VERSION], - ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE], + ATTR_SLUG: addon.slug, + ATTR_PROTECTED: addon.protected, + ATTR_VERSION: addon.version, + ATTR_AUTO_UPDATE: addon.auto_update, } for addon in installed_addons ) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 647c2248d56..73e3ae5d7ff 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -102,7 +102,6 @@ from .handler import ( # noqa: F401 HassioAPIError, async_create_backup, async_get_addon_discovery_info, - async_get_addon_info, async_get_addon_store_info, async_get_green_settings, async_get_yellow_settings, @@ -120,6 +119,7 @@ from .handler import ( # noqa: F401 async_update_diagnostics, async_update_os, async_update_supervisor, + get_supervisor_client, ) from .http import HassIOView from .ingress import async_setup_ingress_view diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index b3c43f16be1..01babdc3a33 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -10,6 +10,12 @@ from functools import partial, wraps import logging from typing import Any, Concatenate +from aiohasupervisor import SupervisorError +from aiohasupervisor.models import ( + AddonState as SupervisorAddonState, + InstalledAddonComplete, +) + from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -17,7 +23,6 @@ from .handler import ( HassioAPIError, async_create_backup, async_get_addon_discovery_info, - async_get_addon_info, async_get_addon_store_info, async_install_addon, async_restart_addon, @@ -26,6 +31,7 @@ from .handler import ( async_stop_addon, async_uninstall_addon, async_update_addon, + get_supervisor_client, ) type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], Awaitable[_R]] @@ -53,7 +59,7 @@ def api_error[_AddonManagerT: AddonManager, **_P, _R]( """Wrap an add-on manager method.""" try: return_value = await func(self, *args, **kwargs) - except HassioAPIError as err: + except (HassioAPIError, SupervisorError) as err: raise AddonError( f"{error_message.format(addon_name=self.addon_name)}: {err}" ) from err @@ -140,6 +146,7 @@ class AddonManager: @api_error("Failed to get the {addon_name} add-on info") async def async_get_addon_info(self) -> AddonInfo: """Return and cache manager add-on info.""" + supervisor_client = get_supervisor_client(self._hass) addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug) self._logger.debug("Add-on store info: %s", addon_store_info) if not addon_store_info["installed"]: @@ -152,23 +159,23 @@ class AddonManager: version=None, ) - addon_info = await async_get_addon_info(self._hass, self.addon_slug) + addon_info = await supervisor_client.addons.addon_info(self.addon_slug) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( - available=addon_info["available"], - hostname=addon_info["hostname"], - options=addon_info["options"], + available=addon_info.available, + hostname=addon_info.hostname, + options=addon_info.options, state=addon_state, - update_available=addon_info["update_available"], - version=addon_info["version"], + update_available=addon_info.update_available, + version=addon_info.version, ) @callback - def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: + def async_get_addon_state(self, addon_info: InstalledAddonComplete) -> AddonState: """Return the current state of the managed add-on.""" addon_state = AddonState.NOT_RUNNING - if addon_info["state"] == "started": + if addon_info.state == SupervisorAddonState.STARTED: addon_state = AddonState.RUNNING if self._install_task and not self._install_task.done(): addon_state = AddonState.INSTALLING diff --git a/homeassistant/components/hassio/coordinator.py b/homeassistant/components/hassio/coordinator.py index 024128f4ef8..dc62f41abb5 100644 --- a/homeassistant/components/hassio/coordinator.py +++ b/homeassistant/components/hassio/coordinator.py @@ -7,6 +7,8 @@ from collections import defaultdict import logging from typing import TYPE_CHECKING, Any +from aiohasupervisor import SupervisorError + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -514,11 +516,15 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]: """Return the info for an add-on.""" try: - info = await self.hassio.get_addon_info(slug) - except HassioAPIError as err: + info = await self.hassio.client.addons.addon_info(slug) + except SupervisorError as err: _LOGGER.warning("Could not fetch info for %s: %s", slug, err) return (slug, None) - return (slug, info) + # Translate to legacy hassio names for compatibility + info_dict = info.to_dict() + info_dict["hassio_api"] = info_dict.pop("supervisor_api") + info_dict["hassio_role"] = info_dict.pop("supervisor_role") + return (slug, info_dict) @callback def async_enable_container_updates( diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 66be8267d53..009f9dfde7e 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -12,7 +12,7 @@ from aiohttp.web_exceptions import HTTPServiceUnavailable from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START +from homeassistant.const import ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow @@ -99,20 +99,21 @@ class HassIODiscovery(HomeAssistantView): # Read additional Add-on info try: - addon_info = await self.hassio.get_addon_info(slug) + addon_info = await self.hassio.client.addons.addon_info(slug) except HassioAPIError as err: _LOGGER.error("Can't read add-on info: %s", err) return - name: str = addon_info[ATTR_NAME] - config_data[ATTR_ADDON] = name + config_data[ATTR_ADDON] = addon_info.name # Use config flow discovery_flow.async_create_flow( self.hass, service, context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo(config=config_data, name=name, slug=slug, uuid=uuid), + data=HassioServiceInfo( + config=config_data, name=addon_info.name, slug=slug, uuid=uuid + ), ) async def async_process_del(self, data: dict[str, Any]) -> None: diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7c8d5c61a22..8db1c616512 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -9,6 +9,7 @@ import logging import os from typing import Any +from aiohasupervisor import SupervisorClient import aiohttp from yarl import URL @@ -62,17 +63,6 @@ def api_data[**_P]( return _wrapper -@bind_hass -async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: - """Return add-on info. - - The add-on must be installed. - The caller of the function should handle HassioAPIError. - """ - hassio: HassIO = hass.data[DOMAIN] - return await hassio.get_addon_info(slug) - - @api_data async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: """Return add-on store info. @@ -332,7 +322,16 @@ class HassIO: self.loop = loop self.websession = websession self._ip = ip - self._base_url = URL(f"http://{ip}") + base_url = f"http://{ip}" + self._base_url = URL(base_url) + self._client = SupervisorClient( + base_url, os.environ.get("SUPERVISOR_TOKEN", ""), session=websession + ) + + @property + def client(self) -> SupervisorClient: + """Return aiohasupervisor client.""" + return self._client @_api_bool def is_connected(self) -> Coroutine: @@ -390,14 +389,6 @@ class HassIO: """ return self.send_command("/network/info", method="get") - @api_data - def get_addon_info(self, addon: str) -> Coroutine: - """Return data for a Add-on. - - This method returns a coroutine. - """ - return self.send_command(f"/addons/{addon}/info", method="get") - @api_data def get_core_stats(self) -> Coroutine: """Return stats for the core. @@ -617,3 +608,9 @@ class HassIO: _LOGGER.error("Client error on %s request %s", command, err) raise HassioAPIError + + +def get_supervisor_client(hass: HomeAssistant) -> SupervisorClient: + """Return supervisor client.""" + hassio: HassIO = hass.data[DOMAIN] + return hassio.client diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index b32e5ebcd53..9d95ea66312 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http", "repairs"], "documentation": "https://www.home-assistant.io/integrations/hassio", "iot_class": "local_polling", - "quality_scale": "internal" + "quality_scale": "internal", + "requirements": ["aiohasupervisor==0.1.0b0"] } diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 8e7650a9225..a7974850e19 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -304,5 +304,5 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity): await async_update_core(self.hass, version=version, backup=backup) except HassioAPIError as err: raise HomeAssistantError( - f"Error updating Home Assistant Core {err}" + f"Error updating Home Assistant Core: {err}" ) from err diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index c1747981b07..f24d141247d 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -13,16 +13,12 @@ from python_otbr_api.tlv_parser import MeshcopTLVType import voluptuous as vol import yarl -from homeassistant.components.hassio import ( - HassioAPIError, - HassioServiceInfo, - async_get_addon_info, -) +from homeassistant.components.hassio import AddonError, AddonManager, HassioServiceInfo from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware from homeassistant.components.thread import async_get_preferred_dataset from homeassistant.config_entries import SOURCE_HASSIO, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -43,6 +39,12 @@ class AlreadyConfigured(HomeAssistantError): """Raised when the router is already configured.""" +@callback +def get_addon_manager(hass: HomeAssistant, slug: str) -> AddonManager: + """Get the add-on manager.""" + return AddonManager(hass, _LOGGER, "OpenThread Border Router", slug) + + def _is_yellow(hass: HomeAssistant) -> bool: """Return True if Home Assistant is running on a Home Assistant Yellow.""" try: @@ -55,10 +57,11 @@ def _is_yellow(hass: HomeAssistant) -> bool: async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str: """Return config entry title.""" device: str | None = None + addon_manager = get_addon_manager(hass, discovery_info.slug) - with suppress(HassioAPIError): - addon_info = await async_get_addon_info(hass, discovery_info.slug) - device = addon_info.get("options", {}).get("device") + with suppress(AddonError): + addon_info = await addon_manager.async_get_addon_info() + device = addon_info.options.get("device") if _is_yellow(hass) and device == "/dev/ttyAMA1": return f"Home Assistant Yellow ({discovery_info.name})" diff --git a/requirements_all.txt b/requirements_all.txt index 96edcc6cb0e..cfea05041c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,6 +257,9 @@ aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.10 +# homeassistant.components.hassio +aiohasupervisor==0.1.0b0 + # homeassistant.components.homekit_controller aiohomekit==3.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f693181f36..ea78e9dbdba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,6 +242,9 @@ aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.10 +# homeassistant.components.hassio +aiohasupervisor==0.1.0b0 + # homeassistant.components.homekit_controller aiohomekit==3.2.3 diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 4b4fdc159de..5542aab4b30 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -67,6 +67,7 @@ def _last_call_payload(aioclient: AiohttpClientMocker) -> dict[str, Any]: return aioclient.mock_calls[-1][2] +@pytest.mark.usefixtures("supervisor_client") async def test_no_send( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -126,6 +127,7 @@ async def test_load_with_supervisor_without_diagnostics(hass: HomeAssistant) -> assert not analytics.preferences[ATTR_DIAGNOSTICS] +@pytest.mark.usefixtures("supervisor_client") async def test_failed_to_send( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -144,6 +146,7 @@ async def test_failed_to_send( ) +@pytest.mark.usefixtures("supervisor_client") async def test_failed_to_send_raises( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -159,7 +162,7 @@ async def test_failed_to_send_raises( assert "Error sending analytics" in caplog.text -@pytest.mark.usefixtures("installation_type_mock") +@pytest.mark.usefixtures("installation_type_mock", "supervisor_client") async def test_send_base( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -182,6 +185,7 @@ async def test_send_base( assert snapshot == submitted_data +@pytest.mark.usefixtures("supervisor_client") async def test_send_base_with_supervisor( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -230,7 +234,7 @@ async def test_send_base_with_supervisor( assert snapshot == submitted_data -@pytest.mark.usefixtures("installation_type_mock") +@pytest.mark.usefixtures("installation_type_mock", "supervisor_client") async def test_send_usage( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -271,6 +275,7 @@ async def test_send_usage_with_supervisor( caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, snapshot: SnapshotAssertion, + supervisor_client: AsyncMock, ) -> None: """Test send usage with supervisor preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) @@ -281,6 +286,9 @@ async def test_send_usage_with_supervisor( assert analytics.preferences[ATTR_USAGE] hass.config.components.add("default_config") + supervisor_client.addons.addon_info.return_value = Mock( + slug="test_addon", protected=True, version="1", auto_update=False + ) with ( patch( "homeassistant.components.hassio.get_supervisor_info", @@ -305,17 +313,6 @@ async def test_send_usage_with_supervisor( "homeassistant.components.hassio.get_host_info", side_effect=Mock(return_value={}), ), - patch( - "homeassistant.components.hassio.async_get_addon_info", - side_effect=AsyncMock( - return_value={ - "slug": "test_addon", - "protected": True, - "version": "1", - "auto_update": False, - } - ), - ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), @@ -330,7 +327,7 @@ async def test_send_usage_with_supervisor( assert snapshot == submitted_data -@pytest.mark.usefixtures("installation_type_mock") +@pytest.mark.usefixtures("installation_type_mock", "supervisor_client") async def test_send_statistics( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -358,9 +355,10 @@ async def test_send_statistics( assert snapshot == submitted_data -@pytest.mark.usefixtures("mock_hass_config") +@pytest.mark.usefixtures("mock_hass_config", "supervisor_client") async def test_send_statistics_one_integration_fails( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test send statistics preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) @@ -381,7 +379,9 @@ async def test_send_statistics_one_integration_fails( assert post_call[2]["integration_count"] == 0 -@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") +@pytest.mark.usefixtures( + "installation_type_mock", "mock_hass_config", "supervisor_client" +) async def test_send_statistics_disabled_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -418,7 +418,9 @@ async def test_send_statistics_disabled_integration( assert snapshot == submitted_data -@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") +@pytest.mark.usefixtures( + "installation_type_mock", "mock_hass_config", "supervisor_client" +) async def test_send_statistics_ignored_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -461,9 +463,10 @@ async def test_send_statistics_ignored_integration( assert snapshot == submitted_data -@pytest.mark.usefixtures("mock_hass_config") +@pytest.mark.usefixtures("mock_hass_config", "supervisor_client") async def test_send_statistics_async_get_integration_unknown_exception( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test send statistics preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) @@ -489,6 +492,7 @@ async def test_send_statistics_with_supervisor( caplog: pytest.LogCaptureFixture, aioclient_mock: AiohttpClientMocker, snapshot: SnapshotAssertion, + supervisor_client: AsyncMock, ) -> None: """Test send statistics preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) @@ -497,6 +501,9 @@ async def test_send_statistics_with_supervisor( assert analytics.preferences[ATTR_BASE] assert analytics.preferences[ATTR_STATISTICS] + supervisor_client.addons.addon_info.return_value = Mock( + slug="test_addon", protected=True, version="1", auto_update=False + ) with ( patch( "homeassistant.components.hassio.get_supervisor_info", @@ -521,17 +528,6 @@ async def test_send_statistics_with_supervisor( "homeassistant.components.hassio.get_host_info", side_effect=Mock(return_value={}), ), - patch( - "homeassistant.components.hassio.async_get_addon_info", - side_effect=AsyncMock( - return_value={ - "slug": "test_addon", - "protected": True, - "version": "1", - "auto_update": False, - } - ), - ), patch( "homeassistant.components.hassio.is_hassio", side_effect=Mock(return_value=True), @@ -546,6 +542,7 @@ async def test_send_statistics_with_supervisor( assert snapshot == submitted_data +@pytest.mark.usefixtures("supervisor_client") async def test_reusing_uuid( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -563,7 +560,9 @@ async def test_reusing_uuid( assert analytics.uuid == "NOT_MOCK_UUID" -@pytest.mark.usefixtures("enable_custom_integrations", "installation_type_mock") +@pytest.mark.usefixtures( + "enable_custom_integrations", "installation_type_mock", "supervisor_client" +) async def test_custom_integrations( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -590,8 +589,10 @@ async def test_custom_integrations( assert snapshot == submitted_data +@pytest.mark.usefixtures("supervisor_client") async def test_dev_url( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test sending payload to dev url.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL_DEV, status=200) @@ -607,6 +608,7 @@ async def test_dev_url( assert str(payload[1]) == ANALYTICS_ENDPOINT_URL_DEV +@pytest.mark.usefixtures("supervisor_client") async def test_dev_url_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -630,8 +632,10 @@ async def test_dev_url_error( ) in caplog.text +@pytest.mark.usefixtures("supervisor_client") async def test_nightly_endpoint( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test sending payload to production url when running nightly.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) @@ -647,7 +651,9 @@ async def test_nightly_endpoint( assert str(payload[1]) == ANALYTICS_ENDPOINT_URL -@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") +@pytest.mark.usefixtures( + "installation_type_mock", "mock_hass_config", "supervisor_client" +) async def test_send_with_no_energy( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -683,7 +689,9 @@ async def test_send_with_no_energy( assert snapshot == submitted_data -@pytest.mark.usefixtures("recorder_mock", "installation_type_mock", "mock_hass_config") +@pytest.mark.usefixtures( + "recorder_mock", "installation_type_mock", "mock_hass_config", "supervisor_client" +) async def test_send_with_no_energy_config( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -714,7 +722,9 @@ async def test_send_with_no_energy_config( ) -@pytest.mark.usefixtures("recorder_mock", "installation_type_mock", "mock_hass_config") +@pytest.mark.usefixtures( + "recorder_mock", "installation_type_mock", "mock_hass_config", "supervisor_client" +) async def test_send_with_energy_config( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -745,7 +755,9 @@ async def test_send_with_energy_config( ) -@pytest.mark.usefixtures("installation_type_mock", "mock_hass_config") +@pytest.mark.usefixtures( + "installation_type_mock", "mock_hass_config", "supervisor_client" +) async def test_send_usage_with_certificate( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -771,7 +783,7 @@ async def test_send_usage_with_certificate( assert snapshot == submitted_data -@pytest.mark.usefixtures("recorder_mock", "installation_type_mock") +@pytest.mark.usefixtures("recorder_mock", "installation_type_mock", "supervisor_client") async def test_send_with_recorder( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -802,6 +814,7 @@ async def test_send_with_recorder( ) +@pytest.mark.usefixtures("supervisor_client") async def test_send_with_problems_loading_yaml( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -821,7 +834,7 @@ async def test_send_with_problems_loading_yaml( assert len(aioclient_mock.mock_calls) == 0 -@pytest.mark.usefixtures("mock_hass_config") +@pytest.mark.usefixtures("mock_hass_config", "supervisor_client") async def test_timeout_while_sending( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -840,7 +853,7 @@ async def test_timeout_while_sending( assert "Timeout sending analytics" in caplog.text -@pytest.mark.usefixtures("installation_type_mock") +@pytest.mark.usefixtures("installation_type_mock", "supervisor_client") async def test_not_check_config_entries_if_yaml( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index cf8d4838415..66000fc5936 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -20,6 +22,7 @@ async def test_setup(hass: HomeAssistant) -> None: assert DOMAIN in hass.data +@pytest.mark.usefixtures("supervisor_client") async def test_websocket( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 1e79248fbeb..e6c685a1342 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Generator from importlib.util import find_spec from pathlib import Path from typing import TYPE_CHECKING, Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest @@ -243,12 +243,14 @@ def addon_info_side_effect_fixture() -> Any | None: @pytest.fixture(name="addon_info") -def addon_info_fixture(addon_info_side_effect: Any | None) -> Generator[AsyncMock]: +def addon_info_fixture( + supervisor_client: AsyncMock, addon_info_side_effect: Any | None +) -> Generator[AsyncMock]: """Mock Supervisor add-on info.""" # pylint: disable-next=import-outside-toplevel from .hassio.common import mock_addon_info - yield from mock_addon_info(addon_info_side_effect) + yield from mock_addon_info(supervisor_client, addon_info_side_effect) @pytest.fixture(name="addon_not_installed") @@ -409,3 +411,29 @@ def update_addon_fixture() -> Generator[AsyncMock]: from .hassio.common import mock_update_addon yield from mock_update_addon() + + +@pytest.fixture(name="supervisor_client") +def supervisor_client() -> Generator[AsyncMock]: + """Mock the supervisor client.""" + supervisor_client = AsyncMock() + supervisor_client.addons = AsyncMock() + with ( + patch( + "homeassistant.components.hassio.get_supervisor_client", + return_value=supervisor_client, + ), + patch( + "homeassistant.components.hassio.handler.get_supervisor_client", + return_value=supervisor_client, + ), + patch( + "homeassistant.components.hassio.addon_manager.get_supervisor_client", + return_value=supervisor_client, + ), + patch( + "homeassistant.components.hassio.handler.HassIO.client", + new=PropertyMock(return_value=supervisor_client), + ), + ): + yield supervisor_client diff --git a/tests/components/hassio/common.py b/tests/components/hassio/common.py index 630368a0a7a..8aee2b35a5f 100644 --- a/tests/components/hassio/common.py +++ b/tests/components/hassio/common.py @@ -3,14 +3,28 @@ from __future__ import annotations from collections.abc import Generator +from dataclasses import fields import logging +from types import MethodType from typing import Any -from unittest.mock import DEFAULT, AsyncMock, patch +from unittest.mock import DEFAULT, AsyncMock, Mock, patch + +from aiohasupervisor.models import InstalledAddonComplete from homeassistant.components.hassio.addon_manager import AddonManager from homeassistant.core import HomeAssistant LOGGER = logging.getLogger(__name__) +INSTALLED_ADDON_FIELDS = [field.name for field in fields(InstalledAddonComplete)] + + +def mock_to_dict(obj: Mock, fields: list[str]) -> dict[str, Any]: + """Aiohasupervisor mocks to dictionary representation.""" + return { + field: getattr(obj, field) + for field in fields + if not isinstance(getattr(obj, field), Mock) + } def mock_addon_manager(hass: HomeAssistant) -> AddonManager: @@ -52,21 +66,31 @@ def mock_addon_store_info( yield addon_store_info -def mock_addon_info(addon_info_side_effect: Any | None) -> Generator[AsyncMock]: +def mock_addon_info( + supervisor_client: AsyncMock, addon_info_side_effect: Any | None +) -> Generator[AsyncMock]: """Mock Supervisor add-on info.""" - with patch( - "homeassistant.components.hassio.addon_manager.async_get_addon_info", - side_effect=addon_info_side_effect, - ) as addon_info: - addon_info.return_value = { - "available": False, - "hostname": None, - "options": {}, - "state": None, - "update_available": False, - "version": None, - } - yield addon_info + supervisor_client.addons.addon_info.side_effect = addon_info_side_effect + + supervisor_client.addons.addon_info.return_value = addon_info = Mock( + spec=InstalledAddonComplete, + slug="test", + repository="core", + available=False, + hostname="", + options={}, + state="unknown", + update_available=False, + version=None, + supervisor_api=False, + supervisor_role="default", + ) + addon_info.name = "test" + addon_info.to_dict = MethodType( + lambda self: mock_to_dict(self, INSTALLED_ADDON_FIELDS), + addon_info, + ) + yield supervisor_client.addons.addon_info def mock_addon_not_installed( @@ -87,10 +111,10 @@ def mock_addon_installed( "state": "stopped", "version": "1.0.0", } - addon_info.return_value["available"] = True - addon_info.return_value["hostname"] = "core-test-addon" - addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0.0" + addon_info.return_value.available = True + addon_info.return_value.hostname = "core-test-addon" + addon_info.return_value.state = "stopped" + addon_info.return_value.version = "1.0.0" return addon_info @@ -102,10 +126,7 @@ def mock_addon_running(addon_store_info: AsyncMock, addon_info: AsyncMock) -> As "state": "started", "version": "1.0.0", } - addon_info.return_value["available"] = True - addon_info.return_value["hostname"] = "core-test-addon" - addon_info.return_value["state"] = "started" - addon_info.return_value["version"] = "1.0.0" + addon_info.return_value.state = "started" return addon_info @@ -122,9 +143,10 @@ def mock_install_addon_side_effect( "state": "stopped", "version": "1.0.0", } - addon_info.return_value["available"] = True - addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0.0" + + addon_info.return_value.available = True + addon_info.return_value.state = "stopped" + addon_info.return_value.version = "1.0.0" return install_addon @@ -152,8 +174,8 @@ def mock_start_addon_side_effect( "state": "started", "version": "1.0.0", } - addon_info.return_value["available"] = True - addon_info.return_value["state"] = "started" + addon_info.return_value.available = True + addon_info.return_value.state = "started" return start_addon @@ -194,7 +216,7 @@ def mock_uninstall_addon() -> Generator[AsyncMock]: def mock_addon_options(addon_info: AsyncMock) -> dict[str, Any]: """Mock add-on options.""" - return addon_info.return_value["options"] + return addon_info.return_value.options def mock_set_addon_options_side_effect(addon_options: dict[str, Any]) -> Any | None: diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 4cb57e5b8d8..c1b47f67d3c 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -43,7 +43,7 @@ async def test_not_available_raises_exception( ) -> None: """Test addon not available raises exception.""" addon_store_info.return_value["available"] = False - addon_info.return_value["available"] = False + addon_info.return_value.available = False with pytest.raises(AddonError) as err: await addon_manager.async_install_addon() @@ -118,7 +118,7 @@ async def test_get_addon_info( addon_state: AddonState, ) -> None: """Test get addon info when addon is installed.""" - addon_installed.return_value["state"] = addon_info_state + addon_installed.return_value.state = addon_info_state assert await addon_manager.async_get_addon_info() == AddonInfo( available=True, hostname="core-test-addon", @@ -198,7 +198,7 @@ async def test_install_addon( ) -> None: """Test install addon.""" addon_store_info.return_value["available"] = True - addon_info.return_value["available"] = True + addon_info.return_value.available = True await addon_manager.async_install_addon() @@ -213,7 +213,7 @@ async def test_install_addon_error( ) -> None: """Test install addon raises error.""" addon_store_info.return_value["available"] = True - addon_info.return_value["available"] = True + addon_info.return_value.available = True install_addon.side_effect = HassioAPIError("Boom") with pytest.raises(AddonError) as err: @@ -501,7 +501,7 @@ async def test_update_addon( update_addon: AsyncMock, ) -> None: """Test update addon.""" - addon_info.return_value["update_available"] = True + addon_info.return_value.update_available = True await addon_manager.async_update_addon() @@ -521,7 +521,7 @@ async def test_update_addon_no_update( update_addon: AsyncMock, ) -> None: """Test update addon without update available.""" - addon_info.return_value["update_available"] = False + addon_info.return_value.update_available = False await addon_manager.async_update_addon() @@ -539,7 +539,7 @@ async def test_update_addon_error( update_addon: AsyncMock, ) -> None: """Test update addon raises error.""" - addon_info.return_value["update_available"] = True + addon_info.return_value.update_available = True update_addon.side_effect = HassioAPIError("Boom") with pytest.raises(AddonError) as err: @@ -564,7 +564,7 @@ async def test_schedule_update_addon( update_addon: AsyncMock, ) -> None: """Test schedule update addon.""" - addon_info.return_value["update_available"] = True + addon_info.return_value.update_available = True update_task = addon_manager.async_schedule_update_addon() @@ -637,7 +637,7 @@ async def test_schedule_update_addon_error( error_message: str, ) -> None: """Test schedule update addon raises error.""" - addon_installed.return_value["update_available"] = True + addon_installed.return_value.update_available = True create_backup.side_effect = create_backup_error update_addon.side_effect = update_addon_error @@ -688,7 +688,7 @@ async def test_schedule_update_addon_logs_error( caplog: pytest.LogCaptureFixture, ) -> None: """Test schedule update addon logs error.""" - addon_installed.return_value["update_available"] = True + addon_installed.return_value.update_available = True create_backup.side_effect = create_backup_error update_addon.side_effect = update_addon_error diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index af72ea9d702..33cfd448b44 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -1,7 +1,7 @@ """The tests for the hassio binary sensors.""" import os -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -17,7 +17,7 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock: AiohttpClientMocker) -> None: +def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) @@ -193,20 +193,23 @@ def mock_all(aioclient_mock: AiohttpClientMocker) -> None: @pytest.mark.parametrize( - ("entity_id", "expected"), + ("entity_id", "expected", "addon_state"), [ - ("binary_sensor.test_running", "on"), - ("binary_sensor.test2_running", "off"), + ("binary_sensor.test_running", "on", "started"), + ("binary_sensor.test2_running", "off", "stopped"), ], ) async def test_binary_sensor( hass: HomeAssistant, - entity_id, - expected, + entity_id: str, + expected: str, + addon_state: str, aioclient_mock: AiohttpClientMocker, entity_registry: er.EntityRegistry, + addon_installed: AsyncMock, ) -> None: """Test hassio OS and addons binary sensor.""" + addon_installed.return_value.state = addon_state config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 0d648ba9bdb..0fcf7933ac0 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -18,7 +18,7 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock: AiohttpClientMocker) -> None: +def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 305b863b3af..a0851ccd9f6 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -43,6 +43,7 @@ async def test_hassio_discovery_startup( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_mqtt: type[config_entries.ConfigFlow], + addon_installed: AsyncMock, ) -> None: """Test startup and discovery after event.""" aioclient_mock.get( @@ -67,10 +68,7 @@ async def test_hassio_discovery_startup( }, }, ) - aioclient_mock.get( - "http://127.0.0.1/addons/mosquitto/info", - json={"result": "ok", "data": {"name": "Mosquitto Test"}}, - ) + addon_installed.return_value.name = "Mosquitto Test" assert aioclient_mock.call_count == 0 @@ -78,7 +76,7 @@ async def test_hassio_discovery_startup( await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 assert mock_mqtt.async_step_hassio.called mock_mqtt.async_step_hassio.assert_called_with( HassioServiceInfo( @@ -102,6 +100,7 @@ async def test_hassio_discovery_startup_done( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, mock_mqtt: type[config_entries.ConfigFlow], + addon_installed: AsyncMock, ) -> None: """Test startup and discovery with hass discovery.""" aioclient_mock.post( @@ -130,10 +129,7 @@ async def test_hassio_discovery_startup_done( }, }, ) - aioclient_mock.get( - "http://127.0.0.1/addons/mosquitto/info", - json={"result": "ok", "data": {"name": "Mosquitto Test"}}, - ) + addon_installed.return_value.name = "Mosquitto Test" with ( patch( @@ -149,7 +145,7 @@ async def test_hassio_discovery_startup_done( await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 assert mock_mqtt.async_step_hassio.called mock_mqtt.async_step_hassio.assert_called_with( HassioServiceInfo( @@ -173,6 +169,7 @@ async def test_hassio_discovery_webhook( aioclient_mock: AiohttpClientMocker, hassio_client: TestClient, mock_mqtt: type[config_entries.ConfigFlow], + addon_installed: AsyncMock, ) -> None: """Test discovery webhook.""" aioclient_mock.get( @@ -193,10 +190,7 @@ async def test_hassio_discovery_webhook( }, }, ) - aioclient_mock.get( - "http://127.0.0.1/addons/mosquitto/info", - json={"result": "ok", "data": {"name": "Mosquitto Test"}}, - ) + addon_installed.return_value.name = "Mosquitto Test" resp = await hassio_client.post( "/api/hassio_push/discovery/testuuid", @@ -207,7 +201,7 @@ async def test_hassio_discovery_webhook( await hass.async_block_till_done() assert resp.status == HTTPStatus.OK - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 1 assert mock_mqtt.async_step_hassio.called mock_mqtt.async_step_hassio.assert_called_with( HassioServiceInfo( diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 949f96ece38..1fb1e44c46d 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -201,20 +201,6 @@ async def test_api_homeassistant_restart( assert aioclient_mock.call_count == 1 -async def test_api_addon_info( - hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker -) -> None: - """Test setup with API Add-on info.""" - aioclient_mock.get( - "http://127.0.0.1/addons/test/info", - json={"result": "ok", "data": {"name": "bla"}}, - ) - - data = await hassio_handler.get_addon_info("test") - assert data["name"] == "bla" - assert aioclient_mock.call_count == 1 - - async def test_api_addon_stats( hassio_handler: HassIO, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index d71e8acfbe0..13626ef19d0 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -509,6 +509,7 @@ async def test_service_calls( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, + addon_installed, ) -> None: """Call service and check the API calls behind that.""" with ( @@ -546,14 +547,14 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 24 + assert aioclient_mock.call_count == 22 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 == 26 + assert aioclient_mock.call_count == 24 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -568,7 +569,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 28 + assert aioclient_mock.call_count == 26 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", "homeassistant": True, @@ -593,7 +594,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 30 + assert aioclient_mock.call_count == 28 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], @@ -612,7 +613,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 31 + assert aioclient_mock.call_count == 29 assert aioclient_mock.mock_calls[-1][2] == { "name": "backup_name", "location": "backup_share", @@ -628,7 +629,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 32 + assert aioclient_mock.call_count == 30 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 03:48:00", "location": None, @@ -647,7 +648,7 @@ async def test_service_calls( ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 34 + assert aioclient_mock.call_count == 32 assert aioclient_mock.mock_calls[-1][2] == { "name": "2021-11-13 11:48:00", "location": None, @@ -749,6 +750,7 @@ async def test_service_calls_core( assert aioclient_mock.call_count == 6 +@pytest.mark.usefixtures("addon_installed") async def test_entry_load_and_unload(hass: HomeAssistant) -> None: """Test loading and unloading config entry.""" with patch.dict(os.environ, MOCK_ENVIRON): @@ -775,6 +777,7 @@ async def test_migration_off_hassio(hass: HomeAssistant) -> None: assert hass.config_entries.async_entries(DOMAIN) == [] +@pytest.mark.usefixtures("addon_installed") async def test_device_registry_calls( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: @@ -927,6 +930,7 @@ async def test_device_registry_calls( assert len(device_registry.devices) == 5 +@pytest.mark.usefixtures("addon_installed") async def test_coordinator_updates( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1002,7 +1006,7 @@ async def test_coordinator_updates( assert "Error on Supervisor API: Unknown" in caplog.text -@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "addon_installed") async def test_coordinator_updates_stats_entities_enabled( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index 1a3d3d83f95..578279dbf79 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -835,7 +835,7 @@ async def test_system_is_not_ready( @pytest.mark.parametrize( "all_setup_requests", [{"include_addons": True}], indirect=True ) -@pytest.mark.usefixtures("all_setup_requests") +@pytest.mark.usefixtures("all_setup_requests", "addon_installed") async def test_supervisor_issues_detached_addon_missing( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 907529ec9c4..7655f657eda 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -563,7 +563,7 @@ async def test_mount_failed_repair_flow( @pytest.mark.parametrize( "all_setup_requests", [{"include_addons": True}], indirect=True ) -@pytest.mark.usefixtures("all_setup_requests") +@pytest.mark.usefixtures("all_setup_requests", "addon_installed") async def test_supervisor_issue_docker_config_repair_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, @@ -786,7 +786,7 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks( @pytest.mark.parametrize( "all_setup_requests", [{"include_addons": True}], indirect=True ) -@pytest.mark.usefixtures("all_setup_requests") +@pytest.mark.usefixtures("all_setup_requests", "addon_installed") async def test_supervisor_issue_detached_addon_removed( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 71b867d849d..bd3de73baf5 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -28,7 +28,7 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock: AiohttpClientMocker) -> None: +def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed) -> None: """Mock all setup requests.""" _install_default_mocks(aioclient_mock) _install_test_addon_stats_mock(aioclient_mock) diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 9a047010cc3..6195e62aaac 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -2,8 +2,9 @@ from datetime import timedelta import os -from unittest.mock import patch +from unittest.mock import AsyncMock, patch +from aiohasupervisor import SupervisorBadRequestError import pytest from homeassistant.components.hassio import DOMAIN, HassioAPIError @@ -21,7 +22,7 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) -def mock_all(aioclient_mock: AiohttpClientMocker) -> None: +def mock_all(aioclient_mock: AiohttpClientMocker, addon_installed) -> None: """Mock all setup requests.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) @@ -217,8 +218,10 @@ async def test_update_entities( expected_state, auto_update, aioclient_mock: AiohttpClientMocker, + addon_installed: AsyncMock, ) -> None: """Test update entities.""" + addon_installed.return_value.auto_update = auto_update config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) @@ -375,7 +378,7 @@ async def test_update_addon_with_error( exc=HassioAPIError, ) - with pytest.raises(HomeAssistantError): + with pytest.raises(HomeAssistantError, match=r"^Error updating test:"): assert not await hass.services.async_call( "update", "install", @@ -404,7 +407,9 @@ async def test_update_os_with_error( exc=HassioAPIError, ) - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match=r"^Error updating Home Assistant Operating System:" + ): assert not await hass.services.async_call( "update", "install", @@ -433,7 +438,9 @@ async def test_update_supervisor_with_error( exc=HassioAPIError, ) - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match=r"^Error updating Home Assistant Supervisor:" + ): assert not await hass.services.async_call( "update", "install", @@ -462,7 +469,9 @@ async def test_update_core_with_error( exc=HassioAPIError, ) - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match=r"^Error updating Home Assistant Core:" + ): assert not await hass.services.async_call( "update", "install", @@ -613,9 +622,12 @@ async def test_no_os_entity(hass: HomeAssistant) -> None: async def test_setting_up_core_update_when_addon_fails( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + addon_installed: AsyncMock, ) -> None: """Test setting up core update when single addon fails.""" + addon_installed.side_effect = SupervisorBadRequestError("Addon Test does not exist") with ( patch.dict(os.environ, MOCK_ENVIRON), patch( @@ -626,10 +638,6 @@ async def test_setting_up_core_update_when_addon_fails( "homeassistant.components.hassio.HassIO.get_addon_changelog", side_effect=HassioAPIError("add-on is not running"), ), - patch( - "homeassistant.components.hassio.HassIO.get_addon_info", - side_effect=HassioAPIError("add-on is not running"), - ), ): result = await async_setup_component( hass, diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 65fab707c0b..7d4b1dc9df0 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -418,7 +418,7 @@ async def test_option_flow_install_multi_pan_addon_zha_other_radio( await hass.async_block_till_done() install_addon.assert_called_once_with(hass, "core_silabs_multiprotocol") - addon_info.return_value["hostname"] = "core-silabs-multiprotocol" + addon_info.return_value.hostname = "core-silabs-multiprotocol" result = await hass.config_entries.options.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" @@ -513,7 +513,7 @@ async def test_option_flow_addon_installed_same_device_reconfigure_unexpected_us ) -> None: """Test reconfiguring the multi pan addon.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( hass @@ -572,7 +572,7 @@ async def test_option_flow_addon_installed_same_device_reconfigure_expected_user ) -> None: """Test reconfiguring the multi pan addon.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" multipan_manager = await silabs_multiprotocol_addon.get_multiprotocol_addon_manager( hass @@ -643,7 +643,7 @@ async def test_option_flow_addon_installed_same_device_uninstall( ) -> None: """Test uninstalling the multi pan addon.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -738,7 +738,7 @@ async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pa ) -> None: """Test uninstalling the multi pan addon.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -781,7 +781,7 @@ async def test_option_flow_flasher_already_running_failure( ) -> None: """Test uninstalling the multi pan addon but with the flasher addon running.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -805,7 +805,7 @@ async def test_option_flow_flasher_already_running_failure( # The flasher addon is already installed and running, this is bad addon_store_info.return_value["installed"] = True - addon_info.return_value["state"] = "started" + addon_info.return_value.state = "started" result = await hass.config_entries.options.async_configure( result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True} @@ -828,7 +828,7 @@ async def test_option_flow_addon_installed_same_device_flasher_already_installed ) -> None: """Test uninstalling the multi pan addon.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -898,7 +898,7 @@ async def test_option_flow_flasher_install_failure( ) -> None: """Test uninstalling the multi pan addon, case where flasher addon fails.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -967,7 +967,7 @@ async def test_option_flow_flasher_addon_flash_failure( ) -> None: """Test where flasher addon fails to flash Zigbee firmware.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -1034,7 +1034,7 @@ async def test_option_flow_uninstall_migration_initiate_failure( ) -> None: """Test uninstalling the multi pan addon, case where ZHA migration init fails.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -1095,7 +1095,7 @@ async def test_option_flow_uninstall_migration_finish_failure( ) -> None: """Test uninstalling the multi pan addon, case where ZHA migration init fails.""" - addon_info.return_value["options"]["device"] = "/dev/ttyTEST123" + addon_info.return_value.options["device"] = "/dev/ttyTEST123" # Setup the config entry config_entry = MockConfigEntry( @@ -1667,7 +1667,7 @@ async def test_check_multi_pan_addon_auto_start( ) -> None: """Test `check_multi_pan_addon` auto starting the addon.""" - addon_info.return_value["state"] = "not_running" + addon_info.return_value.state = "not_running" addon_store_info.return_value = { "installed": True, "available": True, @@ -1686,7 +1686,7 @@ async def test_check_multi_pan_addon( ) -> None: """Test `check_multi_pan_addon`.""" - addon_info.return_value["state"] = "started" + addon_info.return_value.state = "started" addon_store_info.return_value = { "installed": True, "available": True, @@ -1717,7 +1717,7 @@ async def test_multi_pan_addon_using_device_not_running( ) -> None: """Test `multi_pan_addon_using_device` when the addon isn't running.""" - addon_info.return_value["state"] = "not_running" + addon_info.return_value.state = "not_running" addon_store_info.return_value = { "installed": True, "available": True, @@ -1745,8 +1745,8 @@ async def test_multi_pan_addon_using_device( ) -> None: """Test `multi_pan_addon_using_device` when the addon isn't running.""" - addon_info.return_value["state"] = "started" - addon_info.return_value["options"] = { + addon_info.return_value.state = "started" + addon_info.return_value.options = { "autoflash_firmware": True, "device": options_device, "baudrate": "115200", diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 9d43b341abf..4fd2eddb704 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -13,6 +13,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration from tests.typing import WebSocketGenerator +@pytest.mark.usefixtures("supervisor_client") async def test_hardware_info( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, addon_store_info ) -> None: @@ -65,6 +66,7 @@ async def test_hardware_info( @pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) +@pytest.mark.usefixtures("supervisor_client") async def test_hardware_info_fail( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, os_info, addon_store_info ) -> None: diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index cd5ef307cd3..1296604f390 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -411,8 +411,8 @@ async def test_update_addon( connect_side_effect: Exception, ) -> None: """Test update the Matter add-on during entry setup.""" - addon_info.return_value["version"] = addon_version - addon_info.return_value["update_available"] = update_available + addon_info.return_value.version = addon_version + addon_info.return_value.update_available = update_available create_backup.side_effect = create_backup_side_effect update_addon.side_effect = update_addon_side_effect matter_client.connect.side_effect = connect_side_effect diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index edd92591b1b..966f80d0bd8 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -3,7 +3,7 @@ import asyncio from http import HTTPStatus from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import aiohttp import pytest @@ -32,21 +32,16 @@ HASSIO_DATA_2 = hassio.HassioServiceInfo( ) -@pytest.fixture(name="addon_info") -def addon_info_fixture(): - """Mock Supervisor add-on info.""" - with patch( - "homeassistant.components.otbr.config_flow.async_get_addon_info", - ) as addon_info: - addon_info.return_value = { - "available": True, - "hostname": None, - "options": {}, - "state": None, - "update_available": False, - "version": None, - } - yield addon_info +@pytest.fixture(name="otbr_addon_info") +def otbr_addon_info_fixture(addon_info: AsyncMock, addon_installed) -> AsyncMock: + """Mock Supervisor otbr add-on info.""" + addon_info.return_value.available = True + addon_info.return_value.hostname = "" + addon_info.return_value.options = {} + addon_info.return_value.state = "unknown" + addon_info.return_value.update_available = False + addon_info.return_value.version = None + return addon_info @pytest.mark.parametrize( @@ -360,7 +355,7 @@ async def _test_user_flow_connect_error(hass: HomeAssistant, func, error) -> Non @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow.""" url = "http://core-silabs-multiprotocol:8081" @@ -393,20 +388,14 @@ async def test_hassio_discovery_flow( @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_yellow( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow.""" url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", text="aa") - addon_info.return_value = { - "available": True, - "hostname": None, - "options": {"device": "/dev/ttyAMA1"}, - "state": None, - "update_available": False, - "version": None, - } + otbr_addon_info.return_value.available = True + otbr_addon_info.return_value.options = {"device": "/dev/ttyAMA1"} with ( patch( @@ -455,20 +444,14 @@ async def test_hassio_discovery_flow_sky_connect( title: str, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, - addon_info, + otbr_addon_info, ) -> None: """Test the hassio discovery flow.""" url = "http://core-silabs-multiprotocol:8081" aioclient_mock.get(f"{url}/node/dataset/active", text="aa") - addon_info.return_value = { - "available": True, - "hostname": None, - "options": {"device": device}, - "state": None, - "update_available": False, - "version": None, - } + otbr_addon_info.return_value.available = True + otbr_addon_info.return_value.options = {"device": device} with patch( "homeassistant.components.otbr.async_setup_entry", @@ -497,7 +480,7 @@ async def test_hassio_discovery_flow_sky_connect( @pytest.mark.usefixtures("get_active_dataset_tlvs", "get_extended_address") async def test_hassio_discovery_flow_2x_addons( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the user has 2 addons with otbr support.""" url1 = "http://core-silabs-multiprotocol:8081" @@ -507,37 +490,28 @@ async def test_hassio_discovery_flow_2x_addons( aioclient_mock.get(f"{url1}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) aioclient_mock.get(f"{url2}/node/ba-id", json=TEST_BORDER_AGENT_ID_2.hex()) - async def _addon_info(hass: HomeAssistant, slug: str) -> dict[str, Any]: + async def _addon_info(slug: str) -> Mock: await asyncio.sleep(0) if slug == "otbr": - return { - "available": True, - "hostname": None, - "options": { - "device": ( - "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" - "9e2adbd75b8beb119fe564a0f320645d-if00-port0" - ) - }, - "state": None, - "update_available": False, - "version": None, - } - return { - "available": True, - "hostname": None, - "options": { - "device": ( - "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" - "9e2adbd75b8beb119fe564a0f320645d-if00-port1" - ) - }, - "state": None, - "update_available": False, - "version": None, - } + device = ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port0" + ) + else: + device = ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port1" + ) + return Mock( + available=True, + hostname=otbr_addon_info.return_value.hostname, + options={"device": device}, + state=otbr_addon_info.return_value.state, + update_available=otbr_addon_info.return_value.update_available, + version=otbr_addon_info.return_value.version, + ) - addon_info.side_effect = _addon_info + otbr_addon_info.side_effect = _addon_info result1 = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA @@ -590,7 +564,7 @@ async def test_hassio_discovery_flow_2x_addons( @pytest.mark.usefixtures("get_active_dataset_tlvs", "get_extended_address") async def test_hassio_discovery_flow_2x_addons_same_ext_address( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the user has 2 addons with otbr support.""" url1 = "http://core-silabs-multiprotocol:8081" @@ -600,37 +574,28 @@ async def test_hassio_discovery_flow_2x_addons_same_ext_address( aioclient_mock.get(f"{url1}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) aioclient_mock.get(f"{url2}/node/ba-id", json=TEST_BORDER_AGENT_ID.hex()) - async def _addon_info(hass: HomeAssistant, slug: str) -> dict[str, Any]: + async def _addon_info(slug: str) -> Mock: await asyncio.sleep(0) if slug == "otbr": - return { - "available": True, - "hostname": None, - "options": { - "device": ( - "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" - "9e2adbd75b8beb119fe564a0f320645d-if00-port0" - ) - }, - "state": None, - "update_available": False, - "version": None, - } - return { - "available": True, - "hostname": None, - "options": { - "device": ( - "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" - "9e2adbd75b8beb119fe564a0f320645d-if00-port1" - ) - }, - "state": None, - "update_available": False, - "version": None, - } + device = ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port0" + ) + else: + device = ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" + "9e2adbd75b8beb119fe564a0f320645d-if00-port1" + ) + return Mock( + available=True, + hostname=otbr_addon_info.return_value.hostname, + options={"device": device}, + state=otbr_addon_info.return_value.state, + update_available=otbr_addon_info.return_value.update_available, + version=otbr_addon_info.return_value.version, + ) - addon_info.side_effect = _addon_info + otbr_addon_info.side_effect = _addon_info result1 = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA @@ -666,7 +631,7 @@ async def test_hassio_discovery_flow_2x_addons_same_ext_address( @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_router_not_setup( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the border router has no dataset. @@ -724,7 +689,7 @@ async def test_hassio_discovery_flow_router_not_setup( @pytest.mark.usefixtures("get_border_agent_id") async def test_hassio_discovery_flow_router_not_setup_has_preferred( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_addon_info ) -> None: """Test the hassio discovery flow when the border router has no dataset. @@ -780,7 +745,7 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, multiprotocol_addon_manager_mock, - addon_info, + otbr_addon_info, ) -> None: """Test the hassio discovery flow when the border router has no dataset. @@ -920,7 +885,7 @@ async def test_hassio_discovery_flow_new_port(hass: HomeAssistant) -> None: @pytest.mark.usefixtures( - "addon_info", + "otbr_addon_info", "get_active_dataset_tlvs", "get_border_agent_id", "get_extended_address", @@ -962,7 +927,7 @@ async def test_hassio_discovery_flow_new_port_other_addon(hass: HomeAssistant) - ], ) @pytest.mark.usefixtures( - "addon_info", + "otbr_addon_info", "get_active_dataset_tlvs", "get_border_agent_id", "get_extended_address", diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 5ec72b8a46a..a83ed2603dc 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -772,8 +772,8 @@ async def test_update_addon( network_key = "abc123" addon_options["device"] = device addon_options["network_key"] = network_key - addon_info.return_value["version"] = addon_version - addon_info.return_value["update_available"] = update_available + addon_info.return_value.version = addon_version + addon_info.return_value.update_available = update_available create_backup.side_effect = create_backup_side_effect update_addon.side_effect = update_addon_side_effect client.connect.side_effect = InvalidServerVersion(