@matter-server/dashboard 0.3.3 → 0.3.4
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/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-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.map +1 -1
- package/dist/esm/pages/network/base-network-graph.js +10 -2
- 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/web/js/{commission-node-dialog-B1_khzZb.js → commission-node-dialog-CcMuttYO.js} +5 -5
- package/dist/web/js/{commission-node-existing-RpdajrwF.js → commission-node-existing-CqTRDMAr.js} +2 -2
- package/dist/web/js/{commission-node-thread-5f2itkTG.js → commission-node-thread-DgwtTVwK.js} +2 -2
- package/dist/web/js/{commission-node-wifi-DZ_pWqsa.js → commission-node-wifi-XaN2SEnE.js} +2 -2
- package/dist/web/js/{dialog-box-DEUxM4B1.js → dialog-box-COpDD8i7.js} +2 -2
- package/dist/web/js/{fire_event-BczBMT8E.js → fire_event-mDYWi2sw.js} +1 -1
- package/dist/web/js/{log-level-dialog-Cr3PfX1X.js → log-level-dialog-Bc32kZVw.js} +2 -2
- package/dist/web/js/main.js +1 -1
- package/dist/web/js/{matter-dashboard-app-BuCe_Jxf.js → matter-dashboard-app-CrBHT4fT.js} +1824 -208
- package/dist/web/js/{node-binding-dialog-DMiHNDLA.js → node-binding-dialog-C8fqOJiB.js} +2 -2
- package/dist/web/js/prevent_default-D-ohDGsN.js +8 -0
- package/package.json +4 -4
- 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-node-view.ts +19 -4
- package/src/pages/network/base-network-graph.ts +17 -3
- 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/dist/web/js/prevent_default-D4FX_PIh.js +0 -774
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { k as i, G as c, H as clientContext, n, a as e, i as i$1, A, j as b, F as handleAsync, t } from './matter-dashboard-app-
|
|
2
|
-
import { p as preventDefault } from './prevent_default-
|
|
1
|
+
import { k as i, G as c, H as clientContext, n, a as e, i as i$1, A, j as b, F as handleAsync, t } from './matter-dashboard-app-CrBHT4fT.js';
|
|
2
|
+
import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
|
|
3
3
|
import './main.js';
|
|
4
4
|
|
|
5
5
|
var _staticBlock$1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matter-server/dashboard",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Dashboard for OHF Matter Server",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/matter-js/matterjs-server/issues"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@babel/preset-env": "^7.28.6",
|
|
26
|
-
"@matter/main": "0.16.
|
|
26
|
+
"@matter/main": "0.16.9-alpha.0-20260201-8748390b1",
|
|
27
27
|
"@rollup/plugin-babel": "^6.1.0",
|
|
28
28
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
29
29
|
"rollup-plugin-copy": "^3.5.0",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@lit/context": "^1.1.6",
|
|
39
39
|
"@material/web": "^2.4.1",
|
|
40
|
-
"@matter-server/ws-client": "0.3.
|
|
41
|
-
"@matter-server/custom-clusters": "0.3.
|
|
40
|
+
"@matter-server/ws-client": "0.3.4",
|
|
41
|
+
"@matter-server/custom-clusters": "0.3.4",
|
|
42
42
|
"@mdi/js": "^7.4.47",
|
|
43
43
|
"lit": "^3.3.2",
|
|
44
44
|
"tslib": "^2.8.1",
|
|
@@ -79,7 +79,7 @@ export class NodeDetails extends LitElement {
|
|
|
79
79
|
<md-list>
|
|
80
80
|
<md-list-item>
|
|
81
81
|
<div slot="headline">
|
|
82
|
-
<b
|
|
82
|
+
<b>${this.node.nodeLabel || "Node Info"}</b>
|
|
83
83
|
${this.node.available ? nothing : html`<span class="status">OFFLINE</span>`}
|
|
84
84
|
</div>
|
|
85
85
|
</md-list-item>
|
|
@@ -9,8 +9,8 @@ import "@material/web/divider/divider";
|
|
|
9
9
|
import "@material/web/iconbutton/icon-button";
|
|
10
10
|
import "@material/web/list/list";
|
|
11
11
|
import "@material/web/list/list-item";
|
|
12
|
-
import { MatterClient, MatterNode, toBigIntAwareJson } from "@matter-server/ws-client";
|
|
13
|
-
import {
|
|
12
|
+
import { isTestNodeId, MatterClient, MatterNode, toBigIntAwareJson } from "@matter-server/ws-client";
|
|
13
|
+
import { css, html, LitElement } from "lit";
|
|
14
14
|
import { customElement, property } from "lit/decorators.js";
|
|
15
15
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
16
16
|
import { clusters } from "../client/models/descriptions.js";
|
|
@@ -19,7 +19,7 @@ import "../components/ha-svg-icon";
|
|
|
19
19
|
import "../pages/components/node-details";
|
|
20
20
|
import { bindingContext } from "./components/context.js";
|
|
21
21
|
// Cluster command components (auto-register on import)
|
|
22
|
-
import { formatHex } from "../util/format_hex.js";
|
|
22
|
+
import { formatHex, formatNodeAddress, getEffectiveFabricIndex } from "../util/format_hex.js";
|
|
23
23
|
import { getClusterCommandsTag } from "./cluster-commands/index.js";
|
|
24
24
|
|
|
25
25
|
declare global {
|
|
@@ -79,9 +79,16 @@ class MatterClusterView extends LitElement {
|
|
|
79
79
|
`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Format node address for hex display
|
|
83
|
+
const fabricIndex = getEffectiveFabricIndex(
|
|
84
|
+
this.client.serverInfo.fabric_index,
|
|
85
|
+
isTestNodeId(this.node.node_id),
|
|
86
|
+
);
|
|
87
|
+
const nodeHex = formatNodeAddress(fabricIndex, this.node.node_id);
|
|
88
|
+
|
|
82
89
|
return html`
|
|
83
90
|
<dashboard-header
|
|
84
|
-
.title=${`Node ${this.node.node_id} | Endpoint ${this.endpoint} | Cluster ${this.cluster}`}
|
|
91
|
+
.title=${`Node ${this.node.node_id} ${nodeHex} | Endpoint ${this.endpoint} | Cluster ${this.cluster}`}
|
|
85
92
|
.backButton=${`#node/${this.node.node_id}/${this.endpoint}`}
|
|
86
93
|
.client=${this.client}
|
|
87
94
|
></dashboard-header>
|
|
@@ -8,14 +8,14 @@ import "@material/web/divider/divider";
|
|
|
8
8
|
import "@material/web/iconbutton/icon-button";
|
|
9
9
|
import "@material/web/list/list";
|
|
10
10
|
import "@material/web/list/list-item";
|
|
11
|
-
import { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
11
|
+
import { MatterClient, MatterNode, isTestNodeId } from "@matter-server/ws-client";
|
|
12
12
|
import { mdiChevronRight } from "@mdi/js";
|
|
13
13
|
import { LitElement, css, html } from "lit";
|
|
14
14
|
import { customElement, property } from "lit/decorators.js";
|
|
15
15
|
import { guard } from "lit/directives/guard.js";
|
|
16
16
|
import { DeviceType, clusters, device_types } from "../client/models/descriptions.js";
|
|
17
17
|
import "../components/ha-svg-icon";
|
|
18
|
-
import { formatHex } from "../util/format_hex.js";
|
|
18
|
+
import { formatHex, formatNodeAddress, getEffectiveFabricIndex } from "../util/format_hex.js";
|
|
19
19
|
|
|
20
20
|
declare global {
|
|
21
21
|
interface HTMLElementTagNameMap {
|
|
@@ -62,9 +62,16 @@ class MatterEndpointView extends LitElement {
|
|
|
62
62
|
`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Format node address for hex display
|
|
66
|
+
const fabricIndex = getEffectiveFabricIndex(
|
|
67
|
+
this.client.serverInfo.fabric_index,
|
|
68
|
+
isTestNodeId(this.node.node_id),
|
|
69
|
+
);
|
|
70
|
+
const nodeHex = formatNodeAddress(fabricIndex, this.node.node_id);
|
|
71
|
+
|
|
65
72
|
return html`
|
|
66
73
|
<dashboard-header
|
|
67
|
-
.title=${`Node ${this.node.node_id} | Endpoint ${this.endpoint}`}
|
|
74
|
+
.title=${`Node ${this.node.node_id} ${nodeHex} | Endpoint ${this.endpoint}`}
|
|
68
75
|
.backButton=${`#node/${this.node.node_id}`}
|
|
69
76
|
.client=${this.client}
|
|
70
77
|
></dashboard-header>
|
|
@@ -8,12 +8,13 @@ import "@material/web/divider/divider";
|
|
|
8
8
|
import "@material/web/iconbutton/icon-button";
|
|
9
9
|
import "@material/web/list/list";
|
|
10
10
|
import "@material/web/list/list-item";
|
|
11
|
-
import { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
11
|
+
import { isTestNodeId, MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
12
12
|
import { mdiChevronRight, mdiGraphOutline } from "@mdi/js";
|
|
13
|
-
import {
|
|
13
|
+
import { css, html, LitElement } from "lit";
|
|
14
14
|
import { customElement, property } from "lit/decorators.js";
|
|
15
15
|
import { guard } from "lit/directives/guard.js";
|
|
16
16
|
import "../components/ha-svg-icon";
|
|
17
|
+
import { formatNodeAddress, getEffectiveFabricIndex } from "../util/format_hex.js";
|
|
17
18
|
import "./components/header";
|
|
18
19
|
import "./components/node-details";
|
|
19
20
|
import { getEndpointDeviceTypes } from "./matter-endpoint-view.js";
|
|
@@ -54,9 +55,16 @@ class MatterNodeView extends LitElement {
|
|
|
54
55
|
const graphViewType = networkType === "ethernet" ? "wifi" : networkType;
|
|
55
56
|
const graphUrl = showGraphButton ? `#${graphViewType}/${this.node.node_id}` : null;
|
|
56
57
|
|
|
58
|
+
// Format node address for hex display
|
|
59
|
+
const fabricIndex = getEffectiveFabricIndex(
|
|
60
|
+
this.client.serverInfo.fabric_index,
|
|
61
|
+
isTestNodeId(this.node.node_id),
|
|
62
|
+
);
|
|
63
|
+
const nodeHex = formatNodeAddress(fabricIndex, this.node.node_id);
|
|
64
|
+
|
|
57
65
|
return html`
|
|
58
66
|
<dashboard-header
|
|
59
|
-
.title=${
|
|
67
|
+
.title=${`Node ${this.node.node_id} ${nodeHex}`}
|
|
60
68
|
.client=${this.client}
|
|
61
69
|
backButton="#"
|
|
62
70
|
></dashboard-header>
|
|
@@ -64,7 +72,7 @@ class MatterNodeView extends LitElement {
|
|
|
64
72
|
<!-- node details section -->
|
|
65
73
|
<div class="container">
|
|
66
74
|
<div class="node-title-bar">
|
|
67
|
-
<h2>Node ${this.node.node_id}</h2>
|
|
75
|
+
<h2>Node ${this.node.node_id} <span class="node-id-hex">${nodeHex}</span></h2>
|
|
68
76
|
${showGraphButton
|
|
69
77
|
? html`
|
|
70
78
|
<a href=${graphUrl} class="show-in-graph-button" title="Show in ${graphViewType} graph">
|
|
@@ -156,6 +164,13 @@ class MatterNodeView extends LitElement {
|
|
|
156
164
|
color: var(--md-sys-color-on-background, #333);
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
.node-id-hex {
|
|
168
|
+
font-size: 0.75em;
|
|
169
|
+
font-weight: 400;
|
|
170
|
+
color: var(--md-sys-color-on-surface-variant, #666);
|
|
171
|
+
font-family: monospace;
|
|
172
|
+
}
|
|
173
|
+
|
|
159
174
|
.show-in-graph-button {
|
|
160
175
|
display: inline-flex;
|
|
161
176
|
align-items: center;
|
|
@@ -133,17 +133,31 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
133
133
|
|
|
134
134
|
// Subscribe to theme changes - refresh entire graph to update colors
|
|
135
135
|
this._themeUnsubscribe = ThemeService.subscribe(() => {
|
|
136
|
-
if (this._network) {
|
|
137
|
-
|
|
136
|
+
if (this._network && this._nodesDataSet) {
|
|
137
|
+
const fontColor = this._getFontColor();
|
|
138
|
+
|
|
139
|
+
// Update default font colors for new nodes
|
|
138
140
|
this._network.setOptions({
|
|
139
141
|
nodes: {
|
|
140
142
|
font: {
|
|
141
|
-
color:
|
|
143
|
+
color: fontColor,
|
|
142
144
|
},
|
|
143
145
|
},
|
|
144
146
|
});
|
|
147
|
+
|
|
145
148
|
// Regenerate node icons and edges with new theme colors
|
|
146
149
|
this._updateGraph();
|
|
150
|
+
|
|
151
|
+
// Update font color on all existing nodes in the dataset
|
|
152
|
+
const allNodes = this._nodesDataSet.get();
|
|
153
|
+
const nodeUpdates = allNodes.map((node: NetworkGraphNode) => ({
|
|
154
|
+
id: node.id,
|
|
155
|
+
font: { color: fontColor },
|
|
156
|
+
}));
|
|
157
|
+
this._nodesDataSet.update(nodeUpdates);
|
|
158
|
+
|
|
159
|
+
// Force redraw
|
|
160
|
+
this._network.redraw();
|
|
147
161
|
}
|
|
148
162
|
});
|
|
149
163
|
}
|
|
@@ -278,6 +278,6 @@ export function createUnknownDeviceIconDataUrl(isRouter: boolean = false, isSele
|
|
|
278
278
|
* @returns A data URL containing the SVG
|
|
279
279
|
*/
|
|
280
280
|
export function createWiFiRouterIconDataUrl(isSelected: boolean = false): string {
|
|
281
|
-
const color = isSelected ? "#1976d2" : "#
|
|
281
|
+
const color = isSelected ? "#1976d2" : "#ff9800"; // Orange for external infrastructure (same as Thread unknown)
|
|
282
282
|
return createIconDataUrl(mdiWifi, color);
|
|
283
283
|
}
|
|
@@ -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
|
/**
|