Skip to content
34 changes: 34 additions & 0 deletions src/data/mqtt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading