Add repair issue when trying to set up unknown integration (#121089)

* Add repair issue when trying to set up unknown integration

* Add repair issue when trying to set up unknown integration

* Add repair issue when trying to set up unknown integration

* Fix

* Update homeassistant/components/homeassistant/strings.json

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* Update homeassistant/components/homeassistant/strings.json

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* Update homeassistant/setup.py

* Fix

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
Joost Lekkerkerker 2024-07-06 15:02:58 +02:00 committed by GitHub
parent df9ced9768
commit 3c14aa12ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 253 additions and 3 deletions

View file

@ -0,0 +1,59 @@
"""Repairs for Home Assistant."""
from __future__ import annotations
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
from homeassistant.core import DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import issue_registry as ir
class IntegrationNotFoundFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
def __init__(self, data: dict[str, str]) -> None:
"""Initialize."""
self.domain = data["domain"]
self.description_placeholders: dict[str, str] = data
async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the first step of a fix flow."""
return self.async_show_menu(
step_id="init",
menu_options=["confirm", "ignore"],
description_placeholders=self.description_placeholders,
)
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the confirm step of a fix flow."""
entries = self.hass.config_entries.async_entries(self.domain)
for entry in entries:
await self.hass.config_entries.async_remove(entry.entry_id)
return self.async_create_entry(data={})
async def async_step_ignore(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the ignore step of a fix flow."""
ir.async_get(self.hass).async_ignore(
DOMAIN, f"integration_not_found.{self.domain}", True
)
return self.async_abort(
reason="issue_ignored",
description_placeholders=self.description_placeholders,
)
async def async_create_fix_flow(
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
) -> RepairsFlow:
"""Create flow."""
if issue_id.split(".")[0] == "integration_not_found":
assert data
return IntegrationNotFoundFlow(data)
return ConfirmRepairFlow()

View file

@ -56,6 +56,21 @@
"config_entry_reauth": { "config_entry_reauth": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "Reauthentication is needed" "description": "Reauthentication is needed"
},
"integration_not_found": {
"title": "Integration {domain} not found",
"fix_flow": {
"step": {
"remove_entries": {
"title": "[%key:component::homeassistant::issues::integration_not_found::title%]",
"description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `domain` or ignore this.",
"menu_options": {
"confirm": "Remove previous configurations",
"ignore": "Ignore"
}
}
}
}
} }
}, },
"system_health": { "system_health": {

View file

@ -29,7 +29,7 @@ from .core import (
callback, callback,
) )
from .exceptions import DependencyError, HomeAssistantError from .exceptions import DependencyError, HomeAssistantError
from .helpers import singleton, translation from .helpers import issue_registry as ir, singleton, translation
from .helpers.issue_registry import IssueSeverity, async_create_issue from .helpers.issue_registry import IssueSeverity, async_create_issue
from .helpers.typing import ConfigType from .helpers.typing import ConfigType
from .util.async_ import create_eager_task from .util.async_ import create_eager_task
@ -281,6 +281,19 @@ async def _async_setup_component(
integration = await loader.async_get_integration(hass, domain) integration = await loader.async_get_integration(hass, domain)
except loader.IntegrationNotFound: except loader.IntegrationNotFound:
_log_error_setup_error(hass, domain, None, "Integration not found.") _log_error_setup_error(hass, domain, None, "Integration not found.")
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"integration_not_found.{domain}",
is_fixable=True,
issue_domain=HOMEASSISTANT_DOMAIN,
severity=IssueSeverity.ERROR,
translation_key="integration_not_found",
translation_placeholders={
"domain": domain,
},
data={"domain": domain},
)
return False return False
log_error = partial(_log_error_setup_error, hass, domain, integration) log_error = partial(_log_error_setup_error, hass, domain, integration)

View file

@ -0,0 +1,156 @@
"""Test the Homeassistant repairs module."""
from http import HTTPStatus
from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
from homeassistant.components.repairs.issue_handler import (
async_process_repairs_platforms,
)
from homeassistant.components.repairs.websocket_api import (
RepairsFlowIndexView,
RepairsFlowResourceView,
)
from homeassistant.core import DOMAIN, HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator, WebSocketGenerator
async def test_integration_not_found_confirm_step(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the integration_not_found issue confirm step."""
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
await hass.async_block_till_done()
assert await async_setup_component(hass, "test1", {}) is False
await hass.async_block_till_done()
entry1 = MockConfigEntry(domain="test1")
entry1.add_to_hass(hass)
entry2 = MockConfigEntry(domain="test1")
entry2.add_to_hass(hass)
issue_id = "integration_not_found.test1"
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
http_client = await hass_client()
# Assert the issue is present
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
issue = msg["result"]["issues"][0]
assert issue["issue_id"] == issue_id
assert issue["translation_placeholders"] == {"domain": "test1"}
url = RepairsFlowIndexView.url
resp = await http_client.post(url, json={"handler": DOMAIN, "issue_id": issue_id})
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data["step_id"] == "init"
assert data["description_placeholders"] == {"domain": "test1"}
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
# Show menu
resp = await http_client.post(url)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["type"] == "menu"
# Apply fix
resp = await http_client.post(url, json={"next_step_id": "confirm"})
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["type"] == "create_entry"
await hass.async_block_till_done()
assert hass.config_entries.async_get_entry(entry1.entry_id) is None
assert hass.config_entries.async_get_entry(entry2.entry_id) is None
# Assert the issue is resolved
await ws_client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 0
async def test_integration_not_found_ignore_step(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the integration_not_found issue ignore step."""
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
await hass.async_block_till_done()
assert await async_setup_component(hass, "test1", {}) is False
await hass.async_block_till_done()
entry1 = MockConfigEntry(domain="test1")
entry1.add_to_hass(hass)
issue_id = "integration_not_found.test1"
await async_process_repairs_platforms(hass)
ws_client = await hass_ws_client(hass)
http_client = await hass_client()
# Assert the issue is present
await ws_client.send_json({"id": 1, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
issue = msg["result"]["issues"][0]
assert issue["issue_id"] == issue_id
assert issue["translation_placeholders"] == {"domain": "test1"}
url = RepairsFlowIndexView.url
resp = await http_client.post(url, json={"handler": DOMAIN, "issue_id": issue_id})
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data["step_id"] == "init"
assert data["description_placeholders"] == {"domain": "test1"}
url = RepairsFlowResourceView.url.format(flow_id=flow_id)
# Show menu
resp = await http_client.post(url)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["type"] == "menu"
# Apply fix
resp = await http_client.post(url, json={"next_step_id": "ignore"})
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["type"] == "abort"
assert data["reason"] == "issue_ignored"
await hass.async_block_till_done()
assert hass.config_entries.async_get_entry(entry1.entry_id)
# Assert the issue is resolved
await ws_client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert msg["result"]["issues"][0].get("dismissed_version") is not None

View file

@ -10,13 +10,14 @@ import voluptuous as vol
from homeassistant import config_entries, loader, setup from homeassistant import config_entries, loader, setup
from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START
from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.core import DOMAIN, CoreState, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, discovery, translation from homeassistant.helpers import config_validation as cv, discovery, translation
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
) )
from homeassistant.helpers.issue_registry import IssueRegistry
from .common import ( from .common import (
MockConfigEntry, MockConfigEntry,
@ -236,9 +237,15 @@ async def test_validate_platform_config_4(hass: HomeAssistant) -> None:
hass.config.components.remove("platform_conf") hass.config.components.remove("platform_conf")
async def test_component_not_found(hass: HomeAssistant) -> None: async def test_component_not_found(
hass: HomeAssistant, issue_registry: IssueRegistry
) -> None:
"""setup_component should not crash if component doesn't exist.""" """setup_component should not crash if component doesn't exist."""
assert await setup.async_setup_component(hass, "non_existing", {}) is False assert await setup.async_setup_component(hass, "non_existing", {}) is False
assert len(issue_registry.issues) == 1
issue = issue_registry.async_get_issue(DOMAIN, "integration_not_found.non_existing")
assert issue
assert issue.translation_key == "integration_not_found"
async def test_component_not_double_initialized(hass: HomeAssistant) -> None: async def test_component_not_double_initialized(hass: HomeAssistant) -> None: