diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index 83ee0523a3b5c..1010d9abd9017 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -1,12 +1,12 @@ """Repairs for Z-Wave JS.""" from __future__ import annotations -import voluptuous as vol - from homeassistant import data_entry_flow from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from .const import DOMAIN from .helpers import async_get_node_from_device_id @@ -15,34 +15,44 @@ class DeviceConfigFileChangedFlow(RepairsFlow): def __init__(self, data: dict[str, str]) -> None: """Initialize.""" - self.device_name: str = data["device_name"] + self.description_placeholders: dict[str, str] = { + "device_name": data["device_name"] + } self.device_id: str = data["device_id"] 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() + return self.async_show_menu( + menu_options=["confirm", "ignore"], + description_placeholders=self.description_placeholders, + ) 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: - try: - node = async_get_node_from_device_id(self.hass, self.device_id) - except ValueError: - return self.async_abort( - reason="cannot_connect", - description_placeholders={"device_name": self.device_name}, - ) - self.hass.async_create_task(node.async_refresh_info()) - return self.async_create_entry(title="", data={}) - - return self.async_show_form( - step_id="confirm", - data_schema=vol.Schema({}), - description_placeholders={"device_name": self.device_name}, + try: + node = async_get_node_from_device_id(self.hass, self.device_id) + except ValueError: + return self.async_abort( + reason="cannot_connect", + description_placeholders=self.description_placeholders, + ) + self.hass.async_create_task(node.async_refresh_info()) + return self.async_create_entry(title="", data={}) + + async def async_step_ignore( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the ignore step of a fix flow.""" + ir.async_get(self.hass).async_ignore( + DOMAIN, f"device_config_file_changed.{self.device_id}", True + ) + return self.async_abort( + reason="issue_ignored", + description_placeholders=self.description_placeholders, ) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index ee6a7c3d0b738..8dadac12af126 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -155,13 +155,18 @@ "title": "Device configuration file changed: {device_name}", "fix_flow": { "step": { - "confirm": { + "init": { + "menu_options": { + "confirm": "Re-interview device", + "ignore": "Ignore device config update" + }, "title": "Device configuration file changed: {device_name}", - "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you'd like to proceed, click on SUBMIT below. The re-interview will take place in the background." + "description": "The device configuration file for {device_name} has changed.\n\nZ-Wave JS discovers a lot of device metadata by interviewing the device. However, some of the information has to be loaded from a configuration file. Some of this information is only evaluated once, during the device interview.\n\nWhen a device config file is updated, this information may be stale and and the device must be re-interviewed to pick up the changes.\n\n This is not a required operation and device functionality will be impacted during the re-interview process, but you may see improvements for your device once it is complete.\n\nIf you decide to proceed with the re-interview, it will take place in the background." } }, "abort": { - "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant." + "cannot_connect": "Cannot connect to {device_name}. Please try again later after confirming that your Z-Wave network is up and connected to Home Assistant.", + "issue_ignored": "Device config file update for {device_name} ignored." } } } diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index d18bcfa09aa53..d2b702089f290 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -50,7 +50,7 @@ async def _trigger_repair_issue( return node -async def test_device_config_file_changed( +async def test_device_config_file_changed_confirm_step( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_ws_client: WebSocketGenerator, @@ -58,7 +58,7 @@ async def test_device_config_file_changed( multisensor_6_state, integration, ) -> None: - """Test the device_config_file_changed issue.""" + """Test the device_config_file_changed issue confirm step.""" dev_reg = dr.async_get(hass) node = await _trigger_repair_issue(hass, client, multisensor_6_state) @@ -87,16 +87,25 @@ async def test_device_config_file_changed( data = await resp.json() flow_id = data["flow_id"] - assert data["step_id"] == "confirm" + assert data["step_id"] == "init" assert data["description_placeholders"] == {"device_name": device.name} - # Apply fix 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() @@ -114,6 +123,78 @@ async def test_device_config_file_changed( assert len(msg["result"]["issues"]) == 0 +async def test_device_config_file_changed_ignore_step( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, + client, + multisensor_6_state, + integration, +) -> None: + """Test the device_config_file_changed issue ignore step.""" + dev_reg = dr.async_get(hass) + node = await _trigger_repair_issue(hass, client, multisensor_6_state) + + client.async_send_command_no_wait.reset_mock() + + device = dev_reg.async_get_device(identifiers={get_device_id(client.driver, node)}) + assert device + issue_id = f"device_config_file_changed.{device.id}" + + 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"] == {"device_name": device.name} + + 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"] == {"device_name": device.name} + + 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" + + # Ignore the issue + 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" + assert data["description_placeholders"] == {"device_name": device.name} + + await hass.async_block_till_done() + + assert len(client.async_send_command_no_wait.call_args_list) == 0 + + # Assert the issue still exists but is ignored + 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 + + async def test_invalid_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -196,14 +277,14 @@ async def test_abort_confirm( data = await resp.json() flow_id = data["flow_id"] - assert data["step_id"] == "confirm" + assert data["step_id"] == "init" # Unload config entry so we can't connect to the node await hass.config_entries.async_unload(integration.entry_id) # Apply fix url = RepairsFlowResourceView.url.format(flow_id=flow_id) - resp = await http_client.post(url) + resp = await http_client.post(url, json={"next_step_id": "confirm"}) assert resp.status == HTTPStatus.OK data = await resp.json()