1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-20 10:28:45 +00:00

Store and persist OS upgrade map to fix update path evaluation (#6152)

* Store and persist OS upgrade map to fix update path evaluation

The existing logic calculated OS upgrade paths inline during fetch_data,
which will not get reevaluted when the current OS is unsupported
(JobCondition.OS_SUPPORTED). E.g. after updating from 11.4 to 11.5, the
system wouldn't offer the next available update (15.2) because the
upgrade path calculation relied on fresh data from the blocked fetch
operation.

Changes:
- Add ATTR_HASSOS_UPGRADE constant and schema validation
- Store hassos-upgrade map from version JSON in updater data
- Refactor version_hassos property to use stored upgrade map instead of
  inline calculation during fetch_data
- Maintain upgrade path logic: upgrade within major version first, then
  jump to next major version when at the latest in current major
- Add type safety checks for version.major access

This ensures upgrade paths work correctly even when update data refresh
is blocked due to unsupported OS versions, fixing the scenario where
HAOS 11.5 wouldn't show 15.2 as the next available update.

* Update supervisor/updater.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address mypy issue

* Fix pytest

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Stefan Agner
2025-09-04 13:19:31 +02:00
committed by GitHub
parent 236c39cbb0
commit c277f3cad6
4 changed files with 44 additions and 14 deletions

View File

@@ -202,6 +202,7 @@ ATTR_HASSIO_API = "hassio_api"
ATTR_HASSIO_ROLE = "hassio_role" ATTR_HASSIO_ROLE = "hassio_role"
ATTR_HASSOS = "hassos" ATTR_HASSOS = "hassos"
ATTR_HASSOS_UNRESTRICTED = "hassos_unrestricted" ATTR_HASSOS_UNRESTRICTED = "hassos_unrestricted"
ATTR_HASSOS_UPGRADE = "hassos_upgrade"
ATTR_HEALTHY = "healthy" ATTR_HEALTHY = "healthy"
ATTR_HEARTBEAT_LED = "heartbeat_led" ATTR_HEARTBEAT_LED = "heartbeat_led"
ATTR_HOMEASSISTANT = "homeassistant" ATTR_HOMEASSISTANT = "homeassistant"

View File

@@ -17,8 +17,8 @@ from .const import (
ATTR_CHANNEL, ATTR_CHANNEL,
ATTR_CLI, ATTR_CLI,
ATTR_DNS, ATTR_DNS,
ATTR_HASSOS,
ATTR_HASSOS_UNRESTRICTED, ATTR_HASSOS_UNRESTRICTED,
ATTR_HASSOS_UPGRADE,
ATTR_HOMEASSISTANT, ATTR_HOMEASSISTANT,
ATTR_IMAGE, ATTR_IMAGE,
ATTR_MULTICAST, ATTR_MULTICAST,
@@ -93,13 +93,46 @@ class Updater(FileConfiguration, CoreSysAttributes):
@property @property
def version_hassos(self) -> AwesomeVersion | None: def version_hassos(self) -> AwesomeVersion | None:
"""Return latest version of HassOS.""" """Return latest version of HassOS."""
return self._data.get(ATTR_HASSOS) upgrade_map = self.upgrade_map_hassos
unrestricted = self.version_hassos_unrestricted
# If no upgrade map exists, fall back to unrestricted version
if not upgrade_map:
return unrestricted
# If we have no unrestricted version or no current OS version, return unrestricted
if (
not unrestricted
or not self.sys_os.version
or self.sys_os.version.major is None
):
return unrestricted
current_major = str(self.sys_os.version.major)
# Check if there's an upgrade path for current major version
if current_major in upgrade_map:
last_in_major = AwesomeVersion(upgrade_map[current_major])
# If we're not at the last version in our major, upgrade to that first
if self.sys_os.version != last_in_major:
return last_in_major
# If we are at the last version in our major, check for next major
next_major = str(int(self.sys_os.version.major) + 1)
if next_major in upgrade_map:
return AwesomeVersion(upgrade_map[next_major])
# Fall back to unrestricted version
return unrestricted
@property @property
def version_hassos_unrestricted(self) -> AwesomeVersion | None: def version_hassos_unrestricted(self) -> AwesomeVersion | None:
"""Return latest version of HassOS ignoring upgrade restrictions.""" """Return latest version of HassOS ignoring upgrade restrictions."""
return self._data.get(ATTR_HASSOS_UNRESTRICTED) return self._data.get(ATTR_HASSOS_UNRESTRICTED)
@property
def upgrade_map_hassos(self) -> dict[str, str] | None:
"""Return HassOS upgrade map."""
return self._data.get(ATTR_HASSOS_UPGRADE)
@property @property
def version_cli(self) -> AwesomeVersion | None: def version_cli(self) -> AwesomeVersion | None:
"""Return latest version of CLI.""" """Return latest version of CLI."""
@@ -291,18 +324,10 @@ class Updater(FileConfiguration, CoreSysAttributes):
if self.sys_os.board: if self.sys_os.board:
self._data[ATTR_OTA] = data["ota"] self._data[ATTR_OTA] = data["ota"]
if version := data["hassos"].get(self.sys_os.board): if version := data["hassos"].get(self.sys_os.board):
self._data[ATTR_HASSOS_UNRESTRICTED] = version self._data[ATTR_HASSOS_UNRESTRICTED] = AwesomeVersion(version)
# Store the upgrade map for persistent access
self._data[ATTR_HASSOS_UPGRADE] = data.get("hassos-upgrade", {})
events.append("os") events.append("os")
upgrade_map = data.get("hassos-upgrade", {})
if last_in_major := upgrade_map.get(str(self.sys_os.version.major)):
if self.sys_os.version != AwesomeVersion(last_in_major):
version = last_in_major
elif last_in_next_major := upgrade_map.get(
str(int(self.sys_os.version.major) + 1)
):
version = last_in_next_major
self._data[ATTR_HASSOS] = AwesomeVersion(version)
else: else:
_LOGGER.warning( _LOGGER.warning(
"Board '%s' not found in version file. No OS updates.", "Board '%s' not found in version file. No OS updates.",

View File

@@ -24,6 +24,7 @@ from .const import (
ATTR_FORCE_SECURITY, ATTR_FORCE_SECURITY,
ATTR_HASSOS, ATTR_HASSOS,
ATTR_HASSOS_UNRESTRICTED, ATTR_HASSOS_UNRESTRICTED,
ATTR_HASSOS_UPGRADE,
ATTR_HOMEASSISTANT, ATTR_HOMEASSISTANT,
ATTR_ID, ATTR_ID,
ATTR_IMAGE, ATTR_IMAGE,
@@ -129,6 +130,9 @@ SCHEMA_UPDATER_CONFIG = vol.Schema(
vol.Optional(ATTR_SUPERVISOR): version_tag, vol.Optional(ATTR_SUPERVISOR): version_tag,
vol.Optional(ATTR_HASSOS): version_tag, vol.Optional(ATTR_HASSOS): version_tag,
vol.Optional(ATTR_HASSOS_UNRESTRICTED): version_tag, vol.Optional(ATTR_HASSOS_UNRESTRICTED): version_tag,
vol.Optional(ATTR_HASSOS_UPGRADE): vol.Schema(
{vol.Extra: version_tag}, extra=vol.ALLOW_EXTRA
),
vol.Optional(ATTR_CLI): version_tag, vol.Optional(ATTR_CLI): version_tag,
vol.Optional(ATTR_DNS): version_tag, vol.Optional(ATTR_DNS): version_tag,
vol.Optional(ATTR_AUDIO): version_tag, vol.Optional(ATTR_AUDIO): version_tag,

View File

@@ -48,7 +48,7 @@ async def test_api_available_updates(
"version_latest": "9.2.1", "version_latest": "9.2.1",
} }
coresys.updater._data["hassos"] = "321" coresys.updater._data["hassos_unrestricted"] = "321"
coresys.os._version = "123" coresys.os._version = "123"
updates = await available_updates() updates = await available_updates()
assert len(updates) == 2 assert len(updates) == 2