diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index eeeedff00bb..041877e3944 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -37,6 +37,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, discovery_flow, + issue_registry as ir, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.deprecation import ( @@ -51,9 +52,11 @@ from homeassistant.helpers.hassio import ( get_supervisor_ip as _get_supervisor_ip, is_hassio as _is_hassio, ) +from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service_info.hassio import ( HassioServiceInfo as _HassioServiceInfo, ) +from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.async_ import create_eager_task @@ -109,7 +112,7 @@ from .coordinator import ( get_core_info, # noqa: F401 get_core_stats, # noqa: F401 get_host_info, # noqa: F401 - get_info, # noqa: F401 + get_info, get_issues_info, # noqa: F401 get_os_info, get_supervisor_info, # noqa: F401 @@ -168,6 +171,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial" VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$")) +DEPRECATION_URL = ( + "https://www.home-assistant.io/blog/2025/05/22/" + "deprecating-core-and-supervised-installation-methods-and-32-bit-systems/" +) + def valid_addon(value: Any) -> str: """Validate value is a valid addon slug.""" @@ -546,6 +554,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[ADDONS_COORDINATOR] = coordinator + system_info = await async_get_system_info(hass) + + def deprecated_setup_issue() -> None: + os_info = get_os_info(hass) + info = get_info(hass) + if os_info is None or info is None: + return + is_haos = info.get("hassos") is not None + arch = system_info["arch"] + board = os_info.get("board") + supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"} + if is_haos and arch == "armv7" and supported_board: + issue_id = "deprecated_os_" + if board in {"rpi3", "rpi4"}: + issue_id += "aarch64" + elif board in {"tinker", "odroid-xu4", "rpi2"}: + issue_id += "armv7" + ir.async_create_issue( + hass, + "homeassistant", + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_guide": "https://www.home-assistant.io/installation/", + }, + ) + deprecated_architecture = False + if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board): + deprecated_architecture = True + if not is_haos or deprecated_architecture: + issue_id = "deprecated" + if not is_haos: + issue_id += "_method" + if deprecated_architecture: + issue_id += "_architecture" + ir.async_create_issue( + hass, + "homeassistant", + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_type": "OS" if is_haos else "Supervised", + "arch": arch, + }, + ) + listener() + + listener = coordinator.async_add_listener(deprecated_setup_issue) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 5f012c6a054..1433358b568 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import Callable, Coroutine import itertools as it import logging -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -38,7 +38,6 @@ from homeassistant.helpers import ( restore_state, ) from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service import ( async_extract_config_entry_ids, @@ -402,46 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: info = await async_get_system_info(hass) installation_type = info["installation_type"][15:] - deprecated_method = installation_type in { - "Core", - "Supervised", - } - arch = info["arch"] - if arch == "armv7": - if installation_type == "OS": - # Local import to avoid circular dependencies - # We use the import helper because hassio - # may not be loaded yet and we don't want to - # do blocking I/O in the event loop to import it. - if TYPE_CHECKING: - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import hassio - else: - hassio = await async_import_module( - hass, "homeassistant.components.hassio" - ) - os_info = hassio.get_os_info(hass) - assert os_info is not None - issue_id = "deprecated_os_" - board = os_info.get("board") - if board in {"rpi3", "rpi4"}: - issue_id += "aarch64" - elif board in {"tinker", "odroid-xu4", "rpi2"}: - issue_id += "armv7" - ir.async_create_issue( - hass, - DOMAIN, - issue_id, - breaks_in_ha_version="2025.12.0", - learn_more_url=DEPRECATION_URL, - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "installation_guide": "https://www.home-assistant.io/installation/", - }, - ) - elif installation_type == "Container": + if installation_type in {"Core", "Container"}: + deprecated_method = installation_type == "Core" + arch = info["arch"] + if arch == "armv7" and installation_type == "Container": ir.async_create_issue( hass, DOMAIN, @@ -452,29 +415,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: severity=IssueSeverity.WARNING, translation_key="deprecated_container_armv7", ) - deprecated_architecture = False - if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method): - deprecated_architecture = True - if deprecated_method or deprecated_architecture: - issue_id = "deprecated" - if deprecated_method: - issue_id += "_method" - if deprecated_architecture: - issue_id += "_architecture" - ir.async_create_issue( - hass, - DOMAIN, - issue_id, - breaks_in_ha_version="2025.12.0", - learn_more_url=DEPRECATION_URL, - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "installation_type": installation_type, - "arch": arch, - }, - ) + deprecated_architecture = False + if arch in {"i386", "armhf"} or ( + arch == "armv7" and installation_type != "Container" + ): + deprecated_architecture = True + if deprecated_method or deprecated_architecture: + issue_id = "deprecated" + if deprecated_method: + issue_id += "_method" + if deprecated_architecture: + issue_id += "_architecture" + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_type": installation_type, + "arch": arch, + }, + ) return True diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index d34aed608fb..f74ed852a49 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, patch from aiohasupervisor import SupervisorError from aiohasupervisor.models import AddonsStats +from freezegun.api import FrozenDateTimeFactory import pytest from voluptuous import Invalid @@ -23,10 +24,13 @@ from homeassistant.components.hassio import ( is_hassio as deprecated_is_hassio, ) from homeassistant.components.hassio.config import STORAGE_KEY -from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY +from homeassistant.components.hassio.const import ( + HASSIO_UPDATE_INTERVAL, + REQUEST_REFRESH_DELAY, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.setup import async_setup_component @@ -1140,3 +1144,285 @@ def test_deprecated_constants( replacement, "2025.11", ) + + +@pytest.mark.parametrize( + ("board", "issue_id"), + [ + ("rpi3", "deprecated_os_aarch64"), + ("rpi4", "deprecated_os_aarch64"), + ("tinker", "deprecated_os_armv7"), + ("odroid-xu4", "deprecated_os_armv7"), + ("rpi2", "deprecated_os_armv7"), + ], +) +async def test_deprecated_installation_issue_aarch64( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + board: str, + issue_id: str, +) -> None: + """Test deprecated installation issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "armv7", + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "armv7", + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", return_value={"board": board} + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", issue_id) + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_guide": "https://www.home-assistant.io/installation/", + } + + +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + "armv7", + ], +) +async def test_deprecated_installation_issue_32bit_method( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "rpi3-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", "deprecated_architecture") + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == {"installation_type": "OS", "arch": arch} + + +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + "armv7", + ], +) +async def test_deprecated_installation_issue_32bit_supervised( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "rpi3-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": None} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + "homeassistant", "deprecated_method_architecture" + ) + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": "Supervised", + "arch": arch, + } + + +@pytest.mark.parametrize( + ("board", "issue_id"), + [ + ("rpi5", "deprecated_os_aarch64"), + ], +) +async def test_deprecated_installation_issue_supported_board( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + board: str, + issue_id: str, +) -> None: + """Test no deprecated installation issue for a supported board.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "aarch64", + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "aarch64", + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", return_value={"board": board} + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 0 diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 530a729e12d..a5e454221d3 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -640,13 +640,6 @@ async def test_reload_all( assert len(jinja) == 1 -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Core", - "Home Assistant Supervised", - ], -) @pytest.mark.parametrize( "arch", [ @@ -658,14 +651,13 @@ async def test_reload_all( async def test_deprecated_installation_issue_32bit_method( hass: HomeAssistant, issue_registry: ir.IssueRegistry, - installation_type: str, arch: str, ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Core", "arch": arch, }, ): @@ -677,18 +669,11 @@ async def test_deprecated_installation_issue_32bit_method( assert issue.domain == DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Core", "arch": arch, } -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Container", - "Home Assistant OS", - ], -) @pytest.mark.parametrize( "arch", [ @@ -699,14 +684,13 @@ async def test_deprecated_installation_issue_32bit_method( async def test_deprecated_installation_issue_32bit( hass: HomeAssistant, issue_registry: ir.IssueRegistry, - installation_type: str, arch: str, ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Container", "arch": arch, }, ): @@ -718,28 +702,19 @@ async def test_deprecated_installation_issue_32bit( assert issue.domain == DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Container", "arch": arch, } -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Core", - "Home Assistant Supervised", - ], -) async def test_deprecated_installation_issue_method( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - installation_type: str, + hass: HomeAssistant, issue_registry: ir.IssueRegistry ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Core", "arch": "generic-x86-64", }, ): @@ -751,52 +726,11 @@ async def test_deprecated_installation_issue_method( assert issue.domain == DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Core", "arch": "generic-x86-64", } -@pytest.mark.parametrize( - ("board", "issue_id"), - [ - ("rpi3", "deprecated_os_aarch64"), - ("rpi4", "deprecated_os_aarch64"), - ("tinker", "deprecated_os_armv7"), - ("odroid-xu4", "deprecated_os_armv7"), - ("rpi2", "deprecated_os_armv7"), - ], -) -async def test_deprecated_installation_issue_aarch64( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - board: str, - issue_id: str, -) -> None: - """Test deprecated installation issue.""" - with ( - patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": "armv7", - }, - ), - patch( - "homeassistant.components.hassio.get_os_info", return_value={"board": board} - ), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue(DOMAIN, issue_id) - assert issue.domain == DOMAIN - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.translation_placeholders == { - "installation_guide": "https://www.home-assistant.io/installation/", - } - - async def test_deprecated_installation_issue_armv7_container( hass: HomeAssistant, issue_registry: ir.IssueRegistry,