Display MQTT debug info (#5375)

* Add MQTT debug info

* Refactor

* Fix mistake

* Rewrite, improve display.

* Tweak translations

* Add mqtt-payload.ts

* Apply suggestions from code review

Co-Authored-By: Zack Arnett <arnett.zackary@gmail.com>

* Tweak after adding review comments.

* Rewrite to only render the messages when details is opened

* Adapt to core PR #33752

* Address review comments

* Lint

* Lint

* Address review comments

Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
This commit is contained in:
Erik Montnemery 2020-04-22 12:01:43 +02:00 committed by GitHub
parent 1dfb632fc4
commit 9a00078169
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 479 additions and 9 deletions

View File

@ -7,6 +7,31 @@ export interface MQTTMessage {
retain: number;
}
export interface MQTTTopicDebugInfo {
topic: string;
messages: MQTTMessage[];
}
export interface MQTTDiscoveryDebugInfo {
topic: string;
payload: string;
}
export interface MQTTEntityDebugInfo {
entity_id: string;
discovery_data: MQTTDiscoveryDebugInfo;
subscriptions: MQTTTopicDebugInfo[];
}
export interface MQTTTriggerDebugInfo {
discovery_data: MQTTDiscoveryDebugInfo;
}
export interface MQTTDeviceDebugInfo {
entities: MQTTEntityDebugInfo[];
triggers: MQTTTriggerDebugInfo[];
}
export const subscribeMQTTTopic = (
hass: HomeAssistant,
topic: string,
@ -26,3 +51,12 @@ export const removeMQTTDeviceEntry = (
type: "mqtt/device/remove",
device_id: deviceId,
});
export const fetchMQTTDebugInfo = (
hass: HomeAssistant,
deviceId: string
): Promise<MQTTDeviceDebugInfo> =>
hass.callWS<MQTTDeviceDebugInfo>({
type: "mqtt/device/debug_info",
device_id: deviceId,
});

View File

@ -0,0 +1,215 @@
import {
LitElement,
css,
html,
CSSResult,
TemplateResult,
customElement,
property,
} from "lit-element";
import "../../components/ha-dialog";
import "../../components/ha-switch";
import { computeDeviceName } from "../../data/device_registry";
import { computeStateName } from "../../common/entity/compute_state_name";
import { haStyleDialog } from "../../resources/styles";
import type { HaSwitch } from "../../components/ha-switch";
import { HomeAssistant } from "../../types";
import { MQTTDeviceDebugInfoDialogParams } from "./show-dialog-mqtt-device-debug-info";
import { MQTTDeviceDebugInfo, fetchMQTTDebugInfo } from "../../data/mqtt";
import "./mqtt-messages";
import "./mqtt-discovery-payload";
@customElement("dialog-mqtt-device-debug-info")
class DialogMQTTDeviceDebugInfo extends LitElement {
public hass!: HomeAssistant;
@property() private _params?: MQTTDeviceDebugInfoDialogParams;
@property() private _debugInfo?: MQTTDeviceDebugInfo;
@property() private _showAsYaml = true;
@property() private _showDeserialized = true;
public async showDialog(
params: MQTTDeviceDebugInfoDialogParams
): Promise<void> {
this._params = params;
fetchMQTTDebugInfo(this.hass, params.device.id).then((results) => {
this._debugInfo = results;
});
}
protected render(): TemplateResult {
if (!this._params || !this._debugInfo) {
return html``;
}
return html`
<ha-dialog
open
@closing=${this._close}
.heading="${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.title",
"device",
computeDeviceName(this._params.device, this.hass)
)}"
>
<h4>
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.payload_display"
)}
</h4>
<ha-switch
.checked=${this._showDeserialized}
@change=${this._showDeserializedChanged}
>
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.deserialize"
)}
</ha-switch>
<ha-switch
.checked=${this._showAsYaml}
@change=${this._showAsYamlChanged}
>
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.show_as_yaml"
)}
</ha-switch>
<h4>
${this.hass!.localize("ui.dialogs.mqtt_device_debug_info.entities")}
</h4>
<ul>
${this._debugInfo.entities.length
? this._renderEntities()
: html`
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.no_entities"
)}
`}
</ul>
<h4>
${this.hass!.localize("ui.dialogs.mqtt_device_debug_info.triggers")}
</h4>
<ul>
${this._debugInfo.triggers.length
? this._renderTriggers()
: html`
${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.no_triggers"
)}
`}
</ul>
<mwc-button slot="primaryAction" @click=${this._close}>
${this.hass!.localize("ui.dialogs.generic.close")}
</mwc-button>
</ha-dialog>
`;
}
private _close(): void {
this._params = undefined;
this._debugInfo = undefined;
}
private _showAsYamlChanged(ev: Event): void {
this._showAsYaml = (ev.target as HaSwitch).checked;
}
private _showDeserializedChanged(ev: Event): void {
this._showDeserialized = (ev.target as HaSwitch).checked;
}
private _renderEntities(): TemplateResult {
return html`
${this._debugInfo!.entities.map(
(entity) => html`
<li>
'${computeStateName(this.hass.states[entity.entity_id])}'
(<code>${entity.entity_id}</code>)
<br />MQTT discovery data:
<ul>
<li>
Topic:
<code>${entity.discovery_data.topic}</code>
</li>
<li>
<mqtt-discovery-payload
.hass=${this.hass}
.payload=${entity.discovery_data.payload}
.showAsYaml=${this._showAsYaml}
.summary=${"Payload"}
>
</mqtt-discovery-payload>
</li>
</ul>
Subscribed topics:
<ul>
${entity.subscriptions.map(
(topic) => html`
<li>
<code>${topic.topic}</code>
<mqtt-messages
.hass=${this.hass}
.messages=${topic.messages}
.showDeserialized=${this._showDeserialized}
.showAsYaml=${this._showAsYaml}
.subscribedTopic=${topic.topic}
.summary=${this.hass!.localize(
"ui.dialogs.mqtt_device_debug_info.recent_messages",
"n",
topic.messages.length
)}
>
</mqtt-messages>
</li>
`
)}
</ul>
</li>
`
)}
`;
}
private _renderTriggers(): TemplateResult {
return html`
${this._debugInfo!.triggers.map(
(trigger) => html`
<li>
Discovery topic:
<code>${trigger.discovery_data.topic}</code>
<mqtt-discovery-payload
.hass=${this.hass}
.payload=${trigger.discovery_data.payload}
.showAsYaml=${this._showAsYaml}
.summary="Discovery payload"
>
</mqtt-discovery-payload>
</li>
`
)}
`;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 95%;
--mdc-dialog-min-width: 640px;
}
ha-switch {
margin: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-mqtt-device-debug-info": DialogMQTTDeviceDebugInfo;
}
}

