@matter-server/dashboard 0.3.3 → 0.3.5-alpha.0-20260203-66f5d74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -1
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js +44 -17
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
- package/dist/esm/entrypoint/main.js +1 -1
- package/dist/esm/entrypoint/main.js.map +1 -1
- package/dist/esm/pages/components/node-details.js +1 -1
- package/dist/esm/pages/components/node-details.js.map +1 -1
- package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-cluster-view.js +9 -4
- package/dist/esm/pages/matter-cluster-view.js.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.js +8 -2
- package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
- package/dist/esm/pages/matter-network-view.d.ts +5 -0
- package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-network-view.js +96 -13
- package/dist/esm/pages/matter-network-view.js.map +1 -1
- package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-node-view.js +17 -3
- package/dist/esm/pages/matter-node-view.js.map +1 -1
- package/dist/esm/pages/network/base-network-graph.d.ts +34 -0
- package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/base-network-graph.js +116 -25
- package/dist/esm/pages/network/base-network-graph.js.map +1 -1
- package/dist/esm/pages/network/device-icons.js +1 -1
- package/dist/esm/pages/network/network-details.d.ts +31 -1
- package/dist/esm/pages/network/network-details.d.ts.map +1 -1
- package/dist/esm/pages/network/network-details.js +233 -15
- package/dist/esm/pages/network/network-details.js.map +1 -1
- package/dist/esm/pages/network/network-types.d.ts +6 -0
- package/dist/esm/pages/network/network-types.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.d.ts +26 -0
- package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.js +71 -3
- package/dist/esm/pages/network/network-utils.js.map +1 -1
- package/dist/esm/pages/network/thread-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/thread-graph.js +5 -2
- package/dist/esm/pages/network/thread-graph.js.map +1 -1
- package/dist/esm/pages/network/update-connections-dialog.d.ts +55 -0
- package/dist/esm/pages/network/update-connections-dialog.d.ts.map +1 -0
- package/dist/esm/pages/network/update-connections-dialog.js +284 -0
- package/dist/esm/pages/network/update-connections-dialog.js.map +6 -0
- package/dist/esm/pages/network/wifi-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/wifi-graph.js +5 -3
- package/dist/esm/pages/network/wifi-graph.js.map +1 -1
- package/dist/esm/util/format_hex.d.ts +18 -0
- package/dist/esm/util/format_hex.d.ts.map +1 -1
- package/dist/esm/util/format_hex.js +21 -1
- package/dist/esm/util/format_hex.js.map +1 -1
- package/dist/esm/util/matter-status.d.ts +49 -0
- package/dist/esm/util/matter-status.d.ts.map +1 -0
- package/dist/esm/util/matter-status.js +110 -0
- package/dist/esm/util/matter-status.js.map +6 -0
- package/dist/web/js/{commission-node-dialog-B1_khzZb.js → commission-node-dialog-DBugVQOl.js} +5 -5
- package/dist/web/js/{commission-node-existing-RpdajrwF.js → commission-node-existing-ts2MN2bQ.js} +2 -2
- package/dist/web/js/{commission-node-thread-5f2itkTG.js → commission-node-thread-CixfNuM3.js} +2 -2
- package/dist/web/js/{commission-node-wifi-DZ_pWqsa.js → commission-node-wifi-CWC30EYn.js} +2 -2
- package/dist/web/js/{dialog-box-DEUxM4B1.js → dialog-box-SeMuFiWx.js} +2 -2
- package/dist/web/js/{fire_event-BczBMT8E.js → fire_event-B63oGTcK.js} +1 -1
- package/dist/web/js/{log-level-dialog-Cr3PfX1X.js → log-level-dialog-J0gFiLLM.js} +2 -2
- package/dist/web/js/main.js +2 -2
- package/dist/web/js/{matter-dashboard-app-BuCe_Jxf.js → matter-dashboard-app-D7YWrgXj.js} +2041 -246
- package/dist/web/js/{node-binding-dialog-DMiHNDLA.js → node-binding-dialog-DSBwIJcQ.js} +147 -19
- package/dist/web/js/prevent_default-D-ohDGsN.js +8 -0
- package/package.json +4 -4
- package/src/components/dialogs/binding/node-binding-dialog.ts +45 -21
- package/src/entrypoint/main.ts +2 -0
- package/src/pages/components/node-details.ts +1 -1
- package/src/pages/matter-cluster-view.ts +11 -4
- package/src/pages/matter-endpoint-view.ts +10 -3
- package/src/pages/matter-network-view.ts +102 -13
- package/src/pages/matter-node-view.ts +19 -4
- package/src/pages/network/base-network-graph.ts +147 -33
- package/src/pages/network/device-icons.ts +1 -1
- package/src/pages/network/network-details.ts +281 -16
- package/src/pages/network/network-types.ts +6 -0
- package/src/pages/network/network-utils.ts +109 -0
- package/src/pages/network/thread-graph.ts +7 -1
- package/src/pages/network/update-connections-dialog.ts +327 -0
- package/src/pages/network/wifi-graph.ts +4 -3
- package/src/util/format_hex.ts +39 -0
- package/src/util/matter-status.ts +165 -0
- package/dist/web/js/prevent_default-D4FX_PIh.js +0 -774
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { consume } from "@lit/context";
|
|
7
8
|
import "@material/web/divider/divider";
|
|
8
|
-
import type
|
|
9
|
-
import { mdiClose, mdiSignal, mdiSignalCellular1, mdiSignalCellular2 } from "@mdi/js";
|
|
9
|
+
import { isTestNodeId, type MatterClient, type MatterNode } from "@matter-server/ws-client";
|
|
10
|
+
import { mdiClose, mdiRefresh, mdiSignal, mdiSignalCellular1, mdiSignalCellular2 } from "@mdi/js";
|
|
10
11
|
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
|
11
|
-
import { customElement, property } from "lit/decorators.js";
|
|
12
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
13
|
+
import { clientContext } from "../../client/client-context.js";
|
|
12
14
|
import "../../components/ha-svg-icon";
|
|
15
|
+
import { formatNodeAddressFromAny, getEffectiveFabricIndex } from "../../util/format_hex.js";
|
|
13
16
|
import type { ThreadNeighbor } from "./network-types.js";
|
|
14
17
|
import type { NodeConnection } from "./network-utils.js";
|
|
15
18
|
import {
|
|
@@ -17,6 +20,7 @@ import {
|
|
|
17
20
|
getDeviceName,
|
|
18
21
|
getNetworkType,
|
|
19
22
|
getNodeConnections,
|
|
23
|
+
getRoutableDestinationsCount,
|
|
20
24
|
getSignalColor,
|
|
21
25
|
getSignalColorFromRssi,
|
|
22
26
|
getThreadChannel,
|
|
@@ -28,6 +32,7 @@ import {
|
|
|
28
32
|
getWiFiVersionName,
|
|
29
33
|
parseNeighborTable,
|
|
30
34
|
} from "./network-utils.js";
|
|
35
|
+
import "./update-connections-dialog.js";
|
|
31
36
|
|
|
32
37
|
declare global {
|
|
33
38
|
interface HTMLElementTagNameMap {
|
|
@@ -52,6 +57,12 @@ export class NetworkDetails extends LitElement {
|
|
|
52
57
|
@property({ type: Object })
|
|
53
58
|
public wifiAccessPoints: Map<string, { bssid: string; connectedNodes: string[] }> = new Map();
|
|
54
59
|
|
|
60
|
+
@consume({ context: clientContext })
|
|
61
|
+
private client!: MatterClient;
|
|
62
|
+
|
|
63
|
+
@state()
|
|
64
|
+
private _showUpdateDialog: boolean = false;
|
|
65
|
+
|
|
55
66
|
private _handleClose(): void {
|
|
56
67
|
this.dispatchEvent(
|
|
57
68
|
new CustomEvent("close", {
|
|
@@ -100,6 +111,19 @@ export class NetworkDetails extends LitElement {
|
|
|
100
111
|
return mdiSignalCellular1; // Weak
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Format a node ID as hex for Matter log format display.
|
|
116
|
+
* Returns format like "@1:7b" for node ID 123.
|
|
117
|
+
*/
|
|
118
|
+
private _formatNodeIdHex(nodeId: number | bigint | string): string {
|
|
119
|
+
// For unknown devices (not in nodes), we can't determine if it's a test node,
|
|
120
|
+
// so we use the fabric index if available
|
|
121
|
+
const node = this.nodes[String(nodeId)];
|
|
122
|
+
const isTestNode = node ? isTestNodeId(node.node_id) : false;
|
|
123
|
+
const fabricIndex = getEffectiveFabricIndex(this.client?.serverInfo?.fabric_index, isTestNode);
|
|
124
|
+
return formatNodeAddressFromAny(fabricIndex, nodeId);
|
|
125
|
+
}
|
|
126
|
+
|
|
103
127
|
private _renderWiFiInfo(node: MatterNode): TemplateResult | typeof nothing {
|
|
104
128
|
const wifiDiag = getWiFiDiagnostics(node);
|
|
105
129
|
|
|
@@ -190,6 +214,21 @@ export class NetworkDetails extends LitElement {
|
|
|
190
214
|
</div>
|
|
191
215
|
`
|
|
192
216
|
: nothing}
|
|
217
|
+
<div class="info-row">
|
|
218
|
+
<span class="label">Direct neighbors:</span>
|
|
219
|
+
<span class="value">${connections.length}</span>
|
|
220
|
+
</div>
|
|
221
|
+
${(() => {
|
|
222
|
+
const routableCount = getRoutableDestinationsCount(node);
|
|
223
|
+
return routableCount > 0
|
|
224
|
+
? html`
|
|
225
|
+
<div class="info-row">
|
|
226
|
+
<span class="label">Routable destinations:</span>
|
|
227
|
+
<span class="value">${routableCount}</span>
|
|
228
|
+
</div>
|
|
229
|
+
`
|
|
230
|
+
: nothing;
|
|
231
|
+
})()}
|
|
193
232
|
</div>
|
|
194
233
|
|
|
195
234
|
${connections.length > 0
|
|
@@ -214,13 +253,26 @@ export class NetworkDetails extends LitElement {
|
|
|
214
253
|
<div class="neighbor-info">
|
|
215
254
|
<div class="neighbor-name">
|
|
216
255
|
${conn.connectedNode
|
|
217
|
-
? html`Node ${conn.connectedNodeId}
|
|
218
|
-
|
|
256
|
+
? html`Node ${conn.connectedNodeId}
|
|
257
|
+
<span class="node-id-hex"
|
|
258
|
+
>${this._formatNodeIdHex(conn.connectedNodeId)}</span
|
|
259
|
+
>: ${getDeviceName(conn.connectedNode)}`
|
|
219
260
|
: html`External: <span class="mono">${conn.extAddressHex}</span>`}
|
|
220
261
|
</div>
|
|
221
262
|
<div class="neighbor-signal">
|
|
222
|
-
${conn.rssi !== null
|
|
223
|
-
|
|
263
|
+
${conn.rssi !== null
|
|
264
|
+
? html`RSSI: ${conn.rssi} dBm`
|
|
265
|
+
: nothing}${conn.rssi !== null && conn.lqi !== null
|
|
266
|
+
? ", "
|
|
267
|
+
: nothing}${conn.lqi !== null
|
|
268
|
+
? html`LQI: ${conn.lqi}`
|
|
269
|
+
: nothing}${conn.bidirectionalLqi !== undefined
|
|
270
|
+
? html`<span class="route-info"
|
|
271
|
+
>, Bidir: ${conn.bidirectionalLqi}</span
|
|
272
|
+
>`
|
|
273
|
+
: nothing}${conn.pathCost !== undefined
|
|
274
|
+
? html`<span class="route-info">, Cost: ${conn.pathCost}</span>`
|
|
275
|
+
: nothing}
|
|
224
276
|
${!conn.isOutgoing
|
|
225
277
|
? html`<span class="direction-hint">(reverse)</span>`
|
|
226
278
|
: nothing}
|
|
@@ -363,7 +415,11 @@ export class NetworkDetails extends LitElement {
|
|
|
363
415
|
`
|
|
364
416
|
: nothing}
|
|
365
417
|
<div class="neighbor-info">
|
|
366
|
-
<div class="neighbor-name">
|
|
418
|
+
<div class="neighbor-name">
|
|
419
|
+
Node ${nodeId}
|
|
420
|
+
<span class="node-id-hex">${this._formatNodeIdHex(nodeId)}</span>:
|
|
421
|
+
${getDeviceName(node)}
|
|
422
|
+
</div>
|
|
367
423
|
${neighborEntry
|
|
368
424
|
? html`
|
|
369
425
|
<div class="neighbor-signal">
|
|
@@ -391,6 +447,112 @@ export class NetworkDetails extends LitElement {
|
|
|
391
447
|
`;
|
|
392
448
|
}
|
|
393
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Determine if update connections button should be shown.
|
|
452
|
+
*/
|
|
453
|
+
private _canUpdateConnections(): boolean {
|
|
454
|
+
if (this.selectedNodeId === null) return false;
|
|
455
|
+
|
|
456
|
+
// WiFi APs: no update possible (not a Matter device)
|
|
457
|
+
const isAccessPoint = typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("ap_");
|
|
458
|
+
if (isAccessPoint) return false;
|
|
459
|
+
|
|
460
|
+
// Unknown devices: only if they have online seenBy nodes
|
|
461
|
+
const isUnknown = typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("unknown_");
|
|
462
|
+
if (isUnknown) {
|
|
463
|
+
return this._getOnlineSeenByNodes().length > 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Regular nodes: check network type (no update for ethernet)
|
|
467
|
+
const node = this.nodes[this.selectedNodeId.toString()];
|
|
468
|
+
if (!node) return false;
|
|
469
|
+
|
|
470
|
+
const networkType = getNetworkType(node);
|
|
471
|
+
if (networkType === "ethernet" || networkType === "unknown") return false;
|
|
472
|
+
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get the type of the currently selected node for dialog variant.
|
|
478
|
+
*/
|
|
479
|
+
private _getSelectedNodeType(): "online" | "offline" | "unknown" {
|
|
480
|
+
if (typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("unknown_")) {
|
|
481
|
+
return "unknown";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const node = this.nodes[this.selectedNodeId!.toString()];
|
|
485
|
+
if (!node || node.available === false) {
|
|
486
|
+
return "offline";
|
|
487
|
+
}
|
|
488
|
+
return "online";
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Get online neighbors for a Thread node.
|
|
493
|
+
*/
|
|
494
|
+
private _getOnlineNeighbors(nodeId: string): string[] {
|
|
495
|
+
const node = this.nodes[nodeId];
|
|
496
|
+
if (!node) return [];
|
|
497
|
+
|
|
498
|
+
const networkType = getNetworkType(node);
|
|
499
|
+
if (networkType === "thread") {
|
|
500
|
+
const extAddrMap = buildExtAddrMap(this.nodes);
|
|
501
|
+
const connections = getNodeConnections(nodeId, this.nodes, extAddrMap);
|
|
502
|
+
return connections
|
|
503
|
+
.filter(conn => {
|
|
504
|
+
// Only include commissioned nodes (not unknown devices)
|
|
505
|
+
if (typeof conn.connectedNodeId === "string" && conn.connectedNodeId.startsWith("unknown_")) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
const connectedNode = this.nodes[String(conn.connectedNodeId)];
|
|
509
|
+
return connectedNode?.available === true;
|
|
510
|
+
})
|
|
511
|
+
.map(conn => String(conn.connectedNodeId));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// WiFi nodes don't have peer connections (just AP)
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Get online nodes that see an unknown device.
|
|
520
|
+
*/
|
|
521
|
+
private _getOnlineSeenByNodes(): string[] {
|
|
522
|
+
if (typeof this.selectedNodeId !== "string" || !this.selectedNodeId.startsWith("unknown_")) {
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const unknown = this.unknownDevices.get(this.selectedNodeId);
|
|
527
|
+
if (!unknown) return [];
|
|
528
|
+
|
|
529
|
+
return unknown.seenBy.filter(nodeId => {
|
|
530
|
+
const node = this.nodes[nodeId.toString()];
|
|
531
|
+
return node?.available === true;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Get the name of the selected node for display in dialog.
|
|
537
|
+
*/
|
|
538
|
+
private _getSelectedNodeName(): string {
|
|
539
|
+
if (typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("unknown_")) {
|
|
540
|
+
const unknown = this.unknownDevices.get(this.selectedNodeId);
|
|
541
|
+
return unknown ? `Unknown (${unknown.extAddressHex.slice(-8)})` : "Unknown Device";
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const node = this.nodes[this.selectedNodeId!.toString()];
|
|
545
|
+
return node ? getDeviceName(node) : "Unknown";
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private _handleUpdateConnections(): void {
|
|
549
|
+
this._showUpdateDialog = true;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private _handleDialogClose(): void {
|
|
553
|
+
this._showUpdateDialog = false;
|
|
554
|
+
}
|
|
555
|
+
|
|
394
556
|
private _renderWiFiAccessPointInfo(apId: string): TemplateResult | typeof nothing {
|
|
395
557
|
const ap = this.wifiAccessPoints.get(apId);
|
|
396
558
|
if (!ap) {
|
|
@@ -429,7 +591,11 @@ export class NetworkDetails extends LitElement {
|
|
|
429
591
|
@click=${() => this._handleSelectNode(nodeId)}
|
|
430
592
|
@keydown=${(e: KeyboardEvent) => this._handleKeyDown(e, nodeId)}
|
|
431
593
|
>
|
|
432
|
-
<div class="node-name">
|
|
594
|
+
<div class="node-name">
|
|
595
|
+
Node ${nodeId}
|
|
596
|
+
<span class="node-id-hex">${this._formatNodeIdHex(nodeId)}</span>:
|
|
597
|
+
${getDeviceName(node)}
|
|
598
|
+
</div>
|
|
433
599
|
${wifiDiag.rssi !== null
|
|
434
600
|
? html`<div class="node-signal" style="color: ${signalColor}">
|
|
435
601
|
${wifiDiag.rssi} dBm
|
|
@@ -464,16 +630,44 @@ export class NetworkDetails extends LitElement {
|
|
|
464
630
|
const isUnknown = typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("unknown_");
|
|
465
631
|
|
|
466
632
|
if (isUnknown) {
|
|
633
|
+
const onlineSeenByNodes = this._getOnlineSeenByNodes();
|
|
467
634
|
return html`
|
|
468
635
|
<div class="details-panel">
|
|
469
636
|
<div class="header">
|
|
470
637
|
<h3>External Device</h3>
|
|
471
|
-
<
|
|
472
|
-
|
|
473
|
-
|
|
638
|
+
<div class="header-actions">
|
|
639
|
+
${onlineSeenByNodes.length > 0
|
|
640
|
+
? html`
|
|
641
|
+
<button
|
|
642
|
+
class="action-button"
|
|
643
|
+
@click=${this._handleUpdateConnections}
|
|
644
|
+
aria-label="Update connection data"
|
|
645
|
+
title="Update connection data"
|
|
646
|
+
>
|
|
647
|
+
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
|
648
|
+
</button>
|
|
649
|
+
`
|
|
650
|
+
: nothing}
|
|
651
|
+
<button class="close-button" @click=${this._handleClose} aria-label="Close details panel">
|
|
652
|
+
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
|
653
|
+
</button>
|
|
654
|
+
</div>
|
|
474
655
|
</div>
|
|
475
656
|
<div class="content">${this._renderUnknownDeviceInfo(this.selectedNodeId as string)}</div>
|
|
476
657
|
</div>
|
|
658
|
+
${this._showUpdateDialog
|
|
659
|
+
? html`
|
|
660
|
+
<update-connections-dialog
|
|
661
|
+
.client=${this.client}
|
|
662
|
+
.nodes=${this.nodes}
|
|
663
|
+
selectedNodeType="unknown"
|
|
664
|
+
.selectedNodeName=${this._getSelectedNodeName()}
|
|
665
|
+
.selectedNodeId=${this.selectedNodeId}
|
|
666
|
+
.onlineNeighborIds=${onlineSeenByNodes}
|
|
667
|
+
@dialog-closed=${this._handleDialogClose}
|
|
668
|
+
></update-connections-dialog>
|
|
669
|
+
`
|
|
670
|
+
: nothing}
|
|
477
671
|
`;
|
|
478
672
|
}
|
|
479
673
|
|
|
@@ -503,19 +697,53 @@ export class NetworkDetails extends LitElement {
|
|
|
503
697
|
`;
|
|
504
698
|
}
|
|
505
699
|
|
|
700
|
+
const canUpdate = this._canUpdateConnections();
|
|
701
|
+
const nodeType = this._getSelectedNodeType();
|
|
702
|
+
const onlineNeighbors = this._getOnlineNeighbors(String(this.selectedNodeId));
|
|
703
|
+
|
|
506
704
|
return html`
|
|
507
705
|
<div class="details-panel">
|
|
508
706
|
<div class="header">
|
|
509
|
-
<h3>
|
|
510
|
-
|
|
511
|
-
<
|
|
512
|
-
</
|
|
707
|
+
<h3>
|
|
708
|
+
Node ${this.selectedNodeId}
|
|
709
|
+
<span class="node-id-hex">${this._formatNodeIdHex(this.selectedNodeId)}</span>
|
|
710
|
+
</h3>
|
|
711
|
+
<div class="header-actions">
|
|
712
|
+
${canUpdate
|
|
713
|
+
? html`
|
|
714
|
+
<button
|
|
715
|
+
class="action-button"
|
|
716
|
+
@click=${this._handleUpdateConnections}
|
|
717
|
+
aria-label="Update connection data"
|
|
718
|
+
title="Update connection data"
|
|
719
|
+
>
|
|
720
|
+
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
|
721
|
+
</button>
|
|
722
|
+
`
|
|
723
|
+
: nothing}
|
|
724
|
+
<button class="close-button" @click=${this._handleClose} aria-label="Close details panel">
|
|
725
|
+
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
|
726
|
+
</button>
|
|
727
|
+
</div>
|
|
513
728
|
</div>
|
|
514
729
|
<div class="content">${this._renderNodeInfo(node)}</div>
|
|
515
730
|
<div class="footer">
|
|
516
731
|
<a href="#node/${this.selectedNodeId}" class="view-link">View node details</a>
|
|
517
732
|
</div>
|
|
518
733
|
</div>
|
|
734
|
+
${this._showUpdateDialog
|
|
735
|
+
? html`
|
|
736
|
+
<update-connections-dialog
|
|
737
|
+
.client=${this.client}
|
|
738
|
+
.nodes=${this.nodes}
|
|
739
|
+
.selectedNodeType=${nodeType}
|
|
740
|
+
.selectedNodeName=${this._getSelectedNodeName()}
|
|
741
|
+
.selectedNodeId=${this.selectedNodeId}
|
|
742
|
+
.onlineNeighborIds=${onlineNeighbors}
|
|
743
|
+
@dialog-closed=${this._handleDialogClose}
|
|
744
|
+
></update-connections-dialog>
|
|
745
|
+
`
|
|
746
|
+
: nothing}
|
|
519
747
|
`;
|
|
520
748
|
}
|
|
521
749
|
|
|
@@ -561,6 +789,38 @@ export class NetworkDetails extends LitElement {
|
|
|
561
789
|
color: var(--md-sys-color-on-surface, #333);
|
|
562
790
|
}
|
|
563
791
|
|
|
792
|
+
.node-id-hex {
|
|
793
|
+
font-size: 0.75em;
|
|
794
|
+
font-weight: 400;
|
|
795
|
+
color: var(--md-sys-color-on-surface-variant, #666);
|
|
796
|
+
font-family: monospace;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.header-actions {
|
|
800
|
+
display: flex;
|
|
801
|
+
align-items: center;
|
|
802
|
+
gap: 4px;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.action-button {
|
|
806
|
+
background: none;
|
|
807
|
+
border: none;
|
|
808
|
+
padding: 4px;
|
|
809
|
+
cursor: pointer;
|
|
810
|
+
border-radius: 50%;
|
|
811
|
+
display: flex;
|
|
812
|
+
align-items: center;
|
|
813
|
+
justify-content: center;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.action-button:hover {
|
|
817
|
+
background-color: var(--md-sys-color-surface-container-high, #e8e8e8);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.action-button ha-svg-icon {
|
|
821
|
+
--icon-primary-color: var(--md-sys-color-on-surface-variant, #666);
|
|
822
|
+
}
|
|
823
|
+
|
|
564
824
|
.close-button {
|
|
565
825
|
background: none;
|
|
566
826
|
border: none;
|
|
@@ -682,6 +942,11 @@ export class NetworkDetails extends LitElement {
|
|
|
682
942
|
opacity: 0.8;
|
|
683
943
|
}
|
|
684
944
|
|
|
945
|
+
.route-info {
|
|
946
|
+
color: var(--md-sys-color-tertiary, #7d5260);
|
|
947
|
+
font-size: 0.85em;
|
|
948
|
+
}
|
|
949
|
+
|
|
685
950
|
.footer {
|
|
686
951
|
padding: 12px 16px;
|
|
687
952
|
border-top: 1px solid var(--md-sys-color-outline-variant, #ccc);
|
|
@@ -105,6 +105,12 @@ export interface ThreadConnection {
|
|
|
105
105
|
signalColor: string;
|
|
106
106
|
lqi: number;
|
|
107
107
|
rssi: number | null;
|
|
108
|
+
/** Path cost from route table (1 = direct, higher = multi-hop). Only available for routers. */
|
|
109
|
+
pathCost?: number;
|
|
110
|
+
/** Bidirectional LQI from route table (average of lqiIn and lqiOut) */
|
|
111
|
+
bidirectionalLqi?: number;
|
|
112
|
+
/** Whether this connection was supplemented by route table data (vs neighbor table only) */
|
|
113
|
+
fromRouteTable?: boolean;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
/**
|
|
@@ -297,6 +297,38 @@ export function parseRouteTable(node: MatterNode): ThreadRoute[] {
|
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Find a route table entry for a specific destination by extended address.
|
|
302
|
+
* Returns the route entry if found, undefined otherwise.
|
|
303
|
+
*/
|
|
304
|
+
export function findRouteByExtAddress(node: MatterNode, targetExtAddr: bigint): ThreadRoute | undefined {
|
|
305
|
+
const routes = parseRouteTable(node);
|
|
306
|
+
return routes.find(route => route.extAddress === targetExtAddr && route.linkEstablished);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Count the number of routable destinations for a node (from route table).
|
|
311
|
+
* Only counts entries where allocated=true and linkEstablished=true.
|
|
312
|
+
* This is typically only meaningful for router nodes.
|
|
313
|
+
*/
|
|
314
|
+
export function getRoutableDestinationsCount(node: MatterNode): number {
|
|
315
|
+
const routes = parseRouteTable(node);
|
|
316
|
+
return routes.filter(route => route.allocated && route.linkEstablished).length;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Calculate combined bidirectional LQI from route table entry.
|
|
321
|
+
* Returns average of lqiIn and lqiOut if both are non-zero.
|
|
322
|
+
*/
|
|
323
|
+
export function getRouteBidirectionalLqi(route: ThreadRoute): number | undefined {
|
|
324
|
+
if (route.lqiIn > 0 && route.lqiOut > 0) {
|
|
325
|
+
return Math.round((route.lqiIn + route.lqiOut) / 2);
|
|
326
|
+
}
|
|
327
|
+
if (route.lqiIn > 0) return route.lqiIn;
|
|
328
|
+
if (route.lqiOut > 0) return route.lqiOut;
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
300
332
|
/**
|
|
301
333
|
* Gets the RLOC16 (short address) for a Thread node.
|
|
302
334
|
* Uses attribute 0/53/64 (Rloc16, 0x0040).
|
|
@@ -437,6 +469,21 @@ export function getSignalColor(neighbor: ThreadNeighbor): string {
|
|
|
437
469
|
return SIGNAL_COLOR_WEAK;
|
|
438
470
|
}
|
|
439
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Get signal color based on LQI value alone.
|
|
474
|
+
* Used for route table entries where only LQI is available.
|
|
475
|
+
* @param lqi Link Quality Indicator (0-255, higher is better)
|
|
476
|
+
*/
|
|
477
|
+
export function getSignalColorFromLqi(lqi: number): string {
|
|
478
|
+
if (lqi > LQI_STRONG_THRESHOLD) {
|
|
479
|
+
return SIGNAL_COLOR_STRONG;
|
|
480
|
+
}
|
|
481
|
+
if (lqi > LQI_MEDIUM_THRESHOLD) {
|
|
482
|
+
return SIGNAL_COLOR_MEDIUM;
|
|
483
|
+
}
|
|
484
|
+
return SIGNAL_COLOR_WEAK;
|
|
485
|
+
}
|
|
486
|
+
|
|
440
487
|
/**
|
|
441
488
|
* Gets a human-readable display name for a node.
|
|
442
489
|
* Format: nodeLabel || productName (serialNumber)
|
|
@@ -634,12 +681,58 @@ export function buildThreadConnections(
|
|
|
634
681
|
}
|
|
635
682
|
seenConnections.add(connectionKey);
|
|
636
683
|
|
|
684
|
+
// Look up route table entry for supplementary data (pathCost, bidirectional LQI)
|
|
685
|
+
const routeEntry = findRouteByExtAddress(node, neighbor.extAddress);
|
|
686
|
+
const bidirectionalLqi = routeEntry ? getRouteBidirectionalLqi(routeEntry) : undefined;
|
|
687
|
+
|
|
688
|
+
// Always use neighbor table RSSI/LQI for signal color (most accurate link quality)
|
|
637
689
|
connections.push({
|
|
638
690
|
fromNodeId,
|
|
639
691
|
toNodeId,
|
|
640
692
|
signalColor: getSignalColor(neighbor),
|
|
641
693
|
lqi: neighbor.lqi,
|
|
642
694
|
rssi: neighbor.avgRssi ?? neighbor.lastRssi,
|
|
695
|
+
pathCost: routeEntry?.pathCost,
|
|
696
|
+
bidirectionalLqi,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Check route table for connections not in neighbor table (supplementary data)
|
|
701
|
+
// This helps when neighbor table is stale or incomplete
|
|
702
|
+
const routes = parseRouteTable(node);
|
|
703
|
+
for (const route of routes) {
|
|
704
|
+
if (!route.linkEstablished || !route.allocated) continue;
|
|
705
|
+
|
|
706
|
+
let toNodeId: string | undefined = extAddrMap.get(route.extAddress);
|
|
707
|
+
if (toNodeId === undefined) {
|
|
708
|
+
toNodeId = unknownExtAddrMap.get(route.extAddress);
|
|
709
|
+
}
|
|
710
|
+
if (toNodeId === undefined || toNodeId === fromNodeId) continue;
|
|
711
|
+
|
|
712
|
+
const connectionKey = `${fromNodeId}-${toNodeId}`;
|
|
713
|
+
const reverseKey = `${toNodeId}-${fromNodeId}`;
|
|
714
|
+
|
|
715
|
+
// Only add if we don't already have this connection from neighbor table
|
|
716
|
+
if (seenConnections.has(connectionKey) || seenConnections.has(reverseKey)) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
seenConnections.add(connectionKey);
|
|
720
|
+
|
|
721
|
+
const bidirectionalLqi = getRouteBidirectionalLqi(route);
|
|
722
|
+
const signalColor =
|
|
723
|
+
bidirectionalLqi !== undefined
|
|
724
|
+
? getSignalColorFromLqi(bidirectionalLqi)
|
|
725
|
+
: "var(--md-sys-color-outline, grey)"; // Unknown signal
|
|
726
|
+
|
|
727
|
+
connections.push({
|
|
728
|
+
fromNodeId,
|
|
729
|
+
toNodeId,
|
|
730
|
+
signalColor,
|
|
731
|
+
lqi: bidirectionalLqi ?? 0,
|
|
732
|
+
rssi: null,
|
|
733
|
+
pathCost: route.pathCost,
|
|
734
|
+
bidirectionalLqi,
|
|
735
|
+
fromRouteTable: true,
|
|
643
736
|
});
|
|
644
737
|
}
|
|
645
738
|
}
|
|
@@ -666,6 +759,10 @@ export interface NodeConnection {
|
|
|
666
759
|
isOutgoing: boolean;
|
|
667
760
|
/** Whether this is an unknown/external device */
|
|
668
761
|
isUnknown: boolean;
|
|
762
|
+
/** Path cost from route table (1 = direct, higher = multi-hop). Only available for routers. */
|
|
763
|
+
pathCost?: number;
|
|
764
|
+
/** Bidirectional LQI from route table (average of lqiIn and lqiOut) */
|
|
765
|
+
bidirectionalLqi?: number;
|
|
669
766
|
}
|
|
670
767
|
|
|
671
768
|
/**
|
|
@@ -705,6 +802,10 @@ export function getNodeConnections(
|
|
|
705
802
|
|
|
706
803
|
seenConnectedIds.add(displayId);
|
|
707
804
|
|
|
805
|
+
// Look up route table entry for enhanced data
|
|
806
|
+
const routeEntry = findRouteByExtAddress(node, neighbor.extAddress);
|
|
807
|
+
const bidirectionalLqi = routeEntry ? getRouteBidirectionalLqi(routeEntry) : undefined;
|
|
808
|
+
|
|
708
809
|
connections.push({
|
|
709
810
|
connectedNodeId: displayId,
|
|
710
811
|
connectedNode,
|
|
@@ -714,6 +815,8 @@ export function getNodeConnections(
|
|
|
714
815
|
rssi: neighbor.avgRssi ?? neighbor.lastRssi,
|
|
715
816
|
isOutgoing: true,
|
|
716
817
|
isUnknown,
|
|
818
|
+
pathCost: routeEntry?.pathCost,
|
|
819
|
+
bidirectionalLqi,
|
|
717
820
|
});
|
|
718
821
|
}
|
|
719
822
|
|
|
@@ -734,6 +837,10 @@ export function getNodeConnections(
|
|
|
734
837
|
const otherExtAddr = getThreadExtendedAddress(otherNode);
|
|
735
838
|
const extAddrHex = otherExtAddr ? otherExtAddr.toString(16).toUpperCase().padStart(16, "0") : "Unknown";
|
|
736
839
|
|
|
840
|
+
// Look up route table entry from the other node's perspective
|
|
841
|
+
const routeEntry = findRouteByExtAddress(otherNode, thisExtAddr);
|
|
842
|
+
const bidirectionalLqi = routeEntry ? getRouteBidirectionalLqi(routeEntry) : undefined;
|
|
843
|
+
|
|
737
844
|
connections.push({
|
|
738
845
|
connectedNodeId: otherNodeId,
|
|
739
846
|
connectedNode: otherNode,
|
|
@@ -743,6 +850,8 @@ export function getNodeConnections(
|
|
|
743
850
|
rssi: reverseEntry.avgRssi ?? reverseEntry.lastRssi,
|
|
744
851
|
isOutgoing: false,
|
|
745
852
|
isUnknown: false,
|
|
853
|
+
pathCost: routeEntry?.pathCost,
|
|
854
|
+
bidirectionalLqi,
|
|
746
855
|
});
|
|
747
856
|
}
|
|
748
857
|
}
|
|
@@ -115,6 +115,12 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
115
115
|
// Create edge data for vis.js
|
|
116
116
|
const graphEdges: NetworkGraphEdge[] = connections.map((conn, index) => {
|
|
117
117
|
const isToUnknown = typeof conn.toNodeId === "string" && conn.toNodeId.startsWith("unknown_");
|
|
118
|
+
|
|
119
|
+
// Check if either endpoint is offline - connection data may be stale
|
|
120
|
+
const fromNode = this.nodes[String(conn.fromNodeId)];
|
|
121
|
+
const toNode = this.nodes[String(conn.toNodeId)];
|
|
122
|
+
const hasOfflineEndpoint = fromNode?.available === false || toNode?.available === false;
|
|
123
|
+
|
|
118
124
|
return {
|
|
119
125
|
id: `edge_${index}`,
|
|
120
126
|
from: conn.fromNodeId,
|
|
@@ -125,7 +131,7 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
125
131
|
},
|
|
126
132
|
width: 2,
|
|
127
133
|
title: conn.rssi !== null ? `RSSI: ${conn.rssi} dBm, LQI: ${conn.lqi}` : `LQI: ${conn.lqi}`,
|
|
128
|
-
dashes: isToUnknown, // Dashed lines to unknown devices
|
|
134
|
+
dashes: isToUnknown || hasOfflineEndpoint, // Dashed lines to unknown or offline devices
|
|
129
135
|
};
|
|
130
136
|
});
|
|
131
137
|
|