diff --git a/supervisor/homeassistant/const.py b/supervisor/homeassistant/const.py index 192433547..9e3e5bade 100644 --- a/supervisor/homeassistant/const.py +++ b/supervisor/homeassistant/const.py @@ -32,6 +32,7 @@ class WSType(StrEnum): SUPERVISOR_EVENT = "supervisor/event" BACKUP_START = "backup/start" BACKUP_END = "backup/end" + HASSIO_UPDATE_ADDON = "hassio/update/addon" class WSEvent(StrEnum): diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 4cc1c4e30..602526027 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -1,13 +1,11 @@ """A collection of tasks.""" -import asyncio -from collections.abc import Awaitable from datetime import datetime, timedelta import logging from ..addons.const import ADDON_UPDATE_CONDITIONS from ..backups.const import LOCATION_CLOUD_BACKUP -from ..const import AddonState +from ..const import ATTR_TYPE, AddonState from ..coresys import CoreSysAttributes from ..exceptions import ( AddonsError, @@ -15,7 +13,7 @@ from ..exceptions import ( HomeAssistantError, ObserverError, ) -from ..homeassistant.const import LANDINGPAGE +from ..homeassistant.const import LANDINGPAGE, WSType from ..jobs.decorator import Job, JobCondition, JobExecutionLimit from ..plugins.const import PLUGIN_UPDATE_CONDITIONS from ..utils.dt import utcnow @@ -106,7 +104,6 @@ class Tasks(CoreSysAttributes): ) async def _update_addons(self): """Check if an update is available for an Add-on and update it.""" - start_tasks: list[Awaitable[None]] = [] for addon in self.sys_addons.all: if not addon.is_installed or not addon.auto_update: continue @@ -131,16 +128,21 @@ class Tasks(CoreSysAttributes): ) continue - # Run Add-on update sequential - # avoid issue on slow IO _LOGGER.info("Add-on auto update process %s", addon.slug) - try: - if start_task := await self.sys_addons.update(addon.slug, backup=True): - start_tasks.append(start_task) - except AddonsError: - _LOGGER.error("Can't auto update Add-on %s", addon.slug) - - await asyncio.gather(*start_tasks) + # Call Home Assistant Core to update add-on to make sure that backups + # get created through the Home Assistant Core API (categorized correctly). + # Ultimately auto updates should be handled by Home Assistant Core itself + # through a update entity feature. + message = { + ATTR_TYPE: WSType.HASSIO_UPDATE_ADDON, + "addon": addon.slug, + "backup": True, + } + _LOGGER.debug( + "Sending update add-on WebSocket command to Home Assistant Core: %s", + message, + ) + await self.sys_homeassistant.websocket.async_send_command(message) @Job( name="tasks_update_supervisor", diff --git a/tests/misc/test_tasks.py b/tests/misc/test_tasks.py index 01d189ed8..ff787b9be 100644 --- a/tests/misc/test_tasks.py +++ b/tests/misc/test_tasks.py @@ -8,7 +8,8 @@ from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch from awesomeversion import AwesomeVersion import pytest -from supervisor.const import CoreState +from supervisor.addons.addon import Addon +from supervisor.const import ATTR_VERSION_TIMESTAMP, CoreState from supervisor.coresys import CoreSys from supervisor.exceptions import HomeAssistantError from supervisor.homeassistant.api import HomeAssistantAPI @@ -230,3 +231,37 @@ async def test_core_backup_cleanup( assert not coresys.backups.get("7fed74c8") assert new_tar.exists() assert not old_tar.exists() + + +async def test_update_addons_auto_update_success( + tasks: Tasks, + coresys: CoreSys, + tmp_supervisor_data: Path, + ha_ws_client: AsyncMock, + install_addon_example: Addon, +): + """Test that an eligible add-on is auto-updated via websocket command.""" + await coresys.core.set_state(CoreState.RUNNING) + + # Set up the add-on as eligible for auto-update + install_addon_example.auto_update = True + install_addon_example.data_store[ATTR_VERSION_TIMESTAMP] = 0 + with patch.object( + Addon, "version", new=PropertyMock(return_value=AwesomeVersion("1.0")) + ): + assert install_addon_example.need_update is True + assert install_addon_example.auto_update_available is True + + # Make sure all job events from installing the add-on are cleared + ha_ws_client.async_send_command.reset_mock() + + # pylint: disable-next=protected-access + await tasks._update_addons() + + ha_ws_client.async_send_command.assert_any_call( + { + "type": "hassio/update/addon", + "addon": install_addon_example.slug, + "backup": True, + } + )