diagnostics.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. """Diagnostics support for tuya-local."""
  2. from __future__ import annotations
  3. from typing import Any
  4. from homeassistant.components.diagnostics import REDACTED
  5. from homeassistant.config_entries import ConfigEntry
  6. from homeassistant.const import CONF_HOST
  7. from homeassistant.core import HomeAssistant, callback
  8. from homeassistant.helpers import device_registry as dr
  9. from homeassistant.helpers import entity_registry as er
  10. from homeassistant.helpers.device_registry import DeviceEntry
  11. from tinytuya import __version__ as tinytuya_version
  12. from .const import (
  13. API_PROTOCOL_VERSIONS,
  14. CONF_DEVICE_CID,
  15. CONF_PROTOCOL_VERSION,
  16. CONF_TYPE,
  17. DOMAIN,
  18. )
  19. from .device import TuyaLocalDevice
  20. from .helpers.config import get_device_id
  21. async def async_get_config_entry_diagnostics(
  22. hass: HomeAssistant, entry: ConfigEntry
  23. ) -> dict[str, Any]:
  24. """Return diagnostics for a config entry."""
  25. return _async_get_diagnostics(hass, entry)
  26. async def async_get_device_diagnostics(
  27. hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry
  28. ) -> dict[str, Any]:
  29. """Return diagnostics for a device entry."""
  30. return _async_get_diagnostics(hass, entry, device)
  31. @callback
  32. def _async_get_diagnostics(
  33. hass: HomeAssistant,
  34. entry: ConfigEntry,
  35. device: DeviceEntry | None = None,
  36. ) -> dict[str, Any]:
  37. """Return diagnostics for a tuya-local config entry."""
  38. hass_data = hass.data[DOMAIN][get_device_id(entry.data)]
  39. hostname = entry.data.get(CONF_HOST, "")
  40. data = {
  41. "name": entry.title,
  42. "type": entry.data[CONF_TYPE],
  43. "device_id": REDACTED,
  44. "device_cid": REDACTED if entry.data.get(CONF_DEVICE_CID, "") != "" else "",
  45. "local_key": REDACTED,
  46. "host": REDACTED
  47. if hostname != "" and hostname.casefold() != "auto"
  48. else hostname,
  49. "protocol_version": entry.data[CONF_PROTOCOL_VERSION],
  50. "tinytuya_version": tinytuya_version,
  51. }
  52. # The DeviceEntry also has interesting looking data, but this
  53. # integration does not publish anything to it other than some hardcoded
  54. # values that don't change between devices. Instead get the live data
  55. # from the running hass.
  56. data |= _async_device_as_dict(hass, hass_data["device"])
  57. return data
  58. def redact_dps(device: TuyaLocalDevice, dps: dict[str, Any]) -> dict[str, Any]:
  59. """Redact any sensitive data from a list of dps"""
  60. sensitive = []
  61. for entity in device._children:
  62. for dp in entity._config.dps():
  63. if dp.sensitive:
  64. sensitive.append(dp.id)
  65. return {k: (REDACTED if k in sensitive else v) for (k, v) in dps.items()}
  66. def redact_entity(
  67. device: TuyaLocalDevice,
  68. entity_id: str,
  69. state_dict: dict[str, Any],
  70. ) -> dict[str, Any]:
  71. sensitive = []
  72. for entity in device._children:
  73. if entity._config.config_id == entity_id:
  74. for dp in entity._config.dps():
  75. if dp.sensitive:
  76. sensitive.append(dp.name)
  77. return {k: (REDACTED if k in sensitive else v) for (k, v) in state_dict.items()}
  78. @callback
  79. def _async_device_as_dict(
  80. hass: HomeAssistant, device: TuyaLocalDevice
  81. ) -> dict[str, Any]:
  82. """Represent a Tuya Local device as a dictionary."""
  83. # Base device information, without sensitive information
  84. data = {
  85. "name": device.name,
  86. "api_version_set": device._api.version,
  87. "api_version_used": (
  88. "none"
  89. if device._api_protocol_version_index is None
  90. else API_PROTOCOL_VERSIONS[device._api_protocol_version_index]
  91. ),
  92. "api_working": device._api_protocol_working,
  93. "status": device._api.dps_cache,
  94. "cached_state": redact_dps(device, device._cached_state),
  95. "pending_state": redact_dps(device, device._pending_updates),
  96. "connected": device._running,
  97. "force_dps": device._force_dps,
  98. }
  99. device_registry = dr.async_get(hass)
  100. entity_registry = er.async_get(hass)
  101. hass_device = device_registry.async_get_device(
  102. identifiers={(DOMAIN, device.unique_id)}
  103. )
  104. if hass_device:
  105. data["home_assistant"] = {
  106. "name": hass_device.name,
  107. "name_by_user": hass_device.name_by_user,
  108. "disabled": hass_device.disabled,
  109. "disabled_by": hass_device.disabled_by,
  110. "entities": [],
  111. }
  112. hass_entities = er.async_entries_for_device(
  113. entity_registry,
  114. device_id=hass_device.id,
  115. include_disabled_entities=True,
  116. )
  117. for entity_entry in hass_entities:
  118. state = hass.states.get(entity_entry.entity_id)
  119. state_dict = None
  120. if state:
  121. state_dict = redact_entity(
  122. device,
  123. entity_entry.entity_id,
  124. state.as_dict(),
  125. )
  126. # Redact entity_picture in case it is sensitive
  127. if "entity_picture" in state_dict["attributes"]:
  128. state_dict["attributes"] = {
  129. **state_dict["attributes"],
  130. "entity_picture": REDACTED,
  131. }
  132. # Context is not useful information
  133. state_dict.pop("context", None)
  134. data["home_assistant"]["entities"].append(
  135. {
  136. "disabled": entity_entry.disabled,
  137. "disabled_by": entity_entry.disabled_by,
  138. "entity_category": entity_entry.entity_category,
  139. "device_class": entity_entry.device_class,
  140. "original_device_class": entity_entry.original_device_class,
  141. "icon": entity_entry.icon,
  142. "unit_of_measurement": entity_entry.unit_of_measurement,
  143. "state": state_dict,
  144. }
  145. )
  146. return data