diff --git a/requirements.txt b/requirements.txt index c42403b14b6..31d6e977be4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ jinja2==3.1.6 log-rate-limit==1.4.2 orjson==3.10.16 pulsectl==24.12.0 +python-slugify==8.0.4 pyudev==0.24.3 PyYAML==6.0.2 requests==2.32.3 diff --git a/supervisor/homeassistant/const.py b/supervisor/homeassistant/const.py index 19243354742..ce2ddfc6a50 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" + CALL_SERVICE = "call_service" class WSEvent(StrEnum): diff --git a/supervisor/homeassistant/websocket.py b/supervisor/homeassistant/websocket.py index 75d2f352794..78656a2d7b7 100644 --- a/supervisor/homeassistant/websocket.py +++ b/supervisor/homeassistant/websocket.py @@ -287,6 +287,24 @@ async def async_send_command(self, message: dict[str, Any]) -> T | None: raise return None + async def async_call_service( + self, + domain: str, + service: str, + return_response: bool = False, + service_data: dict[str, Any] | None = None, + ) -> None: + """Call a Home Assistant Core service (action).""" + message = { + ATTR_TYPE: WSType.CALL_SERVICE, + "domain": domain, + "service": service, + "return_response": return_response, + "service_data": service_data or {}, + } + _LOGGER.debug("Sending service call: %s", message) + return await self.async_send_command(message) + def send_message(self, message: dict[str, Any]) -> None: """Send a supervisor/event message.""" if self.sys_core.state in CLOSING_STATES: diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 4cc1c4e30f2..b538f05b045 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -1,10 +1,10 @@ """A collection of tasks.""" -import asyncio -from collections.abc import Awaitable from datetime import datetime, timedelta import logging +import slugify + from ..addons.const import ADDON_UPDATE_CONDITIONS from ..backups.const import LOCATION_CLOUD_BACKUP from ..const import AddonState @@ -106,7 +106,6 @@ async def load(self): ) 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 +130,23 @@ async def _update_addons(self): ) 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 this should be handled by Home Assistant Core itself through + # the update entity. + entity_id = f"update.{slugify.slugify(addon.name, separator='_')}_update" + result = await self.sys_homeassistant.websocket.async_call_service( + domain="update", + service="install", + service_data={ + "entity_id": entity_id, + "backup": True, + }, + ) + _LOGGER.info( + "Add-on auto update for %s processed, result %s", addon.slug, result + ) @Job( name="tasks_update_supervisor",