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:
@@ -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"
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user