View File

@ -0,0 +1,49 @@
import {
LitElement,
html,
TemplateResult,
customElement,
property,
} from "lit-element";
import { safeDump } from "js-yaml";
@customElement("mqtt-discovery-payload")
class MQTTDiscoveryPayload extends LitElement {
@property() public payload!: object;
@property() public showAsYaml = false;
@property() public summary!: string;
@property() private _open = false;
protected render(): TemplateResult {
return html`
<details @toggle=${this._handleToggle}>
<summary>
${this.summary}
</summary>
${this._open ? this._renderPayload() : ""}
</details>
`;
}
private _renderPayload(): TemplateResult {
const payload = this.payload;
return html`
${this.showAsYaml
? html` <pre>${safeDump(payload)}</pre> `
: html` <pre>${JSON.stringify(payload, null, 2)}</pre> `}
`;
}
private _handleToggle(ev) {
this._open = ev.target.open;
}
}
declare global {
interface HTMLElementTagNameMap {
"mqtt-discovery-payload": MQTTDiscoveryPayload;
}
}

View File

@ -0,0 +1,130 @@
import {
LitElement,
html,
TemplateResult,
customElement,
property,
} from "lit-element";
import { safeDump } from "js-yaml";
import { MQTTMessage } from "../../data/mqtt";
@customElement("mqtt-messages")
class MQTTMessages extends LitElement {
@property() public messages!: MQTTMessage[];
@property() public showAsYaml = false;
@property() public showDeserialized = false;
@property() public subscribedTopic!: string;
@property() public summary!: string;
@property() private _open = false;
@property() private _payloadsJson = new WeakMap();
@property() private _showTopic = false;
protected firstUpdated(): void {
this.messages.forEach((message) => {
// If any message's topic differs from the subscribed topic, show topics + payload
if (this.subscribedTopic !== message.topic) {
this._showTopic = true;
}
});
}
protected render(): TemplateResult {
return html`
<details @toggle=${this._handleToggle}>
<summary>
${this.summary}
</summary>
${this._open
? html`
<ul>
${this.messages.map(
(message) => html`
<li>
${this._renderSingleMessage(message)}
</li>
`
)}
</ul>
`
: ""}
</details>
`;
}
private _renderSingleMessage(message): TemplateResult {
const topic = message.topic;
return this._showTopic
? html`
<ul>
<li>
Topic: ${topic}
</li>
<li>
Payload: ${this._renderSinglePayload(message)}
</li>
</ul>
`
: this._renderSinglePayload(message);
}
private _renderSinglePayload(message): TemplateResult {
let json;
if (this.showDeserialized) {
if (!this._payloadsJson.has(message)) {
json = this._tryParseJson(message.payload);
this._payloadsJson.set(message, json);
} else {
json = this._payloadsJson.get(message);
}
}
return json
? html`
${this.showAsYaml
? html` <pre>${safeDump(json)}</pre> `
: html` <pre>${JSON.stringify(json, null, 2)}</pre> `}
`
: html` <code>${message.payload}</code> `;
}
private _tryParseJson(payload) {
let jsonPayload = null;
let o = payload;
// If the payload is a string, determine if the payload is valid JSON and if it
// is, assign the object representation to this._payloadJson.
if (typeof payload === "string") {
try {
o = JSON.parse(payload);
} catch (e) {
o = null;
}
}
// Handle non-exception-throwing cases:
// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
// but... JSON.parse(null) returns null, and typeof null === "object",
// so we must check for that, too. Thankfully, null is falsey, so this suffices:
if (o && typeof o === "object") {
jsonPayload = o;
}
return jsonPayload;
}
private _handleToggle(ev) {
this._open = ev.target.open;
}
}
declare global {
interface HTMLElementTagNameMap {
"mqtt-messages": MQTTMessages;
}
}

