From d8d6490fb45045c95fae0b34593994b7f734afcf Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Fri, 24 Oct 2025 14:23:52 -0400 Subject: [PATCH] Add repair for deprecated addon issue (#151287) --- homeassistant/components/hassio/const.py | 4 + homeassistant/components/hassio/issues.py | 2 + homeassistant/components/hassio/repairs.py | 22 +++++ homeassistant/components/hassio/strings.json | 13 +++ tests/components/hassio/test_repairs.py | 89 ++++++++++++++++++++ 5 files changed, 130 insertions(+) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 1653c33e5ec..b93f25142c5 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -109,6 +109,8 @@ DATA_KEY_HOST = "host" DATA_KEY_SUPERVISOR_ISSUES = "supervisor_issues" PLACEHOLDER_KEY_ADDON = "addon" +PLACEHOLDER_KEY_ADDON_INFO = "addon_info" +PLACEHOLDER_KEY_ADDON_DOCUMENTATION = "addon_documentation" PLACEHOLDER_KEY_ADDON_URL = "addon_url" PLACEHOLDER_KEY_REFERENCE = "reference" PLACEHOLDER_KEY_COMPONENTS = "components" @@ -120,6 +122,7 @@ ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing" ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed" ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned" ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space" +ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon" CORE_CONTAINER = "homeassistant" SUPERVISOR_CONTAINER = "hassio_supervisor" @@ -156,6 +159,7 @@ EXTRA_PLACEHOLDERS = { ISSUE_KEY_ADDON_PWNED: { "more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords", }, + ISSUE_KEY_ADDON_DEPRECATED: HELP_URLS, } diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index df1ca87fe0b..3ec1f5389ce 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -43,6 +43,7 @@ from .const import ( EVENT_SUPPORTED_CHANGED, EXTRA_PLACEHOLDERS, ISSUE_KEY_ADDON_BOOT_FAIL, + ISSUE_KEY_ADDON_DEPRECATED, ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING, ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED, ISSUE_KEY_ADDON_PWNED, @@ -84,6 +85,7 @@ ISSUE_KEYS_FOR_REPAIRS = { "issue_system_disk_lifetime", ISSUE_KEY_SYSTEM_FREE_SPACE, ISSUE_KEY_ADDON_PWNED, + ISSUE_KEY_ADDON_DEPRECATED, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index ff32e2cbab9..de90026be5b 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -18,10 +18,13 @@ from . import get_addons_info, get_issues_info from .const import ( EXTRA_PLACEHOLDERS, ISSUE_KEY_ADDON_BOOT_FAIL, + ISSUE_KEY_ADDON_DEPRECATED, ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED, ISSUE_KEY_ADDON_PWNED, ISSUE_KEY_SYSTEM_DOCKER_CONFIG, PLACEHOLDER_KEY_ADDON, + PLACEHOLDER_KEY_ADDON_DOCUMENTATION, + PLACEHOLDER_KEY_ADDON_INFO, PLACEHOLDER_KEY_COMPONENTS, PLACEHOLDER_KEY_REFERENCE, ) @@ -195,6 +198,23 @@ class AddonIssueRepairFlow(SupervisorIssueRepairFlow): return placeholders or None +class DeprecatedAddonIssueRepairFlow(AddonIssueRepairFlow): + """Handler for deprecated addon issue fixing flows.""" + + @property + def description_placeholders(self) -> dict[str, str] | None: + """Get description placeholders for steps.""" + placeholders: dict[str, str] = super().description_placeholders or {} + if self.issue and self.issue.reference: + placeholders[PLACEHOLDER_KEY_ADDON_INFO] = ( + f"homeassistant://hassio/addon/{self.issue.reference}/info" + ) + placeholders[PLACEHOLDER_KEY_ADDON_DOCUMENTATION] = ( + f"homeassistant://hassio/addon/{self.issue.reference}/documentation" + ) + return placeholders or None + + async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, @@ -205,6 +225,8 @@ async def async_create_fix_flow( issue = supervisor_issues and supervisor_issues.get_issue(issue_id) if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG: return DockerConfigIssueRepairFlow(hass, issue_id) + if issue and issue.key == ISSUE_KEY_ADDON_DEPRECATED: + return DeprecatedAddonIssueRepairFlow(hass, issue_id) if issue and issue.key in { ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED, ISSUE_KEY_ADDON_BOOT_FAIL, diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index d93fff8d06d..1c3e51069c1 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -56,6 +56,19 @@ "title": "Insecure secrets detected in add-on configuration", "description": "Add-on {addon} uses secrets/passwords in its configuration which are detected as not secure. See [pwned passwords and secrets]({more_info_pwned}) for more information on this issue." }, + "issue_addon_deprecated_addon": { + "title": "Installed add-on is deprecated", + "fix_flow": { + "step": { + "addon_execute_remove": { + "description": "Add-on {addon} is marked deprecated by the developer. This means it is no longer being maintained and so may break or become a security issue over time.\n\nReview the [readme]({addon_info}) and [documentation]({addon_documentation}) of the add-on to see if the developer provided instructions.\n\nSelecting **Submit** will uninstall this deprecated add-on. Alternatively, you can check [Home Assistant help]({help_url}) and the [community forum]({community_url}) for alternatives to migrate to." + } + }, + "abort": { + "apply_suggestion_fail": "Could not uninstall the add-on. Check the Supervisor logs for more details." + } + } + }, "issue_mount_mount_failed": { "title": "Network storage device failed", "fix_flow": { diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 4234aab40c1..8f15959234a 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -994,3 +994,92 @@ async def test_supervisor_issue_addon_boot_fail( assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex) supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid) + + +@pytest.mark.parametrize( + "all_setup_requests", [{"include_addons": True}], indirect=True +) +@pytest.mark.usefixtures("all_setup_requests") +async def test_supervisor_issue_deprecated_addon( + hass: HomeAssistant, + supervisor_client: AsyncMock, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, +) -> None: + """Test fix flow for supervisor issue for deprecated add-on.""" + mock_resolution_info( + supervisor_client, + issues=[ + Issue( + type=IssueType.DEPRECATED_ADDON, + context=ContextType.ADDON, + reference="test", + uuid=(issue_uuid := uuid4()), + ), + ], + suggestions_by_issue={ + issue_uuid: [ + Suggestion( + type=SuggestionType.EXECUTE_REMOVE, + context=ContextType.ADDON, + reference="test", + uuid=(sugg_uuid := uuid4()), + auto=False, + ), + ] + }, + ) + + assert await async_setup_component(hass, "hassio", {}) + + repair_issue = issue_registry.async_get_issue( + domain="hassio", issue_id=issue_uuid.hex + ) + assert repair_issue + + client = await hass_client() + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": "hassio", "issue_id": repair_issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": "hassio", + "step_id": "addon_execute_remove", + "data_schema": [], + "errors": None, + "description_placeholders": { + "reference": "test", + "addon": "test", + "help_url": "https://www.home-assistant.io/help/", + "community_url": "https://community.home-assistant.io/", + "addon_info": "homeassistant://hassio/addon/test/info", + "addon_documentation": "homeassistant://hassio/addon/test/documentation", + }, + "last_step": True, + "preview": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "create_entry", + "flow_id": flow_id, + "handler": "hassio", + "description": None, + "description_placeholders": None, + } + + assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex) + supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)