Fix add-on ingress panels missing after Supervisor timeout or restart (#179025)

This commit is contained in:
Mike Degatano
2026-08-21 11:47:00 +02:00
committed by GitHub
parent 1e89370971
commit 59f40da229
5 changed files with 317 additions and 130 deletions
+2 -1
View File
@@ -49,7 +49,7 @@ from . import ( # noqa: F401
update,
)
from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState
from .addon_panel import async_setup_addon_panel
from .addon_panel import async_setup_addon_panel, async_setup_addon_panel_coordinator
from .auth import async_setup_auth_view
from .config import HassioConfigStore, StoredHassioConfig
from .config_entry import async_get_hassio_entry
@@ -426,6 +426,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = HassioMainDataUpdateCoordinator(hass, entry, dev_reg)
await coordinator.async_config_entry_first_refresh()
hass.data[MAIN_COORDINATOR] = coordinator
entry.async_on_unload(async_setup_addon_panel_coordinator(hass, coordinator))
jobs_coordinator = SupervisorJobsCoordinator(hass, entry)
await jobs_coordinator.async_config_entry_first_refresh()
+62 -30
View File
@@ -9,33 +9,52 @@ from aiohttp import web
from homeassistant.components import frontend
from homeassistant.components.http import HomeAssistantView, require_admin
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import Event, HomeAssistant
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from .const import MAIN_COORDINATOR
from .coordinator import HassioMainDataUpdateCoordinator
from .handler import get_supervisor_client
_LOGGER = logging.getLogger(__name__)
def async_setup_addon_panel(hass: HomeAssistant) -> None:
"""Add-on Ingress Panel setup."""
hassio_addon_panel = HassIOAddonPanel(hass)
hass.http.register_view(hassio_addon_panel)
"""Register the add-on panel push API view."""
hass.http.register_view(HassIOAddonPanel(hass))
# Handle existing panels on startup
async def _async_panel_start_handler(event: Event) -> None:
"""Process all existing panels on startup."""
# Check if there are panels to register
if not (panels := await hassio_addon_panel.get_panels()):
return
# Register available panels
for addon, data in panels.items():
if not data.enable:
continue
_register_panel(hass, addon, data)
@callback
def async_setup_addon_panel_coordinator(
hass: HomeAssistant, coordinator: HassioMainDataUpdateCoordinator
) -> CALLBACK_TYPE:
"""Reconcile add-on panels registered with the frontend against coordinator data.
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_panel_start_handler)
Registers the panels present after the coordinator's first refresh, then keeps
the frontend in sync with coordinator.data.panels on every following update:
periodic refreshes, a refresh triggered by a Supervisor restart, and a post/
delete pushed by Supervisor and cached via coordinator.async_push_panel /
coordinator.async_push_panel_removal.
Returns a function that unsubscribes from the coordinator.
"""
registered: set[str] = set()
@callback
def _async_reconcile_panels() -> None:
"""Register or remove panels to match the coordinator's cached data."""
panels = coordinator.data.panels
wanted = {addon for addon, panel in panels.items() if panel.enable}
for addon in wanted - registered:
_register_panel(hass, addon, panels[addon])
for addon in registered - wanted:
frontend.async_remove_panel(hass, addon, warn_if_unknown=False)
registered.clear()
registered.update(wanted)
_async_reconcile_panels()
return coordinator.async_add_listener(_async_reconcile_panels)
class HassIOAddonPanel(HomeAssistantView):
@@ -52,34 +71,46 @@ class HassIOAddonPanel(HomeAssistantView):
@require_admin
async def post(self, request: web.Request, addon: str) -> web.Response:
"""Handle new add-on panel requests."""
panels = await self.get_panels()
# Supervisor calls this endpoint because an add-on's panel state just
# changed, so fetch it fresh instead of relying on the coordinator's
# cache, which may still hold the value from before this change.
try:
panels = await self.client.ingress.panels()
except SupervisorError as err:
_LOGGER.error("Can't read panel info: %s", err)
return web.Response(status=HTTPStatus.BAD_REQUEST)
# Panel exists for add-on slug
if addon not in panels or not panels[addon].enable:
_LOGGER.error("Panel is not enabled for %s", addon)
return web.Response(status=HTTPStatus.BAD_REQUEST)
# Register panel
_register_panel(self.hass, addon, panels[addon])
if (coordinator := self.hass.data.get(MAIN_COORDINATOR)) is not None:
# Update the cache; the coordinator listener registers it with the frontend.
coordinator.async_push_panel(addon, panels[addon])
else:
_register_panel(self.hass, addon, panels[addon])
return web.Response()
@require_admin
async def delete(self, request: web.Request, addon: str) -> web.Response:
"""Handle remove add-on panel requests."""
frontend.async_remove_panel(self.hass, addon)
if (coordinator := self.hass.data.get(MAIN_COORDINATOR)) is not None:
# Update the cache; the coordinator listener removes it from the frontend.
coordinator.async_push_panel_removal(addon)
else:
frontend.async_remove_panel(self.hass, addon, warn_if_unknown=False)
return web.Response()
async def get_panels(self) -> dict[str, IngressPanel]:
"""Return panels add-on info data."""
try:
return await self.client.ingress.panels()
except SupervisorError as err:
_LOGGER.error("Can't read panel info: %s", err)
return {}
def _register_panel(hass: HomeAssistant, addon: str, data: IngressPanel) -> None:
"""Helper to register the panel.
def _register_panel(hass: HomeAssistant, addon: str, data: IngressPanel):
"""Helper to register the panel."""
Uses update=True so this is idempotent: a config entry reload can run this
for a panel the frontend still has registered from before the reload, and
the push API's early-startup fallback can register one before the
coordinator's own reconciliation runs for the first time.
"""
frontend.async_register_built_in_panel(
hass,
"app",
@@ -88,4 +119,5 @@ def _register_panel(hass: HomeAssistant, addon: str, data: IngressPanel):
sidebar_icon=data.icon,
require_admin=data.admin,
config={"addon": addon},
update=True,
)
+27 -1
View File
@@ -16,6 +16,7 @@ from aiohasupervisor.models import (
HomeAssistantInfo,
HomeAssistantStats,
HostInfo,
IngressPanel,
InstalledAddon,
InstalledAddonComplete,
Issue as SupervisorIssue,
@@ -777,6 +778,7 @@ class HassioMainData:
host: HostInfo
mounts: dict[str, CIFSMountResponse | NFSMountResponse]
os: OSInfo | None
panels: dict[str, IngressPanel]
def to_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the data."""
@@ -786,6 +788,7 @@ class HassioMainData:
"host": self.host.to_dict(),
"mounts": {name: mount.to_dict() for name, mount in self.mounts.items()},
"os": self.os.to_dict() if self.os is not None else None,
"panels": {slug: panel.to_dict() for slug, panel in self.panels.items()},
}
@@ -1526,6 +1529,25 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
):
self.config_entry.async_create_task(self.hass, self.async_request_refresh())
@callback
def async_push_panel(self, addon: str, panel: IngressPanel) -> None:
"""Apply a Supervisor panel push to cached data without touching refresh state."""
self.data = replace(self.data, panels={**self.data.panels, addon: panel})
self.async_update_listeners()
@callback
def async_push_panel_removal(self, addon: str) -> None:
"""Apply a Supervisor panel removal push to cached data."""
if addon not in self.data.panels:
return
self.data = replace(
self.data,
panels={
slug: panel for slug, panel in self.data.panels.items() if slug != addon
},
)
self.async_update_listeners()
@override
async def _async_update_data(self) -> HassioMainData:
"""Update data via library."""
@@ -1535,7 +1557,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
try:
# Cast is required here because asyncio.gather only has overloads to
# maintain typing for 6 arguments. It falls back to list[<common parent>]
# after that which is what mypy sees here since we have 7 API calls.
# after that which is what mypy sees here since we have 8 API calls.
(
info,
core_info,
@@ -1544,6 +1566,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
host_info,
store_info,
network_info,
panels_info,
) = cast(
tuple[
RootInfo,
@@ -1553,6 +1576,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
HostInfo,
StoreInfo,
NetworkInfo,
dict[str, IngressPanel],
],
await asyncio.gather(
client.info(),
@@ -1562,6 +1586,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
client.host.info(),
client.store.info(),
client.network.info(),
client.ingress.panels(),
),
)
mounts_info = await client.mounts.info()
@@ -1576,6 +1601,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
host=host_info,
mounts={mount.name: mount for mount in mounts_info.mounts},
os=os_info if self.is_hass_os else None,
panels=panels_info,
)
# Update hass.data for legacy accessor functions
+215 -87
View File
@@ -1,19 +1,26 @@
"""Test add-on panel."""
from datetime import timedelta
from http import HTTPStatus
import os
from unittest.mock import AsyncMock, patch
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import IngressPanel
import pytest
from homeassistant.components.hassio import DOMAIN
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED
from homeassistant.components.hassio.const import (
MAIN_COORDINATOR,
REQUEST_REFRESH_DELAY,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from tests.common import MockUser
from tests.typing import ClientSessionGenerator
from tests.common import MockUser, async_fire_time_changed
from tests.typing import ClientSessionGenerator, WebSocketGenerator
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
@@ -24,10 +31,15 @@ def mock_all(all_setup_requests: None) -> None:
@pytest.mark.usefixtures("supervisor_client")
async def test_hassio_addon_panel_startup(
async def test_hassio_addon_panel_registered_on_setup(
hass: HomeAssistant, ingress_panels: AsyncMock
) -> None:
"""Test startup and panel setup after event."""
"""Test enabled panels are registered as part of config entry setup.
Regression test for https://github.com/home-assistant/supervisor/issues/7015:
registration must not depend on the one-shot EVENT_HOMEASSISTANT_START handler
that used to swallow Supervisor timeouts and never retry.
"""
ingress_panels.return_value = {
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
"test2": IngressPanel(
@@ -35,24 +47,145 @@ async def test_hassio_addon_panel_startup(
),
}
with (
patch(
"homeassistant.components.hassio.addon_panel._register_panel"
) as mock_panel,
patch.dict(os.environ, MOCK_ENVIRON),
):
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
mock_panel.assert_called_once_with(
hass,
"test1",
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
)
@pytest.mark.usefixtures("supervisor_client")
async def test_hassio_addon_panel_registration(
hass: HomeAssistant, ingress_panels: AsyncMock
) -> None:
"""Test panel registration calls frontend.async_register_built_in_panel."""
ingress_panels.return_value = {
"test_addon": IngressPanel(
enable=True, title="Test Addon", icon="mdi:test-tube", admin=True
),
}
with (
patch(
"homeassistant.components.hassio.addon_panel.frontend.async_register_built_in_panel"
) as mock_register,
patch.dict(os.environ, MOCK_ENVIRON),
):
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
mock_register.assert_any_call(
hass,
"app",
frontend_url_path="test_addon",
sidebar_title="Test Addon",
sidebar_icon="mdi:test-tube",
require_admin=True,
config={"addon": "test_addon"},
update=True,
)
async def test_hassio_addon_panel_setup_retries_after_transient_error(
hass: HomeAssistant, ingress_panels: AsyncMock
) -> None:
"""Test a transient Supervisor error fetching panels causes setup to retry.
Regression test for https://github.com/home-assistant/supervisor/issues/7015:
previously a timeout fetching panels at startup was logged and swallowed,
leaving panels missing forever with no retry. Panel data is now fetched as
part of the main coordinator's first refresh, so a transient failure causes
the whole config entry setup to retry until Supervisor is reachable again.
"""
ingress_panels.side_effect = SupervisorError("Timeout connecting to Supervisor")
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
assert result
entry = hass.config_entries.async_entries(DOMAIN)[0]
assert entry.state is ConfigEntryState.SETUP_RETRY
ingress_panels.side_effect = None
ingress_panels.return_value = {
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
}
with patch(
"homeassistant.components.hassio.addon_panel._register_panel",
"homeassistant.components.hassio.addon_panel._register_panel"
) as mock_panel:
with patch.dict(os.environ, MOCK_ENVIRON):
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
mock_panel.assert_called_once_with(
hass,
"test1",
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
)
async def test_hassio_addon_panel_recovers_after_supervisor_restart(
hass: HomeAssistant,
hass_supervisor_ws_client: WebSocketGenerator,
ingress_panels: AsyncMock,
) -> None:
"""Test panels are refreshed when Supervisor reports it has restarted.
Regression test for the "Supervisor restarts while Core keeps running"
scenario: Supervisor fires a supervisor_update/startup:complete event on
every one of its own (re)starts, which the main coordinator already listens
for and uses to trigger a refresh.
"""
ingress_panels.return_value = {}
with (
patch(
"homeassistant.components.hassio.addon_panel._register_panel"
) as mock_panel,
patch.dict(os.environ, MOCK_ENVIRON),
):
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
ingress_panels.assert_not_called()
mock_panel.assert_not_called()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
ingress_panels.return_value = {
"test1": IngressPanel(
enable=True, title="Test", icon="mdi:test", admin=False
),
}
client = await hass_supervisor_ws_client()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "supervisor_update",
"update_key": "supervisor",
"data": {"startup": "complete"},
},
}
)
await client.receive_json()
async_fire_time_changed(
hass, dt_util.utcnow() + timedelta(seconds=REQUEST_REFRESH_DELAY + 1)
)
await hass.async_block_till_done()
ingress_panels.assert_called_once()
assert mock_panel.called
mock_panel.assert_called_with(
mock_panel.assert_called_once_with(
hass,
"test1",
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
@@ -60,10 +193,10 @@ async def test_hassio_addon_panel_startup(
@pytest.mark.usefixtures("supervisor_client")
async def test_hassio_addon_panel_api(
async def test_hassio_addon_panel_api_post(
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
) -> None:
"""Test panel api after event."""
"""Test posting a panel push registers it via the coordinator cache."""
ingress_panels.return_value = {
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
"test2": IngressPanel(
@@ -75,37 +208,77 @@ async def test_hassio_addon_panel_api(
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
hass_client = await hass_client()
with patch(
"homeassistant.components.hassio.addon_panel._register_panel",
"homeassistant.components.hassio.addon_panel._register_panel"
) as mock_panel:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ingress_panels.assert_called_once()
assert mock_panel.called
mock_panel.assert_called_with(
hass,
"test1",
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
)
hass_client = await hass_client()
# Panel is not enabled yet according to Supervisor
resp = await hass_client.post("/api/hassio_push/panel/test2")
assert resp.status == HTTPStatus.BAD_REQUEST
mock_panel.assert_not_called()
# Supervisor enables the panel and pushes the change
ingress_panels.return_value["test2"] = IngressPanel(
enable=True, title="Test 2", icon="mdi:test2", admin=True
)
resp = await hass_client.post("/api/hassio_push/panel/test2")
assert resp.status == HTTPStatus.OK
mock_panel.assert_called_once_with(
hass,
"test2",
IngressPanel(enable=True, title="Test 2", icon="mdi:test2", admin=True),
)
# Posting again for an already-registered, unchanged panel is a no-op
mock_panel.reset_mock()
resp = await hass_client.post("/api/hassio_push/panel/test1")
assert resp.status == HTTPStatus.OK
assert mock_panel.call_count == 2
mock_panel.assert_not_called()
mock_panel.assert_called_with(
@pytest.mark.usefixtures("supervisor_client")
async def test_hassio_addon_panel_api_before_coordinator_ready(
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
) -> None:
"""Test panel push api falls back to a fresh Supervisor call before setup completes.
Other callers besides Supervisor may rely on this API before the config
entry (and its main coordinator) finishes setting up, so it must keep
working via a direct Supervisor call and frontend registration instead of
failing with a 503.
"""
ingress_panels.return_value = {
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
}
with patch.dict(os.environ, MOCK_ENVIRON):
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
hass_client = await hass_client()
# Simulate the main coordinator not being ready yet
del hass.data[MAIN_COORDINATOR]
with patch(
"homeassistant.components.hassio.addon_panel._register_panel"
) as mock_panel:
resp = await hass_client.post("/api/hassio_push/panel/test1")
assert resp.status == HTTPStatus.OK
mock_panel.assert_called_once_with(
hass,
"test1",
IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
)
with patch(
"homeassistant.components.hassio.addon_panel.frontend.async_remove_panel"
) as mock_remove:
resp = await hass_client.delete("/api/hassio_push/panel/test1")
assert resp.status == HTTPStatus.OK
mock_remove.assert_called_once_with(hass, "test1", warn_if_unknown=False)
@pytest.mark.usefixtures("supervisor_client")
async def test_hassio_addon_panel_api_non_admin(
@@ -123,21 +296,12 @@ async def test_hassio_addon_panel_api_non_admin(
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
hass_admin_user.groups = []
hass_client = await hass_client()
with patch(
"homeassistant.components.hassio.addon_panel._register_panel",
"homeassistant.components.hassio.addon_panel._register_panel"
) as mock_panel:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
ingress_panels.assert_called_once()
mock_panel.assert_called_once()
mock_panel.reset_mock()
hass_admin_user.groups = []
hass_client = await hass_client()
# Both should return unauthorized regardless of enabled as the endpoint requires
# admin and the user is not admin
resp = await hass_client.post("/api/hassio_push/panel/test2")
@@ -149,47 +313,11 @@ async def test_hassio_addon_panel_api_non_admin(
mock_panel.assert_not_called()
@pytest.mark.usefixtures("supervisor_client")
async def test_hassio_addon_panel_registration(
hass: HomeAssistant, ingress_panels: AsyncMock
) -> None:
"""Test panel registration calls frontend.async_register_built_in_panel."""
ingress_panels.return_value = {
"test_addon": IngressPanel(
enable=True, title="Test Addon", icon="mdi:test-tube", admin=True
),
}
with patch.dict(os.environ, MOCK_ENVIRON):
await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
with patch(
"homeassistant.components.hassio.addon_panel.frontend.async_register_built_in_panel"
) as mock_register:
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
# Verify that async_register_built_in_panel was called with correct arguments
# for our test addon
mock_register.assert_any_call(
hass,
"app",
frontend_url_path="test_addon",
sidebar_title="Test Addon",
sidebar_icon="mdi:test-tube",
require_admin=True,
config={"addon": "test_addon"},
)
@pytest.mark.usefixtures("supervisor_client")
async def test_hassio_addon_panel_api_delete(
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
) -> None:
"""Test panel api delete."""
"""Test panel api delete removes it via the coordinator cache."""
ingress_panels.return_value = {
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
}
@@ -204,7 +332,7 @@ async def test_hassio_addon_panel_api_delete(
) as mock_remove:
resp = await hass_client.delete("/api/hassio_push/panel/test1")
assert resp.status == HTTPStatus.OK
mock_remove.assert_called_once_with(hass, "test1")
mock_remove.assert_called_once_with(hass, "test1", warn_if_unknown=False)
@pytest.mark.usefixtures("supervisor_client")
+11 -11
View File
@@ -174,7 +174,7 @@ async def test_setup_api_ping(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
assert get_core_info(hass)["version_latest"] == "1.0.0"
assert is_hassio(hass)
@@ -310,7 +310,7 @@ async def test_setup_api_push_api_data(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY)
)
@@ -326,7 +326,7 @@ async def test_setup_api_push_api_data_error(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
assert "Failed to update Home Assistant options in Supervisor: boom" in caplog.text
@@ -347,7 +347,7 @@ async def test_setup_api_push_api_data_server_host(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY)
)
@@ -362,7 +362,7 @@ async def test_setup_api_push_api_data_default(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=80, refresh_token=ANY)
)
@@ -438,7 +438,7 @@ async def test_setup_api_existing_hassio_user(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=80, refresh_token=token.token)
)
@@ -483,7 +483,7 @@ async def test_setup_migrates_legacy_hassio_store_to_config_entry(
assert entry.options[OPTION_ADD_ON_BACKUP_RETAIN_COPIES] == 2
assert entry.options[OPTION_CORE_BACKUP_BEFORE_UPDATE] is True
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
supervisor_client.homeassistant.set_options.assert_called_once_with(
HomeAssistantOptions(ssl=False, port=80, refresh_token=token.token)
)
@@ -548,7 +548,7 @@ async def test_setup_core_push_config(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
supervisor_client.supervisor.set_options.assert_called_once_with(
SupervisorOptions(timezone="testzone")
)
@@ -573,7 +573,7 @@ async def test_setup_core_push_config_error(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
assert "Failed to update Supervisor options: boom" in caplog.text
@@ -589,7 +589,7 @@ async def test_setup_hassio_no_additional_data(
await hass.async_block_till_done()
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
async def test_fail_setup_without_environ_var(hass: HomeAssistant) -> None:
@@ -1320,7 +1320,7 @@ async def test_setup_hardware_integration(
await hass.async_block_till_done(wait_background_tasks=True)
assert result
assert len(supervisor_client.mock_calls) == 16
assert len(supervisor_client.mock_calls) == 17
assert len(mock_setup_entry.mock_calls) == 1