From 45a80f182df3d67acc8b95b26985b38322511f65 Mon Sep 17 00:00:00 2001
From: Raman Gupta <7243222+raman325@users.noreply.github.com>
Date: Tue, 22 Mar 2022 23:50:02 -0400
Subject: [PATCH] Dump entities in zwave_js device diagnostics (#68536)

---
 .../components/zwave_js/diagnostics.py        | 61 ++++++++++++++++++-
 tests/components/zwave_js/test_diagnostics.py |  8 ++-
 2 files changed, 67 insertions(+), 2 deletions(-)

diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py
index c9e45f09685..dd88f2b6d07 100644
--- a/homeassistant/components/zwave_js/diagnostics.py
+++ b/homeassistant/components/zwave_js/diagnostics.py
@@ -1,9 +1,11 @@
 """Provides diagnostics for Z-Wave JS."""
 from __future__ import annotations
 
+from typing import Any
+
 from zwave_js_server.client import Client
 from zwave_js_server.dump import dump_msgs
-from zwave_js_server.model.node import NodeDataType
+from zwave_js_server.model.node import Node, NodeDataType
 
 from homeassistant.components.diagnostics.util import async_redact_data
 from homeassistant.config_entries import ConfigEntry
@@ -11,6 +13,8 @@ from homeassistant.const import CONF_URL
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers import device_registry as dr
 from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.device_registry import DeviceEntry
+from homeassistant.helpers.entity_registry import async_entries_for_device, async_get
 
 from .const import DATA_CLIENT, DOMAIN
 from .helpers import get_home_and_node_id_from_device_entry
@@ -18,6 +22,59 @@ from .helpers import get_home_and_node_id_from_device_entry
 TO_REDACT = {"homeId", "location"}
 
 
+def get_device_entities(
+    hass: HomeAssistant, node: Node, device: DeviceEntry
+) -> list[dict[str, Any]]:
+    """Get entities for a device."""
+    entity_entries = async_entries_for_device(
+        async_get(hass), device.id, include_disabled_entities=True
+    )
+    entities = []
+    for entry in entity_entries:
+        state_key = None
+        split_unique_id = entry.unique_id.split(".")
+        # If the unique ID has three parts, it's either one of the generic per node
+        # entities (node status sensor, ping button) or a binary sensor for a particular
+        # state. If we can get the state key, we will add it to the dictionary.
+        if len(split_unique_id) == 3:
+            try:
+                state_key = int(split_unique_id[-1])
+            # If the third part of the unique ID isn't a state key, the entity must be a
+            # generic entity. We won't add those since they won't help with
+            # troubleshooting.
+            except ValueError:
+                continue
+        value_id = split_unique_id[1]
+        zwave_value = node.values[value_id]
+        primary_value_data = {
+            "command_class": zwave_value.command_class,
+            "command_class_name": zwave_value.command_class_name,
+            "endpoint": zwave_value.endpoint,
+            "property": zwave_value.property_,
+            "property_name": zwave_value.property_name,
+            "property_key": zwave_value.property_key,
+            "property_key_name": zwave_value.property_key_name,
+        }
+        if state_key is not None:
+            primary_value_data["state_key"] = state_key
+        entity = {
+            "domain": entry.domain,
+            "entity_id": entry.entity_id,
+            "original_name": entry.original_name,
+            "original_device_class": entry.original_device_class,
+            "disabled": entry.disabled,
+            "disabled_by": entry.disabled_by,
+            "hidden_by": entry.hidden_by,
+            "original_icon": entry.original_icon,
+            "entity_category": entry.entity_category,
+            "supported_features": entry.supported_features,
+            "unit_of_measurement": entry.unit_of_measurement,
+            "primary_value": primary_value_data,
+        }
+        entities.append(entity)
+    return entities
+
+
 async def async_get_config_entry_diagnostics(
     hass: HomeAssistant, config_entry: ConfigEntry
 ) -> list[dict]:
@@ -38,6 +95,7 @@ async def async_get_device_diagnostics(
     if node_id is None or node_id not in client.driver.controller.nodes:
         raise ValueError(f"Node for device {device.id} can't be found")
     node = client.driver.controller.nodes[node_id]
+    entities = get_device_entities(hass, node, device)
     return {
         "versionInfo": {
             "driverVersion": client.version.driver_version,
@@ -45,5 +103,6 @@ async def async_get_device_diagnostics(
             "minSchemaVersion": client.version.min_schema_version,
             "maxSchemaVersion": client.version.max_schema_version,
         },
+        "entities": entities,
         "state": async_redact_data(node.data, TO_REDACT),
     }
diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py
index 332c8c84635..84fb401e31b 100644
--- a/tests/components/zwave_js/test_diagnostics.py
+++ b/tests/components/zwave_js/test_diagnostics.py
@@ -5,6 +5,7 @@ import pytest
 from zwave_js_server.event import Event
 
 from homeassistant.components.zwave_js.diagnostics import async_get_device_diagnostics
+from homeassistant.components.zwave_js.discovery import async_discover_node_values
 from homeassistant.components.zwave_js.helpers import get_device_id
 from homeassistant.helpers.device_registry import async_get
 
@@ -69,7 +70,12 @@ async def test_device_diagnostics(
         "minSchemaVersion": 0,
         "maxSchemaVersion": 0,
     }
-
+    # Assert that we only have the entities that were discovered for this device
+    # Entities that are created outside of discovery (e.g. node status sensor and
+    # ping button) should not be in dump.
+    assert len(diagnostics_data["entities"]) == len(
+        list(async_discover_node_values(multisensor_6, device, {device.id: set()}))
+    )
     assert diagnostics_data["state"] == multisensor_6.data
 
 
-- 
GitLab