Add zwave_js device statistics (#12794)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Raman Gupta 2022-06-07 10:40:28 -04:00 committed by GitHub
parent f020add6be
commit 54377225ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 636 additions and 1 deletions

View File

@ -244,6 +244,41 @@ export interface ZWaveJSControllerStatisticsUpdatedMessage {
timeout_callback: number;
}
export enum RssiError {
NotAvailable = 127,
ReceiverSaturated = 126,
NoSignalDetected = 125,
}
export enum ProtocolDataRate {
ZWave_9k6 = 0x01,
ZWave_40k = 0x02,
ZWave_100k = 0x03,
LongRange_100k = 0x04,
}
export interface ZWaveJSNodeStatisticsUpdatedMessage {
event: "statistics updated";
source: "node";
commands_tx: number;
commands_rx: number;
commands_dropped_tx: number;
commands_dropped_rx: number;
timeout_response: number;
rtt: number | null;
rssi: RssiError | number | null;
lwr: ZWaveJSRouteStatistics | null;
nlwr: ZWaveJSRouteStatistics | null;
}
export interface ZWaveJSRouteStatistics {
protocol_data_rate: number;
repeaters: string[];
rssi: RssiError | number | null;
repeater_rssi: (RssiError | number)[];
route_failed_between: [string, string] | null;
}
export interface ZWaveJSRemovedNode {
node_id: number;
manufacturer: string;
@ -597,6 +632,19 @@ export const subscribeZwaveControllerStatistics = (
}
);
export const subscribeZwaveNodeStatistics = (
hass: HomeAssistant,
device_id: string,
callbackFunction: (message: ZWaveJSNodeStatisticsUpdatedMessage) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(
(message: any) => callbackFunction(message),
{
type: "zwave_js/subscribe_node_statistics",
device_id,
}
);
export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => {

View File

@ -3,6 +3,7 @@ import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZwaveNodeStatus } from "../../../../../../data/zwave_js";
import type { HomeAssistant } from "../../../../../../types";
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
import { showZWaveJSNodeStatisticsDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-node-statistics";
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
import type { DeviceAction } from "../../../ha-config-device-page";
@ -64,5 +65,14 @@ export const getZwaveDeviceActions = async (
device_id: device.id,
}),
},
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.node_statistics"
),
action: () =>
showZWaveJSNodeStatisticsDialog(el, {
device: device,
}),
},
];
};

View File

