Home Assistant Unofficial Reference 2024.12.1
diagnostics.py
Go to the documentation of this file.
1 """Provides diagnostics for Z-Wave JS."""
2 
3 from __future__ import annotations
4 
5 from copy import deepcopy
6 from typing import Any
7 
8 from zwave_js_server.client import Client
9 from zwave_js_server.const import CommandClass
10 from zwave_js_server.dump import dump_msgs
11 from zwave_js_server.model.node import Node
12 from zwave_js_server.model.value import ValueDataType
13 from zwave_js_server.util.node import dump_node_state
14 
15 from homeassistant.components.diagnostics import REDACTED, async_redact_data
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import CONF_URL
18 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers import device_registry as dr, entity_registry as er
20 from homeassistant.helpers.aiohttp_client import async_get_clientsession
21 
22 from .const import DATA_CLIENT, USER_AGENT
23 from .helpers import (
24  ZwaveValueMatcher,
25  get_home_and_node_id_from_device_entry,
26  get_state_key_from_unique_id,
27  get_value_id_from_unique_id,
28  value_matches_matcher,
29 )
30 
31 KEYS_TO_REDACT = {"homeId", "location"}
32 
33 VALUES_TO_REDACT = (
34  ZwaveValueMatcher(property_="userCode", command_class=CommandClass.USER_CODE),
35 )
36 
37 
38 def _redacted_value(zwave_value: ValueDataType) -> ValueDataType:
39  """Return redacted value of a Z-Wave value."""
40  redacted_value: ValueDataType = deepcopy(zwave_value)
41  redacted_value["value"] = REDACTED
42  return redacted_value
43 
44 
45 def optionally_redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType:
46  """Redact value of a Z-Wave value if it matches criteria to redact."""
47  # If the value has no value, there is nothing to redact
48  if zwave_value.get("value") in (None, ""):
49  return zwave_value
50  if zwave_value.get("metadata", {}).get("secret"):
51  return _redacted_value(zwave_value)
52  for value_to_redact in VALUES_TO_REDACT:
53  if value_matches_matcher(value_to_redact, zwave_value):
54  return _redacted_value(zwave_value)
55  return zwave_value
56 
57 
58 def redact_node_state(node_state: dict) -> dict:
59  """Redact node state."""
60  redacted_state: dict = deepcopy(node_state)
61  # dump_msgs returns values in a list but dump_node_state returns them in a dict
62  if isinstance(node_state["values"], list):
63  redacted_state["values"] = [
65  for zwave_value in node_state["values"]
66  ]
67  else:
68  redacted_state["values"] = {
69  value_id: optionally_redact_value_of_zwave_value(zwave_value)
70  for value_id, zwave_value in node_state["values"].items()
71  }
72  return redacted_state
73 
74 
76  hass: HomeAssistant, node: Node, config_entry: ConfigEntry, device: dr.DeviceEntry
77 ) -> list[dict[str, Any]]:
78  """Get entities for a device."""
79  entity_entries = er.async_entries_for_device(
80  er.async_get(hass), device.id, include_disabled_entities=True
81  )
82  entities = []
83  for entry in sorted(entity_entries):
84  # Skip entities that are not part of this integration
85  if entry.config_entry_id != config_entry.entry_id:
86  continue
87 
88  # If the value ID returns as None, we don't need to include this entity
89  if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None:
90  continue
91 
92  primary_value_data = None
93  if (zwave_value := node.values.get(value_id)) is not None:
94  primary_value_data = {
95  "command_class": zwave_value.command_class,
96  "command_class_name": zwave_value.command_class_name,
97  "endpoint": zwave_value.endpoint,
98  "property": zwave_value.property_,
99  "property_name": zwave_value.property_name,
100  "property_key": zwave_value.property_key,
101  "property_key_name": zwave_value.property_key_name,
102  }
103 
104  state_key = get_state_key_from_unique_id(entry.unique_id)
105  if state_key is not None:
106  primary_value_data["state_key"] = state_key
107 
108  entity = {
109  "domain": entry.domain,
110  "entity_id": entry.entity_id,
111  "original_name": entry.original_name,
112  "original_device_class": entry.original_device_class,
113  "disabled": entry.disabled,
114  "disabled_by": entry.disabled_by,
115  "hidden_by": entry.hidden_by,
116  "original_icon": entry.original_icon,
117  "entity_category": entry.entity_category,
118  "supported_features": entry.supported_features,
119  "unit_of_measurement": entry.unit_of_measurement,
120  "value_id": value_id,
121  "primary_value": primary_value_data,
122  }
123  entities.append(entity)
124  return entities
125 
126 
128  hass: HomeAssistant, config_entry: ConfigEntry
129 ) -> dict[str, Any]:
130  """Return diagnostics for a config entry."""
131  msgs: list[dict] = async_redact_data(
132  await dump_msgs(
133  config_entry.data[CONF_URL], async_get_clientsession(hass), USER_AGENT
134  ),
135  KEYS_TO_REDACT,
136  )
137  handshake_msgs = msgs[:-1]
138  network_state = msgs[-1]
139  network_state["result"]["state"]["nodes"] = [
140  redact_node_state(async_redact_data(node_data, KEYS_TO_REDACT))
141  for node_data in network_state["result"]["state"]["nodes"]
142  ]
143  return {"messages": [*handshake_msgs, network_state]}
144 
145 
147  hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry
148 ) -> dict[str, Any]:
149  """Return diagnostics for a device."""
150  client: Client = config_entry.runtime_data[DATA_CLIENT]
151  identifiers = get_home_and_node_id_from_device_entry(device)
152  node_id = identifiers[1] if identifiers else None
153  driver = client.driver
154  assert driver
155  if node_id is None or node_id not in driver.controller.nodes:
156  raise ValueError(f"Node for device {device.id} can't be found")
157  node = driver.controller.nodes[node_id]
158  entities = get_device_entities(hass, node, config_entry, device)
159  assert client.version
160  node_state = redact_node_state(
161  async_redact_data(dump_node_state(node), KEYS_TO_REDACT)
162  )
163  return {
164  "versionInfo": {
165  "driverVersion": client.version.driver_version,
166  "serverVersion": client.version.server_version,
167  "minSchemaVersion": client.version.min_schema_version,
168  "maxSchemaVersion": client.version.max_schema_version,
169  },
170  "entities": entities,
171  "state": node_state,
172  }
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict async_redact_data(Mapping data, Iterable[Any] to_redact)
Definition: util.py:14
dict[str, Any] async_get_device_diagnostics(HomeAssistant hass, ConfigEntry config_entry, dr.DeviceEntry device)
Definition: diagnostics.py:148
ValueDataType _redacted_value(ValueDataType zwave_value)
Definition: diagnostics.py:38
dict[str, Any] async_get_config_entry_diagnostics(HomeAssistant hass, ConfigEntry config_entry)
Definition: diagnostics.py:129
list[dict[str, Any]] get_device_entities(HomeAssistant hass, Node node, ConfigEntry config_entry, dr.DeviceEntry device)
Definition: diagnostics.py:77
ValueDataType optionally_redact_value_of_zwave_value(ValueDataType zwave_value)
Definition: diagnostics.py:45
tuple[str, int]|None get_home_and_node_id_from_device_entry(dr.DeviceEntry device_entry)
Definition: helpers.py:231
int|None get_state_key_from_unique_id(str unique_id)
Definition: helpers.py:117
str|None get_value_id_from_unique_id(str unique_id)
Definition: helpers.py:104
bool value_matches_matcher(ZwaveValueMatcher matcher, ValueDataType value_data)
Definition: helpers.py:85
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)