Add support for ignoring zwave_js device config file changes (#108990)

* Add support for ignoring zwave_js device config file changes

* mistake

* fixes

* Small tweaks and add/update tests
This commit is contained in:
Raman Gupta 2024-01-30 04:16:08 -05:00 committed by GitHub
parent dcb5c0d439
commit 821d273e4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 27 deletions

View File

@ -1,12 +1,12 @@
"""Repairs for Z-Wave JS.""" """Repairs for Z-Wave JS."""
from __future__ import annotations from __future__ import annotations
import voluptuous as vol
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow
from homeassistant.core import HomeAssistant 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 from .helpers import async_get_node_from_device_id
@ -15,34 +15,44 @@ class DeviceConfigFileChangedFlow(RepairsFlow):
def __init__(self, data: dict[str, str]) -> None: def __init__(self, data: dict[str, str]) -> None:
"""Initialize.""" """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"] self.device_id: str = data["device_id"]
async def async_step_init( async def async_step_init(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult: ) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow.""" """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( async def async_step_confirm(
self, user_input: dict[str, str] | None = None self, user_input: dict[str, str] | None = None
) -> data_entry_flow.FlowResult: ) -> data_entry_flow.FlowResult:
"""Handle the confirm step of a fix flow.""" """Handle the confirm step of a fix flow."""
if user_input is not None: try:
try: node = async_get_node_from_device_id(self.hass, self.device_id)
node = async_get_node_from_device_id(self.hass, self.device_id) except ValueError:
except ValueError: return self.async_abort(
return self.async_abort( reason="cannot_connect",
reason="cannot_connect", description_placeholders=self.description_placeholders,
description_placeholders={"device_name": self.device_name}, )
) self.hass.async_create_task(node.async_refresh_info())
self.hass.async_create_task(node.async_refresh_info()) return self.async_create_entry(title="", data={})
return self.async_create_entry(title="", data={})
return self.async_show_form( async def async_step_ignore(
step_id="confirm", self, user_input: dict[str, str] | None = None
data_schema=vol.Schema({}), ) -> data_entry_flow.FlowResult:
description_placeholders={"device_name": self.device_name}, """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,
) )

View File

@ -155,13 +155,18 @@
"title": "Device configuration file changed: {device_name}", "title": "Device configuration file changed: {device_name}",
"fix_flow": { "fix_flow": {
"step": { "step": {
"confirm": { "init": {
"menu_options": {
"confirm": "Re-interview device",
"ignore": "Ignore device config update"
},
"title": "Device configuration file changed: {device_name}", "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": { "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."
} }
} }
} }

View File

@ -50,7 +50,7 @@ async def _trigger_repair_issue(
return node return node
async def test_device_config_file_changed( async def test_device_config_file_changed_confirm_step(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
@ -58,7 +58,7 @@ async def test_device_config_file_changed(
multisensor_6_state, multisensor_6_state,
integration, integration,
) -> None: ) -> None:
"""Test the device_config_file_changed issue.""" """Test the device_config_file_changed issue confirm step."""
dev_reg = dr.async_get(hass) dev_reg = dr.async_get(hass)
node = await _trigger_repair_issue(hass, client, multisensor_6_state) 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() data = await resp.json()
flow_id = data["flow_id"] flow_id = data["flow_id"]
assert data["step_id"] == "confirm" assert data["step_id"] == "init"
assert data["description_placeholders"] == {"device_name": device.name} assert data["description_placeholders"] == {"device_name": device.name}
# Apply fix
url = RepairsFlowResourceView.url.format(flow_id=flow_id) url = RepairsFlowResourceView.url.format(flow_id=flow_id)
# Show menu
resp = await http_client.post(url) resp = await http_client.post(url)
assert resp.status == HTTPStatus.OK assert resp.status == HTTPStatus.OK
data = await resp.json() 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" assert data["type"] == "create_entry"
await hass.async_block_till_done() await hass.async_block_till_done()
@ -114,6 +123,78 @@ async def test_device_config_file_changed(
assert len(msg["result"]["issues"]) == 0 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( async def test_invalid_issue(
hass: HomeAssistant, hass: HomeAssistant,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
@ -196,14 +277,14 @@ async def test_abort_confirm(
data = await resp.json() data = await resp.json()
flow_id = data["flow_id"] 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 # Unload config entry so we can't connect to the node
await hass.config_entries.async_unload(integration.entry_id) await hass.config_entries.async_unload(integration.entry_id)
# Apply fix # Apply fix
url = RepairsFlowResourceView.url.format(flow_id=flow_id) 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 assert resp.status == HTTPStatus.OK
data = await resp.json() data = await resp.json()