@ -0,0 +1,477 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-help-tooltip";
import "../../../../../components/ha-svg-icon";
import { mdiSwapHorizontal } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import {
DeviceRegistryEntry,
computeDeviceName,
subscribeDeviceRegistry,
} from "../../../../../data/device_registry";
import {
subscribeZwaveNodeStatistics,
ProtocolDataRate,
ZWaveJSNodeStatisticsUpdatedMessage,
ZWaveJSRouteStatistics,
RssiError,
} from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZWaveJSNodeStatisticsDialogParams } from "./show-dialog-zwave_js-node-statistics";
import { createCloseHeading } from "../../../../../components/ha-dialog";
type WorkingRouteStatistics =
| (ZWaveJSRouteStatistics & {
repeater_rssi_table?: TemplateResult;
rssi_translated?: TemplateResult | string;
route_failed_between_translated?: [string, string];
})
| undefined;
@customElement("dialog-zwave_js-node-statistics")
class DialogZWaveJSNodeStatistics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private device?: DeviceRegistryEntry;
@state() private _nodeStatistics?: ZWaveJSNodeStatisticsUpdatedMessage & {
rssi_translated?: TemplateResult | string;
};
@state() private _deviceIDsToName: { [key: string]: string } = {};
@state() private _workingRoutes: {
lwr?: WorkingRouteStatistics;
nlwr?: WorkingRouteStatistics;
} = {};
private _subscribedNodeStatistics?: Promise<UnsubscribeFunc>;
private _subscribedDeviceRegistry?: UnsubscribeFunc;
public showDialog(params: ZWaveJSNodeStatisticsDialogParams): void {
this.device = params.device;
this._subscribeDeviceRegistry();
this._subscribeNodeStatistics();
}
public closeDialog(): void {
this._nodeStatistics = undefined;
this.device = undefined;
this._unsubscribe();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this.device) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zwave_js.node_statistics.title")
)}
>
<mwc-list noninteractive>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_tx.label"
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_tx.tooltip"
)}
</span>
<span slot="meta">${this._nodeStatistics?.commands_tx}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_rx.label"
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_rx.tooltip"
)}
</span>
<span slot="meta">${this._nodeStatistics?.commands_rx}</span>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_dropped_tx.label"
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_dropped_tx.tooltip"
)}
</span>
<span slot="meta"
>${this._nodeStatistics?.commands_dropped_tx}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_dropped_rx.label"
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.commands_dropped_rx.tooltip"
)}
</span>
<span slot="meta"
>${this._nodeStatistics?.commands_dropped_rx}</span
>
</mwc-list-item>
<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.timeout_response.label"
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.timeout_response.tooltip"
)}
</span>
<span slot="meta">${this._nodeStatistics?.timeout_response}</span>
</mwc-list-item>
${this._nodeStatistics?.rtt
? html`<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.rtt.label"
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.rtt.tooltip"
)}
</span>
<span slot="meta">${this._nodeStatistics.rtt}</span>
</mwc-list-item>`
: ``}
${this._nodeStatistics?.rssi_translated
? html`<mwc-list-item twoline hasmeta>
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.rssi.label"
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.node_statistics.rssi.tooltip"
)}
</span>
<span slot="meta">${this._nodeStatistics.rssi_translated}</span>
</mwc-list-item>`
: ``}
</mwc-list>
${Object.entries(this._workingRoutes).map(([wrKey, wrValue]) =>
wrValue
? html`
<ha-expansion-panel
.header=${this.hass.localize(
`ui.panel.config.zwave_js.node_statistics.${wrKey}`
)}
>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.protocol.label"
)}<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.protocol.tooltip"
)}
>
</ha-help-tooltip
></span>
<span
>${this.hass.localize(
`ui.panel.config.zwave_js.route_statistics.protocol.protocol_data_rate.${
ProtocolDataRate[wrValue.protocol_data_rate]
}`
)}</span
>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.data_rate.label"
)}<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.data_rate.tooltip"
)}
>
</ha-help-tooltip
></span>
<span
>${this.hass.localize(
`ui.panel.config.zwave_js.route_statistics.data_rate.protocol_data_rate.${
ProtocolDataRate[wrValue.protocol_data_rate]
}`
)}</span
>
</div>
${wrValue.rssi_translated
? html`<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.rssi.label"
)}<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.rssi.tooltip"
)}
>
</ha-help-tooltip
></span>
<span>${wrValue.rssi_translated}</span>
</div>`
: ``}
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.route_failed_between.label"
)}<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.route_failed_between.tooltip"
)}
>
</ha-help-tooltip
></span>
<span>
${wrValue.route_failed_between_translated
? html`${wrValue
.route_failed_between_translated[0]}<ha-svg-icon
.path=${mdiSwapHorizontal}
></ha-svg-icon
>${wrValue.route_failed_between_translated[1]}`
: this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.route_failed_between.not_applicable"
)}
</span>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.repeaters.label"
)}<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.repeaters.tooltip"
)}
>
</ha-help-tooltip></span
><span>
${wrValue.repeater_rssi_table
? html`<div class="row">
<span class="key-cell"
><b
>${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.repeaters.repeaters"
)}:</b
></span
>
<span class="value-cell"
><b
>${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.repeaters.rssi"
)}:</b
></span
>
</div>
${wrValue.repeater_rssi_table}`
: html`${this.hass.localize(
"ui.panel.config.zwave_js.route_statistics.repeaters.direct"
)}`}</span
>
</div>
</ha-expansion-panel>
`
: ``
)}
</ha-dialog>
`;
}
private _computeRSSI(
rssi: number,
includeUnit: boolean
): TemplateResult | string {
if (Object.values(RssiError).includes(rssi)) {
return html`<ha-help-tooltip
.label=${this.hass.localize(
`ui.panel.config.zwave_js.rssi.rssi_error.${RssiError[rssi]}`
)}
></ha-help-tooltip>`;
}
if (includeUnit) {
return `${rssi}
${this.hass.localize("ui.panel.config.zwave_js.rssi.unit")}`;
}
return rssi.toString();
}
private _computeDeviceNameById(device_id: string): "unknown device" | string {
if (!this._deviceIDsToName) {
return "unknown device";
}
const device = this._deviceIDsToName[device_id];
if (!device) {
return "unknown device";
}
return this._deviceIDsToName[device_id] || "unknown device";
}
private _subscribeNodeStatistics(): void {
if (!this.hass) {
return;
}
this._subscribedNodeStatistics = subscribeZwaveNodeStatistics(
this.hass,
this.device!.id,
(message: ZWaveJSNodeStatisticsUpdatedMessage) => {
this._nodeStatistics = {
...message,
rssi_translated: message.rssi
? this._computeRSSI(message.rssi, false)
: undefined,
};
const workingRoutesValueMap: [
string,
WorkingRouteStatistics | null | undefined
][] = [
["lwr", this._nodeStatistics?.lwr],
["nlwr", this._nodeStatistics?.nlwr],
];
const workingRoutes: {
lwr?: WorkingRouteStatistics;
nlwr?: WorkingRouteStatistics;
} = {};
workingRoutesValueMap.forEach(([wrKey, wrValue]) => {
workingRoutes[wrKey] = wrValue;
if (wrValue) {
if (wrValue.rssi) {
wrValue.rssi_translated = this._computeRSSI(wrValue.rssi, true);
}
if (wrValue.route_failed_between) {
wrValue.route_failed_between_translated = [
this._computeDeviceNameById(wrValue.route_failed_between[0]),
this._computeDeviceNameById(wrValue.route_failed_between[1]),
];
}
if (wrValue.repeaters && wrValue.repeaters.length) {
wrValue.repeater_rssi_table = html`${wrValue.repeaters.map(
(_, idx) =>
html`<div class="row">
<span class="key-cell"
>${this._computeDeviceNameById(
wrValue.repeaters[idx]
)}:</span
>
<span class="value-cell"
>${this._computeRSSI(
wrValue.repeater_rssi[idx],
true
)}</span
>
</div>`
)}`;
}
}
});
this._workingRoutes = workingRoutes;
}
);
}
private _subscribeDeviceRegistry(): void {
if (!this.hass) {
return;
}
this._subscribedDeviceRegistry = subscribeDeviceRegistry(
this.hass.connection,
(devices: DeviceRegistryEntry[]) => {
const devicesIdToName = {};
devices.forEach((device) => {
devicesIdToName[device.id] = computeDeviceName(device, this.hass);
});
this._deviceIDsToName = devicesIdToName;
}
);
}
private _unsubscribe(): void {
if (this._subscribedNodeStatistics) {
this._subscribedNodeStatistics.then((unsub) => unsub());
this._subscribedNodeStatistics = undefined;
}
if (this._subscribedDeviceRegistry) {
this._subscribedDeviceRegistry();
this._subscribedDeviceRegistry = undefined;
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
mwc-list-item {
height: 60px;
}
.row {
display: flex;
justify-content: space-between;
}
.table {
display: table;
}
.key-cell {
display: table-cell;
padding-right: 5px;
}
.value-cell {
display: table-cell;
padding-left: 5px;
}
span[slot="meta"] {
font-size: 0.95em;
color: var(--primary-text-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zwave_js-node-statistics": DialogZWaveJSNodeStatistics;
}
}

View File

@ -0,0 +1,20 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
export interface ZWaveJSNodeStatisticsDialogParams {
device: DeviceRegistryEntry;
}
export const loadNodeStatisticsDialog = () =>
import("./dialog-zwave_js-node-statistics");
export const showZWaveJSNodeStatisticsDialog = (
element: HTMLElement,
nodeStatisticsDialogParams: ZWaveJSNodeStatisticsDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-zwave_js-node-statistics",
dialogImport: loadNodeStatisticsDialog,
dialogParams: nodeStatisticsDialogParams,
});
};

View File

@ -3103,7 +3103,87 @@
"highest_security": "Highest Security",
"unknown": "Unknown",
"zwave_plus": "Z-Wave Plus",
"zwave_plus_version": "Version {version}"
"zwave_plus_version": "Version {version}",
"node_statistics": "Show Device Statistics"
},
"node_statistics": {
"title": "Device Statistics",
"commands_tx": {
"label": "Commands TX",
"tooltip": "# of commands successfully sent to the node"
},
"commands_rx": {
"label": "Commands RX",
"tooltip": "# of commands received from the node, including responses to sent commands"
},
"commands_dropped_tx": {
"label": "Commands Dropped TX",
"tooltip": "# of outgoing commands that were dropped because they could not be sent"
},
"commands_dropped_rx": {
"label": "Commands Dropped RX",
"tooltip": "# of commands from the node that were dropped by the host"
},
"timeout_response": {
"label": "Timeout Response",
"tooltip": "# of Get-type commands where the node's response did not come in time"
},
"rtt": {
"label": "RTT",
"tooltip": "Average round-trip-time in ms of commands to this node"
},
"rssi": {
"label": "RSSI",
"tooltip": "Average RSSI in dBm of frames received by this node"
},
"lwr": "Last Working Route",
"nlwr": "Next to Last Working Route"
},
"route_statistics": {
"protocol": {
"label": "Protocol",
"tooltip": "The protocol for this route",
"protocol_data_rate": {
"ZWave_9k6": "Z-Wave",
"ZWave_40k": "Z-Wave",
"ZWave_100k": "Z-Wave",
"LongRange_100k": "Z-Wave Long Range"
}
},
"data_rate": {
"label": "Data Rate",
"tooltip": "The used data rate for this route",
"protocol_data_rate": {
"ZWave_9k6": "9.6 kbps",
"ZWave_40k": "40 kbps",
"ZWave_100k": "100 kbps",
"LongRange_100k": "100 kbps"
}
},
"repeaters": {
"label": "Repeaters + RSSI",
"tooltip": "Which nodes are repeaters for this route and their RSSI",
"repeaters": "Repeater Device",
"rssi": "RSSI",
"direct": "None, direct connection"
},
"rssi": {
"label": "RSSI",
"tooltip": "The RSSI of the ACK frame received by the controller"
},
"route_failed_between": {
"label": "Route Failed Between",
"tooltip": "The nodes between which the transmission failed most recently",
"not_applicable": "N/A"
}
},
"rssi": {
"unit": "dBm",
"rssi_error": {
"NotAvailable": "Not available",
"ReceiverSaturated": "Receiver saturated",
"NoSignalDetected": "No signal detected"
}
},
"node_config": {
"header": "Z-Wave Device Configuration",