Allow creating fixable repairs issues without flows (#76224)

* Allow creating fixable repairs issues without flows

* Add test

* Adjust test
This commit is contained in:
Erik Montnemery 2022-08-05 12:07:51 +02:00 committed by GitHub
parent 861b694cff
commit b366090175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 140 additions and 42 deletions

View File

@ -211,6 +211,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
translation_key="unfixable_problem",
)
async_create_issue(
hass,
DOMAIN,
"bad_psu",
is_fixable=True,
learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU",
severity=IssueSeverity.CRITICAL,
translation_key="bad_psu",
)
return True

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
class DemoFixFlow(RepairsFlow):
@ -23,11 +23,16 @@ class DemoFixFlow(RepairsFlow):
) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow."""
if user_input is not None:
return self.async_create_entry(title="Fixed issue", data={})
return self.async_create_entry(title="", data={})
return self.async_show_form(step_id="confirm", data_schema=vol.Schema({}))
async def async_create_fix_flow(hass, issue_id):
"""Create flow."""
if issue_id == "bad_psu":
# The bad_psu issue doesn't have its own flow
return ConfirmRepairFlow()
# Other issues have a custom flow
return DemoFixFlow()

View File

@ -1,13 +1,24 @@
{
"title": "Demo",
"issues": {
"bad_psu": {
"title": "The power supply is not stable",
"fix_flow": {
"step": {
"confirm": {
"title": "The power supply needs to be replaced",
"description": "Press SUBMIT to confirm the power supply has been replaced"
}
}
}
},
"out_of_blinker_fluid": {
"title": "The blinker fluid is empty and needs to be refilled",
"fix_flow": {
"step": {
"confirm": {
"title": "Blinker fluid needs to be refilled",
"description": "Press OK when blinker fluid has been refilled"
"description": "Press SUBMIT when blinker fluid has been refilled"
}
}
}

View File

@ -1,10 +1,21 @@
{
"issues": {
"bad_psu": {
"fix_flow": {
"step": {
"confirm": {
"description": "Press SUBMIT to confirm the power supply has been replaced",
"title": "The power supply needs to be replaced"
}
}
},
"title": "The power supply is not stable"
},
"out_of_blinker_fluid": {
"fix_flow": {
"step": {
"confirm": {
"description": "Press OK when blinker fluid has been refilled",
"description": "Press SUBMIT when blinker fluid has been refilled",
"title": "Blinker fluid needs to be refilled"
}
}

View File

@ -7,6 +7,7 @@ from homeassistant.helpers.typing import ConfigType
from . import issue_handler, websocket_api
from .const import DOMAIN
from .issue_handler import (
ConfirmRepairFlow,
async_create_issue,
async_delete_issue,
create_issue,
@ -21,6 +22,7 @@ __all__ = [
"create_issue",
"delete_issue",
"DOMAIN",
"ConfirmRepairFlow",
"IssueSeverity",
"RepairsFlow",
]

View File

@ -5,6 +5,7 @@ import functools as ft
from typing import Any
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.core import HomeAssistant, callback
@ -19,6 +20,26 @@ from .issue_registry import async_get as async_get_issue_registry
from .models import IssueSeverity, RepairsFlow, RepairsProtocol
class ConfirmRepairFlow(RepairsFlow):
"""Handler for an issue fixing flow without any side effects."""
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="", data={})
return self.async_show_form(step_id="confirm", data_schema=vol.Schema({}))
class RepairsFlowManager(data_entry_flow.FlowManager):
"""Manage repairs flows."""
@ -30,14 +51,6 @@ class RepairsFlowManager(data_entry_flow.FlowManager):
data: dict[str, Any] | None = None,
) -> RepairsFlow:
"""Create a flow. platform is a repairs module."""
if "platforms" not in self.hass.data[DOMAIN]:
await async_process_repairs_platforms(self.hass)
platforms: dict[str, RepairsProtocol] = 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"]
@ -46,6 +59,14 @@ class RepairsFlowManager(data_entry_flow.FlowManager):
if issue is None or not issue.is_fixable:
raise data_entry_flow.UnknownStep
if "platforms" not in self.hass.data[DOMAIN]:
await async_process_repairs_platforms(self.hass)
platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"]
if handler_key not in platforms:
return ConfirmRepairFlow()
platform = platforms[handler_key]
return await platform.async_create_fix_flow(self.hass, issue_id)
async def async_finish_flow(

View File

@ -129,6 +129,20 @@ async def test_issues_created(hass, hass_client, hass_ws_client):
"translation_key": "unfixable_problem",
"translation_placeholders": None,
},
{
"breaks_in_ha_version": None,
"created": ANY,
"dismissed_version": None,
"domain": "demo",
"ignored": False,
"is_fixable": True,
"issue_domain": None,
"issue_id": "bad_psu",
"learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU",
"severity": "critical",
"translation_key": "bad_psu",
"translation_placeholders": None,
},
]
}
@ -164,7 +178,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client):
"description_placeholders": None,
"flow_id": flow_id,
"handler": "demo",
"title": "Fixed issue",
"title": "",
"type": "create_entry",
"version": 1,
}
@ -203,5 +217,19 @@ async def test_issues_created(hass, hass_client, hass_ws_client):
"translation_key": "unfixable_problem",
"translation_placeholders": None,
},
{
"breaks_in_ha_version": None,
"created": ANY,
"dismissed_version": None,
"domain": "demo",
"ignored": False,
"is_fixable": True,
"issue_domain": None,
"issue_id": "bad_psu",
"learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU",
"severity": "critical",
"translation_key": "bad_psu",
"translation_placeholders": None,
},
]
}

View File

@ -21,21 +21,24 @@ from homeassistant.setup import async_setup_component
from tests.common import mock_platform
DEFAULT_ISSUES = [
{
"breaks_in_ha_version": "2022.9",
"domain": "fake_integration",
"issue_id": "issue_1",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com",
"severity": "error",
"translation_key": "abc_123",
"translation_placeholders": {"abc": "123"},
}
]
async def create_issues(hass, ws_client):
async def create_issues(hass, ws_client, issues=None):
"""Create issues."""
issues = [
{
"breaks_in_ha_version": "2022.9",
"domain": "fake_integration",
"issue_id": "issue_1",
"is_fixable": True,
"learn_more_url": "https://theuselessweb.com",
"severity": "error",
"translation_key": "abc_123",
"translation_placeholders": {"abc": "123"},
},
]
if issues is None:
issues = DEFAULT_ISSUES
for issue in issues:
async_create_issue(
@ -79,23 +82,22 @@ class MockFixFlow(RepairsFlow):
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
return await (self.async_step_confirm())
return await (self.async_step_custom_step())
async def async_step_confirm(
async def async_step_custom_step(
self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow."""
"""Handle a custom_step step of a fix flow."""
if user_input is not None:
return self.async_create_entry(title=None, data=None)
return self.async_create_entry(title="", data={})
return self.async_show_form(step_id="confirm", data_schema=vol.Schema({}))
return self.async_show_form(step_id="custom_step", data_schema=vol.Schema({}))
@pytest.fixture(autouse=True)
async def mock_repairs_integration(hass):
"""Mock a repairs integration."""
hass.config.components.add("fake_integration")
hass.config.components.add("integration_without_diagnostics")
def async_create_fix_flow(hass, issue_id):
return MockFixFlow()
@ -107,7 +109,7 @@ async def mock_repairs_integration(hass):
)
mock_platform(
hass,
"integration_without_diagnostics.repairs",
"integration_without_repairs.repairs",
Mock(spec=[]),
)
@ -237,7 +239,16 @@ async def test_fix_non_existing_issue(
}
async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None:
@pytest.mark.parametrize(
"domain, step",
(
("fake_integration", "custom_step"),
("fake_integration_default_handler", "confirm"),
),
)
async def test_fix_issue(
hass: HomeAssistant, hass_client, hass_ws_client, domain, step
) -> None:
"""Test we can fix an issue."""
assert await async_setup_component(hass, "http", {})
assert await async_setup_component(hass, DOMAIN, {})
@ -245,12 +256,11 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No
ws_client = await hass_ws_client(hass)
client = await hass_client()
await create_issues(hass, ws_client)
issues = [{**DEFAULT_ISSUES[0], "domain": domain}]
await create_issues(hass, ws_client, issues=issues)
url = "/api/repairs/issues/fix"
resp = await client.post(
url, json={"handler": "fake_integration", "issue_id": "issue_1"}
)
resp = await client.post(url, json={"handler": domain, "issue_id": "issue_1"})
assert resp.status == HTTPStatus.OK
data = await resp.json()
@ -261,9 +271,9 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No
"description_placeholders": None,
"errors": None,
"flow_id": ANY,
"handler": "fake_integration",
"handler": domain,
"last_step": None,
"step_id": "confirm",
"step_id": step,
"type": "form",
}
@ -286,8 +296,8 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No
"description": None,
"description_placeholders": None,
"flow_id": flow_id,
"handler": "fake_integration",
"title": None,
"handler": domain,
"title": "",
"type": "create_entry",
"version": 1,
}