From 46f2ad9eb29651ddcb62cfa8ac8f729dfbc0bc91 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 1 Jun 2026 04:34:16 -0400 Subject: [PATCH] During onboarding, ensure Supervisor is up to date during hassio setup (#171129) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- homeassistant/components/hassio/__init__.py | 25 +++++++- homeassistant/components/hassio/strings.json | 3 + tests/components/hassio/test_init.py | 63 +++++++++++++++++++- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8c06abaf7ab4..b65bea04e2a6 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -8,7 +8,7 @@ import os import struct from typing import Any -from aiohasupervisor import SupervisorError +from aiohasupervisor import SupervisorBadRequestError, SupervisorError from aiohasupervisor.models import ( GreenOptions, HomeAssistantOptions, @@ -25,6 +25,7 @@ from homeassistant.components.http import ( CONF_SERVER_PORT, CONF_SSL_CERTIFICATE, ) +from homeassistant.components.onboarding import async_is_onboarded from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import ( EVENT_CORE_CONFIG_UPDATE, @@ -301,6 +302,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: translation_key="supervisor_not_connected", ) from err + # During onboarding, Supervisor may be out of date. Attempt an update now + # so that core loads against an up-to-date Supervisor. A + # SupervisorBadRequestError means there is no update available, proceed + # normally. No exception means an update was triggered and we must wait for + # it to complete. Any other SupervisorError means something unexpected went + # wrong and we cannot proceed right now. + if not async_is_onboarded(hass): + try: + await supervisor_client.supervisor.update() + except SupervisorBadRequestError: + pass # No update available, proceed normally. + except SupervisorError as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="supervisor_not_connected", + ) from err + else: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="supervisor_update_pending", + ) + # Get or create a refresh token for the Supervisor user user = hass.data[DATA_HASSIO_SUPERVISOR_USER] if user.refresh_tokens: diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 6d20743cc574..533bf4d0a91f 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -55,6 +55,9 @@ }, "supervisor_not_connected": { "message": "Not connected with the supervisor / system too busy" + }, + "supervisor_update_pending": { + "message": "Supervisor was out-of-date during onboarding. Update triggered, will retry when complete" } }, "issues": { diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f32d8173ddd1..e6f2dd34a4bb 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,7 +8,7 @@ from typing import Any from unittest.mock import ANY, AsyncMock, Mock, call, patch from uuid import uuid4 -from aiohasupervisor import SupervisorError +from aiohasupervisor import SupervisorBadRequestError, SupervisorError from aiohasupervisor.models import ( AddonsStats, AddonStage, @@ -191,6 +191,67 @@ async def test_setup_api_ping_fails( assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_setup_onboarding_supervisor_update( + hass: HomeAssistant, + supervisor_client: AsyncMock, +) -> None: + """Test that during onboarding, supervisor.update() success triggers retry.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch("homeassistant.components.hassio.async_is_onboarded", return_value=False), + ): + result = await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() + + assert result + assert is_hassio(hass) + entry = hass.config_entries.async_entries("hassio")[0] + assert entry.state is ConfigEntryState.SETUP_RETRY + supervisor_client.supervisor.update.assert_called_once() + + +async def test_setup_onboarding_supervisor_no_update( + hass: HomeAssistant, + supervisor_client: AsyncMock, +) -> None: + """Test that during onboarding, SupervisorBadRequestError means no update needed.""" + supervisor_client.supervisor.update.side_effect = SupervisorBadRequestError + + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch("homeassistant.components.hassio.async_is_onboarded", return_value=False), + ): + result = await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() + + assert result + assert is_hassio(hass) + entry = hass.config_entries.async_entries("hassio")[0] + assert entry.state is ConfigEntryState.LOADED + supervisor_client.supervisor.update.assert_called_once() + + +async def test_setup_onboarding_supervisor_update_error( + hass: HomeAssistant, + supervisor_client: AsyncMock, +) -> None: + """Test that during onboarding, an unknown SupervisorError causes retry.""" + supervisor_client.supervisor.update.side_effect = SupervisorError + + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch("homeassistant.components.hassio.async_is_onboarded", return_value=False), + ): + result = await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() + + assert result + assert is_hassio(hass) + entry = hass.config_entries.async_entries("hassio")[0] + assert entry.state is ConfigEntryState.SETUP_RETRY + supervisor_client.supervisor.update.assert_called_once() + + async def test_setup_app_panel(hass: HomeAssistant) -> None: """Test app panel is registered.""" with patch.dict(os.environ, MOCK_ENVIRON):