Trigger auto-update through Core WebSocket call (#5896)

* Trigger auto-update through Core WebSocket call

Instead of auto-updating add-ons on Supervisor side trigger an update
through Core via a WebSocket command. This makes sure that the backup
is categorized correctly and all backup features like retention are
applied.

* Add pytest

* Fix pytest

* Fix pytest

* Fix pytest

* Fix pytest

* Fix pytest cleaner

* Set timestamp of add-on far into the past
This commit is contained in:
Stefan Agner 2025-05-20 15:18:37 +02:00 committed by GitHub
parent b5a7e521ae
commit 6e6fe5ba39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 15 deletions

View File

@ -32,6 +32,7 @@ class WSType(StrEnum):
SUPERVISOR_EVENT = "supervisor/event" SUPERVISOR_EVENT = "supervisor/event"
BACKUP_START = "backup/start" BACKUP_START = "backup/start"
BACKUP_END = "backup/end" BACKUP_END = "backup/end"
HASSIO_UPDATE_ADDON = "hassio/update/addon"
class WSEvent(StrEnum): class WSEvent(StrEnum):

View File

@ -1,13 +1,11 @@
"""A collection of tasks.""" """A collection of tasks."""
import asyncio
from collections.abc import Awaitable
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from ..addons.const import ADDON_UPDATE_CONDITIONS from ..addons.const import ADDON_UPDATE_CONDITIONS
from ..backups.const import LOCATION_CLOUD_BACKUP from ..backups.const import LOCATION_CLOUD_BACKUP
from ..const import AddonState from ..const import ATTR_TYPE, AddonState
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import ( from ..exceptions import (
AddonsError, AddonsError,
@ -15,7 +13,7 @@ from ..exceptions import (
HomeAssistantError, HomeAssistantError,
ObserverError, ObserverError,
) )
from ..homeassistant.const import LANDINGPAGE from ..homeassistant.const import LANDINGPAGE, WSType
from ..jobs.decorator import Job, JobCondition, JobExecutionLimit from ..jobs.decorator import Job, JobCondition, JobExecutionLimit
from ..plugins.const import PLUGIN_UPDATE_CONDITIONS from ..plugins.const import PLUGIN_UPDATE_CONDITIONS
from ..utils.dt import utcnow from ..utils.dt import utcnow
@ -106,7 +104,6 @@ class Tasks(CoreSysAttributes):
) )
async def _update_addons(self): async def _update_addons(self):
"""Check if an update is available for an Add-on and update it.""" """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: for addon in self.sys_addons.all:
if not addon.is_installed or not addon.auto_update: if not addon.is_installed or not addon.auto_update:
continue continue
@ -131,16 +128,21 @@ class Tasks(CoreSysAttributes):
) )
continue continue
# Run Add-on update sequential
# avoid issue on slow IO
_LOGGER.info("Add-on auto update process %s", addon.slug) _LOGGER.info("Add-on auto update process %s", addon.slug)
try: # Call Home Assistant Core to update add-on to make sure that backups
if start_task := await self.sys_addons.update(addon.slug, backup=True): # get created through the Home Assistant Core API (categorized correctly).
start_tasks.append(start_task) # Ultimately auto updates should be handled by Home Assistant Core itself
except AddonsError: # through a update entity feature.
_LOGGER.error("Can't auto update Add-on %s", addon.slug) message = {
ATTR_TYPE: WSType.HASSIO_UPDATE_ADDON,
await asyncio.gather(*start_tasks) "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( @Job(
name="tasks_update_supervisor", name="tasks_update_supervisor",

View File

@ -8,7 +8,8 @@ from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
import pytest 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.coresys import CoreSys
from supervisor.exceptions import HomeAssistantError from supervisor.exceptions import HomeAssistantError
from supervisor.homeassistant.api import HomeAssistantAPI from supervisor.homeassistant.api import HomeAssistantAPI
@ -230,3 +231,37 @@ async def test_core_backup_cleanup(
assert not coresys.backups.get("7fed74c8") assert not coresys.backups.get("7fed74c8")
assert new_tar.exists() assert new_tar.exists()
assert not old_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,
}
)