mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
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:
parent
1dfb632fc4
commit
9a00078169
@ -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,
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
130
src/dialogs/mqtt-device-debug-info-dialog/mqtt-messages.ts
Normal file
130
src/dialogs/mqtt-device-debug-info-dialog/mqtt-messages.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user