diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index d2858bfcdf1..f2339e6bd1a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,13 @@ from .const import ( SIGNAL_BOOTSTRAP_INTEGRATONS, ) from .exceptions import HomeAssistantError -from .helpers import area_registry, device_registry, entity_registry, recorder +from .helpers import ( + area_registry, + device_registry, + entity_registry, + issue_registry, + recorder, +) from .helpers.dispatcher import async_dispatcher_send from .helpers.typing import ConfigType from .setup import ( @@ -521,9 +527,10 @@ async def _async_set_up_integrations( # Load the registries and cache the result of platform.uname().processor await asyncio.gather( + area_registry.async_load(hass), device_registry.async_load(hass), entity_registry.async_load(hass), - area_registry.async_load(hass), + issue_registry.async_load(hass), hass.async_add_executor_job(_cache_uname_processor), ) diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 5014baff834..726102d4b08 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -2,19 +2,19 @@ from __future__ import annotations from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -from . import issue_handler, websocket_api -from .const import DOMAIN -from .issue_handler import ( - ConfirmRepairFlow, +from homeassistant.helpers.issue_registry import ( + IssueSeverity, async_create_issue, async_delete_issue, create_issue, delete_issue, ) -from .issue_registry import async_load as async_load_issue_registry -from .models import IssueSeverity, RepairsFlow +from homeassistant.helpers.typing import ConfigType + +from . import issue_handler, websocket_api +from .const import DOMAIN +from .issue_handler import ConfirmRepairFlow +from .models import RepairsFlow __all__ = [ "async_create_issue", @@ -34,6 +34,5 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: issue_handler.async_setup(hass) websocket_api.async_setup(hass) - await async_load_issue_registry(hass) return True diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index eecbfe59bde..1201497f0c1 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -1,10 +1,8 @@ """The repairs integration.""" from __future__ import annotations -import functools as ft from typing import Any -from awesomeversion import AwesomeVersion, AwesomeVersionStrategy import voluptuous as vol from homeassistant import data_entry_flow @@ -13,11 +11,16 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from homeassistant.util.async_ import run_callback_threadsafe + +# pylint: disable-next=unused-import +from homeassistant.helpers.issue_registry import ( # noqa: F401; Remove when integrations have been updated + async_create_issue, + async_delete_issue, + async_get as async_get_issue_registry, +) from .const import DOMAIN -from .issue_registry import async_get as async_get_issue_registry -from .models import IssueSeverity, RepairsFlow, RepairsProtocol +from .models import RepairsFlow, RepairsProtocol class ConfirmRepairFlow(RepairsFlow): @@ -111,112 +114,3 @@ async def _register_repairs_platform( if not hasattr(platform, "async_create_fix_flow"): raise HomeAssistantError(f"Invalid repairs platform {platform}") hass.data[DOMAIN]["platforms"][integration_domain] = platform - - -@callback -def async_create_issue( - hass: HomeAssistant, - domain: str, - issue_id: str, - *, - breaks_in_ha_version: str | None = None, - data: dict[str, str | int | float | None] | None = None, - is_fixable: bool, - is_persistent: bool = False, - issue_domain: str | None = None, - learn_more_url: str | None = None, - severity: IssueSeverity, - translation_key: str, - translation_placeholders: dict[str, str] | None = None, -) -> None: - """Create an issue, or replace an existing one.""" - # Verify the breaks_in_ha_version is a valid version string - if breaks_in_ha_version: - AwesomeVersion( - breaks_in_ha_version, - ensure_strategy=AwesomeVersionStrategy.CALVER, - find_first_match=False, - ) - - issue_registry = async_get_issue_registry(hass) - issue_registry.async_get_or_create( - domain, - issue_id, - breaks_in_ha_version=breaks_in_ha_version, - data=data, - is_fixable=is_fixable, - is_persistent=is_persistent, - issue_domain=issue_domain, - learn_more_url=learn_more_url, - severity=severity, - translation_key=translation_key, - translation_placeholders=translation_placeholders, - ) - - -def create_issue( - hass: HomeAssistant, - domain: str, - issue_id: str, - *, - breaks_in_ha_version: str | None = None, - data: dict[str, str | int | float | None] | None = None, - is_fixable: bool, - is_persistent: bool = False, - issue_domain: str | None = None, - learn_more_url: str | None = None, - severity: IssueSeverity, - translation_key: str, - translation_placeholders: dict[str, str] | None = None, -) -> None: - """Create an issue, or replace an existing one.""" - return run_callback_threadsafe( - hass.loop, - ft.partial( - async_create_issue, - hass, - domain, - issue_id, - breaks_in_ha_version=breaks_in_ha_version, - data=data, - is_fixable=is_fixable, - is_persistent=is_persistent, - issue_domain=issue_domain, - learn_more_url=learn_more_url, - severity=severity, - translation_key=translation_key, - translation_placeholders=translation_placeholders, - ), - ).result() - - -@callback -def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: - """Delete an issue. - - It is not an error to delete an issue that does not exist. - """ - issue_registry = async_get_issue_registry(hass) - issue_registry.async_delete(domain, issue_id) - - -def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: - """Delete an issue. - - It is not an error to delete an issue that does not exist. - """ - return run_callback_threadsafe( - hass.loop, async_delete_issue, hass, domain, issue_id - ).result() - - -@callback -def async_ignore_issue( - hass: HomeAssistant, domain: str, issue_id: str, ignore: bool -) -> None: - """Ignore an issue. - - Will raise if the issue does not exist. - """ - issue_registry = async_get_issue_registry(hass) - issue_registry.async_ignore(domain, issue_id, ignore) diff --git a/homeassistant/components/repairs/models.py b/homeassistant/components/repairs/models.py index 1022c50e1f2..045b7bd55dc 100644 --- a/homeassistant/components/repairs/models.py +++ b/homeassistant/components/repairs/models.py @@ -4,16 +4,12 @@ from __future__ import annotations from typing import Protocol from homeassistant import data_entry_flow -from homeassistant.backports.enum import StrEnum from homeassistant.core import HomeAssistant - -class IssueSeverity(StrEnum): - """Issue severity.""" - - CRITICAL = "critical" - ERROR = "error" - WARNING = "warning" +# pylint: disable-next=unused-import +from homeassistant.helpers.issue_registry import ( # noqa: F401; Remove when integrations have been updated + IssueSeverity, +) class RepairsFlow(data_entry_flow.FlowHandler): diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 192c9f5ac66..b4ccca7c894 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -18,10 +18,12 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, ) +from homeassistant.helpers.issue_registry import ( + async_get as async_get_issue_registry, + async_ignore_issue, +) from .const import DOMAIN -from .issue_handler import async_ignore_issue -from .issue_registry import async_get as async_get_issue_registry @callback diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/helpers/issue_registry.py similarity index 73% rename from homeassistant/components/repairs/issue_registry.py rename to homeassistant/helpers/issue_registry.py index a8843011023..b01d56942ac 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -3,14 +3,18 @@ from __future__ import annotations import dataclasses from datetime import datetime +import functools as ft from typing import Any, cast +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.backports.enum import StrEnum from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.storage import Store +from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util -from .models import IssueSeverity +from .storage import Store DATA_REGISTRY = "issue_registry" EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" @@ -20,6 +24,14 @@ STORAGE_VERSION_MINOR = 2 SAVE_DELAY = 10 +class IssueSeverity(StrEnum): + """Issue severity.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" + + @dataclasses.dataclass(frozen=True) class IssueEntry: """Issue Registry Entry.""" @@ -267,3 +279,112 @@ async def async_load(hass: HomeAssistant) -> None: assert DATA_REGISTRY not in hass.data hass.data[DATA_REGISTRY] = IssueRegistry(hass) await hass.data[DATA_REGISTRY].async_load() + + +@callback +def async_create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, + is_fixable: bool, + is_persistent: bool = False, + issue_domain: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + # Verify the breaks_in_ha_version is a valid version string + if breaks_in_ha_version: + AwesomeVersion( + breaks_in_ha_version, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + issue_registry = async_get(hass) + issue_registry.async_get_or_create( + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + data=data, + is_fixable=is_fixable, + is_persistent=is_persistent, + issue_domain=issue_domain, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + +def create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, + is_fixable: bool, + is_persistent: bool = False, + issue_domain: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + return run_callback_threadsafe( + hass.loop, + ft.partial( + async_create_issue, + hass, + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + data=data, + is_fixable=is_fixable, + is_persistent=is_persistent, + issue_domain=issue_domain, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ), + ).result() + + +@callback +def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + issue_registry = async_get(hass) + issue_registry.async_delete(domain, issue_id) + + +def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + return run_callback_threadsafe( + hass.loop, async_delete_issue, hass, domain, issue_id + ).result() + + +@callback +def async_ignore_issue( + hass: HomeAssistant, domain: str, issue_id: str, ignore: bool +) -> None: + """Ignore an issue. + + Will raise if the issue does not exist. + """ + issue_registry = async_get(hass) + issue_registry.async_ignore(domain, issue_id, ignore) diff --git a/tests/common.py b/tests/common.py index acc50e26889..89d1a1d9116 100644 --- a/tests/common.py +++ b/tests/common.py @@ -50,6 +50,7 @@ from homeassistant.helpers import ( entity_platform, entity_registry, intent, + issue_registry, recorder as recorder_helper, restore_state, storage, @@ -297,9 +298,10 @@ async def async_test_home_assistant(loop, load_registries=True): # Load the registries if load_registries: await asyncio.gather( + area_registry.async_load(hass), device_registry.async_load(hass), entity_registry.async_load(hass), - area_registry.async_load(hass), + issue_registry.async_load(hass), ) await hass.async_block_till_done() diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 1fc8367e4c3..9071785aeea 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -14,12 +14,11 @@ from homeassistant.components.repairs import ( ) from homeassistant.components.repairs.const import DOMAIN from homeassistant.components.repairs.issue_handler import ( - async_ignore_issue, async_process_repairs_platforms, ) -from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueSeverity, async_ignore_issue from homeassistant.setup import async_setup_component from tests.common import mock_platform diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 2a506c7a248..508e2edeb92 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -9,14 +9,11 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.repairs import ( - RepairsFlow, - async_create_issue, - issue_registry, -) +from homeassistant.components.repairs import RepairsFlow, async_create_issue from homeassistant.components.repairs.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry from homeassistant.setup import async_setup_component from tests.common import mock_platform diff --git a/tests/components/repairs/test_issue_registry.py b/tests/helpers/test_issue_registry.py similarity index 92% rename from tests/components/repairs/test_issue_registry.py rename to tests/helpers/test_issue_registry.py index 76faafce1c7..01c2d83fb46 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/helpers/test_issue_registry.py @@ -1,20 +1,14 @@ """Test the repairs websocket API.""" -from homeassistant.components.repairs import async_create_issue, issue_registry -from homeassistant.components.repairs.const import DOMAIN -from homeassistant.components.repairs.issue_handler import ( - async_delete_issue, - async_ignore_issue, -) +import pytest + from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import issue_registry from tests.common import async_capture_events, flush_store async def test_load_issues(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" - assert await async_setup_component(hass, DOMAIN, {}) - issues = [ { "breaks_in_ha_version": "2022.9", @@ -68,7 +62,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: ) for issue in issues: - async_create_issue( + issue_registry.async_create_issue( hass, issue["domain"], issue["issue_id"], @@ -105,7 +99,9 @@ async def test_load_issues(hass: HomeAssistant) -> None: "issue_id": "issue_4", } - async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) + issue_registry.async_ignore_issue( + hass, issues[0]["domain"], issues[0]["issue_id"], True + ) await hass.async_block_till_done() assert len(events) == 5 @@ -115,7 +111,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "issue_id": "issue_1", } - async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) + issue_registry.async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) await hass.async_block_till_done() assert len(events) == 6 @@ -175,6 +171,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert issue4_registry2 == issue4 +@pytest.mark.parametrize("load_registries", [False]) async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: """Test loading stored issues on start.""" hass_storage[issue_registry.STORAGE_KEY] = { @@ -215,12 +212,13 @@ async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> }, } - assert await async_setup_component(hass, DOMAIN, {}) + await issue_registry.async_load(hass) registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 3 +@pytest.mark.parametrize("load_registries", [False]) async def test_migration_1_1(hass: HomeAssistant, hass_storage) -> None: """Test migration from version 1.1.""" hass_storage[issue_registry.STORAGE_KEY] = { @@ -244,7 +242,7 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage) -> None: }, } - assert await async_setup_component(hass, DOMAIN, {}) + await issue_registry.async_load(hass) registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 2