From d3c5b9777b4dcbf82d28ec4ea90c31631231f325 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 4 Oct 2023 04:18:48 -0400 Subject: [PATCH] Notify users when zwave device gets reset (#101362) * Notify users when zwave device gets reset * review comments --- homeassistant/components/zwave_js/__init__.py | 42 +++++++++----- homeassistant/components/zwave_js/helpers.py | 13 +++++ tests/components/zwave_js/test_init.py | 58 +++++++++++++++++++ 3 files changed, 99 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index c9decc92a67..a8b3d300e3b 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -113,6 +113,7 @@ from .helpers import ( async_enable_statistics, get_device_id, get_device_id_ext, + get_network_identifier_for_notification, get_unique_id, get_valueless_base_unique_id, ) @@ -448,6 +449,28 @@ class ControllerEvents: "remove_entity" ), ) + elif reason == RemoveNodeReason.RESET: + device_name = device.name_by_user or device.name or f"Node {node.node_id}" + identifier = get_network_identifier_for_notification( + self.hass, self.config_entry, self.driver_events.driver.controller + ) + notification_msg = ( + f"`{device_name}` has been factory reset " + "and removed from the Z-Wave network" + ) + if identifier: + # Remove trailing comma if it's there + if identifier[-1] == ",": + identifier = identifier[:-1] + notification_msg = f"{notification_msg} {identifier}." + else: + notification_msg = f"{notification_msg}." + async_create( + self.hass, + notification_msg, + "Device Was Factory Reset!", + f"{DOMAIN}.node_reset_and_removed.{dev_id[1]}", + ) else: self.remove_device(device) @@ -459,26 +482,17 @@ class ControllerEvents: dev_id = get_device_id(self.driver_events.driver, node) device = self.dev_reg.async_get_device(identifiers={dev_id}) assert device - device_name = device.name_by_user or device.name - home_id = self.driver_events.driver.controller.home_id - # We do this because we know at this point the controller has its home ID as - # as it is part of the device ID - assert home_id + device_name = device.name_by_user or device.name or f"Node {node.node_id}" # In case the user has multiple networks, we should give them more information # about the network for the controller being identified. - identifier = "" - if len(self.hass.config_entries.async_entries(DOMAIN)) > 1: - if str(home_id) != self.config_entry.title: - identifier = ( - f"`{self.config_entry.title}`, with the home ID `{home_id}`, " - ) - else: - identifier = f"with the home ID `{home_id}` " + identifier = get_network_identifier_for_notification( + self.hass, self.config_entry, self.driver_events.driver.controller + ) async_create( self.hass, ( f"`{device_name}` has just requested the controller for your Z-Wave " - f"network {identifier}to identify itself. No action is needed from " + f"network {identifier} to identify itself. No action is needed from " "you other than to note the source of the request, and you can safely " "dismiss this notification when ready." ), diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 8774bcea73f..5d78d3e57e7 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -14,6 +14,7 @@ from zwave_js_server.const import ( ConfigurationValueType, LogLevel, ) +from zwave_js_server.model.controller import Controller from zwave_js_server.model.driver import Driver from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.node import Node as ZwaveNode @@ -512,3 +513,15 @@ def get_device_info(driver: Driver, node: ZwaveNode) -> DeviceInfo: manufacturer=node.device_config.manufacturer, suggested_area=node.location if node.location else None, ) + + +def get_network_identifier_for_notification( + hass: HomeAssistant, config_entry: ConfigEntry, controller: Controller +) -> str: + """Return the network identifier string for persistent notifications.""" + home_id = str(controller.home_id) + if len(hass.config_entries.async_entries(DOMAIN)) > 1: + if str(home_id) != config_entry.title: + return f"`{config_entry.title}`, with the home ID `{home_id}`," + return f"with the home ID `{home_id}`" + return "" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 1203997839e..c57e3b1f868 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1644,3 +1644,61 @@ async def test_server_logging(hass: HomeAssistant, client) -> None: assert len(client.async_send_command.call_args_list) == 0 assert not client.enable_server_logging.called assert not client.disable_server_logging.called + + +async def test_factory_reset_node( + hass: HomeAssistant, client, multisensor_6, multisensor_6_state, integration +) -> None: + """Test when a node is removed because it was reset.""" + # One config entry scenario + remove_event = Event( + type="node removed", + data={ + "source": "controller", + "event": "node removed", + "reason": 5, + "node": deepcopy(multisensor_6_state), + }, + ) + dev_id = get_device_id(client.driver, multisensor_6) + msg_id = f"{DOMAIN}.node_reset_and_removed.{dev_id[1]}" + + client.driver.controller.receive_event(remove_event) + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + assert list(notifications)[0] == msg_id + assert notifications[msg_id]["message"].startswith("`Multisensor 6`") + assert "with the home ID" not in notifications[msg_id]["message"] + async_dismiss(hass, msg_id) + + # Add mock config entry to simulate having multiple entries + new_entry = MockConfigEntry(domain=DOMAIN) + new_entry.add_to_hass(hass) + + # Re-add the node then remove it again + client.driver.controller.nodes[multisensor_6_state["nodeId"]] = Node( + client, deepcopy(multisensor_6_state) + ) + remove_event.data["node"] = deepcopy(multisensor_6_state) + client.driver.controller.receive_event(remove_event) + # Test case where config entry title and home ID don't match + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + assert list(notifications)[0] == msg_id + assert ( + "network `Mock Title`, with the home ID `3245146787`." + in notifications[msg_id]["message"] + ) + async_dismiss(hass, msg_id) + + # Test case where config entry title and home ID do match + hass.config_entries.async_update_entry(integration, title="3245146787") + client.driver.controller.nodes[multisensor_6_state["nodeId"]] = Node( + client, deepcopy(multisensor_6_state) + ) + remove_event.data["node"] = deepcopy(multisensor_6_state) + client.driver.controller.receive_event(remove_event) + notifications = async_get_persistent_notifications(hass) + assert len(notifications) == 1 + assert list(notifications)[0] == msg_id + assert "network with the home ID `3245146787`" in notifications[msg_id]["message"]