Allow downloading device diagnostics (#11370)

This commit is contained in:
Paulus Schoutsen 2022-01-19 20:48:24 -08:00 committed by GitHub
parent f7fc83ac12
commit 81faae6f74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 209 additions and 97 deletions

View File

@ -1,9 +1,10 @@
import { HomeAssistant } from "../types";
interface DiagnosticInfo {
export interface DiagnosticInfo {
domain: string;
handlers: {
config_entry: boolean;
device: boolean;
};
}
@ -14,5 +15,19 @@ export const fetchDiagnosticHandlers = (
type: "diagnostics/list",
});
export const fetchDiagnosticHandler = (
hass: HomeAssistant,
domain: string
): Promise<DiagnosticInfo> =>
hass.callWS<DiagnosticInfo>({
type: "diagnostics/get",
domain,
});
export const getConfigEntryDiagnosticsDownloadUrl = (entry_id: string) =>
`/api/diagnostics/config_entry/${entry_id}`;
export const getDeviceDiagnosticsDownloadUrl = (
entry_id: string,
device_id: string
) => `/api/diagnostics/config_entry/${entry_id}/device/${device_id}`;

View File

@ -3,6 +3,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
@ -17,6 +18,7 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { getSignedPath } from "../../../data/auth";
import {
ConfigEntry,
disableConfigEntry,
@ -27,6 +29,11 @@ import {
DeviceRegistryEntry,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import {
fetchDiagnosticHandler,
getDeviceDiagnosticsDownloadUrl,
getConfigEntryDiagnosticsDownloadUrl,
} from "../../../data/diagnostics";
import {
EntityRegistryEntry,
findBatteryChargingEntity,
@ -44,6 +51,7 @@ import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { fileDownload } from "../../../util/file_download";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "./device-detail/ha-device-entities-card";
@ -82,6 +90,10 @@ export class HaConfigDevicePage extends LitElement {
@state() private _related?: RelatedResult;
@state() private _diagnosticDownloadLinks?: Promise<
(TemplateResult | string)[]
>;
private _device = memoizeOne(
(
deviceId: string,
@ -91,10 +103,8 @@ export class HaConfigDevicePage extends LitElement {
);
private _integrations = memoizeOne(
(device: DeviceRegistryEntry, entries: ConfigEntry[]): string[] =>
entries
.filter((entry) => device.config_entries.includes(entry.entry_id))
.map((entry) => entry.domain)
(device: DeviceRegistryEntry, entries: ConfigEntry[]): ConfigEntry[] =>
entries.filter((entry) => device.config_entries.includes(entry.entry_id))
);
private _entities = memoizeOne(
@ -165,6 +175,63 @@ export class HaConfigDevicePage extends LitElement {
findBatteryChargingEntity(this.hass, entities)
);
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
changedProps.has("deviceId") ||
changedProps.has("devices") ||
changedProps.has("deviceId") ||
changedProps.has("entries")
) {
this._diagnosticDownloadLinks = undefined;
}
if (
this._diagnosticDownloadLinks ||
!this.devices ||
!this.deviceId ||
!this.entries
) {
return;
}
this._diagnosticDownloadLinks = this._renderDiagnosticButtons();
}
private async _renderDiagnosticButtons(): Promise<
(TemplateResult | string)[]
> {
const result: TemplateResult[] = [];
const device = this._device(this.deviceId, this.devices);
if (!device) {
return result;
}
return Promise.all(
this._integrations(device, this.entries).map(async (entry) => {
const info = await fetchDiagnosticHandler(this.hass, entry.domain);
if (!info.handlers.device && !info.handlers.config_entry) {
return "";
}
const link = info.handlers.device
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id);
return html`
<a href=${link} @click=${this._signUrl}>
<mwc-button>
${this.hass.localize(
`ui.panel.config.devices.download_diagnostics`
)}
</mwc-button>
</a>
`;
})
);
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
loadDeviceRegistryDetailDialog();
@ -214,6 +281,66 @@ export class HaConfigDevicePage extends LitElement {
? device.configuration_url!.replace("homeassistant://", "/")
: device.configuration_url;
const deviceInfo: TemplateResult[] = [];
if (device.disabled_by) {
deviceInfo.push(
html`
<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.devices.enabled_cause",
"cause",
this.hass.localize(
`ui.panel.config.devices.disabled_by.${device.disabled_by}`
)
)}
</ha-alert>
${device.disabled_by === "user"
? html` <div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>`
: ""}
`
);
}
const deviceActions: TemplateResult[] = [];
if (configurationUrl) {
deviceActions.push(html`
<a
href=${configurationUrl}
rel="noopener noreferrer"
.target=${configurationUrlIsHomeAssistant ? "_self" : "_blank"}
>
<mwc-button>
${this.hass.localize(
`ui.panel.config.devices.open_configuration_url_${
device.entry_type || "device"
}`
)}
<ha-svg-icon
.path=${mdiOpenInNew}
slot="trailingIcon"
></ha-svg-icon>
</mwc-button>
</a>
`);
}
this._renderIntegrationInfo(
device,
integrations,
deviceInfo,
deviceActions
);
if (this._diagnosticDownloadLinks) {
deviceActions.push(html`${until(this._diagnosticDownloadLinks)}`);
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
@ -291,7 +418,7 @@ export class HaConfigDevicePage extends LitElement {
? html`
<img
src=${brandsUrl({
domain: integrations[0],
domain: integrations[0].domain,
type: "logo",
darkOptimized: this.hass.themes?.darkMode,
})}
@ -312,56 +439,16 @@ export class HaConfigDevicePage extends LitElement {
.devices=${this.devices}
.device=${device}
>
${
device.disabled_by
? html`
<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.devices.enabled_cause",
"cause",
this.hass.localize(
`ui.panel.config.devices.disabled_by.${device.disabled_by}`
)
)}
</ha-alert>
${device.disabled_by === "user"
? html` <div class="card-actions" slot="actions">
<mwc-button unelevated @click=${this._enableDevice}>
${this.hass.localize("ui.common.enable")}
</mwc-button>
</div>`
: ""}
`
: html``
}
${
configurationUrl
? html`
<div class="card-actions" slot="actions">
<a
href=${configurationUrl}
rel="noopener noreferrer"
.target=${configurationUrlIsHomeAssistant
? "_self"
: "_blank"}
>
<mwc-button>
${this.hass.localize(
`ui.panel.config.devices.open_configuration_url_${
device.entry_type || "device"
}`
)}
<ha-svg-icon
.path=${mdiOpenInNew}
slot="trailingIcon"
></ha-svg-icon>
</mwc-button>
</a>
</div>
`
: ""
}
${this._renderIntegrationInfo(device, integrations)}
${deviceInfo}
${
deviceActions.length
? html`
<div class="card-actions" slot="actions">
${deviceActions}
</div>
`
: ""
}
</ha-device-info-card>
</div>
<div class="column">
@ -648,85 +735,84 @@ export class HaConfigDevicePage extends LitElement {
private _renderIntegrationInfo(
device,
integrations: string[]
integrations: ConfigEntry[],
deviceInfo: TemplateResult[],
deviceActions: TemplateResult[]
): TemplateResult[] {
const domains = integrations.map((int) => int.domain);
const templates: TemplateResult[] = [];
if (integrations.includes("mqtt")) {
if (domains.includes("mqtt")) {
import(
"./device-detail/integration-elements/mqtt/ha-device-actions-mqtt"
);
templates.push(html`
<div class="card-actions" slot="actions">
<ha-device-actions-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-actions-mqtt>
</div>
deviceActions.push(html`
<ha-device-actions-mqtt
.hass=${this.hass}
.device=${device}
></ha-device-actions-mqtt>
`);
}
if (integrations.includes("ozw")) {
if (domains.includes("ozw")) {
import("./device-detail/integration-elements/ozw/ha-device-actions-ozw");
import("./device-detail/integration-elements/ozw/ha-device-info-ozw");
templates.push(html`
deviceInfo.push(html`
<ha-device-info-ozw
.hass=${this.hass}
.device=${device}
></ha-device-info-ozw>
<div class="card-actions" slot="actions">
<ha-device-actions-ozw
.hass=${this.hass}
.device=${device}
></ha-device-actions-ozw>
</div>
`);
deviceActions.push(html`
<ha-device-actions-ozw
.hass=${this.hass}
.device=${device}
></ha-device-actions-ozw>
`);
}
if (integrations.includes("tasmota")) {
if (domains.includes("tasmota")) {
import(
"./device-detail/integration-elements/tasmota/ha-device-actions-tasmota"
);
templates.push(html`
<div class="card-actions" slot="actions">
<ha-device-actions-tasmota
.hass=${this.hass}
.device=${device}
></ha-device-actions-tasmota>
</div>
deviceActions.push(html`
<ha-device-actions-tasmota
.hass=${this.hass}
.device=${device}
></ha-device-actions-tasmota>
`);
}
if (integrations.includes("zha")) {
if (domains.includes("zha")) {
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
import("./device-detail/integration-elements/zha/ha-device-info-zha");
templates.push(html`
deviceInfo.push(html`
<ha-device-info-zha
.hass=${this.hass}
.device=${device}
></ha-device-info-zha>
<div class="card-actions" slot="actions">
<ha-device-actions-zha
.hass=${this.hass}
.device=${device}
></ha-device-actions-zha>
</div>
`);
deviceActions.push(html`
<ha-device-actions-zha
.hass=${this.hass}
.device=${device}
></ha-device-actions-zha>
`);
}
if (integrations.includes("zwave_js")) {
if (domains.includes("zwave_js")) {
import(
"./device-detail/integration-elements/zwave_js/ha-device-info-zwave_js"
);
import(
"./device-detail/integration-elements/zwave_js/ha-device-actions-zwave_js"
);
templates.push(html`
deviceInfo.push(html`
<ha-device-info-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-info-zwave_js>
<div class="card-actions" slot="actions">
<ha-device-actions-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-actions-zwave_js>
</div>
`);
deviceActions.push(html`
<ha-device-actions-zwave_js
.hass=${this.hass}
.device=${device}
></ha-device-actions-zwave_js>
`);
}
return templates;
@ -866,6 +952,16 @@ export class HaConfigDevicePage extends LitElement {
});
}
private async _signUrl(ev) {
const anchor = ev.target.closest("a");
ev.preventDefault();
const signedUrl = await getSignedPath(
this.hass,
anchor.getAttribute("href")
);
fileDownload(signedUrl.path);
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -2204,6 +2204,7 @@
"enabled_description": "Disabled devices will not be shown and entities belonging to the device will be disabled and not added to Home Assistant.",
"open_configuration_url_device": "Visit device",
"open_configuration_url_service": "Visit service",
"download_diagnostics": "Download diagnostics",
"automation": {
"automations": "Automations",
"no_automations": "No automations",