diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 56e29f7902b..eb2e9887d0e 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -10,7 +10,7 @@ from ..coresys import CoreSysAttributes from ..exceptions import AddonsError, HomeAssistantError, ObserverError from ..homeassistant.const import LANDINGPAGE -from ..jobs.decorator import Job, JobCondition +from ..jobs.decorator import Job, JobCondition, JobExecutionLimit from ..plugins.const import PLUGIN_UPDATE_CONDITIONS from ..utils.dt import utcnow from ..utils.sentry import capture_exception @@ -136,6 +136,7 @@ async def _update_addons(self): JobCondition.INTERNET_HOST, JobCondition.RUNNING, ], + limit=JobExecutionLimit.ONCE, ) async def _update_supervisor(self): """Check and run update of Supervisor Supervisor.""" @@ -333,3 +334,12 @@ async def _watchdog_addon_application(self): async def _reload_store(self) -> None: """Reload store and check for addon updates.""" await self.sys_store.reload() + + @Job(name="tasks_reload_updater") + async def _reload_updater(self) -> None: + """Check for new versions of Home Assistant, Supervisor, OS, etc.""" + await self.sys_updater.reload() + + # If there's a new version of supervisor, start update immediately + if self.sys_supervisor.need_update: + await self._update_supervisor() diff --git a/tests/fixtures/version_stable.json b/tests/fixtures/version_stable.json new file mode 100644 index 00000000000..460f2126178 --- /dev/null +++ b/tests/fixtures/version_stable.json @@ -0,0 +1,85 @@ +{ + "channel": "stable", + "supervisor": "2024.10.0", + "homeassistant": { + "default": "2024.10.4", + "qemux86": "2024.10.4", + "qemux86-64": "2024.10.4", + "qemuarm": "2024.10.4", + "qemuarm-64": "2024.10.4", + "generic-x86-64": "2024.10.4", + "intel-nuc": "2024.10.4", + "khadas-vim3": "2024.10.4", + "raspberrypi": "2024.10.4", + "raspberrypi2": "2024.10.4", + "raspberrypi3": "2024.10.4", + "raspberrypi3-64": "2024.10.4", + "raspberrypi4": "2024.10.4", + "raspberrypi4-64": "2024.10.4", + "raspberrypi5-64": "2024.10.4", + "yellow": "2024.10.4", + "green": "2024.10.4", + "tinker": "2024.10.4", + "odroid-c2": "2024.10.4", + "odroid-c4": "2024.10.4", + "odroid-m1": "2024.10.4", + "odroid-n2": "2024.10.4", + "odroid-xu": "2024.10.4" + }, + "hassos": { + "ova": "13.2", + "rpi2": "13.2", + "rpi3": "13.2", + "rpi3-64": "13.2", + "rpi4": "13.2", + "rpi4-64": "13.2", + "rpi5-64": "13.2", + "yellow": "13.2", + "green": "13.2", + "tinker": "13.2", + "odroid-c2": "13.2", + "odroid-c4": "13.2", + "odroid-m1": "13.2", + "odroid-m1s": "13.2", + "odroid-n2": "13.2", + "odroid-xu4": "13.2", + "generic-x86-64": "13.2", + "generic-aarch64": "13.2", + "khadas-vim3": "13.2" + }, + "hassos-upgrade": { + "11": "11.5", + "10": "10.5", + "9": "9.5", + "8": "8.5", + "7": "7.6", + "6": "6.6", + "5": "5.13", + "4": "4.20", + "3": "3.13" + }, + "ota": "https://os-artifacts.home-assistant.io/{version}/{os_name}_{board}-{version}.raucb", + "cli": "2024.09.0", + "dns": "2024.04.0", + "audio": "2023.12.0", + "multicast": "2024.03.0", + "observer": "2023.06.0", + "image": { + "core": "homeassistant/{machine}-homeassistant", + "supervisor": "homeassistant/{arch}-hassio-supervisor", + "cli": "homeassistant/{arch}-hassio-cli", + "audio": "homeassistant/{arch}-hassio-audio", + "dns": "homeassistant/{arch}-hassio-dns", + "observer": "homeassistant/{arch}-hassio-observer", + "multicast": "homeassistant/{arch}-hassio-multicast" + }, + "images": { + "core": "ghcr.io/home-assistant/{machine}-homeassistant", + "supervisor": "ghcr.io/home-assistant/{arch}-hassio-supervisor", + "cli": "ghcr.io/home-assistant/{arch}-hassio-cli", + "audio": "ghcr.io/home-assistant/{arch}-hassio-audio", + "dns": "ghcr.io/home-assistant/{arch}-hassio-dns", + "observer": "ghcr.io/home-assistant/{arch}-hassio-observer", + "multicast": "ghcr.io/home-assistant/{arch}-hassio-multicast" + } +} diff --git a/tests/misc/test_tasks.py b/tests/misc/test_tasks.py index 795ea994802..0c98bc79cc3 100644 --- a/tests/misc/test_tasks.py +++ b/tests/misc/test_tasks.py @@ -1,22 +1,30 @@ """Test scheduled tasks.""" -from unittest.mock import MagicMock, Mock, patch +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager +from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch from awesomeversion import AwesomeVersion import pytest +from supervisor.const import CoreState from supervisor.coresys import CoreSys from supervisor.exceptions import HomeAssistantError from supervisor.homeassistant.api import HomeAssistantAPI from supervisor.homeassistant.const import LANDINGPAGE from supervisor.homeassistant.core import HomeAssistantCore from supervisor.misc.tasks import Tasks +from supervisor.supervisor import Supervisor + +from tests.common import load_fixture # pylint: disable=protected-access @pytest.fixture(name="tasks") -async def fixture_tasks(coresys: CoreSys, container: MagicMock) -> Tasks: +async def fixture_tasks( + coresys: CoreSys, container: MagicMock +) -> AsyncGenerator[Tasks]: """Return task manager.""" coresys.homeassistant.watchdog = True coresys.homeassistant.version = AwesomeVersion("2023.12.0") @@ -159,3 +167,44 @@ async def test_watchdog_homeassistant_api_reanimation_limit( assert not caplog.text restart.assert_not_called() rebuild.assert_not_called() + + +async def test_reload_updater_triggers_supervisor_update( + tasks: Tasks, coresys: CoreSys +): + """Test an updater reload triggers a supervisor update if there is one.""" + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + coresys.core.state = CoreState.RUNNING + coresys.security.content_trust = False + + version_data = load_fixture("version_stable.json") + version_resp = AsyncMock() + version_resp.status = 200 + version_resp.read.return_value = version_data + + @asynccontextmanager + async def mock_get_for_version(*args, **kwargs) -> AsyncGenerator[AsyncMock]: + """Mock get call for version information.""" + yield version_resp + + with ( + patch("supervisor.coresys.aiohttp.ClientSession.get", new=mock_get_for_version), + patch.object( + Supervisor, + "version", + new=PropertyMock(return_value=AwesomeVersion("2024.10.0")), + ), + patch.object(Supervisor, "update") as update, + ): + # Set supervisor's version intially + await coresys.updater.reload() + assert coresys.supervisor.latest_version == AwesomeVersion("2024.10.0") + + # No change in version means no update + await tasks._reload_updater() + update.assert_not_called() + + # Version change causes an update + version_resp.read.return_value = version_data.replace("2024.10.0", "2024.10.1") + await tasks._reload_updater() + update.assert_called_once()