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:
parent
df9ced9768
commit
3c14aa12ab
5 changed files with 253 additions and 3 deletions
59
homeassistant/components/homeassistant/repairs.py
Normal file
59
homeassistant/components/homeassistant/repairs.py
Normal 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()
|
|
@ -56,6 +56,21 @@
|
|||
"config_entry_reauth": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"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": {
|
||||
|
|
|
@ -29,7 +29,7 @@ from .core import (
|
|||
callback,
|
||||
)
|
||||
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.typing import ConfigType
|
||||
from .util.async_ import create_eager_task
|
||||
|
@ -281,6 +281,19 @@ async def _async_setup_component(
|
|||
integration = await loader.async_get_integration(hass, domain)
|
||||
except loader.IntegrationNotFound:
|
||||
_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
|
||||
|
||||
log_error = partial(_log_error_setup_error, hass, domain, integration)
|
||||
|
|
156
tests/components/homeassistant/test_repairs.py
Normal file
156
tests/components/homeassistant/test_repairs.py
Normal 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
|
|
@ -10,13 +10,14 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant import config_entries, loader, setup
|
||||
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.helpers import config_validation as cv, discovery, translation
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueRegistry
|
||||
|
||||
from .common import (
|
||||
MockConfigEntry,
|
||||
|
@ -236,9 +237,15 @@ async def test_validate_platform_config_4(hass: HomeAssistant) -> None:
|
|||
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."""
|
||||
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:
|
||||
|
|
Loading…
Add table
Reference in a new issue