Use aiohasupervisor for addon info calls (#125926)

* Use aiohasupervisor for addon info calls

* Fix issue/repair tests in supervisor

* Fixes from feedback
This commit is contained in:
Mike Degatano 2024-09-17 17:22:35 -04:00 committed by GitHub
parent 37cdc6d500
commit 97d0d91d2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 367 additions and 317 deletions

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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:

View File

@ -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

View File

@ -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"]
}

View File

@ -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

View File

@ -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})"

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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"})

View File

@ -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(

View File

@ -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:

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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",

View File

@ -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:

View File

@ -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

View File

@ -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",

View File

@ -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(