mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 02:37:08 +00:00
Teach resolution center about fixing issues (#74694)
This commit is contained in:
parent
cf612c4bec
commit
b0fde206b8
@ -4,16 +4,19 @@ from __future__ import annotations
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import websocket_api
|
from . import issue_handler, websocket_api
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .issue_handler import async_create_issue, async_delete_issue
|
from .issue_handler import ResolutionCenterFlow, async_create_issue, async_delete_issue
|
||||||
from .issue_registry import async_load as async_load_issue_registry
|
from .issue_registry import async_load as async_load_issue_registry
|
||||||
|
|
||||||
__all__ = ["DOMAIN", "async_create_issue", "async_delete_issue"]
|
__all__ = ["DOMAIN", "ResolutionCenterFlow", "async_create_issue", "async_delete_issue"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up Resolution Center."""
|
"""Set up Resolution Center."""
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
|
||||||
|
issue_handler.async_setup(hass)
|
||||||
websocket_api.async_setup(hass)
|
websocket_api.async_setup(hass)
|
||||||
await async_load_issue_registry(hass)
|
await async_load_issue_registry(hass)
|
||||||
|
|
||||||
|
@ -1,12 +1,85 @@
|
|||||||
"""The resolution center integration."""
|
"""The resolution center integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
|
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.integration_platform import (
|
||||||
|
async_process_integration_platforms,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .issue_registry import async_get as async_get_issue_registry
|
from .issue_registry import async_get as async_get_issue_registry
|
||||||
from .models import IssueSeverity
|
from .models import IssueSeverity, ResolutionCenterFlow, ResolutionCenterProtocol
|
||||||
|
|
||||||
|
|
||||||
|
class ResolutionCenterFlowManager(data_entry_flow.FlowManager):
|
||||||
|
"""Manage resolution center flows."""
|
||||||
|
|
||||||
|
async def async_create_flow(
|
||||||
|
self,
|
||||||
|
handler_key: Any,
|
||||||
|
*,
|
||||||
|
context: dict[str, Any] | None = None,
|
||||||
|
data: dict[str, Any] | None = None,
|
||||||
|
) -> ResolutionCenterFlow:
|
||||||
|
"""Create a flow. platform is a resolution center module."""
|
||||||
|
if "platforms" not in self.hass.data[DOMAIN]:
|
||||||
|
await async_process_resolution_center_platforms(self.hass)
|
||||||
|
|
||||||
|
platforms: dict[str, ResolutionCenterProtocol] = self.hass.data[DOMAIN][
|
||||||
|
"platforms"
|
||||||
|
]
|
||||||
|
if handler_key not in platforms:
|
||||||
|
raise data_entry_flow.UnknownHandler
|
||||||
|
platform = platforms[handler_key]
|
||||||
|
|
||||||
|
assert data and "issue_id" in data
|
||||||
|
issue_id = data["issue_id"]
|
||||||
|
|
||||||
|
issue_registry = async_get_issue_registry(self.hass)
|
||||||
|
issue = issue_registry.async_get_issue(handler_key, issue_id)
|
||||||
|
if issue is None or not issue.is_fixable:
|
||||||
|
raise data_entry_flow.UnknownStep
|
||||||
|
|
||||||
|
return await platform.async_create_fix_flow(self.hass, issue_id)
|
||||||
|
|
||||||
|
async def async_finish_flow(
|
||||||
|
self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Complete a fix flow."""
|
||||||
|
async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"])
|
||||||
|
if "result" not in result:
|
||||||
|
result["result"] = None
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize resolution center."""
|
||||||
|
hass.data[DOMAIN]["flow_manager"] = ResolutionCenterFlowManager(hass)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_process_resolution_center_platforms(hass: HomeAssistant) -> None:
|
||||||
|
"""Start processing resolution center platforms."""
|
||||||
|
hass.data[DOMAIN]["platforms"] = {}
|
||||||
|
|
||||||
|
await async_process_integration_platforms(
|
||||||
|
hass, DOMAIN, _register_resolution_center_platform
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _register_resolution_center_platform(
|
||||||
|
hass: HomeAssistant, integration_domain: str, platform: ResolutionCenterProtocol
|
||||||
|
) -> None:
|
||||||
|
"""Register a resolution center platform."""
|
||||||
|
if not hasattr(platform, "async_create_fix_flow"):
|
||||||
|
raise HomeAssistantError(f"Invalid resolution center platform {platform}")
|
||||||
|
hass.data[DOMAIN]["platforms"][integration_domain] = platform
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -16,6 +89,7 @@ def async_create_issue(
|
|||||||
issue_id: str,
|
issue_id: str,
|
||||||
*,
|
*,
|
||||||
breaks_in_ha_version: str | None = None,
|
breaks_in_ha_version: str | None = None,
|
||||||
|
is_fixable: bool,
|
||||||
learn_more_url: str | None = None,
|
learn_more_url: str | None = None,
|
||||||
severity: IssueSeverity,
|
severity: IssueSeverity,
|
||||||
translation_key: str,
|
translation_key: str,
|
||||||
@ -35,6 +109,7 @@ def async_create_issue(
|
|||||||
domain,
|
domain,
|
||||||
issue_id,
|
issue_id,
|
||||||
breaks_in_ha_version=breaks_in_ha_version,
|
breaks_in_ha_version=breaks_in_ha_version,
|
||||||
|
is_fixable=is_fixable,
|
||||||
learn_more_url=learn_more_url,
|
learn_more_url=learn_more_url,
|
||||||
severity=severity,
|
severity=severity,
|
||||||
translation_key=translation_key,
|
translation_key=translation_key,
|
||||||
|
@ -25,6 +25,7 @@ class IssueEntry:
|
|||||||
breaks_in_ha_version: str | None
|
breaks_in_ha_version: str | None
|
||||||
dismissed_version: str | None
|
dismissed_version: str | None
|
||||||
domain: str
|
domain: str
|
||||||
|
is_fixable: bool | None
|
||||||
issue_id: str
|
issue_id: str
|
||||||
learn_more_url: str | None
|
learn_more_url: str | None
|
||||||
severity: IssueSeverity | None
|
severity: IssueSeverity | None
|
||||||
@ -55,6 +56,7 @@ class IssueRegistry:
|
|||||||
issue_id: str,
|
issue_id: str,
|
||||||
*,
|
*,
|
||||||
breaks_in_ha_version: str | None = None,
|
breaks_in_ha_version: str | None = None,
|
||||||
|
is_fixable: bool,
|
||||||
learn_more_url: str | None = None,
|
learn_more_url: str | None = None,
|
||||||
severity: IssueSeverity,
|
severity: IssueSeverity,
|
||||||
translation_key: str,
|
translation_key: str,
|
||||||
@ -68,6 +70,7 @@ class IssueRegistry:
|
|||||||
breaks_in_ha_version=breaks_in_ha_version,
|
breaks_in_ha_version=breaks_in_ha_version,
|
||||||
dismissed_version=None,
|
dismissed_version=None,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
|
is_fixable=is_fixable,
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
learn_more_url=learn_more_url,
|
learn_more_url=learn_more_url,
|
||||||
severity=severity,
|
severity=severity,
|
||||||
@ -81,6 +84,7 @@ class IssueRegistry:
|
|||||||
issue,
|
issue,
|
||||||
active=True,
|
active=True,
|
||||||
breaks_in_ha_version=breaks_in_ha_version,
|
breaks_in_ha_version=breaks_in_ha_version,
|
||||||
|
is_fixable=is_fixable,
|
||||||
learn_more_url=learn_more_url,
|
learn_more_url=learn_more_url,
|
||||||
severity=severity,
|
severity=severity,
|
||||||
translation_key=translation_key,
|
translation_key=translation_key,
|
||||||
@ -127,6 +131,7 @@ class IssueRegistry:
|
|||||||
breaks_in_ha_version=None,
|
breaks_in_ha_version=None,
|
||||||
dismissed_version=issue["dismissed_version"],
|
dismissed_version=issue["dismissed_version"],
|
||||||
domain=issue["domain"],
|
domain=issue["domain"],
|
||||||
|
is_fixable=None,
|
||||||
issue_id=issue["issue_id"],
|
issue_id=issue["issue_id"],
|
||||||
learn_more_url=None,
|
learn_more_url=None,
|
||||||
severity=None,
|
severity=None,
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
"name": "Resolution Center",
|
"name": "Resolution Center",
|
||||||
"config_flow": false,
|
"config_flow": false,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/resolution_center",
|
"documentation": "https://www.home-assistant.io/integrations/resolution_center",
|
||||||
"codeowners": ["@home-assistant/core"]
|
"codeowners": ["@home-assistant/core"],
|
||||||
|
"dependencies": ["http"]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
"""Models for Resolution Center."""
|
"""Models for Resolution Center."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.backports.enum import StrEnum
|
from homeassistant.backports.enum import StrEnum
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
class IssueSeverity(StrEnum):
|
class IssueSeverity(StrEnum):
|
||||||
@ -10,3 +14,16 @@ class IssueSeverity(StrEnum):
|
|||||||
CRITICAL = "critical"
|
CRITICAL = "critical"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
WARNING = "warning"
|
WARNING = "warning"
|
||||||
|
|
||||||
|
|
||||||
|
class ResolutionCenterFlow(data_entry_flow.FlowHandler):
|
||||||
|
"""Handle a flow for fixing an issue."""
|
||||||
|
|
||||||
|
|
||||||
|
class ResolutionCenterProtocol(Protocol):
|
||||||
|
"""Define the format of resolution center platforms."""
|
||||||
|
|
||||||
|
async def async_create_fix_flow(
|
||||||
|
self, hass: HomeAssistant, issue_id: str
|
||||||
|
) -> ResolutionCenterFlow:
|
||||||
|
"""Create a flow to fix a fixable issue."""
|
||||||
|
@ -2,13 +2,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
from http import HTTPStatus
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.auth.permissions.const import POLICY_EDIT
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import Unauthorized
|
||||||
|
from homeassistant.helpers.data_entry_flow import (
|
||||||
|
FlowManagerIndexView,
|
||||||
|
FlowManagerResourceView,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .issue_handler import async_dismiss_issue
|
from .issue_handler import async_dismiss_issue
|
||||||
from .issue_registry import async_get as async_get_issue_registry
|
from .issue_registry import async_get as async_get_issue_registry
|
||||||
|
|
||||||
@ -19,6 +30,13 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||||||
websocket_api.async_register_command(hass, ws_dismiss_issue)
|
websocket_api.async_register_command(hass, ws_dismiss_issue)
|
||||||
websocket_api.async_register_command(hass, ws_list_issues)
|
websocket_api.async_register_command(hass, ws_list_issues)
|
||||||
|
|
||||||
|
hass.http.register_view(
|
||||||
|
ResolutionCenterFlowIndexView(hass.data[DOMAIN]["flow_manager"])
|
||||||
|
)
|
||||||
|
hass.http.register_view(
|
||||||
|
ResolutionCenterFlowResourceView(hass.data[DOMAIN]["flow_manager"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
@ -60,3 +78,63 @@ def ws_list_issues(
|
|||||||
]
|
]
|
||||||
|
|
||||||
connection.send_result(msg["id"], {"issues": issues})
|
connection.send_result(msg["id"], {"issues": issues})
|
||||||
|
|
||||||
|
|
||||||
|
class ResolutionCenterFlowIndexView(FlowManagerIndexView):
|
||||||
|
"""View to create issue fix flows."""
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix"
|
||||||
|
name = "api:resolution_center:issues:fix"
|
||||||
|
|
||||||
|
@RequestDataValidator(
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("handler"): str,
|
||||||
|
vol.Required("issue_id"): str,
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
|
||||||
|
"""Handle a POST request."""
|
||||||
|
if not request["hass_user"].is_admin:
|
||||||
|
raise Unauthorized(permission=POLICY_EDIT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self._flow_mgr.async_init(
|
||||||
|
data["handler"],
|
||||||
|
data={"issue_id": data["issue_id"]},
|
||||||
|
)
|
||||||
|
except data_entry_flow.UnknownHandler:
|
||||||
|
return self.json_message("Invalid handler specified", HTTPStatus.NOT_FOUND)
|
||||||
|
except data_entry_flow.UnknownStep:
|
||||||
|
return self.json_message(
|
||||||
|
"Handler does not support user", HTTPStatus.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self._prepare_result_json(result)
|
||||||
|
|
||||||
|
return self.json(result) # pylint: disable=arguments-differ
|
||||||
|
|
||||||
|
|
||||||
|
class ResolutionCenterFlowResourceView(FlowManagerResourceView):
|
||||||
|
"""View to interact with the option flow manager."""
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix/{flow_id}"
|
||||||
|
name = "api:resolution_center:issues:fix:resource"
|
||||||
|
|
||||||
|
async def get(self, request: web.Request, flow_id: str) -> web.Response:
|
||||||
|
"""Get the current state of a data_entry_flow."""
|
||||||
|
if not request["hass_user"].is_admin:
|
||||||
|
raise Unauthorized(permission=POLICY_EDIT)
|
||||||
|
|
||||||
|
return await super().get(request, flow_id)
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
async def post(self, request: web.Request, flow_id: str) -> web.Response:
|
||||||
|
"""Handle a POST request."""
|
||||||
|
if not request["hass_user"].is_admin:
|
||||||
|
raise Unauthorized(permission=POLICY_EDIT)
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
return await super().post(request, flow_id) # type: ignore[no-any-return]
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Test the resolution center websocket API."""
|
"""Test the resolution center websocket API."""
|
||||||
|
from unittest.mock import AsyncMock, Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.resolution_center import (
|
from homeassistant.components.resolution_center import (
|
||||||
@ -6,11 +8,16 @@ from homeassistant.components.resolution_center import (
|
|||||||
async_delete_issue,
|
async_delete_issue,
|
||||||
)
|
)
|
||||||
from homeassistant.components.resolution_center.const import DOMAIN
|
from homeassistant.components.resolution_center.const import DOMAIN
|
||||||
from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue
|
from homeassistant.components.resolution_center.issue_handler import (
|
||||||
|
async_dismiss_issue,
|
||||||
|
async_process_resolution_center_platforms,
|
||||||
|
)
|
||||||
from homeassistant.const import __version__ as ha_version
|
from homeassistant.const import __version__ as ha_version
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import mock_platform
|
||||||
|
|
||||||
|
|
||||||
async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
"""Test creating and updating issues."""
|
"""Test creating and updating issues."""
|
||||||
@ -29,6 +36,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
"breaks_in_ha_version": "2022.9.0dev0",
|
"breaks_in_ha_version": "2022.9.0dev0",
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
|
"is_fixable": True,
|
||||||
"learn_more_url": "https://theuselessweb.com",
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
"translation_key": "abc_123",
|
"translation_key": "abc_123",
|
||||||
@ -38,6 +46,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
"breaks_in_ha_version": "2022.8",
|
"breaks_in_ha_version": "2022.8",
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
"issue_id": "issue_2",
|
"issue_id": "issue_2",
|
||||||
|
"is_fixable": False,
|
||||||
"learn_more_url": "https://theuselessweb.com/abc",
|
"learn_more_url": "https://theuselessweb.com/abc",
|
||||||
"severity": "other",
|
"severity": "other",
|
||||||
"translation_key": "even_worse",
|
"translation_key": "even_worse",
|
||||||
@ -51,6 +60,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
issue["domain"],
|
issue["domain"],
|
||||||
issue["issue_id"],
|
issue["issue_id"],
|
||||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
learn_more_url=issue["learn_more_url"],
|
learn_more_url=issue["learn_more_url"],
|
||||||
severity=issue["severity"],
|
severity=issue["severity"],
|
||||||
translation_key=issue["translation_key"],
|
translation_key=issue["translation_key"],
|
||||||
@ -78,6 +88,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
issues[0]["domain"],
|
issues[0]["domain"],
|
||||||
issues[0]["issue_id"],
|
issues[0]["issue_id"],
|
||||||
breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
|
breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
|
||||||
|
is_fixable=issues[0]["is_fixable"],
|
||||||
learn_more_url="blablabla",
|
learn_more_url="blablabla",
|
||||||
severity=issues[0]["severity"],
|
severity=issues[0]["severity"],
|
||||||
translation_key=issues[0]["translation_key"],
|
translation_key=issues[0]["translation_key"],
|
||||||
@ -109,6 +120,7 @@ async def test_create_issue_invalid_version(
|
|||||||
"breaks_in_ha_version": ha_version,
|
"breaks_in_ha_version": ha_version,
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
|
"is_fixable": True,
|
||||||
"learn_more_url": "https://theuselessweb.com",
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
"translation_key": "abc_123",
|
"translation_key": "abc_123",
|
||||||
@ -121,6 +133,7 @@ async def test_create_issue_invalid_version(
|
|||||||
issue["domain"],
|
issue["domain"],
|
||||||
issue["issue_id"],
|
issue["issue_id"],
|
||||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
learn_more_url=issue["learn_more_url"],
|
learn_more_url=issue["learn_more_url"],
|
||||||
severity=issue["severity"],
|
severity=issue["severity"],
|
||||||
translation_key=issue["translation_key"],
|
translation_key=issue["translation_key"],
|
||||||
@ -150,6 +163,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
{
|
{
|
||||||
"breaks_in_ha_version": "2022.9",
|
"breaks_in_ha_version": "2022.9",
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
|
"is_fixable": True,
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
"learn_more_url": "https://theuselessweb.com",
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
@ -164,6 +178,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
issue["domain"],
|
issue["domain"],
|
||||||
issue["issue_id"],
|
issue["issue_id"],
|
||||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
learn_more_url=issue["learn_more_url"],
|
learn_more_url=issue["learn_more_url"],
|
||||||
severity=issue["severity"],
|
severity=issue["severity"],
|
||||||
translation_key=issue["translation_key"],
|
translation_key=issue["translation_key"],
|
||||||
@ -246,6 +261,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
issues[0]["domain"],
|
issues[0]["domain"],
|
||||||
issues[0]["issue_id"],
|
issues[0]["issue_id"],
|
||||||
breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
|
breaks_in_ha_version=issues[0]["breaks_in_ha_version"],
|
||||||
|
is_fixable=issues[0]["is_fixable"],
|
||||||
learn_more_url="blablabla",
|
learn_more_url="blablabla",
|
||||||
severity=issues[0]["severity"],
|
severity=issues[0]["severity"],
|
||||||
translation_key=issues[0]["translation_key"],
|
translation_key=issues[0]["translation_key"],
|
||||||
@ -275,6 +291,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
"breaks_in_ha_version": "2022.9",
|
"breaks_in_ha_version": "2022.9",
|
||||||
"domain": "fake_integration",
|
"domain": "fake_integration",
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
|
"is_fixable": True,
|
||||||
"learn_more_url": "https://theuselessweb.com",
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
"translation_key": "abc_123",
|
"translation_key": "abc_123",
|
||||||
@ -288,6 +305,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
issue["domain"],
|
issue["domain"],
|
||||||
issue["issue_id"],
|
issue["issue_id"],
|
||||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
learn_more_url=issue["learn_more_url"],
|
learn_more_url=issue["learn_more_url"],
|
||||||
severity=issue["severity"],
|
severity=issue["severity"],
|
||||||
translation_key=issue["translation_key"],
|
translation_key=issue["translation_key"],
|
||||||
@ -344,3 +362,25 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
|
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
assert msg["result"] == {"issues": []}
|
assert msg["result"] == {"issues": []}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
|
"""Test non-compliant platforms are not registered."""
|
||||||
|
|
||||||
|
hass.config.components.add("fake_integration")
|
||||||
|
hass.config.components.add("integration_without_diagnostics")
|
||||||
|
mock_platform(
|
||||||
|
hass,
|
||||||
|
"fake_integration.resolution_center",
|
||||||
|
Mock(async_create_fix_flow=AsyncMock(return_value=True)),
|
||||||
|
)
|
||||||
|
mock_platform(
|
||||||
|
hass,
|
||||||
|
"integration_without_diagnostics.resolution_center",
|
||||||
|
Mock(spec=[]),
|
||||||
|
)
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
await async_process_resolution_center_platforms(hass)
|
||||||
|
|
||||||
|
assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"]
|
||||||
|
@ -20,6 +20,7 @@ async def test_load_issues(hass: HomeAssistant) -> None:
|
|||||||
"breaks_in_ha_version": "2022.9",
|
"breaks_in_ha_version": "2022.9",
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
|
"is_fixable": True,
|
||||||
"learn_more_url": "https://theuselessweb.com",
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
"translation_key": "abc_123",
|
"translation_key": "abc_123",
|
||||||
@ -29,6 +30,7 @@ async def test_load_issues(hass: HomeAssistant) -> None:
|
|||||||
"breaks_in_ha_version": "2022.8",
|
"breaks_in_ha_version": "2022.8",
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
"issue_id": "issue_2",
|
"issue_id": "issue_2",
|
||||||
|
"is_fixable": True,
|
||||||
"learn_more_url": "https://theuselessweb.com/abc",
|
"learn_more_url": "https://theuselessweb.com/abc",
|
||||||
"severity": "other",
|
"severity": "other",
|
||||||
"translation_key": "even_worse",
|
"translation_key": "even_worse",
|
||||||
@ -42,6 +44,7 @@ async def test_load_issues(hass: HomeAssistant) -> None:
|
|||||||
issue["domain"],
|
issue["domain"],
|
||||||
issue["issue_id"],
|
issue["issue_id"],
|
||||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
learn_more_url=issue["learn_more_url"],
|
learn_more_url=issue["learn_more_url"],
|
||||||
severity=issue["severity"],
|
severity=issue["severity"],
|
||||||
translation_key=issue["translation_key"],
|
translation_key=issue["translation_key"],
|
||||||
|
@ -1,22 +1,33 @@
|
|||||||
"""Test the resolution center websocket API."""
|
"""Test the resolution center websocket API."""
|
||||||
from homeassistant.components.resolution_center import async_create_issue
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from http import HTTPStatus
|
||||||
|
from unittest.mock import ANY, AsyncMock, Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.resolution_center import (
|
||||||
|
ResolutionCenterFlow,
|
||||||
|
async_create_issue,
|
||||||
|
)
|
||||||
from homeassistant.components.resolution_center.const import DOMAIN
|
from homeassistant.components.resolution_center.const import DOMAIN
|
||||||
from homeassistant.const import __version__ as ha_version
|
from homeassistant.const import __version__ as ha_version
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import mock_platform
|
||||||
|
|
||||||
async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|
||||||
"""Test we can dismiss an issue."""
|
|
||||||
assert await async_setup_component(hass, DOMAIN, {})
|
|
||||||
|
|
||||||
client = await hass_ws_client(hass)
|
|
||||||
|
|
||||||
|
async def create_issues(hass, ws_client):
|
||||||
|
"""Create issues."""
|
||||||
issues = [
|
issues = [
|
||||||
{
|
{
|
||||||
"breaks_in_ha_version": "2022.9",
|
"breaks_in_ha_version": "2022.9",
|
||||||
"domain": "test",
|
"domain": "fake_integration",
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
|
"is_fixable": True,
|
||||||
"learn_more_url": "https://theuselessweb.com",
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
"translation_key": "abc_123",
|
"translation_key": "abc_123",
|
||||||
@ -30,14 +41,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
issue["domain"],
|
issue["domain"],
|
||||||
issue["issue_id"],
|
issue["issue_id"],
|
||||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
learn_more_url=issue["learn_more_url"],
|
learn_more_url=issue["learn_more_url"],
|
||||||
severity=issue["severity"],
|
severity=issue["severity"],
|
||||||
translation_key=issue["translation_key"],
|
translation_key=issue["translation_key"],
|
||||||
translation_placeholders=issue["translation_placeholders"],
|
translation_placeholders=issue["translation_placeholders"],
|
||||||
)
|
)
|
||||||
|
|
||||||
await client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
await ws_client.send_json({"id": 1, "type": "resolution_center/list_issues"})
|
||||||
msg = await client.receive_json()
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
assert msg["result"] == {
|
assert msg["result"] == {
|
||||||
@ -51,11 +63,63 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
class MockFixFlow(ResolutionCenterFlow):
|
||||||
|
"""Handler for an issue fixing flow."""
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, str] | None = None
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Handle the first step of a fix flow."""
|
||||||
|
|
||||||
|
return await (self.async_step_confirm())
|
||||||
|
|
||||||
|
async def async_step_confirm(
|
||||||
|
self, user_input: dict[str, str] | None = None
|
||||||
|
) -> data_entry_flow.FlowResult:
|
||||||
|
"""Handle the confirm step of a fix flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title=None, data=None)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="confirm", data_schema=vol.Schema({}))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def mock_resolution_center_integration(hass):
|
||||||
|
"""Mock a resolution_center integration."""
|
||||||
|
hass.config.components.add("fake_integration")
|
||||||
|
hass.config.components.add("integration_without_diagnostics")
|
||||||
|
|
||||||
|
def async_create_fix_flow(hass, issue_id):
|
||||||
|
return MockFixFlow()
|
||||||
|
|
||||||
|
mock_platform(
|
||||||
|
hass,
|
||||||
|
"fake_integration.resolution_center",
|
||||||
|
Mock(async_create_fix_flow=AsyncMock(wraps=async_create_fix_flow)),
|
||||||
|
)
|
||||||
|
mock_platform(
|
||||||
|
hass,
|
||||||
|
"integration_without_diagnostics.resolution_center",
|
||||||
|
Mock(spec=[]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
|
"""Test we can dismiss an issue."""
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
issues = await create_issues(hass, client)
|
||||||
|
|
||||||
await client.send_json(
|
await client.send_json(
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"type": "resolution_center/dismiss_issue",
|
"type": "resolution_center/dismiss_issue",
|
||||||
"domain": "test",
|
"domain": "fake_integration",
|
||||||
"issue_id": "no_such_issue",
|
"issue_id": "no_such_issue",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -66,7 +130,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"type": "resolution_center/dismiss_issue",
|
"type": "resolution_center/dismiss_issue",
|
||||||
"domain": "test",
|
"domain": "fake_integration",
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -90,6 +154,185 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fix_non_existing_issue(
|
||||||
|
hass: HomeAssistant, hass_client, hass_ws_client
|
||||||
|
) -> None:
|
||||||
|
"""Test trying to fix an issue that doesn't exist."""
|
||||||
|
assert await async_setup_component(hass, "http", {})
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
issues = await create_issues(hass, ws_client)
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix"
|
||||||
|
resp = await client.post(
|
||||||
|
url, json={"handler": "no_such_integration", "issue_id": "no_such_issue"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status != HTTPStatus.OK
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix"
|
||||||
|
resp = await client.post(
|
||||||
|
url, json={"handler": "fake_integration", "issue_id": "no_such_issue"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status != HTTPStatus.OK
|
||||||
|
|
||||||
|
await ws_client.send_json({"id": 3, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {
|
||||||
|
"issues": [
|
||||||
|
dict(
|
||||||
|
issue,
|
||||||
|
dismissed=False,
|
||||||
|
dismissed_version=None,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None:
|
||||||
|
"""Test we can fix an issue."""
|
||||||
|
assert await async_setup_component(hass, "http", {})
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
await create_issues(hass, ws_client)
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix"
|
||||||
|
resp = await client.post(
|
||||||
|
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
flow_id = data["flow_id"]
|
||||||
|
assert data == {
|
||||||
|
"data_schema": [],
|
||||||
|
"description_placeholders": None,
|
||||||
|
"errors": None,
|
||||||
|
"flow_id": ANY,
|
||||||
|
"handler": "fake_integration",
|
||||||
|
"last_step": None,
|
||||||
|
"step_id": "confirm",
|
||||||
|
"type": "form",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"/api/resolution_center/issues/fix/{flow_id}"
|
||||||
|
# Test we can get the status of the flow
|
||||||
|
resp2 = await client.get(url)
|
||||||
|
|
||||||
|
assert resp2.status == HTTPStatus.OK
|
||||||
|
data2 = await resp2.json()
|
||||||
|
|
||||||
|
assert data == data2
|
||||||
|
|
||||||
|
resp = await client.post(url)
|
||||||
|
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
flow_id = data["flow_id"]
|
||||||
|
assert data == {
|
||||||
|
"description": None,
|
||||||
|
"description_placeholders": None,
|
||||||
|
"flow_id": flow_id,
|
||||||
|
"handler": "fake_integration",
|
||||||
|
"title": None,
|
||||||
|
"type": "create_entry",
|
||||||
|
"version": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws_client.send_json({"id": 4, "type": "resolution_center/list_issues"})
|
||||||
|
msg = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"] == {"issues": []}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fix_issue_unauth(
|
||||||
|
hass: HomeAssistant, hass_client, hass_admin_user
|
||||||
|
) -> None:
|
||||||
|
"""Test we can't query the result if not authorized."""
|
||||||
|
assert await async_setup_component(hass, "http", {})
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
hass_admin_user.groups = []
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix"
|
||||||
|
resp = await client.post(
|
||||||
|
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == HTTPStatus.UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_progress_unauth(
|
||||||
|
hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user
|
||||||
|
) -> None:
|
||||||
|
"""Test we can't fix an issue if not authorized."""
|
||||||
|
assert await async_setup_component(hass, "http", {})
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
await create_issues(hass, ws_client)
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix"
|
||||||
|
resp = await client.post(
|
||||||
|
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
|
||||||
|
)
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
data = await resp.json()
|
||||||
|
flow_id = data["flow_id"]
|
||||||
|
|
||||||
|
hass_admin_user.groups = []
|
||||||
|
|
||||||
|
url = f"/api/resolution_center/issues/fix/{flow_id}"
|
||||||
|
# Test we can't get the status of the flow
|
||||||
|
resp = await client.get(url)
|
||||||
|
assert resp.status == HTTPStatus.UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_unauth(
|
||||||
|
hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user
|
||||||
|
) -> None:
|
||||||
|
"""Test we can't fix an issue if not authorized."""
|
||||||
|
assert await async_setup_component(hass, "http", {})
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
await create_issues(hass, ws_client)
|
||||||
|
|
||||||
|
url = "/api/resolution_center/issues/fix"
|
||||||
|
resp = await client.post(
|
||||||
|
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
|
||||||
|
)
|
||||||
|
assert resp.status == HTTPStatus.OK
|
||||||
|
data = await resp.json()
|
||||||
|
flow_id = data["flow_id"]
|
||||||
|
|
||||||
|
hass_admin_user.groups = []
|
||||||
|
|
||||||
|
url = f"/api/resolution_center/issues/fix/{flow_id}"
|
||||||
|
# Test we can't get the status of the flow
|
||||||
|
resp = await client.post(url)
|
||||||
|
assert resp.status == HTTPStatus.UNAUTHORIZED
|
||||||
|
|
||||||
|
|
||||||
async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
|
async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
|
||||||
"""Test we can list issues."""
|
"""Test we can list issues."""
|
||||||
assert await async_setup_component(hass, DOMAIN, {})
|
assert await async_setup_component(hass, DOMAIN, {})
|
||||||
@ -106,6 +349,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
{
|
{
|
||||||
"breaks_in_ha_version": "2022.9",
|
"breaks_in_ha_version": "2022.9",
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
|
"is_fixable": True,
|
||||||
"issue_id": "issue_1",
|
"issue_id": "issue_1",
|
||||||
"learn_more_url": "https://theuselessweb.com",
|
"learn_more_url": "https://theuselessweb.com",
|
||||||
"severity": "error",
|
"severity": "error",
|
||||||
@ -115,6 +359,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
{
|
{
|
||||||
"breaks_in_ha_version": "2022.8",
|
"breaks_in_ha_version": "2022.8",
|
||||||
"domain": "test",
|
"domain": "test",
|
||||||
|
"is_fixable": False,
|
||||||
"issue_id": "issue_2",
|
"issue_id": "issue_2",
|
||||||
"learn_more_url": "https://theuselessweb.com/abc",
|
"learn_more_url": "https://theuselessweb.com/abc",
|
||||||
"severity": "other",
|
"severity": "other",
|
||||||
@ -129,6 +374,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None:
|
|||||||
issue["domain"],
|
issue["domain"],
|
||||||
issue["issue_id"],
|
issue["issue_id"],
|
||||||
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
breaks_in_ha_version=issue["breaks_in_ha_version"],
|
||||||
|
is_fixable=issue["is_fixable"],
|
||||||
learn_more_url=issue["learn_more_url"],
|
learn_more_url=issue["learn_more_url"],
|
||||||
severity=issue["severity"],
|
severity=issue["severity"],
|
||||||
translation_key=issue["translation_key"],
|
translation_key=issue["translation_key"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user