Compare commits

...

1 Commits

Author SHA1 Message Date
Mike Degatano 696f0d15f2 Ensure Supervisor is up to date before hassio setup completes
During onboarding, Supervisor may be out of date relative to the core
version being installed. Attempt a Supervisor update at the start of
async_setup_entry when not yet onboarded:

- SupervisorBadRequestError: no update available, proceed normally.
- No exception: update was triggered, raise ConfigEntryNotReady to
  retry once Supervisor has restarted with the new version.
- Any other SupervisorError: unexpected failure communicating with
  Supervisor, raise ConfigEntryNotReady to retry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-20 20:10:04 +00:00
3 changed files with 89 additions and 2 deletions
+24 -1
View File
@@ -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:
@@ -55,6 +55,9 @@
},
"supervisor_not_connected": {
"message": "Not connected with the supervisor / system too busy"
},
"supervisor_update_pending": {
"message": "Supervisor update in progress, will retry when complete"
}
},
"issues": {
+62 -1
View File
@@ -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):