View File

@ -0,0 +1,22 @@
import { fireEvent } from "../../common/dom/fire_event";
import { DeviceRegistryEntry } from "../../data/device_registry";
export interface MQTTDeviceDebugInfoDialogParams {
device: DeviceRegistryEntry;
}
export const loadMQTTDeviceDebugInfoDialog = () =>
import(
/* webpackChunkName: "dialog-mqtt-device-debug-info" */ "./dialog-mqtt-device-debug-info"
);
export const showMQTTDeviceDebugInfoDialog = (
element: HTMLElement,
mqttDeviceInfoParams: MQTTDeviceDebugInfoDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-mqtt-device-debug-info",
dialogImport: loadMQTTDeviceDebugInfoDialog,
dialogParams: mqttDeviceInfoParams,
});
};

View File

@ -1,16 +1,17 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../data/device_registry";
import { removeMQTTDeviceEntry } from "../../../../data/mqtt";
import {
LitElement,
html,
customElement,
property,
TemplateResult,
CSSResult,
} from "lit-element";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import { showMQTTDeviceDebugInfoDialog } from "../../../../dialogs/mqtt-device-debug-info-dialog/show-dialog-mqtt-device-debug-info";
import { HomeAssistant } from "../../../../types";
import { haStyle } from "../../../../resources/styles";
@customElement("ha-device-card-mqtt")
export class HaDeviceCardMqtt extends LitElement {
@ -20,6 +21,9 @@ export class HaDeviceCardMqtt extends LitElement {
protected render(): TemplateResult {
return html`
<mwc-button @click=${this._showDebugInfo}>
MQTT Info
</mwc-button>
<mwc-button class="warning" @click="${this._confirmDeleteEntry}">
${this.hass.localize("ui.panel.config.devices.delete")}
</mwc-button>
@ -38,6 +42,11 @@ export class HaDeviceCardMqtt extends LitElement {
await removeMQTTDeviceEntry(this.hass!, this.device.id);
}
private async _showDebugInfo(): Promise<void> {
const device = this.device;
await showMQTTDeviceDebugInfoDialog(this, { device });
}
static get styles(): CSSResult {
return haStyle;
}

View File

@ -456,6 +456,17 @@
},
"domain_toggler": {
"title": "Toggle Domains"
},
"mqtt_device_debug_info": {
"title": "{device} debug info",
"deserialize": "Attempt to parse MQTT messages as JSON",
"entities": "Entities",
"no_entities": "No entities",
"no_triggers": "No triggers",
"payload_display": "Payload display",
"recent_messages": "{n} most recently received message(s)",
"show_as_yaml": "Show as YAML",
"triggers": "Triggers"
}
},
"duration": {