1 """The Diagnostics integration."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Coroutine, Mapping
6 from dataclasses
import dataclass, field
7 from http
import HTTPStatus
10 from typing
import Any, Protocol
12 from aiohttp
import web
13 import voluptuous
as vol
19 config_validation
as cv,
20 device_registry
as dr,
26 find_paths_unserializable_data,
32 async_get_custom_components,
33 async_get_integration,
38 from .const
import DOMAIN, REDACTED, DiagnosticsSubType, DiagnosticsType
39 from .util
import async_redact_data
41 __all__ = [
"REDACTED",
"async_redact_data"]
43 _LOGGER = logging.getLogger(__name__)
46 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
49 @dataclass(slots=True)
51 """Diagnostic platform data."""
53 config_entry_diagnostics: (
54 Callable[[HomeAssistant, ConfigEntry], Coroutine[Any, Any, Mapping[str, Any]]]
59 [HomeAssistant, ConfigEntry, DeviceEntry],
60 Coroutine[Any, Any, Mapping[str, Any]],
66 @dataclass(slots=True)
68 """Diagnostic data."""
70 platforms: dict[str, DiagnosticsPlatformData] = field(default_factory=dict)
73 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
74 """Set up Diagnostics from a config entry."""
77 await integration_platform.async_process_integration_platforms(
78 hass, DOMAIN, _register_diagnostics_platform
81 websocket_api.async_register_command(hass, handle_info)
82 websocket_api.async_register_command(hass, handle_get)
83 hass.http.register_view(DownloadDiagnosticsView)
89 """Define the format that diagnostics platforms can have."""
92 self, hass: HomeAssistant, config_entry: ConfigEntry
93 ) -> Mapping[str, Any]:
94 """Return diagnostics for a config entry."""
97 self, hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
98 ) -> Mapping[str, Any]:
99 """Return diagnostics for a device."""
104 hass: HomeAssistant, integration_domain: str, platform: DiagnosticsProtocol
106 """Register a diagnostics platform."""
107 diagnostics_data: DiagnosticsData = hass.data[DOMAIN]
109 getattr(platform,
"async_get_config_entry_diagnostics",
None),
110 getattr(platform,
"async_get_device_diagnostics",
None),
114 @websocket_api.require_admin
115 @websocket_api.websocket_command({vol.Required("type"):
"diagnostics/list"})
120 """List all possible diagnostic handlers."""
121 diagnostics_data: DiagnosticsData = hass.data[DOMAIN]
126 DiagnosticsType.CONFIG_ENTRY: info.config_entry_diagnostics
is not None,
127 DiagnosticsSubType.DEVICE: info.device_diagnostics
is not None,
130 for domain, info
in diagnostics_data.platforms.items()
132 connection.send_result(msg[
"id"], result)
135 @websocket_api.require_admin
136 @websocket_api.websocket_command(
{
vol.Required("type"):
"diagnostics/get",
137 vol.Required(
"domain"): str,
144 """List all diagnostic handlers for a domain."""
145 domain = msg[
"domain"]
146 diagnostics_data: DiagnosticsData = hass.data[DOMAIN]
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"
154 connection.send_result(
159 DiagnosticsType.CONFIG_ENTRY: info.config_entry_diagnostics
is not None,
160 DiagnosticsSubType.DEVICE: info.device_diagnostics
is not None,
168 """Format manifest for diagnostics.
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.
175 manifest_copy = manifest.copy()
176 if "codeowners" in manifest_copy:
177 manifest_copy[
"codeowners"] = [
178 codeowner.lstrip(
"@")
for codeowner
in manifest_copy[
"codeowners"]
185 data: Mapping[str, Any],
189 sub_id: str |
None =
None,
191 """Return JSON file from dictionary."""
193 hass_sys_info[
"run_as_root"] = hass_sys_info[
"user"] ==
"root"
194 del hass_sys_info[
"user"]
197 custom_components = {}
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,
206 "home_assistant": hass_sys_info,
207 "custom_components": custom_components,
213 json_data = json.dumps(payload, indent=2, cls=ExtendedJSONEncoder)
216 "Failed to serialize to JSON: %s/%s%s. Bad data at %s",
217 DiagnosticsType.CONFIG_ENTRY.value,
219 f
"/{DiagnosticsSubType.DEVICE.value}/{sub_id}"
220 if sub_id
is not None
224 return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
228 content_type=
"application/json",
229 headers={
"Content-Disposition": f
'attachment; filename="{filename}.json"'},
234 """Download diagnostics view."""
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"
242 request: web.Request,
245 sub_type: str |
None =
None,
246 sub_id: str |
None =
None,
248 """Download diagnostics."""
253 return web.Response(status=HTTPStatus.BAD_REQUEST)
255 if sub_type
is not None:
259 return web.Response(status=HTTPStatus.BAD_REQUEST)
261 device_diagnostics = sub_type
is not None
263 hass = request.app[http.KEY_HASS]
265 if (config_entry := hass.config_entries.async_get_entry(d_id))
is None:
266 return web.Response(status=HTTPStatus.NOT_FOUND)
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)
272 filename = f
"{config_entry.domain}-{config_entry.entry_id}"
274 if not device_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}"
281 hass, data, filename, config_entry.domain, d_id
285 dev_reg = dr.async_get(hass)
287 return web.Response(status=HTTPStatus.BAD_REQUEST)
289 if (device := dev_reg.async_get(sub_id))
is None:
290 return web.Response(status=HTTPStatus.NOT_FOUND)
292 filename += f
"-{device.name}-{device.id}"
294 if info.device_diagnostics
is None:
295 return web.Response(status=HTTPStatus.NOT_FOUND)
297 data = await info.device_diagnostics(hass, config_entry, device)
299 hass, data, filename, config_entry.domain, d_id, sub_id
301
Mapping[str, Any] async_get_config_entry_diagnostics(self, HomeAssistant hass, ConfigEntry config_entry)
web.Response get(self, web.Request request, str d_type, str d_id, str|None sub_type=None, str|None sub_id=None)
dict[str, Any] async_get_device_diagnostics(HomeAssistant hass, BMWConfigEntry config_entry, DeviceEntry device)
None handle_get(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
bool async_setup(HomeAssistant hass, ConfigType config)
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)
Manifest async_format_manifest(Manifest manifest)
None _register_diagnostics_platform(HomeAssistant hass, str integration_domain, DiagnosticsProtocol platform)
None handle_info(HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
dict[str, Any] find_paths_unserializable_data(Any bad_data, *Callable[[Any], str] dump=json.dumps)
dict[str, Any] async_get_system_info(HomeAssistant hass)
Integration async_get_integration(HomeAssistant hass, str domain)
dict[str, Integration] async_get_custom_components(HomeAssistant hass)
Mapping[str|None, dict[SetupPhases, float]] async_get_domain_setup_times(core.HomeAssistant hass, str domain)
str format_unserializable_data(dict[str, Any] data)