@matter-server/dashboard 0.6.2 → 0.6.3
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/dist/esm/pages/matter-network-view.d.ts +15 -0
- package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-network-view.js +171 -1
- package/dist/esm/pages/matter-network-view.js.map +1 -1
- package/dist/esm/pages/network/base-network-graph.d.ts +4 -0
- package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/base-network-graph.js +9 -0
- package/dist/esm/pages/network/base-network-graph.js.map +1 -1
- package/dist/esm/pages/network/border-router-store.d.ts +20 -0
- package/dist/esm/pages/network/border-router-store.d.ts.map +1 -0
- package/dist/esm/pages/network/border-router-store.js +29 -0
- package/dist/esm/pages/network/border-router-store.js.map +6 -0
- package/dist/esm/pages/network/network-details.d.ts +40 -12
- package/dist/esm/pages/network/network-details.d.ts.map +1 -1
- package/dist/esm/pages/network/network-details.js +440 -112
- package/dist/esm/pages/network/network-details.js.map +1 -1
- package/dist/esm/pages/network/network-types.d.ts +76 -0
- package/dist/esm/pages/network/network-types.d.ts.map +1 -1
- package/dist/esm/pages/network/network-types.js.map +1 -1
- package/dist/esm/pages/network/network-utils.d.ts +89 -22
- package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.js +233 -95
- package/dist/esm/pages/network/network-utils.js.map +1 -1
- package/dist/esm/pages/network/thread-graph.d.ts +68 -9
- package/dist/esm/pages/network/thread-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/thread-graph.js +388 -50
- package/dist/esm/pages/network/thread-graph.js.map +2 -2
- package/dist/esm/util/device-icons.d.ts +6 -0
- package/dist/esm/util/device-icons.d.ts.map +1 -1
- package/dist/esm/util/device-icons.js +6 -0
- package/dist/esm/util/device-icons.js.map +1 -1
- package/dist/web/js/{attribute-write-dialog-g4B6BoRt.js → attribute-write-dialog-DlMTUiLK.js} +1 -1
- package/dist/web/js/{command-invoke-dialog-D6G704VK.js → command-invoke-dialog-DO3IyFcm.js} +1 -1
- package/dist/web/js/{commission-node-dialog-Bg3oo5ub.js → commission-node-dialog-CMSvCm0i.js} +4 -4
- package/dist/web/js/{commission-node-existing-DO3g1aQJ.js → commission-node-existing-D08jghFu.js} +2 -2
- package/dist/web/js/{commission-node-thread-DM432aH1.js → commission-node-thread-D5waY758.js} +2 -2
- package/dist/web/js/{commission-node-wifi-Bx40FXij.js → commission-node-wifi-ClBlCFTZ.js} +2 -2
- package/dist/web/js/{dialog-box-DjyfULWB.js → dialog-box-D9vS2SmP.js} +1 -1
- package/dist/web/js/{fire_event-BstgNPuh.js → fire_event-BPhROjTC.js} +1 -1
- package/dist/web/js/main.js +1 -1
- package/dist/web/js/{matter-dashboard-app-Cj88TtbZ.js → matter-dashboard-app-C9zTE5uH.js} +1359 -302
- package/dist/web/js/{node-binding-dialog-9yy2LE3_.js → node-binding-dialog-B5p-gbim.js} +1 -1
- package/dist/web/js/{settings-dialog-Cs2xMsXb.js → settings-dialog-BMFhom0W.js} +1 -1
- package/package.json +4 -4
- package/src/pages/matter-network-view.ts +185 -1
- package/src/pages/network/base-network-graph.ts +10 -0
- package/src/pages/network/border-router-store.ts +38 -0
- package/src/pages/network/network-details.ts +535 -140
- package/src/pages/network/network-types.ts +76 -0
- package/src/pages/network/network-utils.ts +390 -171
- package/src/pages/network/thread-graph.ts +532 -73
- package/src/util/device-icons.ts +13 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { consume } from "@lit/context";
|
|
8
8
|
import "@material/web/divider/divider";
|
|
9
|
-
import { isTestNodeId, type MatterClient, type MatterNode } from "@matter-server/ws-client";
|
|
9
|
+
import { isTestNodeId, type BorderRouterEntry, type MatterClient, type MatterNode } from "@matter-server/ws-client";
|
|
10
10
|
import { mdiClose, mdiRefresh, mdiSignalCellular1, mdiSignalCellular2, mdiSignalCellular3 } from "@mdi/js";
|
|
11
11
|
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
|
12
12
|
import { customElement, property, state } from "lit/decorators.js";
|
|
@@ -14,16 +14,14 @@ import { clientContext } from "../../client/client-context.js";
|
|
|
14
14
|
import "../../components/ha-svg-icon";
|
|
15
15
|
import { formatNodeAddressFromAny, getEffectiveFabricIndex } from "../../util/format_hex.js";
|
|
16
16
|
import { getCssVar, reducedMotionStyles } from "../../util/shared-styles.js";
|
|
17
|
-
import type {
|
|
17
|
+
import type { ThreadEdgePair, ThreadExternalDevice } from "./network-types.js";
|
|
18
18
|
import type { NodeConnection } from "./network-utils.js";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
buildRloc16Map,
|
|
20
|
+
decodeMeshcopStateBitmap,
|
|
22
21
|
getDeviceName,
|
|
23
22
|
getNetworkType,
|
|
24
|
-
|
|
23
|
+
getNodeConnectionsFromPairs,
|
|
25
24
|
getRoutableDestinationsCount,
|
|
26
|
-
getSignalColor,
|
|
27
25
|
getSignalColorFromRssi,
|
|
28
26
|
getThreadChannel,
|
|
29
27
|
getThreadExtendedAddressHex,
|
|
@@ -32,7 +30,6 @@ import {
|
|
|
32
30
|
getWiFiDiagnostics,
|
|
33
31
|
getWiFiSecurityTypeName,
|
|
34
32
|
getWiFiVersionName,
|
|
35
|
-
parseNeighborTable,
|
|
36
33
|
} from "./network-utils.js";
|
|
37
34
|
import "./update-connections-dialog.js";
|
|
38
35
|
|
|
@@ -47,18 +44,33 @@ export class NetworkDetails extends LitElement {
|
|
|
47
44
|
@property()
|
|
48
45
|
public selectedNodeId: number | string | null = null;
|
|
49
46
|
|
|
47
|
+
@property({ type: Boolean })
|
|
48
|
+
public hideOfflineNodes = false;
|
|
49
|
+
|
|
50
|
+
@property({ type: Boolean })
|
|
51
|
+
public hideWeakSignalEdges = false;
|
|
52
|
+
|
|
53
|
+
@property({ type: Boolean })
|
|
54
|
+
public hideMediumSignalEdges = false;
|
|
55
|
+
|
|
56
|
+
@property({ type: Boolean })
|
|
57
|
+
public hideStrongSignalEdges = false;
|
|
58
|
+
|
|
50
59
|
@property({ type: Object })
|
|
51
60
|
public nodes: Record<string, MatterNode> = {};
|
|
52
61
|
|
|
53
62
|
@property({ type: Object })
|
|
54
|
-
public unknownDevices: Map
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
> = new Map();
|
|
63
|
+
public unknownDevices: ReadonlyMap<string, ThreadExternalDevice> = new Map();
|
|
64
|
+
|
|
65
|
+
@property({ attribute: false })
|
|
66
|
+
public borderRouters: ReadonlyMap<string, BorderRouterEntry> = new Map();
|
|
58
67
|
|
|
59
68
|
@property({ type: Object })
|
|
60
69
|
public wifiAccessPoints: Map<string, { bssid: string; connectedNodes: string[] }> = new Map();
|
|
61
70
|
|
|
71
|
+
@property({ type: Object })
|
|
72
|
+
public threadEdgePairs: Map<string, ThreadEdgePair> = new Map();
|
|
73
|
+
|
|
62
74
|
@consume({ context: clientContext })
|
|
63
75
|
private client!: MatterClient;
|
|
64
76
|
|
|
@@ -92,18 +104,6 @@ export class NetworkDetails extends LitElement {
|
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
private _formatExtAddress(extAddr: bigint | string | undefined): string {
|
|
96
|
-
if (extAddr === undefined || extAddr === "") return "Unknown";
|
|
97
|
-
if (typeof extAddr === "bigint") {
|
|
98
|
-
return extAddr.toString(16).toUpperCase().padStart(16, "0");
|
|
99
|
-
}
|
|
100
|
-
return extAddr;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private _getSignalIcon(neighbor: ThreadNeighbor): string {
|
|
104
|
-
return this._getSignalIconFromColor(getSignalColor(neighbor));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
107
|
private _getSignalIconFromColor(color: string): string {
|
|
108
108
|
const strongColor = getCssVar("--signal-color-strong", "#4caf50");
|
|
109
109
|
const mediumColor = getCssVar("--signal-color-medium", "#ff9800");
|
|
@@ -185,13 +185,14 @@ export class NetworkDetails extends LitElement {
|
|
|
185
185
|
const threadRole = getThreadRole(node);
|
|
186
186
|
const channel = getThreadChannel(node);
|
|
187
187
|
const extAddressHex = getThreadExtendedAddressHex(node);
|
|
188
|
-
|
|
189
|
-
const rloc16Map = buildRloc16Map(this.nodes);
|
|
190
|
-
|
|
191
|
-
// Get all connections (bidirectional) - this matches what the graph shows
|
|
192
|
-
// Use string to avoid BigInt precision loss
|
|
188
|
+
// Get connections from edge pairs with the same filter pipeline as the graph
|
|
193
189
|
const nodeId = String(node.node_id);
|
|
194
|
-
const connections =
|
|
190
|
+
const connections = getNodeConnectionsFromPairs(nodeId, this.threadEdgePairs, this.nodes, {
|
|
191
|
+
hideOfflineNodes: this.hideOfflineNodes,
|
|
192
|
+
hideWeakSignalEdges: this.hideWeakSignalEdges,
|
|
193
|
+
hideMediumSignalEdges: this.hideMediumSignalEdges,
|
|
194
|
+
hideStrongSignalEdges: this.hideStrongSignalEdges,
|
|
195
|
+
});
|
|
195
196
|
|
|
196
197
|
return html`
|
|
197
198
|
<div class="section">
|
|
@@ -294,9 +295,17 @@ export class NetworkDetails extends LitElement {
|
|
|
294
295
|
>, Cost: ${conn.pathCost}</span
|
|
295
296
|
>`
|
|
296
297
|
: nothing}
|
|
297
|
-
${
|
|
298
|
-
? html`
|
|
299
|
-
|
|
298
|
+
${conn.isReverseOnly
|
|
299
|
+
? html`
|
|
300
|
+
<span
|
|
301
|
+
class="direction-hint reverse-only"
|
|
302
|
+
title="Peer reports this node but this node has no matching neighbor-table entry. Possible one-way visibility (range, TX power, or stale neighbor table)."
|
|
303
|
+
>← one-way</span
|
|
304
|
+
>
|
|
305
|
+
`
|
|
306
|
+
: !conn.isOutgoing
|
|
307
|
+
? html` <span class="direction-hint">(reverse)</span> `
|
|
308
|
+
: nothing}
|
|
300
309
|
</div>
|
|
301
310
|
</div>
|
|
302
311
|
</div>
|
|
@@ -362,25 +371,12 @@ export class NetworkDetails extends LitElement {
|
|
|
362
371
|
`;
|
|
363
372
|
}
|
|
364
373
|
|
|
365
|
-
/**
|
|
366
|
-
* Find the neighbor entry for an unknown device from a node's neighbor table.
|
|
367
|
-
*/
|
|
368
|
-
private _findNeighborEntry(node: MatterNode, unknownExtAddrHex: string): ThreadNeighbor | null {
|
|
369
|
-
const neighbors = parseNeighborTable(node);
|
|
370
|
-
for (const neighbor of neighbors) {
|
|
371
|
-
const neighborHex = this._formatExtAddress(neighbor.extAddress);
|
|
372
|
-
if (neighborHex === unknownExtAddrHex) {
|
|
373
|
-
return neighbor;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
374
|
private _renderUnknownDeviceInfo(deviceId: string): TemplateResult | typeof nothing {
|
|
380
|
-
const
|
|
381
|
-
if (!unknown) {
|
|
375
|
+
const device = this.unknownDevices.get(deviceId);
|
|
376
|
+
if (!device || device.kind !== "unknown") {
|
|
382
377
|
return html` <p>Unknown device data not available</p> `;
|
|
383
378
|
}
|
|
379
|
+
const unknown = device;
|
|
384
380
|
|
|
385
381
|
return html`
|
|
386
382
|
<div class="section">
|
|
@@ -393,6 +389,22 @@ export class NetworkDetails extends LitElement {
|
|
|
393
389
|
<span class="label">Extended Address:</span>
|
|
394
390
|
<span class="value mono">${unknown.extAddressHex}</span>
|
|
395
391
|
</div>
|
|
392
|
+
${unknown.networkName !== undefined
|
|
393
|
+
? html`
|
|
394
|
+
<div class="info-row">
|
|
395
|
+
<span class="label">Thread Network:</span>
|
|
396
|
+
<span class="value">${unknown.networkName}</span>
|
|
397
|
+
</div>
|
|
398
|
+
`
|
|
399
|
+
: nothing}
|
|
400
|
+
${unknown.extendedPanIdHex !== undefined
|
|
401
|
+
? html`
|
|
402
|
+
<div class="info-row">
|
|
403
|
+
<span class="label">Extended PAN ID:</span>
|
|
404
|
+
<span class="value mono">${unknown.extendedPanIdHex}</span>
|
|
405
|
+
</div>
|
|
406
|
+
`
|
|
407
|
+
: nothing}
|
|
396
408
|
${unknown.bestRssi !== null
|
|
397
409
|
? html`
|
|
398
410
|
<div class="info-row">
|
|
@@ -403,84 +415,349 @@ export class NetworkDetails extends LitElement {
|
|
|
403
415
|
: nothing}
|
|
404
416
|
</div>
|
|
405
417
|
|
|
406
|
-
${
|
|
418
|
+
${this._renderExternalDeviceNeighbors(deviceId)}
|
|
419
|
+
|
|
420
|
+
<md-divider></md-divider>
|
|
421
|
+
<div class="section">
|
|
422
|
+
<p class="hint-text">
|
|
423
|
+
This device appears in Thread neighbor tables but is not commissioned to this fabric. It may be a
|
|
424
|
+
Thread Border Router whose Thread radio MAC differs from its MeshCoP border-agent ID (common with
|
|
425
|
+
Apple and Aqara), or a device from another Matter ecosystem.
|
|
426
|
+
</p>
|
|
427
|
+
</div>
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Neighbor list shared by external-device panels (unknown + BR). Uses the
|
|
433
|
+
* same edge pairs as the graph so panel and graph agree on which links
|
|
434
|
+
* survive filtering. Sorted by best RSSI/LQI signal, descending.
|
|
435
|
+
*/
|
|
436
|
+
private _renderExternalDeviceNeighbors(deviceId: string): TemplateResult | typeof nothing {
|
|
437
|
+
const connections = getNodeConnectionsFromPairs(deviceId, this.threadEdgePairs, this.nodes, {
|
|
438
|
+
hideOfflineNodes: this.hideOfflineNodes,
|
|
439
|
+
hideWeakSignalEdges: this.hideWeakSignalEdges,
|
|
440
|
+
hideMediumSignalEdges: this.hideMediumSignalEdges,
|
|
441
|
+
hideStrongSignalEdges: this.hideStrongSignalEdges,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (connections.length === 0) return nothing;
|
|
445
|
+
|
|
446
|
+
return html`
|
|
447
|
+
<md-divider></md-divider>
|
|
448
|
+
<div class="section">
|
|
449
|
+
<h4>Neighbors (${connections.length})</h4>
|
|
450
|
+
<div class="neighbors-list">
|
|
451
|
+
${connections
|
|
452
|
+
.toSorted((a, b) => {
|
|
453
|
+
const score = (conn: NodeConnection): number => {
|
|
454
|
+
if (conn.rssi !== null && conn.rssi !== undefined) return conn.rssi;
|
|
455
|
+
if (conn.lqi !== null && conn.lqi !== undefined) return conn.lqi;
|
|
456
|
+
return -Infinity;
|
|
457
|
+
};
|
|
458
|
+
return score(b) - score(a);
|
|
459
|
+
})
|
|
460
|
+
.map((conn: NodeConnection) => {
|
|
461
|
+
if (!conn.connectedNode) return nothing;
|
|
462
|
+
|
|
463
|
+
return html`
|
|
464
|
+
<div
|
|
465
|
+
class="neighbor-item clickable"
|
|
466
|
+
role="button"
|
|
467
|
+
tabindex="0"
|
|
468
|
+
@click=${() => this._handleSelectNode(conn.connectedNodeId)}
|
|
469
|
+
@keydown=${(e: KeyboardEvent) => this._handleKeyDown(e, conn.connectedNodeId)}
|
|
470
|
+
>
|
|
471
|
+
<ha-svg-icon
|
|
472
|
+
.path=${this._getSignalIconFromColor(conn.signalColor)}
|
|
473
|
+
style="--icon-primary-color: ${conn.signalColor}"
|
|
474
|
+
></ha-svg-icon>
|
|
475
|
+
<div class="neighbor-info">
|
|
476
|
+
<div class="neighbor-name">
|
|
477
|
+
Node ${conn.connectedNodeId}
|
|
478
|
+
<span class="node-id-hex"
|
|
479
|
+
>${this._formatNodeIdHex(conn.connectedNodeId)}</span
|
|
480
|
+
>: ${getDeviceName(conn.connectedNode)}
|
|
481
|
+
</div>
|
|
482
|
+
<div class="neighbor-signal">
|
|
483
|
+
${conn.rssi !== null ? html`RSSI: ${conn.rssi} dBm, ` : nothing}
|
|
484
|
+
${conn.lqi !== null ? html`LQI: ${conn.lqi}` : nothing}
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
`;
|
|
489
|
+
})}
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Identity rows for a Border Router (network name, vendor, model, Thread version, ext address).
|
|
497
|
+
* Caller controls the surrounding <div class="section"> + heading.
|
|
498
|
+
*/
|
|
499
|
+
private _renderBorderRouterIdentityRows(br: BorderRouterEntry, includeExtAddr: boolean): TemplateResult {
|
|
500
|
+
return html`
|
|
501
|
+
${br.networkName
|
|
407
502
|
? html`
|
|
408
|
-
<
|
|
409
|
-
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
503
|
+
<div class="info-row">
|
|
504
|
+
<span class="label">Network name:</span>
|
|
505
|
+
<span class="value">${br.networkName}</span>
|
|
506
|
+
</div>
|
|
507
|
+
`
|
|
508
|
+
: nothing}
|
|
509
|
+
${br.vendorName
|
|
510
|
+
? html`
|
|
511
|
+
<div class="info-row">
|
|
512
|
+
<span class="label">Vendor:</span>
|
|
513
|
+
<span class="value">${br.vendorName}</span>
|
|
514
|
+
</div>
|
|
515
|
+
`
|
|
516
|
+
: nothing}
|
|
517
|
+
${br.modelName
|
|
518
|
+
? html`
|
|
519
|
+
<div class="info-row">
|
|
520
|
+
<span class="label">Model:</span>
|
|
521
|
+
<span class="value">${br.modelName}</span>
|
|
522
|
+
</div>
|
|
523
|
+
`
|
|
524
|
+
: nothing}
|
|
525
|
+
${br.threadVersion
|
|
526
|
+
? html`
|
|
527
|
+
<div class="info-row">
|
|
528
|
+
<span class="label">Thread version:</span>
|
|
529
|
+
<span class="value">${br.threadVersion}</span>
|
|
530
|
+
</div>
|
|
531
|
+
`
|
|
532
|
+
: nothing}
|
|
533
|
+
${includeExtAddr
|
|
534
|
+
? html`
|
|
535
|
+
<div class="info-row">
|
|
536
|
+
<span class="label">Extended Address:</span>
|
|
537
|
+
<span class="value mono">${br.extAddressHex}</span>
|
|
538
|
+
</div>
|
|
539
|
+
`
|
|
540
|
+
: nothing}
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
429
543
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Render the MeshCoP state bitmap as decoded fields (BBR role, connection mode, Thread
|
|
546
|
+
* interface status, availability, ePSKc) plus the raw hex underneath. Reserved values are
|
|
547
|
+
* rendered as numeric so a future spec extension stays visible.
|
|
548
|
+
*/
|
|
549
|
+
private _renderStateBitmap(hex: string | undefined): TemplateResult | typeof nothing {
|
|
550
|
+
if (hex === undefined) return nothing;
|
|
551
|
+
const decoded = decodeMeshcopStateBitmap(hex);
|
|
552
|
+
if (decoded === undefined) {
|
|
553
|
+
return html`
|
|
554
|
+
<div class="info-row">
|
|
555
|
+
<span class="label">State bitmap:</span>
|
|
556
|
+
<span class="value mono">${hex}</span>
|
|
557
|
+
</div>
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
437
560
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
})}
|
|
472
|
-
</div>
|
|
561
|
+
const stateParts = new Array<string>();
|
|
562
|
+
stateParts.push(decoded.bbr ? `BBR (${decoded.bbrFunction ?? "?"})` : "not BBR");
|
|
563
|
+
if (decoded.threadRole !== undefined) {
|
|
564
|
+
stateParts.push(`Thread ${decoded.threadRole}`);
|
|
565
|
+
}
|
|
566
|
+
if (decoded.threadInterfaceStatus !== undefined) {
|
|
567
|
+
stateParts.push(decoded.threadInterfaceStatus);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return html`
|
|
571
|
+
<div class="info-row">
|
|
572
|
+
<span class="label">State:</span>
|
|
573
|
+
<span class="value">${stateParts.join(", ")}</span>
|
|
574
|
+
</div>
|
|
575
|
+
<div class="info-row">
|
|
576
|
+
<span class="label">Connection:</span>
|
|
577
|
+
<span class="value">${decoded.connectionMode ?? `reserved (${decoded.connectionModeValue})`}</span>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="info-row">
|
|
580
|
+
<span class="label">Availability:</span>
|
|
581
|
+
<span class="value">${decoded.availability ?? `reserved (${decoded.availabilityValue})`}</span>
|
|
582
|
+
</div>
|
|
583
|
+
<div class="info-row">
|
|
584
|
+
<span class="label">ePSKc:</span>
|
|
585
|
+
<span class="value">${decoded.epskcSupported ? "supported" : "not supported"}</span>
|
|
586
|
+
</div>
|
|
587
|
+
${decoded.multiAilStateValue !== 0
|
|
588
|
+
? html`
|
|
589
|
+
<div class="info-row">
|
|
590
|
+
<span class="label">Multi-AIL:</span>
|
|
591
|
+
<span class="value">
|
|
592
|
+
${decoded.multiAilState ?? `reserved (${decoded.multiAilStateValue})`}
|
|
593
|
+
</span>
|
|
473
594
|
</div>
|
|
474
595
|
`
|
|
475
596
|
: nothing}
|
|
597
|
+
<div class="info-row">
|
|
598
|
+
<span class="label">State bitmap (raw):</span>
|
|
599
|
+
<span class="value mono">${hex}</span>
|
|
600
|
+
</div>
|
|
601
|
+
`;
|
|
602
|
+
}
|
|
476
603
|
|
|
477
|
-
|
|
604
|
+
/**
|
|
605
|
+
* Network-info rows for a Border Router (extended PAN ID, partition, timestamps, state, domain, agent ID).
|
|
606
|
+
* Returns nothing if no fields are populated, so the caller can skip the surrounding section.
|
|
607
|
+
*/
|
|
608
|
+
private _renderBorderRouterNetworkRows(br: BorderRouterEntry): TemplateResult | typeof nothing {
|
|
609
|
+
const hasAny =
|
|
610
|
+
br.extendedPanIdHex !== undefined ||
|
|
611
|
+
br.partitionIdHex !== undefined ||
|
|
612
|
+
br.activeTimestampHex !== undefined ||
|
|
613
|
+
br.stateBitmapHex !== undefined ||
|
|
614
|
+
br.domainName !== undefined ||
|
|
615
|
+
br.borderAgentIdHex !== undefined;
|
|
616
|
+
if (!hasAny) return nothing;
|
|
617
|
+
|
|
618
|
+
return html`
|
|
619
|
+
${br.extendedPanIdHex
|
|
620
|
+
? html`
|
|
621
|
+
<div class="info-row">
|
|
622
|
+
<span class="label">Extended PAN ID:</span>
|
|
623
|
+
<span class="value mono">${br.extendedPanIdHex}</span>
|
|
624
|
+
</div>
|
|
625
|
+
`
|
|
626
|
+
: nothing}
|
|
627
|
+
${br.partitionIdHex
|
|
628
|
+
? html`
|
|
629
|
+
<div class="info-row">
|
|
630
|
+
<span class="label">Partition ID:</span>
|
|
631
|
+
<span class="value mono">${br.partitionIdHex}</span>
|
|
632
|
+
</div>
|
|
633
|
+
`
|
|
634
|
+
: nothing}
|
|
635
|
+
${br.activeTimestampHex
|
|
636
|
+
? html`
|
|
637
|
+
<div class="info-row">
|
|
638
|
+
<span class="label">Active timestamp:</span>
|
|
639
|
+
<span class="value mono">${br.activeTimestampHex}</span>
|
|
640
|
+
</div>
|
|
641
|
+
`
|
|
642
|
+
: nothing}
|
|
643
|
+
${this._renderStateBitmap(br.stateBitmapHex)}
|
|
644
|
+
${br.domainName
|
|
645
|
+
? html`
|
|
646
|
+
<div class="info-row">
|
|
647
|
+
<span class="label">Domain:</span>
|
|
648
|
+
<span class="value">${br.domainName}</span>
|
|
649
|
+
</div>
|
|
650
|
+
`
|
|
651
|
+
: nothing}
|
|
652
|
+
${br.borderAgentIdHex
|
|
653
|
+
? html`
|
|
654
|
+
<div class="info-row">
|
|
655
|
+
<span class="label">Border agent ID:</span>
|
|
656
|
+
<span class="value mono">${br.borderAgentIdHex}</span>
|
|
657
|
+
</div>
|
|
658
|
+
`
|
|
659
|
+
: nothing}
|
|
660
|
+
`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Address rows for a Border Router (hostname, IPs, ports, sources).
|
|
665
|
+
*/
|
|
666
|
+
private _renderBorderRouterAddressRows(br: BorderRouterEntry): TemplateResult | typeof nothing {
|
|
667
|
+
const hasAny =
|
|
668
|
+
br.hostname !== undefined ||
|
|
669
|
+
br.addresses.length > 0 ||
|
|
670
|
+
br.meshcopPort !== undefined ||
|
|
671
|
+
br.trelPort !== undefined ||
|
|
672
|
+
br.sources.length > 0;
|
|
673
|
+
if (!hasAny) return nothing;
|
|
674
|
+
|
|
675
|
+
return html`
|
|
676
|
+
${br.hostname
|
|
677
|
+
? html`
|
|
678
|
+
<div class="info-row">
|
|
679
|
+
<span class="label">Hostname:</span>
|
|
680
|
+
<span class="value mono">${br.hostname}</span>
|
|
681
|
+
</div>
|
|
682
|
+
`
|
|
683
|
+
: nothing}
|
|
684
|
+
${br.addresses.map(
|
|
685
|
+
addr => html`
|
|
686
|
+
<div class="info-row">
|
|
687
|
+
<span class="label">Address:</span>
|
|
688
|
+
<span class="value mono">${addr}</span>
|
|
689
|
+
</div>
|
|
690
|
+
`,
|
|
691
|
+
)}
|
|
692
|
+
${br.meshcopPort !== undefined
|
|
693
|
+
? html`
|
|
694
|
+
<div class="info-row">
|
|
695
|
+
<span class="label">meshcop port:</span>
|
|
696
|
+
<span class="value">${br.meshcopPort}</span>
|
|
697
|
+
</div>
|
|
698
|
+
`
|
|
699
|
+
: nothing}
|
|
700
|
+
${br.trelPort !== undefined
|
|
701
|
+
? html`
|
|
702
|
+
<div class="info-row">
|
|
703
|
+
<span class="label">trel port:</span>
|
|
704
|
+
<span class="value">${br.trelPort}</span>
|
|
705
|
+
</div>
|
|
706
|
+
`
|
|
707
|
+
: nothing}
|
|
708
|
+
${br.sources.length > 0
|
|
709
|
+
? html`
|
|
710
|
+
<div class="info-row">
|
|
711
|
+
<span class="label">Sources:</span>
|
|
712
|
+
<span class="value">${br.sources.join(", ")}</span>
|
|
713
|
+
</div>
|
|
714
|
+
`
|
|
715
|
+
: nothing}
|
|
716
|
+
`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private _renderBorderRouterInfo(deviceId: string): TemplateResult | typeof nothing {
|
|
720
|
+
const device = this.unknownDevices.get(deviceId);
|
|
721
|
+
if (!device || device.kind !== "br") {
|
|
722
|
+
return html` <p>Border router data not available</p> `;
|
|
723
|
+
}
|
|
724
|
+
const br = device;
|
|
725
|
+
const networkRows = this._renderBorderRouterNetworkRows(br);
|
|
726
|
+
const addressRows = this._renderBorderRouterAddressRows(br);
|
|
727
|
+
|
|
728
|
+
return html`
|
|
478
729
|
<div class="section">
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
730
|
+
<h4>Border Router</h4>
|
|
731
|
+
${this._renderBorderRouterIdentityRows(br, true)}
|
|
732
|
+
${br.bestRssi !== null
|
|
733
|
+
? html`
|
|
734
|
+
<div class="info-row">
|
|
735
|
+
<span class="label">Best RSSI:</span>
|
|
736
|
+
<span class="value">${br.bestRssi} dBm</span>
|
|
737
|
+
</div>
|
|
738
|
+
`
|
|
739
|
+
: nothing}
|
|
483
740
|
</div>
|
|
741
|
+
|
|
742
|
+
${networkRows !== nothing
|
|
743
|
+
? html`
|
|
744
|
+
<md-divider></md-divider>
|
|
745
|
+
<div class="section">
|
|
746
|
+
<h4>Thread Network</h4>
|
|
747
|
+
${networkRows}
|
|
748
|
+
</div>
|
|
749
|
+
`
|
|
750
|
+
: nothing}
|
|
751
|
+
${addressRows !== nothing
|
|
752
|
+
? html`
|
|
753
|
+
<md-divider></md-divider>
|
|
754
|
+
<div class="section">
|
|
755
|
+
<h4>Addresses</h4>
|
|
756
|
+
${addressRows}
|
|
757
|
+
</div>
|
|
758
|
+
`
|
|
759
|
+
: nothing}
|
|
760
|
+
${this._renderExternalDeviceNeighbors(deviceId)}
|
|
484
761
|
`;
|
|
485
762
|
}
|
|
486
763
|
|
|
@@ -494,9 +771,11 @@ export class NetworkDetails extends LitElement {
|
|
|
494
771
|
const isAccessPoint = typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("ap_");
|
|
495
772
|
if (isAccessPoint) return false;
|
|
496
773
|
|
|
497
|
-
//
|
|
498
|
-
const
|
|
499
|
-
|
|
774
|
+
// External devices (unknown or BR) gate on having online seenBy nodes
|
|
775
|
+
const isExternal =
|
|
776
|
+
typeof this.selectedNodeId === "string" &&
|
|
777
|
+
(this.selectedNodeId.startsWith("unknown_") || this.selectedNodeId.startsWith("br_"));
|
|
778
|
+
if (isExternal) {
|
|
500
779
|
return this._getOnlineSeenByNodes().length > 0;
|
|
501
780
|
}
|
|
502
781
|
|
|
@@ -514,7 +793,10 @@ export class NetworkDetails extends LitElement {
|
|
|
514
793
|
* Get the type of the currently selected node for dialog variant.
|
|
515
794
|
*/
|
|
516
795
|
private _getSelectedNodeType(): "online" | "offline" | "unknown" {
|
|
517
|
-
if (
|
|
796
|
+
if (
|
|
797
|
+
typeof this.selectedNodeId === "string" &&
|
|
798
|
+
(this.selectedNodeId.startsWith("unknown_") || this.selectedNodeId.startsWith("br_"))
|
|
799
|
+
) {
|
|
518
800
|
return "unknown";
|
|
519
801
|
}
|
|
520
802
|
|
|
@@ -534,17 +816,12 @@ export class NetworkDetails extends LitElement {
|
|
|
534
816
|
|
|
535
817
|
const networkType = getNetworkType(node);
|
|
536
818
|
if (networkType === "thread") {
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
const connections = getNodeConnections(nodeId, this.nodes, extAddrMap, rloc16Map);
|
|
819
|
+
// Use edge pairs without filters to get ALL connections (for update dialog)
|
|
820
|
+
const connections = getNodeConnectionsFromPairs(nodeId, this.threadEdgePairs, this.nodes);
|
|
540
821
|
return connections
|
|
541
822
|
.filter(conn => {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
return false;
|
|
545
|
-
}
|
|
546
|
-
const connectedNode = this.nodes[String(conn.connectedNodeId)];
|
|
547
|
-
return connectedNode?.available === true;
|
|
823
|
+
if (conn.isUnknown) return false;
|
|
824
|
+
return conn.connectedNode?.available === true;
|
|
548
825
|
})
|
|
549
826
|
.map(conn => String(conn.connectedNodeId));
|
|
550
827
|
}
|
|
@@ -557,14 +834,17 @@ export class NetworkDetails extends LitElement {
|
|
|
557
834
|
* Get online nodes that see an unknown device.
|
|
558
835
|
*/
|
|
559
836
|
private _getOnlineSeenByNodes(): string[] {
|
|
560
|
-
if (
|
|
837
|
+
if (
|
|
838
|
+
typeof this.selectedNodeId !== "string" ||
|
|
839
|
+
(!this.selectedNodeId.startsWith("unknown_") && !this.selectedNodeId.startsWith("br_"))
|
|
840
|
+
) {
|
|
561
841
|
return [];
|
|
562
842
|
}
|
|
563
843
|
|
|
564
|
-
const
|
|
565
|
-
if (!
|
|
844
|
+
const device = this.unknownDevices.get(this.selectedNodeId);
|
|
845
|
+
if (!device) return [];
|
|
566
846
|
|
|
567
|
-
return
|
|
847
|
+
return device.seenBy.filter(nodeId => {
|
|
568
848
|
const node = this.nodes[nodeId.toString()];
|
|
569
849
|
return node?.available === true;
|
|
570
850
|
});
|
|
@@ -574,11 +854,19 @@ export class NetworkDetails extends LitElement {
|
|
|
574
854
|
* Get the name of the selected node for display in dialog.
|
|
575
855
|
*/
|
|
576
856
|
private _getSelectedNodeName(): string {
|
|
577
|
-
if (typeof this.selectedNodeId === "string"
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
857
|
+
if (typeof this.selectedNodeId === "string") {
|
|
858
|
+
if (this.selectedNodeId.startsWith("br_")) {
|
|
859
|
+
const device = this.unknownDevices.get(this.selectedNodeId);
|
|
860
|
+
if (!device || device.kind !== "br") return "Border Router";
|
|
861
|
+
const label = device.networkName ?? device.vendorName ?? "Border Router";
|
|
862
|
+
return `${label} (${device.extAddressHex.slice(-8)})`;
|
|
863
|
+
}
|
|
864
|
+
if (this.selectedNodeId.startsWith("unknown_")) {
|
|
865
|
+
const device = this.unknownDevices.get(this.selectedNodeId);
|
|
866
|
+
if (!device || device.kind !== "unknown") return "External Device";
|
|
867
|
+
const typeLabel = device.isRouter ? "External Router" : "External Device";
|
|
868
|
+
return `${typeLabel} (${device.extAddressHex.slice(-8)})`;
|
|
869
|
+
}
|
|
582
870
|
}
|
|
583
871
|
|
|
584
872
|
const node = this.nodes[this.selectedNodeId!.toString()];
|
|
@@ -591,6 +879,12 @@ export class NetworkDetails extends LitElement {
|
|
|
591
879
|
|
|
592
880
|
private _handleDialogClose(): void {
|
|
593
881
|
this._showUpdateDialog = false;
|
|
882
|
+
this.dispatchEvent(
|
|
883
|
+
new CustomEvent("connections-updated", {
|
|
884
|
+
bubbles: true,
|
|
885
|
+
composed: true,
|
|
886
|
+
}),
|
|
887
|
+
);
|
|
594
888
|
}
|
|
595
889
|
|
|
596
890
|
private _renderWiFiAccessPointInfo(apId: string): TemplateResult | typeof nothing {
|
|
@@ -665,6 +959,40 @@ export class NetworkDetails extends LitElement {
|
|
|
665
959
|
`;
|
|
666
960
|
}
|
|
667
961
|
|
|
962
|
+
/**
|
|
963
|
+
* Annotation shown on a commissioned Thread node that is also a discovered Border Router.
|
|
964
|
+
* Mirrors the BR Identity/Network/Addresses sections, sans the redundant ext-address row.
|
|
965
|
+
*/
|
|
966
|
+
private _renderCommissionedNodeBorderRouterAnnotation(node: MatterNode): TemplateResult | typeof nothing {
|
|
967
|
+
const xaHex = getThreadExtendedAddressHex(node);
|
|
968
|
+
if (!xaHex) return nothing;
|
|
969
|
+
const br = this.borderRouters.get(xaHex);
|
|
970
|
+
if (!br) return nothing;
|
|
971
|
+
|
|
972
|
+
const networkRows = this._renderBorderRouterNetworkRows(br);
|
|
973
|
+
const addressRows = this._renderBorderRouterAddressRows(br);
|
|
974
|
+
|
|
975
|
+
return html`
|
|
976
|
+
<md-divider></md-divider>
|
|
977
|
+
<div class="section">
|
|
978
|
+
<h4>Also a Border Router</h4>
|
|
979
|
+
${this._renderBorderRouterIdentityRows(br, false)}
|
|
980
|
+
${networkRows !== nothing
|
|
981
|
+
? html`
|
|
982
|
+
<div class="subsection-label">Thread Network</div>
|
|
983
|
+
${networkRows}
|
|
984
|
+
`
|
|
985
|
+
: nothing}
|
|
986
|
+
${addressRows !== nothing
|
|
987
|
+
? html`
|
|
988
|
+
<div class="subsection-label">Addresses</div>
|
|
989
|
+
${addressRows}
|
|
990
|
+
`
|
|
991
|
+
: nothing}
|
|
992
|
+
</div>
|
|
993
|
+
`;
|
|
994
|
+
}
|
|
995
|
+
|
|
668
996
|
override render() {
|
|
669
997
|
if (this.selectedNodeId === null) {
|
|
670
998
|
return html`
|
|
@@ -719,6 +1047,54 @@ export class NetworkDetails extends LitElement {
|
|
|
719
1047
|
`;
|
|
720
1048
|
}
|
|
721
1049
|
|
|
1050
|
+
// Check if this is a discovered Border Router (mDNS-enriched external device)
|
|
1051
|
+
const borderRouterId =
|
|
1052
|
+
typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("br_")
|
|
1053
|
+
? this.selectedNodeId
|
|
1054
|
+
: null;
|
|
1055
|
+
|
|
1056
|
+
if (borderRouterId !== null) {
|
|
1057
|
+
const onlineSeenByNodes = this._getOnlineSeenByNodes();
|
|
1058
|
+
return html`
|
|
1059
|
+
<div class="details-panel">
|
|
1060
|
+
<div class="header">
|
|
1061
|
+
<h3>Border Router</h3>
|
|
1062
|
+
<div class="header-actions">
|
|
1063
|
+
${onlineSeenByNodes.length > 0
|
|
1064
|
+
? html`
|
|
1065
|
+
<button
|
|
1066
|
+
class="action-button"
|
|
1067
|
+
@click=${this._handleUpdateConnections}
|
|
1068
|
+
aria-label="Update connection data"
|
|
1069
|
+
title="Update connection data"
|
|
1070
|
+
>
|
|
1071
|
+
<ha-svg-icon .path=${mdiRefresh}></ha-svg-icon>
|
|
1072
|
+
</button>
|
|
1073
|
+
`
|
|
1074
|
+
: nothing}
|
|
1075
|
+
<button class="close-button" @click=${this._handleClose} aria-label="Close details panel">
|
|
1076
|
+
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
|
1077
|
+
</button>
|
|
1078
|
+
</div>
|
|
1079
|
+
</div>
|
|
1080
|
+
<div class="content">${this._renderBorderRouterInfo(borderRouterId)}</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
${this._showUpdateDialog
|
|
1083
|
+
? html`
|
|
1084
|
+
<update-connections-dialog
|
|
1085
|
+
.client=${this.client}
|
|
1086
|
+
.nodes=${this.nodes}
|
|
1087
|
+
selectedNodeType="unknown"
|
|
1088
|
+
.selectedNodeName=${this._getSelectedNodeName()}
|
|
1089
|
+
.selectedNodeId=${this.selectedNodeId}
|
|
1090
|
+
.onlineNeighborIds=${onlineSeenByNodes}
|
|
1091
|
+
@dialog-closed=${this._handleDialogClose}
|
|
1092
|
+
></update-connections-dialog>
|
|
1093
|
+
`
|
|
1094
|
+
: nothing}
|
|
1095
|
+
`;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
722
1098
|
// Check if this is a WiFi access point
|
|
723
1099
|
const isAccessPoint = typeof this.selectedNodeId === "string" && this.selectedNodeId.startsWith("ap_");
|
|
724
1100
|
|
|
@@ -774,7 +1150,9 @@ export class NetworkDetails extends LitElement {
|
|
|
774
1150
|
</button>
|
|
775
1151
|
</div>
|
|
776
1152
|
</div>
|
|
777
|
-
<div class="content"
|
|
1153
|
+
<div class="content">
|
|
1154
|
+
${this._renderNodeInfo(node)}${this._renderCommissionedNodeBorderRouterAnnotation(node)}
|
|
1155
|
+
</div>
|
|
778
1156
|
<div class="footer">
|
|
779
1157
|
<a href="#node/${this.selectedNodeId}" class="view-link">View node details</a>
|
|
780
1158
|
</div>
|
|
@@ -909,6 +1287,15 @@ export class NetworkDetails extends LitElement {
|
|
|
909
1287
|
letter-spacing: 0.5px;
|
|
910
1288
|
}
|
|
911
1289
|
|
|
1290
|
+
.subsection-label {
|
|
1291
|
+
margin: 12px 0 4px 0;
|
|
1292
|
+
font-size: 0.75rem;
|
|
1293
|
+
font-weight: 500;
|
|
1294
|
+
color: var(--md-sys-color-on-surface-variant, #666);
|
|
1295
|
+
text-transform: uppercase;
|
|
1296
|
+
letter-spacing: 0.4px;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
912
1299
|
.info-row {
|
|
913
1300
|
display: flex;
|
|
914
1301
|
justify-content: space-between;
|
|
@@ -992,6 +1379,14 @@ export class NetworkDetails extends LitElement {
|
|
|
992
1379
|
opacity: 0.8;
|
|
993
1380
|
}
|
|
994
1381
|
|
|
1382
|
+
.direction-hint.reverse-only {
|
|
1383
|
+
font-style: normal;
|
|
1384
|
+
font-weight: 500;
|
|
1385
|
+
opacity: 1;
|
|
1386
|
+
color: var(--md-sys-color-error, #b3261e);
|
|
1387
|
+
cursor: help;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
995
1390
|
.route-info {
|
|
996
1391
|
color: var(--md-sys-color-tertiary, #7d5260);
|
|
997
1392
|
font-size: 0.85em;
|