Files

199 lines
6.8 KiB
Python

"""The repairs websocket API."""
from collections.abc import Callable
from http import HTTPStatus
from typing import Any, override
from aiohttp import web
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.http.data_validator import RequestDataValidator
from homeassistant.components.http.decorators import require_admin
from homeassistant.config_entries import ConfigEntry, UnknownEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView,
FlowManagerResourceView,
)
from .const import DOMAIN
from .issue_handler import RepairsFlowManager
from .models import RepairsFlowResult
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the repairs websocket API."""
websocket_api.async_register_command(hass, ws_get_issue_data)
websocket_api.async_register_command(hass, ws_ignore_issue)
websocket_api.async_register_command(hass, ws_list_issues)
hass.http.register_view(RepairsFlowIndexView(hass.data[DOMAIN]["flow_manager"]))
hass.http.register_view(RepairsFlowResourceView(hass.data[DOMAIN]["flow_manager"]))
@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "repairs/get_issue_data",
vol.Required("domain"): str,
vol.Required("issue_id"): str,
}
)
def ws_get_issue_data(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Fix an issue."""
issue_registry = ir.async_get(hass)
if not (issue := issue_registry.async_get_issue(msg["domain"], msg["issue_id"])):
connection.send_error(
msg["id"],
"unknown_issue",
f"Issue '{msg['issue_id']}' not found",
)
return
connection.send_result(msg["id"], {"issue_data": issue.data})
@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "repairs/ignore_issue",
vol.Required("domain"): str,
vol.Required("issue_id"): str,
vol.Required("ignore"): bool,
}
)
def ws_ignore_issue(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Fix an issue."""
ir.async_ignore_issue(hass, msg["domain"], msg["issue_id"], msg["ignore"])
connection.send_result(msg["id"])
@websocket_api.websocket_command(
{
vol.Required("type"): "repairs/list_issues",
}
)
@callback
def ws_list_issues(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Return a list of issues."""
issue_registry = ir.async_get(hass)
issues = [
{
"breaks_in_ha_version": issue.breaks_in_ha_version,
"created": issue.created,
"dismissed_version": issue.dismissed_version,
"ignored": issue.dismissed_version is not None,
"domain": issue.domain,
"is_fixable": issue.is_fixable,
"issue_domain": issue.issue_domain,
"issue_id": issue.issue_id,
"learn_more_url": issue.learn_more_url,
"severity": issue.severity,
"translation_key": issue.translation_key,
"translation_placeholders": issue.translation_placeholders,
}
for issue in issue_registry.issues.values()
if issue.active
]
connection.send_result(msg["id"], {"issues": issues})
def _prepare_repairs_flow_result_json(
result: RepairsFlowResult,
prepare_result_json: Callable[[RepairsFlowResult], dict[str, Any]],
) -> dict[str, Any]:
"""Convert result to serializable JSON dict."""
entry: ConfigEntry | None = result.pop("result", None)
data = prepare_result_json(result)
if entry is not None:
# Overwrite the ConfigEntry object with its json representation for frontend.
data["result"] = entry.as_json_fragment
return data
class RepairsFlowIndexView(FlowManagerIndexView[RepairsFlowManager, RepairsFlowResult]):
"""View to create issue fix flows."""
url = "/api/repairs/issues/fix"
name = "api:repairs:issues:fix"
@require_admin(permission=POLICY_EDIT)
@RequestDataValidator(
vol.Schema(
{
vol.Required("handler"): str,
vol.Required("issue_id"): str,
},
extra=vol.ALLOW_EXTRA,
)
)
@override
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
"""Handle a POST request."""
try:
result = await self._flow_mgr.async_init(
data["handler"],
data={"issue_id": data["issue_id"]},
)
except data_entry_flow.UnknownFlow as ex:
return self.json_message(
f"Unknown flow{f': {ex!s}' if str(ex) else ''}",
HTTPStatus.NOT_FOUND,
)
except data_entry_flow.UnknownStep as ex:
return self.json_message(str(ex), HTTPStatus.BAD_REQUEST)
except UnknownEntry as ex:
return self.json_message(
f"Config entry {ex!s} not found in next_flow", HTTPStatus.BAD_REQUEST
)
return self.json(self._prepare_result_json(result))
@override
def _prepare_result_json(self, result: RepairsFlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
return _prepare_repairs_flow_result_json(result, super()._prepare_result_json)
class RepairsFlowResourceView(
FlowManagerResourceView[RepairsFlowManager, RepairsFlowResult]
):
"""View to interact with the option flow manager."""
url = "/api/repairs/issues/fix/{flow_id}"
name = "api:repairs:issues:fix:resource"
@require_admin(permission=POLICY_EDIT)
@override
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(permission=POLICY_EDIT)
@override
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
try:
result = await super().post(request, flow_id)
except UnknownEntry as ex:
# Raised by _async_set_next_flow_if_valid in a RepairsFlow
return self.json_message(
f"Config entry {ex!s} not found in next_flow", HTTPStatus.BAD_REQUEST
)
return result
@override
def _prepare_result_json(self, result: RepairsFlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
return _prepare_repairs_flow_result_json(result, super()._prepare_result_json)