@matter-server/dashboard 0.7.1 → 0.7.2-alpha.0-20260602-cef59f5
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/components/dialogs/node-label-dialog/node-label-dialog.d.ts +28 -0
- package/dist/esm/components/dialogs/node-label-dialog/node-label-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/node-label-dialog/node-label-dialog.js +99 -0
- package/dist/esm/components/dialogs/node-label-dialog/node-label-dialog.js.map +6 -0
- package/dist/esm/components/dialogs/node-label-dialog/show-node-label-dialog.d.ts +8 -0
- package/dist/esm/components/dialogs/node-label-dialog/show-node-label-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/node-label-dialog/show-node-label-dialog.js +16 -0
- package/dist/esm/components/dialogs/node-label-dialog/show-node-label-dialog.js.map +6 -0
- package/dist/esm/components/dialogs/settings/log-level-section.d.ts.map +1 -1
- package/dist/esm/components/dialogs/settings/log-level-section.js +1 -0
- package/dist/esm/components/dialogs/settings/log-level-section.js.map +1 -1
- package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts.map +1 -1
- package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js +7 -18
- package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js.map +1 -1
- package/dist/esm/pages/components/node-details.d.ts +1 -0
- package/dist/esm/pages/components/node-details.d.ts.map +1 -1
- package/dist/esm/pages/components/node-details.js +28 -2
- package/dist/esm/pages/components/node-details.js.map +1 -1
- package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-network-view.js +5 -7
- package/dist/esm/pages/matter-network-view.js.map +1 -1
- package/dist/esm/pages/network/thread-graph.d.ts +5 -6
- package/dist/esm/pages/network/thread-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/thread-graph.js +40 -18
- package/dist/esm/pages/network/thread-graph.js.map +1 -1
- package/dist/esm/util/device-icons.d.ts +13 -2
- package/dist/esm/util/device-icons.d.ts.map +1 -1
- package/dist/esm/util/device-icons.js +29 -5
- package/dist/esm/util/device-icons.js.map +1 -1
- package/dist/esm/util/node-label.d.ts +16 -0
- package/dist/esm/util/node-label.d.ts.map +1 -0
- package/dist/esm/util/node-label.js +20 -0
- package/dist/esm/util/node-label.js.map +6 -0
- package/dist/web/index.html +8 -0
- package/dist/web/js/{attribute-write-dialog-CMEJgY9e.js → attribute-write-dialog-DxoudqTg.js} +1 -1
- package/dist/web/js/{command-invoke-dialog-BmlCVREh.js → command-invoke-dialog-BfBkbgp6.js} +1 -1
- package/dist/web/js/{commission-node-dialog-C1H9TRDH.js → commission-node-dialog-BJNntAAz.js} +5 -5
- package/dist/web/js/{commission-node-existing-CDaSgrTM.js → commission-node-existing-CCTW_OsF.js} +2 -2
- package/dist/web/js/{commission-node-thread-CiCKtsE6.js → commission-node-thread-hxemHL-m.js} +2 -2
- package/dist/web/js/{commission-node-wifi-DjXZOV3e.js → commission-node-wifi-BVp8_5oD.js} +2 -2
- package/dist/web/js/{dialog-box-v7mOYgwS.js → dialog-box-GqaRe3Hh.js} +1 -1
- package/dist/web/js/{fire_event-DmJTXjDw.js → fire_event-BYpJ8gvz.js} +1 -1
- package/dist/web/js/main.js +4 -4
- package/dist/web/js/{matter-dashboard-app-CRf1z2eY.js → matter-dashboard-app-FxzNxVJ6.js} +159 -57
- package/dist/web/js/{node-binding-dialog-D9tXIOEj.js → node-binding-dialog-BUK6iP8t.js} +1 -1
- package/dist/web/js/node-label-dialog-BuNRrozE.js +80 -0
- package/dist/web/js/{settings-dialog-DlL0QdYN.js → settings-dialog-DxDp7265.js} +4 -1
- package/package.json +8 -8
- package/src/components/dialogs/node-label-dialog/node-label-dialog.ts +93 -0
- package/src/components/dialogs/node-label-dialog/show-node-label-dialog.ts +15 -0
- package/src/components/dialogs/settings/log-level-section.ts +1 -0
- package/src/pages/cluster-commands/clusters/basic-information-commands.ts +7 -26
- package/src/pages/components/node-details.ts +31 -2
- package/src/pages/matter-network-view.ts +5 -7
- package/src/pages/network/thread-graph.ts +47 -20
- package/src/util/device-icons.ts +59 -5
- package/src/util/node-label.ts +22 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i, c, a as clientContext,
|
|
1
|
+
import { i, c, a as clientContext, t as tickContext, r, e, b as i$1, f as fireAndForget, s as showAlertDialog, d as b, A, h as handleAsync, g as t, n, D as DevModeService, m as mdiWifi, j as mdiAccessPoint, k as mdiEyeOff, l as mdiEye } from './matter-dashboard-app-FxzNxVJ6.js';
|
|
2
2
|
import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
|
|
3
3
|
import './main.js';
|
|
4
4
|
|
|
@@ -19,6 +19,9 @@ const LOG_LEVELS = [{
|
|
|
19
19
|
}, {
|
|
20
20
|
value: "warning",
|
|
21
21
|
label: "Warning"
|
|
22
|
+
}, {
|
|
23
|
+
value: "notice",
|
|
24
|
+
label: "Notice"
|
|
22
25
|
}, {
|
|
23
26
|
value: "info",
|
|
24
27
|
label: "Info"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matter-server/dashboard",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2-alpha.0-20260602-cef59f5",
|
|
4
4
|
"description": "Dashboard for OHF Matter Server",
|
|
5
5
|
"homepage": "https://github.com/matter-js/matterjs-server",
|
|
6
6
|
"bugs": {
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@lit/context": "^1.1.6",
|
|
35
35
|
"@material/web": "^2.4.1",
|
|
36
|
-
"@matter-server/custom-clusters": "0.7.
|
|
37
|
-
"@matter-server/ws-client": "0.7.
|
|
36
|
+
"@matter-server/custom-clusters": "0.7.2-alpha.0-20260602-cef59f5",
|
|
37
|
+
"@matter-server/ws-client": "0.7.2-alpha.0-20260602-cef59f5",
|
|
38
38
|
"@mdi/js": "^7.4.47",
|
|
39
39
|
"lit": "^3.3.3",
|
|
40
40
|
"tslib": "^2.8.1",
|
|
41
41
|
"vis-network": "^10.1.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@babel/preset-env": "^7.29.
|
|
45
|
-
"@matter/main": "0.17.0",
|
|
46
|
-
"@rollup/plugin-babel": "^7.
|
|
47
|
-
"@rollup/plugin-commonjs": "^29.0.
|
|
44
|
+
"@babel/preset-env": "^7.29.7",
|
|
45
|
+
"@matter/main": "0.17.1-alpha.0-20260601-9386e30f9",
|
|
46
|
+
"@rollup/plugin-babel": "^7.1.0",
|
|
47
|
+
"@rollup/plugin-commonjs": "^29.0.3",
|
|
48
48
|
"@rollup/plugin-json": "^6.1.0",
|
|
49
49
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
50
50
|
"@rollup/plugin-terser": "^1.0.0",
|
|
51
51
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
52
|
-
"rollup": "^4.
|
|
52
|
+
"rollup": "^4.61.0",
|
|
53
53
|
"rollup-plugin-copy": "^3.5.0",
|
|
54
54
|
"serve": "^14.2.6"
|
|
55
55
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import "@material/web/button/text-button";
|
|
8
|
+
import "@material/web/dialog/dialog";
|
|
9
|
+
import "@material/web/textfield/outlined-text-field";
|
|
10
|
+
import type { MdDialog } from "@material/web/dialog/dialog.js";
|
|
11
|
+
import type { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
12
|
+
import { html, LitElement } from "lit";
|
|
13
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
14
|
+
import { MAX_NODE_LABEL_LENGTH, writeNodeLabel } from "../../../util/node-label.js";
|
|
15
|
+
import { preventDefault } from "../../../util/prevent_default.js";
|
|
16
|
+
import { showAlertDialog } from "../../dialog-box/show-dialog-box.js";
|
|
17
|
+
|
|
18
|
+
@customElement("node-label-dialog")
|
|
19
|
+
export class NodeLabelDialog extends LitElement {
|
|
20
|
+
@property({ attribute: false })
|
|
21
|
+
public client!: MatterClient;
|
|
22
|
+
|
|
23
|
+
@property({ attribute: false })
|
|
24
|
+
public node!: MatterNode;
|
|
25
|
+
|
|
26
|
+
@state()
|
|
27
|
+
private _nodeLabel: string = "";
|
|
28
|
+
|
|
29
|
+
@state()
|
|
30
|
+
private _saving: boolean = false;
|
|
31
|
+
|
|
32
|
+
protected override firstUpdated() {
|
|
33
|
+
this._nodeLabel = this.node.nodeLabel;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected override render() {
|
|
37
|
+
return html`
|
|
38
|
+
<md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
|
|
39
|
+
<div slot="headline">Edit Node Label</div>
|
|
40
|
+
<div slot="content">
|
|
41
|
+
<md-outlined-text-field
|
|
42
|
+
label="Node Label"
|
|
43
|
+
.value=${this._nodeLabel}
|
|
44
|
+
@input=${this._handleInput}
|
|
45
|
+
maxlength=${MAX_NODE_LABEL_LENGTH}
|
|
46
|
+
?disabled=${this._saving}
|
|
47
|
+
supporting-text="Max ${MAX_NODE_LABEL_LENGTH} characters"
|
|
48
|
+
style="width: 100%; margin-top: 8px;"
|
|
49
|
+
></md-outlined-text-field>
|
|
50
|
+
</div>
|
|
51
|
+
<div slot="actions">
|
|
52
|
+
<md-text-button @click=${this._close} ?disabled=${this._saving}>Cancel</md-text-button>
|
|
53
|
+
<md-text-button @click=${this._save} ?disabled=${this._saving}>Save</md-text-button>
|
|
54
|
+
</div>
|
|
55
|
+
</md-dialog>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private _handleInput(e: Event) {
|
|
60
|
+
const input = e.target as HTMLInputElement;
|
|
61
|
+
this._nodeLabel = input.value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private _close() {
|
|
65
|
+
this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _handleClosed() {
|
|
69
|
+
this.parentNode!.removeChild(this);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async _save() {
|
|
73
|
+
this._saving = true;
|
|
74
|
+
try {
|
|
75
|
+
await writeNodeLabel(this.client, this.node, this._nodeLabel);
|
|
76
|
+
this._close();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
79
|
+
showAlertDialog({
|
|
80
|
+
title: "Failed to set node label",
|
|
81
|
+
text: errorMessage,
|
|
82
|
+
});
|
|
83
|
+
} finally {
|
|
84
|
+
this._saving = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
declare global {
|
|
90
|
+
interface HTMLElementTagNameMap {
|
|
91
|
+
"node-label-dialog": NodeLabelDialog;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
8
|
+
|
|
9
|
+
export const showNodeLabelDialog = async (client: MatterClient, node: MatterNode) => {
|
|
10
|
+
await import("./node-label-dialog.js");
|
|
11
|
+
const dialog = document.createElement("node-label-dialog");
|
|
12
|
+
dialog.client = client;
|
|
13
|
+
dialog.node = node;
|
|
14
|
+
document.querySelector("matter-dashboard-app")?.renderRoot.appendChild(dialog);
|
|
15
|
+
};
|
|
@@ -20,6 +20,7 @@ const LOG_LEVELS: { value: LogLevelString; label: string }[] = [
|
|
|
20
20
|
{ value: "critical", label: "Critical" },
|
|
21
21
|
{ value: "error", label: "Error" },
|
|
22
22
|
{ value: "warning", label: "Warning" },
|
|
23
|
+
{ value: "notice", label: "Notice" },
|
|
23
24
|
{ value: "info", label: "Info" },
|
|
24
25
|
{ value: "debug", label: "Debug" },
|
|
25
26
|
];
|
|
@@ -10,13 +10,10 @@ import { css, html, nothing, type CSSResultGroup } from "lit";
|
|
|
10
10
|
import { customElement, state } from "lit/decorators.js";
|
|
11
11
|
import { showAlertDialog } from "../../../components/dialog-box/show-dialog-box.js";
|
|
12
12
|
import { handleAsync } from "../../../util/async-handler.js";
|
|
13
|
+
import { MAX_NODE_LABEL_LENGTH, NODE_LABEL_CLUSTER_ID, writeNodeLabel } from "../../../util/node-label.js";
|
|
13
14
|
import { BaseClusterCommands } from "../base-cluster-commands.js";
|
|
14
15
|
import { registerClusterCommands } from "../registry.js";
|
|
15
16
|
|
|
16
|
-
const CLUSTER_ID = 0x28; // BasicInformation cluster (40 decimal)
|
|
17
|
-
const NODE_LABEL_ATTRIBUTE_ID = 5;
|
|
18
|
-
const MAX_NODE_LABEL_LENGTH = 32;
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
18
|
* Command panel for BasicInformation cluster (ID: 0x28 / 40).
|
|
22
19
|
* Provides ability to edit the NodeLabel attribute.
|
|
@@ -44,22 +41,7 @@ export class BasicInformationClusterCommands extends BaseClusterCommands {
|
|
|
44
41
|
if (!this.node) {
|
|
45
42
|
return;
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
// First check the direct nodeLabel property on the node
|
|
49
|
-
if (this.node.nodeLabel) {
|
|
50
|
-
this._nodeLabel = this.node.nodeLabel;
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Fallback to attribute path: endpoint/cluster/attribute = 0/40/5
|
|
55
|
-
// BasicInformation cluster is always on endpoint 0 per Matter specification
|
|
56
|
-
const attributePath = `0/${CLUSTER_ID}/${NODE_LABEL_ATTRIBUTE_ID}`;
|
|
57
|
-
const currentValue = this.node.attributes[attributePath];
|
|
58
|
-
if (typeof currentValue === "string") {
|
|
59
|
-
this._nodeLabel = currentValue;
|
|
60
|
-
} else {
|
|
61
|
-
this._nodeLabel = "";
|
|
62
|
-
}
|
|
44
|
+
this._nodeLabel = this.node.nodeLabel;
|
|
63
45
|
}
|
|
64
46
|
|
|
65
47
|
/**
|
|
@@ -115,13 +97,13 @@ export class BasicInformationClusterCommands extends BaseClusterCommands {
|
|
|
115
97
|
this._saving = true;
|
|
116
98
|
|
|
117
99
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
const label = this._nodeLabel.trim();
|
|
101
|
+
await writeNodeLabel(this.client, this.node, label);
|
|
102
|
+
this._nodeLabel = label;
|
|
121
103
|
|
|
122
104
|
showAlertDialog({
|
|
123
105
|
title: "Success",
|
|
124
|
-
text: `Node label set to "${
|
|
106
|
+
text: `Node label set to "${label}"`,
|
|
125
107
|
});
|
|
126
108
|
} catch (error) {
|
|
127
109
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -161,8 +143,7 @@ export class BasicInformationClusterCommands extends BaseClusterCommands {
|
|
|
161
143
|
];
|
|
162
144
|
}
|
|
163
145
|
|
|
164
|
-
|
|
165
|
-
registerClusterCommands(CLUSTER_ID, "basic-information-cluster-commands");
|
|
146
|
+
registerClusterCommands(NODE_LABEL_CLUSTER_ID, "basic-information-cluster-commands");
|
|
166
147
|
|
|
167
148
|
declare global {
|
|
168
149
|
interface HTMLElementTagNameMap {
|
|
@@ -13,13 +13,14 @@ import "@material/web/list/list";
|
|
|
13
13
|
import "@material/web/list/list-item";
|
|
14
14
|
import { consume } from "@lit/context";
|
|
15
15
|
import { MatterClient, MatterNode, UpdateSource } from "@matter-server/ws-client";
|
|
16
|
-
import { mdiChatProcessing, mdiLink, mdiShareVariant, mdiTrashCan, mdiUpdate, mdiVideo } from "@mdi/js";
|
|
16
|
+
import { mdiChatProcessing, mdiLink, mdiPencil, mdiShareVariant, mdiTrashCan, mdiUpdate, mdiVideo } from "@mdi/js";
|
|
17
17
|
import { LitElement, css, html, nothing } from "lit";
|
|
18
18
|
import { customElement, property, state } from "lit/decorators.js";
|
|
19
19
|
import { clientContext, tickContext } from "../../client/client-context.js";
|
|
20
20
|
import { DeviceType } from "../../client/models/descriptions.js";
|
|
21
21
|
import { showAlertDialog, showPromptDialog } from "../../components/dialog-box/show-dialog-box.js";
|
|
22
22
|
import { showNodeBindingDialog } from "../../components/dialogs/binding/show-node-binding-dialog.js";
|
|
23
|
+
import { showNodeLabelDialog } from "../../components/dialogs/node-label-dialog/show-node-label-dialog.js";
|
|
23
24
|
import { handleAsync } from "../../util/async-handler.js";
|
|
24
25
|
import "../../components/ha-svg-icon";
|
|
25
26
|
import "../camera-overlay.js";
|
|
@@ -86,8 +87,19 @@ export class NodeDetails extends LitElement {
|
|
|
86
87
|
<md-list>
|
|
87
88
|
<md-list-item>
|
|
88
89
|
<ha-svg-icon slot="start" class="device-icon" .path=${getDeviceIcon(this.node)}></ha-svg-icon>
|
|
89
|
-
<div slot="headline">
|
|
90
|
+
<div slot="headline" class="node-label-row">
|
|
90
91
|
<b>${this.node.nodeLabel || "Node Info"}</b>
|
|
92
|
+
${this.node.available
|
|
93
|
+
? html`
|
|
94
|
+
<md-icon-button
|
|
95
|
+
@click=${() => this._editNodeLabel()}
|
|
96
|
+
aria-label="Edit node label"
|
|
97
|
+
title="Edit node label"
|
|
98
|
+
>
|
|
99
|
+
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
|
|
100
|
+
</md-icon-button>
|
|
101
|
+
`
|
|
102
|
+
: nothing}
|
|
91
103
|
${this.node.available ? nothing : html` <span class="status">OFFLINE</span> `}
|
|
92
104
|
</div>
|
|
93
105
|
</md-list-item>
|
|
@@ -168,6 +180,10 @@ export class NodeDetails extends LitElement {
|
|
|
168
180
|
`;
|
|
169
181
|
}
|
|
170
182
|
|
|
183
|
+
private _editNodeLabel() {
|
|
184
|
+
showNodeLabelDialog(this.client, this.node!);
|
|
185
|
+
}
|
|
186
|
+
|
|
171
187
|
private async _reinterview() {
|
|
172
188
|
if (
|
|
173
189
|
!(await showPromptDialog({
|
|
@@ -318,6 +334,19 @@ export class NodeDetails extends LitElement {
|
|
|
318
334
|
}
|
|
319
335
|
|
|
320
336
|
static override styles = css`
|
|
337
|
+
.node-label-row {
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 8px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.node-label-row md-icon-button {
|
|
344
|
+
width: 24px;
|
|
345
|
+
height: 24px;
|
|
346
|
+
--md-icon-button-state-layer-width: 32px;
|
|
347
|
+
--md-icon-button-state-layer-height: 32px;
|
|
348
|
+
}
|
|
349
|
+
|
|
321
350
|
.device-icon {
|
|
322
351
|
--icon-primary-color: var(--md-sys-color-on-surface-variant, #666);
|
|
323
352
|
}
|
|
@@ -291,7 +291,7 @@ class MatterNetworkView extends LitElement {
|
|
|
291
291
|
return;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
const found = graph.
|
|
294
|
+
const found = graph.selectBySearch(searchValue);
|
|
295
295
|
this._threadAddressSearchStatus = found ? "found" : "not-found";
|
|
296
296
|
}
|
|
297
297
|
|
|
@@ -306,8 +306,8 @@ class MatterNetworkView extends LitElement {
|
|
|
306
306
|
type="text"
|
|
307
307
|
.value=${this._threadAddressSearch}
|
|
308
308
|
@input=${this._handleThreadAddressSearchInput}
|
|
309
|
-
placeholder="Search
|
|
310
|
-
title="Find device by
|
|
309
|
+
placeholder="Search label, node id, or address"
|
|
310
|
+
title="Find a Thread device by node label, node id, or extended address (EUI-64)"
|
|
311
311
|
/>
|
|
312
312
|
<button type="submit" class="search-button">Find</button>
|
|
313
313
|
</form>
|
|
@@ -371,7 +371,7 @@ class MatterNetworkView extends LitElement {
|
|
|
371
371
|
: html`<div class="thread-search-status ${this._threadAddressSearchStatus}">
|
|
372
372
|
${this._threadAddressSearchStatus === "found"
|
|
373
373
|
? "Node highlighted."
|
|
374
|
-
: "No matching
|
|
374
|
+
: "No matching device found."}
|
|
375
375
|
</div>`}
|
|
376
376
|
<thread-graph
|
|
377
377
|
.nodes=${this.nodes}
|
|
@@ -479,8 +479,6 @@ class MatterNetworkView extends LitElement {
|
|
|
479
479
|
flex: 1 1 0;
|
|
480
480
|
padding: 8px 16px;
|
|
481
481
|
gap: 8px;
|
|
482
|
-
max-width: 1600px;
|
|
483
|
-
margin: 0 auto;
|
|
484
482
|
width: 100%;
|
|
485
483
|
box-sizing: border-box;
|
|
486
484
|
min-height: 0;
|
|
@@ -683,7 +681,7 @@ class MatterNetworkView extends LitElement {
|
|
|
683
681
|
}
|
|
684
682
|
|
|
685
683
|
.details-sidebar {
|
|
686
|
-
width: 320px;
|
|
684
|
+
width: clamp(320px, 22vw, 480px);
|
|
687
685
|
flex-shrink: 0;
|
|
688
686
|
display: none;
|
|
689
687
|
min-height: 0;
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
buildExtAddrMap,
|
|
25
25
|
buildRloc16Map,
|
|
26
26
|
buildThreadEdgePairs,
|
|
27
|
+
decodeMeshcopStateBitmap,
|
|
27
28
|
findUnknownDevices,
|
|
28
29
|
getDeviceName,
|
|
29
30
|
getEdgeSignalScore,
|
|
@@ -113,36 +114,56 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
|
-
* Searches for a Thread node (known or unknown)
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* -
|
|
120
|
-
* - 0xAABBCCDDEEFF0011
|
|
117
|
+
* Searches for a Thread node (known or unknown) and selects it. Matches, in priority order:
|
|
118
|
+
* 1. Extended address (EUI-64), accepting `AABBCCDDEEFF0011`, `AA:BB:...`, or `0x...` forms.
|
|
119
|
+
* 2. Node id (exact).
|
|
120
|
+
* 3. Visible device label (case-insensitive substring).
|
|
121
121
|
* Returns true when a match is found.
|
|
122
122
|
*/
|
|
123
|
-
public
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
123
|
+
public selectBySearch(query: string): boolean {
|
|
124
|
+
const trimmed = query.trim();
|
|
125
|
+
if (!trimmed) {
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
// Only visible nodes are selectable — focusing a hidden node would report a
|
|
130
|
+
// match the user can't see on the graph.
|
|
131
|
+
const threadNodes = Object.values(this.nodes).filter(
|
|
132
|
+
node => getNetworkType(node) === "thread" && !(this.hideOfflineNodes && node.available === false),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// 1. Extended address — commissioned devices first, then unknown/external neighbors.
|
|
136
|
+
const normalized = normalizeExtendedAddressInput(trimmed);
|
|
137
|
+
if (normalized) {
|
|
138
|
+
for (const node of threadNodes) {
|
|
139
|
+
const extAddressHex = getThreadExtendedAddressHex(node);
|
|
140
|
+
if (extAddressHex && extAddressHex === normalized) {
|
|
141
|
+
this.selectNode(String(node.node_id));
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
133
144
|
}
|
|
134
145
|
|
|
135
|
-
const
|
|
136
|
-
|
|
146
|
+
for (const [unknownId, unknown] of this._unknownDevicesMapCache) {
|
|
147
|
+
if (normalizeExtendedAddressInput(unknown.extAddressHex) === normalized) {
|
|
148
|
+
this.selectNode(unknownId);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Node id (exact).
|
|
155
|
+
for (const node of threadNodes) {
|
|
156
|
+
if (String(node.node_id) === trimmed) {
|
|
137
157
|
this.selectNode(String(node.node_id));
|
|
138
158
|
return true;
|
|
139
159
|
}
|
|
140
160
|
}
|
|
141
161
|
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
// 3. Device label as shown on the graph (case-insensitive substring).
|
|
163
|
+
const needle = trimmed.toLowerCase();
|
|
164
|
+
for (const node of threadNodes) {
|
|
165
|
+
if (getDeviceName(node).toLowerCase().includes(needle)) {
|
|
166
|
+
this.selectNode(String(node.node_id));
|
|
146
167
|
return true;
|
|
147
168
|
}
|
|
148
169
|
}
|
|
@@ -257,10 +278,13 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
257
278
|
? `\n${device.networkName}`
|
|
258
279
|
: "";
|
|
259
280
|
const label = `${top}${suffix}`;
|
|
281
|
+
const decodedState = decodeMeshcopStateBitmap(device.stateBitmapHex);
|
|
282
|
+
const isLeader = decodedState?.threadRoleValue === 3;
|
|
283
|
+
const isPrimaryBbr = decodedState?.bbr === true && decodedState.bbrFunction === "primary";
|
|
260
284
|
graphNodes.push({
|
|
261
285
|
id: device.id,
|
|
262
286
|
label,
|
|
263
|
-
image: createBorderRouterIconDataUrl(isSelected),
|
|
287
|
+
image: createBorderRouterIconDataUrl(isSelected, isLeader, isPrimaryBbr),
|
|
264
288
|
shape: "image" as const,
|
|
265
289
|
networkType: "thread" as const,
|
|
266
290
|
isUnknown: false,
|
|
@@ -620,9 +644,12 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
620
644
|
}
|
|
621
645
|
const external = this._unknownDevicesMapCache.get(nodeId);
|
|
622
646
|
if (external?.kind === "br") {
|
|
647
|
+
const decodedState = decodeMeshcopStateBitmap(external.stateBitmapHex);
|
|
648
|
+
const isLeader = decodedState?.threadRoleValue === 3;
|
|
649
|
+
const isPrimaryBbr = decodedState?.bbr === true && decodedState.bbrFunction === "primary";
|
|
623
650
|
this._nodesDataSet.update({
|
|
624
651
|
id: nodeId,
|
|
625
|
-
image: createBorderRouterIconDataUrl(isHighlighted),
|
|
652
|
+
image: createBorderRouterIconDataUrl(isHighlighted, isLeader, isPrimaryBbr),
|
|
626
653
|
});
|
|
627
654
|
} else if (nodeId.startsWith("unknown_") || nodeId.startsWith("br_")) {
|
|
628
655
|
this._nodesDataSet.update({
|
package/src/util/device-icons.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
mdiCast,
|
|
18
18
|
mdiCctv,
|
|
19
19
|
mdiChip,
|
|
20
|
+
mdiCircleMedium,
|
|
21
|
+
mdiCrown,
|
|
20
22
|
mdiDishwasher,
|
|
21
23
|
mdiDoorbell,
|
|
22
24
|
mdiDoorbellVideo,
|
|
@@ -40,12 +42,15 @@ import {
|
|
|
40
42
|
mdiRobotVacuum,
|
|
41
43
|
mdiRouter,
|
|
42
44
|
mdiRouterWireless,
|
|
45
|
+
mdiSleep,
|
|
43
46
|
mdiSmokeDetector,
|
|
44
47
|
mdiSnowflakeAlert,
|
|
45
48
|
mdiSolarPower,
|
|
46
49
|
mdiSpeaker,
|
|
47
50
|
mdiSprinkler,
|
|
51
|
+
mdiStar,
|
|
48
52
|
mdiStove,
|
|
53
|
+
mdiSwapHorizontal,
|
|
49
54
|
mdiTelevision,
|
|
50
55
|
mdiThermometer,
|
|
51
56
|
mdiToggleSwitch,
|
|
@@ -303,6 +308,19 @@ const threadRoleToIcon: Record<number, string> = {
|
|
|
303
308
|
6: mdiAccessPoint, // Leader
|
|
304
309
|
};
|
|
305
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Corner badge marking a node's Thread RoutingRole (attr 0/53/1) — a role-rank indicator overlaid on
|
|
313
|
+
* the device icon. The Leader is the rare, high-signal exception (amber crown); routers and end
|
|
314
|
+
* devices use progressively lower-key glyphs. Unassigned/Unspecified and unknown roles get no badge.
|
|
315
|
+
*/
|
|
316
|
+
const THREAD_ROLE_BADGES: Record<number, { iconPath: string; colorVar: string; colorFallback: string }> = {
|
|
317
|
+
2: { iconPath: mdiSleep, colorVar: "--node-color-thread-enddevice", colorFallback: "#90a4ae" }, // Sleepy End Device
|
|
318
|
+
3: { iconPath: mdiCircleMedium, colorVar: "--node-color-thread-enddevice", colorFallback: "#90a4ae" }, // End Device
|
|
319
|
+
4: { iconPath: mdiCircleMedium, colorVar: "--node-color-thread-enddevice", colorFallback: "#90a4ae" }, // REED
|
|
320
|
+
5: { iconPath: mdiSwapHorizontal, colorVar: "--node-color-thread-router", colorFallback: "#1e88e5" }, // Router
|
|
321
|
+
6: { iconPath: mdiCrown, colorVar: "--node-color-thread-leader", colorFallback: "#f9a825" }, // Leader
|
|
322
|
+
};
|
|
323
|
+
|
|
306
324
|
/**
|
|
307
325
|
* Utility device types (per Matter spec) deprioritized when selecting the primary type for icon display.
|
|
308
326
|
* These are commonly reported alongside the actual application type (e.g., a light also reports as
|
|
@@ -453,14 +471,26 @@ export function getNetworkTypeIcon(networkType: string): string {
|
|
|
453
471
|
* @param iconPath - The MDI icon path
|
|
454
472
|
* @param color - The icon color (CSS color string)
|
|
455
473
|
* @param size - The icon size in pixels
|
|
474
|
+
* @param badge - Optional top-right corner badge: an MDI glyph filled white on a colored disc
|
|
456
475
|
* @returns A data URL containing the SVG
|
|
457
476
|
*/
|
|
458
|
-
export function createIconDataUrl(
|
|
477
|
+
export function createIconDataUrl(
|
|
478
|
+
iconPath: string,
|
|
479
|
+
color: string,
|
|
480
|
+
size: number = 48,
|
|
481
|
+
badge?: { iconPath: string; color: string },
|
|
482
|
+
): string {
|
|
459
483
|
// MDI icons use a 24x24 viewBox
|
|
484
|
+
const badgeMarkup =
|
|
485
|
+
badge !== undefined
|
|
486
|
+
? `<circle cx="18" cy="6" r="4" fill="${badge.color}"/>
|
|
487
|
+
<path d="${badge.iconPath}" fill="white" transform="translate(14.64,2.64) scale(0.28)"/>`
|
|
488
|
+
: "";
|
|
460
489
|
const svg = `
|
|
461
490
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${size}" height="${size}">
|
|
462
491
|
<circle cx="12" cy="12" r="11" fill="white" stroke="${color}" stroke-width="1"/>
|
|
463
492
|
<path d="${iconPath}" fill="${color}" transform="scale(0.6) translate(8,8)"/>
|
|
493
|
+
${badgeMarkup}
|
|
464
494
|
</svg>
|
|
465
495
|
`.trim();
|
|
466
496
|
|
|
@@ -492,7 +522,14 @@ export function createNodeIconDataUrl(
|
|
|
492
522
|
} else {
|
|
493
523
|
color = getDefaultIconColor(); // Theme-aware default
|
|
494
524
|
}
|
|
495
|
-
|
|
525
|
+
// Thread RoutingRole (incl. Leader) applies to any router node, not just BRs. Badge it over the
|
|
526
|
+
// device icon rather than replacing the icon, preserving device identity.
|
|
527
|
+
const roleBadge = threadRole !== undefined ? THREAD_ROLE_BADGES[threadRole] : undefined;
|
|
528
|
+
const badge =
|
|
529
|
+
roleBadge !== undefined
|
|
530
|
+
? { iconPath: roleBadge.iconPath, color: getCssVar(roleBadge.colorVar, roleBadge.colorFallback) }
|
|
531
|
+
: undefined;
|
|
532
|
+
return createIconDataUrl(iconPath, color, 48, badge);
|
|
496
533
|
}
|
|
497
534
|
|
|
498
535
|
/**
|
|
@@ -511,14 +548,31 @@ export function createUnknownDeviceIconDataUrl(isRouter: boolean = false, isSele
|
|
|
511
548
|
|
|
512
549
|
/**
|
|
513
550
|
* Creates an SVG data URL for a Thread Border Router identified via mDNS.
|
|
551
|
+
*
|
|
552
|
+
* Thread Leader (mesh routing role) and Primary BBR (backbone role) are orthogonal: the central
|
|
553
|
+
* glyph reflects the mesh role (crown for leader, router otherwise) while a corner star badge marks
|
|
554
|
+
* the primary BBR. A BR that is both shows both.
|
|
555
|
+
*
|
|
514
556
|
* @param isSelected - Whether the node is selected
|
|
557
|
+
* @param isLeader - Whether this BR is the Thread network leader (from MeshCoP state bitmap)
|
|
558
|
+
* @param isPrimaryBbr - Whether this BR is the primary Backbone Border Router (from MeshCoP state bitmap)
|
|
515
559
|
* @returns A data URL containing the SVG
|
|
516
560
|
*/
|
|
517
|
-
export function createBorderRouterIconDataUrl(
|
|
561
|
+
export function createBorderRouterIconDataUrl(
|
|
562
|
+
isSelected: boolean = false,
|
|
563
|
+
isLeader: boolean = false,
|
|
564
|
+
isPrimaryBbr: boolean = false,
|
|
565
|
+
): string {
|
|
566
|
+
const glyph = isLeader ? mdiCrown : mdiRouterWireless;
|
|
518
567
|
const color = isSelected
|
|
519
568
|
? getCssVar("--node-color-selected", "#1976d2")
|
|
520
|
-
:
|
|
521
|
-
|
|
569
|
+
: isLeader
|
|
570
|
+
? getCssVar("--node-color-thread-leader", "#f9a825")
|
|
571
|
+
: getCssVar("--md-sys-color-primary", "#03a9f4");
|
|
572
|
+
const badge = isPrimaryBbr
|
|
573
|
+
? { iconPath: mdiStar, color: getCssVar("--node-color-primary-bbr", "#00897b") }
|
|
574
|
+
: undefined;
|
|
575
|
+
return createIconDataUrl(glyph, color, 48, badge);
|
|
522
576
|
}
|
|
523
577
|
|
|
524
578
|
/**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
8
|
+
|
|
9
|
+
// BasicInformation cluster (0x28 / 40), always on endpoint 0 per Matter specification.
|
|
10
|
+
export const NODE_LABEL_CLUSTER_ID = 0x28;
|
|
11
|
+
export const NODE_LABEL_ATTRIBUTE_ID = 5;
|
|
12
|
+
export const MAX_NODE_LABEL_LENGTH = 32;
|
|
13
|
+
|
|
14
|
+
export const NODE_LABEL_ATTRIBUTE_PATH = `0/${NODE_LABEL_CLUSTER_ID}/${NODE_LABEL_ATTRIBUTE_ID}`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write the BasicInformation NodeLabel attribute for a node.
|
|
18
|
+
* Trims surrounding whitespace so the stored value matches what MatterNode.nodeLabel reads back.
|
|
19
|
+
*/
|
|
20
|
+
export function writeNodeLabel(client: MatterClient, node: MatterNode, label: string): Promise<unknown> {
|
|
21
|
+
return client.writeAttribute(node.node_id, NODE_LABEL_ATTRIBUTE_PATH, label.trim());
|
|
22
|
+
}
|