Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Diagnostics integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Coroutine, Mapping
6 from dataclasses import dataclass, field
7 from http import HTTPStatus
8 import json
9 import logging
10 from typing import Any, Protocol
11 
12 from aiohttp import web
13 import voluptuous as vol
14 
15 from homeassistant.components import http, websocket_api
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.core import HomeAssistant, callback
18 from homeassistant.helpers import (
19  config_validation as cv,
20  device_registry as dr,
21  integration_platform,
22 )
23 from homeassistant.helpers.device_registry import DeviceEntry
24 from homeassistant.helpers.json import (
25  ExtendedJSONEncoder,
26  find_paths_unserializable_data,
27 )
28 from homeassistant.helpers.system_info import async_get_system_info
29 from homeassistant.helpers.typing import ConfigType
30 from homeassistant.loader import (
31  Manifest,
32  async_get_custom_components,
33  async_get_integration,
34 )
35 from homeassistant.setup import async_get_domain_setup_times
36 from homeassistant.util.json import format_unserializable_data
37 
38 from .const import DOMAIN, REDACTED, DiagnosticsSubType, DiagnosticsType
39 from .util import async_redact_data
40 
41 __all__ = ["REDACTED", "async_redact_data"]
42 
43 _LOGGER = logging.getLogger(__name__)
44 
45 
46 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
47 
48 
49 @dataclass(slots=True)
51  """Diagnostic platform data."""
52 
53  config_entry_diagnostics: (
54  Callable[[HomeAssistant, ConfigEntry], Coroutine[Any, Any, Mapping[str, Any]]]
55  | None
56  )
57  device_diagnostics: (
58  Callable[
59  [HomeAssistant, ConfigEntry, DeviceEntry],
60  Coroutine[Any, Any, Mapping[str, Any]],
61  ]
62  | None
63  )
64 
65 
66 @dataclass(slots=True)
68  """Diagnostic data."""
69 
70  platforms: dict[str, DiagnosticsPlatformData] = field(default_factory=dict)
71 
72 
73 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
74  """Set up Diagnostics from a config entry."""
75  hass.data[DOMAIN] = DiagnosticsData()
76 
77  await integration_platform.async_process_integration_platforms(
78  hass, DOMAIN, _register_diagnostics_platform
79  )
80 
81  websocket_api.async_register_command(hass, handle_info)
82  websocket_api.async_register_command(hass, handle_get)
83  hass.http.register_view(DownloadDiagnosticsView)
84 
85  return True
86 
87 
88 class DiagnosticsProtocol(Protocol):
89  """Define the format that diagnostics platforms can have."""
90 
92  self, hass: HomeAssistant, config_entry: ConfigEntry
93  ) -> Mapping[str, Any]:
94  """Return diagnostics for a config entry."""
95 
97  self, hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
98  ) -> Mapping[str, Any]:
99  """Return diagnostics for a device."""
100 
101 
102 @callback
104  hass: HomeAssistant, integration_domain: str, platform: DiagnosticsProtocol
105 ) -> None:
106  """Register a diagnostics platform."""
107  diagnostics_data: DiagnosticsData = hass.data[DOMAIN]
108  diagnostics_data.platforms[integration_domain] = DiagnosticsPlatformData(
109  getattr(platform, "async_get_config_entry_diagnostics", None),
110  getattr(platform, "async_get_device_diagnostics", None),
111  )
112 
113 
114 @websocket_api.require_admin
115 @websocket_api.websocket_command({vol.Required("type"): "diagnostics/list"})
116 @callback
118  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
119 ) -> None:
120  """List all possible diagnostic handlers."""
121  diagnostics_data: DiagnosticsData = hass.data[DOMAIN]
122  result = [
123  {
124  "domain": domain,
125  "handlers": {
126  DiagnosticsType.CONFIG_ENTRY: info.config_entry_diagnostics is not None,
127  DiagnosticsSubType.DEVICE: info.device_diagnostics is not None,
128  },
129  }
130  for domain, info in diagnostics_data.platforms.items()
131  ]
132  connection.send_result(msg["id"], result)
133 
134 
135 @websocket_api.require_admin
136 @websocket_api.websocket_command( { vol.Required("type"): "diagnostics/get",
137  vol.Required("domain"): str,
138  }
139 )
140 @callback
141 def handle_get(
142  hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
143 ) -> None:
144  """List all diagnostic handlers for a domain."""
145  domain = msg["domain"]
146  diagnostics_data: DiagnosticsData = hass.data[DOMAIN]
147 
148  if (info := diagnostics_data.platforms.get(domain)) is None:
149  connection.send_error(
150  msg["id"], websocket_api.ERR_NOT_FOUND, "Domain not supported"
151  )
152  return
153 
154  connection.send_result(
155  msg["id"],
156  {
157  "domain": domain,
158  "handlers": {
159  DiagnosticsType.CONFIG_ENTRY: info.config_entry_diagnostics is not None,
160  DiagnosticsSubType.DEVICE: info.device_diagnostics is not None,
161  },
162  },
163  )
164 
165 
166 @callback
167 def async_format_manifest(manifest: Manifest) -> Manifest:
168  """Format manifest for diagnostics.
169 
170  Remove the @ from codeowners so that
171  when users download the diagnostics and paste
172  the codeowners into the repository, it will
173  not notify the users in the codeowners file.
174  """
175  manifest_copy = manifest.copy()
176  if "codeowners" in manifest_copy:
177  manifest_copy["codeowners"] = [
178  codeowner.lstrip("@") for codeowner in manifest_copy["codeowners"]
179  ]
180  return manifest_copy
181 
182 
184  hass: HomeAssistant,
185  data: Mapping[str, Any],
186  filename: str,
187  domain: str,
188  d_id: str,
189  sub_id: str | None = None,
190 ) -> web.Response:
191  """Return JSON file from dictionary."""
192  hass_sys_info = await async_get_system_info(hass)
193  hass_sys_info["run_as_root"] = hass_sys_info["user"] == "root"
194  del hass_sys_info["user"]
195 
196  integration = await async_get_integration(hass, domain)
197  custom_components = {}
198  all_custom_components = await async_get_custom_components(hass)
199  for cc_domain, cc_obj in all_custom_components.items():
200  custom_components[cc_domain] = {
201  "documentation": cc_obj.documentation,
202  "version": cc_obj.version,
203  "requirements": cc_obj.requirements,
204  }
205  payload = {
206  "home_assistant": hass_sys_info,
207  "custom_components": custom_components,
208  "integration_manifest": async_format_manifest(integration.manifest),
209  "setup_times": async_get_domain_setup_times(hass, domain),
210  "data": data,
211  }
212  try:
213  json_data = json.dumps(payload, indent=2, cls=ExtendedJSONEncoder)
214  except TypeError:
215  _LOGGER.error(
216  "Failed to serialize to JSON: %s/%s%s. Bad data at %s",
217  DiagnosticsType.CONFIG_ENTRY.value,
218  d_id,
219  f"/{DiagnosticsSubType.DEVICE.value}/{sub_id}"
220  if sub_id is not None
221  else "",
223  )
224  return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
225 
226  return web.Response(
227  body=json_data,
228  content_type="application/json",
229  headers={"Content-Disposition": f'attachment; filename="{filename}.json"'},
230  )
231 
232 
233 class DownloadDiagnosticsView(http.HomeAssistantView):
234  """Download diagnostics view."""
235 
236  url = "/api/diagnostics/{d_type}/{d_id}"
237  extra_urls = ["/api/diagnostics/{d_type}/{d_id}/{sub_type}/{sub_id}"]
238  name = "api:diagnostics"
239 
240  async def get(
241  self,
242  request: web.Request,
243  d_type: str,
244  d_id: str,
245  sub_type: str | None = None,
246  sub_id: str | None = None,
247  ) -> web.Response:
248  """Download diagnostics."""
249  # Validate d_type and sub_type
250  try:
251  DiagnosticsType(d_type)
252  except ValueError:
253  return web.Response(status=HTTPStatus.BAD_REQUEST)
254 
255  if sub_type is not None:
256  try:
257  DiagnosticsSubType(sub_type)
258  except ValueError:
259  return web.Response(status=HTTPStatus.BAD_REQUEST)
260 
261  device_diagnostics = sub_type is not None
262 
263  hass = request.app[http.KEY_HASS]
264 
265  if (config_entry := hass.config_entries.async_get_entry(d_id)) is None:
266  return web.Response(status=HTTPStatus.NOT_FOUND)
267 
268  diagnostics_data: DiagnosticsData = hass.data[DOMAIN]
269  if (info := diagnostics_data.platforms.get(config_entry.domain)) is None:
270  return web.Response(status=HTTPStatus.NOT_FOUND)
271 
272  filename = f"{config_entry.domain}-{config_entry.entry_id}"
273 
274  if not device_diagnostics:
275  # Config entry diagnostics
276  if info.config_entry_diagnostics is None:
277  return web.Response(status=HTTPStatus.NOT_FOUND)
278  data = await info.config_entry_diagnostics(hass, config_entry)
279  filename = f"{DiagnosticsType.CONFIG_ENTRY}-{filename}"
280  return await _async_get_json_file_response(
281  hass, data, filename, config_entry.domain, d_id
282  )
283 
284  # Device diagnostics
285  dev_reg = dr.async_get(hass)
286  if sub_id is None:
287  return web.Response(status=HTTPStatus.BAD_REQUEST)
288 
289  if (device := dev_reg.async_get(sub_id)) is None:
290  return web.Response(status=HTTPStatus.NOT_FOUND)
291 
292  filename += f"-{device.name}-{device.id}"
293 
294  if info.device_diagnostics is None:
295  return web.Response(status=HTTPStatus.NOT_FOUND)
296 
297  data = await info.device_diagnostics(hass, config_entry, device)
298  return await _async_get_json_file_response(
299  hass, data, filename, config_entry.domain, d_id, sub_id
300  )
301 
Mapping[str, Any] async_get_config_entry_diagnostics(self, HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:93
web.Response get(self, web.Request request, str d_type, str d_id, str|None sub_type=None, str|None sub_id=None)
Definition: __init__.py:249
dict[str, Any] async_get_device_diagnostics(HomeAssistant hass, BMWConfigEntry config_entry, DeviceEntry device)
Definition: diagnostics.py:76
None handle_get(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: __init__.py:145
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:73
web.Response _async_get_json_file_response(HomeAssistant hass, Mapping[str, Any] data, str filename, str domain, str d_id, str|None sub_id=None)
Definition: __init__.py:192
Manifest async_format_manifest(Manifest manifest)
Definition: __init__.py:169
None _register_diagnostics_platform(HomeAssistant hass, str integration_domain, DiagnosticsProtocol platform)
Definition: __init__.py:105
None handle_info(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: __init__.py:119
dict[str, Any] find_paths_unserializable_data(Any bad_data, *Callable[[Any], str] dump=json.dumps)
Definition: json.py:233
dict[str, Any] async_get_system_info(HomeAssistant hass)
Definition: system_info.py:44
Integration async_get_integration(HomeAssistant hass, str domain)
Definition: loader.py:1354
dict[str, Integration] async_get_custom_components(HomeAssistant hass)
Definition: loader.py:317
Mapping[str|None, dict[SetupPhases, float]] async_get_domain_setup_times(core.HomeAssistant hass, str domain)
Definition: setup.py:822
str format_unserializable_data(dict[str, Any] data)
Definition: json.py:126