@matter-server/dashboard 0.2.0-alpha.0-00000000-000000000
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/LICENSE +201 -0
- package/README.md +11 -0
- package/dist/esm/client/client-context.d.ts +10 -0
- package/dist/esm/client/client-context.d.ts.map +1 -0
- package/dist/esm/client/client-context.js +11 -0
- package/dist/esm/client/client-context.js.map +6 -0
- package/dist/esm/client/models/descriptions.d.ts +20 -0
- package/dist/esm/client/models/descriptions.d.ts.map +1 -0
- package/dist/esm/client/models/descriptions.js +10929 -0
- package/dist/esm/client/models/descriptions.js.map +6 -0
- package/dist/esm/components/dialog-box/dialog-box.d.ts +25 -0
- package/dist/esm/components/dialog-box/dialog-box.d.ts.map +1 -0
- package/dist/esm/components/dialog-box/dialog-box.js +66 -0
- package/dist/esm/components/dialog-box/dialog-box.js.map +6 -0
- package/dist/esm/components/dialog-box/show-dialog-box.d.ts +18 -0
- package/dist/esm/components/dialog-box/show-dialog-box.d.ts.map +1 -0
- package/dist/esm/components/dialog-box/show-dialog-box.js +22 -0
- package/dist/esm/components/dialog-box/show-dialog-box.js.map +6 -0
- package/dist/esm/components/dialogs/acl/model.d.ts +33 -0
- package/dist/esm/components/dialogs/acl/model.d.ts.map +1 -0
- package/dist/esm/components/dialogs/acl/model.js +79 -0
- package/dist/esm/components/dialogs/acl/model.js.map +6 -0
- package/dist/esm/components/dialogs/binding/model.d.ts +20 -0
- package/dist/esm/components/dialogs/binding/model.d.ts.map +1 -0
- package/dist/esm/components/dialogs/binding/model.js +45 -0
- package/dist/esm/components/dialogs/binding/model.js.map +6 -0
- package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts +49 -0
- package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js +357 -0
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +6 -0
- package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts +8 -0
- package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js +17 -0
- package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js.map +6 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts +31 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js +94 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js.map +6 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.d.ts +17 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.d.ts.map +1 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.js +64 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.js.map +6 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.d.ts +19 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.d.ts.map +1 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js +91 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js.map +6 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.d.ts +20 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.d.ts.map +1 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js +106 -0
- package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js.map +6 -0
- package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts +8 -0
- package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js +15 -0
- package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js.map +6 -0
- package/dist/esm/components/ha-svg-icon.d.ts +19 -0
- package/dist/esm/components/ha-svg-icon.d.ts.map +1 -0
- package/dist/esm/components/ha-svg-icon.js +77 -0
- package/dist/esm/components/ha-svg-icon.js.map +6 -0
- package/dist/esm/entrypoint/main.d.ts +7 -0
- package/dist/esm/entrypoint/main.d.ts.map +1 -0
- package/dist/esm/entrypoint/main.js +45 -0
- package/dist/esm/entrypoint/main.js.map +6 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/pages/components/context.d.ts +9 -0
- package/dist/esm/pages/components/context.d.ts.map +1 -0
- package/dist/esm/pages/components/context.js +11 -0
- package/dist/esm/pages/components/context.js.map +6 -0
- package/dist/esm/pages/components/footer.d.ts +11 -0
- package/dist/esm/pages/components/footer.d.ts.map +1 -0
- package/dist/esm/pages/components/footer.js +52 -0
- package/dist/esm/pages/components/footer.js.map +6 -0
- package/dist/esm/pages/components/header.d.ts +27 -0
- package/dist/esm/pages/components/header.d.ts.map +1 -0
- package/dist/esm/pages/components/header.js +90 -0
- package/dist/esm/pages/components/header.js.map +6 -0
- package/dist/esm/pages/components/node-details.d.ts +29 -0
- package/dist/esm/pages/components/node-details.d.ts.map +1 -0
- package/dist/esm/pages/components/node-details.js +241 -0
- package/dist/esm/pages/components/node-details.js.map +6 -0
- package/dist/esm/pages/components/server-details.d.ts +24 -0
- package/dist/esm/pages/components/server-details.d.ts.map +1 -0
- package/dist/esm/pages/components/server-details.js +130 -0
- package/dist/esm/pages/components/server-details.js.map +6 -0
- package/dist/esm/pages/matter-cluster-view.d.ts +30 -0
- package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -0
- package/dist/esm/pages/matter-cluster-view.js +154 -0
- package/dist/esm/pages/matter-cluster-view.js.map +6 -0
- package/dist/esm/pages/matter-dashboard-app.d.ts +27 -0
- package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -0
- package/dist/esm/pages/matter-dashboard-app.js +122 -0
- package/dist/esm/pages/matter-dashboard-app.js.map +6 -0
- package/dist/esm/pages/matter-endpoint-view.d.ts +29 -0
- package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -0
- package/dist/esm/pages/matter-endpoint-view.js +149 -0
- package/dist/esm/pages/matter-endpoint-view.js.map +6 -0
- package/dist/esm/pages/matter-node-view.d.ts +28 -0
- package/dist/esm/pages/matter-node-view.d.ts.map +1 -0
- package/dist/esm/pages/matter-node-view.js +122 -0
- package/dist/esm/pages/matter-node-view.js.map +6 -0
- package/dist/esm/pages/matter-server-view.d.ts +31 -0
- package/dist/esm/pages/matter-server-view.d.ts.map +1 -0
- package/dist/esm/pages/matter-server-view.js +113 -0
- package/dist/esm/pages/matter-server-view.js.map +6 -0
- package/dist/esm/util/clone_class.d.ts +7 -0
- package/dist/esm/util/clone_class.d.ts.map +1 -0
- package/dist/esm/util/clone_class.js +10 -0
- package/dist/esm/util/clone_class.js.map +6 -0
- package/dist/esm/util/fire_event.d.ts +34 -0
- package/dist/esm/util/fire_event.d.ts.map +1 -0
- package/dist/esm/util/fire_event.js +21 -0
- package/dist/esm/util/fire_event.js.map +6 -0
- package/dist/esm/util/prevent_default.d.ts +7 -0
- package/dist/esm/util/prevent_default.d.ts.map +1 -0
- package/dist/esm/util/prevent_default.js +10 -0
- package/dist/esm/util/prevent_default.js.map +6 -0
- package/dist/esm/util/routing.d.ts +10 -0
- package/dist/esm/util/routing.d.ts.map +1 -0
- package/dist/esm/util/routing.js +6 -0
- package/dist/esm/util/routing.js.map +6 -0
- package/dist/web/index.html +40 -0
- package/dist/web/js/commission-node-dialog-BJsfA4IV.js +78 -0
- package/dist/web/js/commission-node-dialog-DEZ3EqYO.js +78 -0
- package/dist/web/js/commission-node-existing-CzRtUgBm.js +50 -0
- package/dist/web/js/commission-node-existing-OK1ybPFI.js +50 -0
- package/dist/web/js/commission-node-thread-DLmclivF.js +75 -0
- package/dist/web/js/commission-node-thread-FcLFz84I.js +75 -0
- package/dist/web/js/commission-node-wifi-C8ho-UYb.js +88 -0
- package/dist/web/js/commission-node-wifi-C8iGfy7c.js +88 -0
- package/dist/web/js/dialog-box-BPz-oO3d.js +52 -0
- package/dist/web/js/dialog-box-DN32sjfR.js +52 -0
- package/dist/web/js/fire_event-BERTqZpV.js +169 -0
- package/dist/web/js/fire_event-BlsbXpOL.js +169 -0
- package/dist/web/js/main.js +547 -0
- package/dist/web/js/matter-dashboard-app-5UjO1Ik8.js +16068 -0
- package/dist/web/js/matter-dashboard-app-BazvuIIi.js +16068 -0
- package/dist/web/js/node-binding-dialog-2yitVn0R.js +443 -0
- package/dist/web/js/node-binding-dialog-Cw6QEmL3.js +443 -0
- package/dist/web/js/outlined-text-field-BMLYwwlc.js +2086 -0
- package/dist/web/js/outlined-text-field-Sqd4JHxo.js +2086 -0
- package/dist/web/js/prevent_default-BsT53c0u.js +814 -0
- package/dist/web/js/prevent_default-D4GG_QeD.js +814 -0
- package/package.json +54 -0
- package/src/client/client-context.ts +10 -0
- package/src/client/models/descriptions.ts +10948 -0
- package/src/components/dialog-box/dialog-box.ts +62 -0
- package/src/components/dialog-box/show-dialog-box.ts +32 -0
- package/src/components/dialogs/acl/model.ts +105 -0
- package/src/components/dialogs/binding/model.ts +58 -0
- package/src/components/dialogs/binding/node-binding-dialog.ts +419 -0
- package/src/components/dialogs/binding/show-node-binding-dialog.ts +16 -0
- package/src/components/dialogs/commission-node-dialog/commission-node-dialog.ts +102 -0
- package/src/components/dialogs/commission-node-dialog/commission-node-existing.ts +49 -0
- package/src/components/dialogs/commission-node-dialog/commission-node-thread.ts +76 -0
- package/src/components/dialogs/commission-node-dialog/commission-node-wifi.ts +90 -0
- package/src/components/dialogs/commission-node-dialog/show-commission-node-dialog.ts +14 -0
- package/src/components/ha-svg-icon.ts +66 -0
- package/src/entrypoint/main.ts +60 -0
- package/src/pages/components/context.ts +10 -0
- package/src/pages/components/footer.ts +39 -0
- package/src/pages/components/header.ts +87 -0
- package/src/pages/components/node-details.ts +252 -0
- package/src/pages/components/server-details.ts +124 -0
- package/src/pages/matter-cluster-view.ts +162 -0
- package/src/pages/matter-dashboard-app.ts +125 -0
- package/src/pages/matter-endpoint-view.ts +152 -0
- package/src/pages/matter-node-view.ts +126 -0
- package/src/pages/matter-server-view.ts +117 -0
- package/src/tsconfig.json +16 -0
- package/src/util/clone_class.ts +7 -0
- package/src/util/fire_event.ts +83 -0
- package/src/util/prevent_default.ts +7 -0
- package/src/util/routing.ts +10 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import "@material/web/button/filled-button";
|
|
8
|
+
import "@material/web/button/outlined-button";
|
|
9
|
+
import "@material/web/button/text-button";
|
|
10
|
+
import "@material/web/divider/divider";
|
|
11
|
+
import "@material/web/iconbutton/icon-button";
|
|
12
|
+
import "@material/web/list/list";
|
|
13
|
+
import "@material/web/list/list-item";
|
|
14
|
+
import { mdiChatProcessing, mdiLink, mdiShareVariant, mdiTrashCan, mdiUpdate } from "@mdi/js";
|
|
15
|
+
|
|
16
|
+
import { consume } from "@lit/context";
|
|
17
|
+
import { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
18
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
19
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
20
|
+
import { DeviceType } from "../../client/models/descriptions.js";
|
|
21
|
+
import { showAlertDialog, showPromptDialog } from "../../components/dialog-box/show-dialog-box.js";
|
|
22
|
+
import { showNodeBindingDialog } from "../../components/dialogs/binding/show-node-binding-dialog.js";
|
|
23
|
+
import "../../components/ha-svg-icon";
|
|
24
|
+
import { getEndpointDeviceTypes } from "../matter-endpoint-view.js";
|
|
25
|
+
import { bindingContext } from "./context.js";
|
|
26
|
+
|
|
27
|
+
function getNodeDeviceTypes(node: MatterNode): DeviceType[] {
|
|
28
|
+
const uniqueEndpoints = new Set(Object.keys(node.attributes).map(key => Number(key.split("/")[0])));
|
|
29
|
+
const allDeviceTypes: Set<DeviceType> = new Set();
|
|
30
|
+
uniqueEndpoints.forEach(endpointId => {
|
|
31
|
+
getEndpointDeviceTypes(node, endpointId).forEach(deviceType => {
|
|
32
|
+
allDeviceTypes.add(deviceType);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
return Array.from(allDeviceTypes);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@customElement("node-details")
|
|
39
|
+
export class NodeDetails extends LitElement {
|
|
40
|
+
public client!: MatterClient;
|
|
41
|
+
|
|
42
|
+
@property() public node?: MatterNode;
|
|
43
|
+
|
|
44
|
+
@state()
|
|
45
|
+
private _updateInitiated: boolean = false;
|
|
46
|
+
|
|
47
|
+
@consume({ context: bindingContext })
|
|
48
|
+
@property({ attribute: false })
|
|
49
|
+
endpoint!: number;
|
|
50
|
+
|
|
51
|
+
protected override render() {
|
|
52
|
+
if (!this.node) return html``;
|
|
53
|
+
|
|
54
|
+
const bindings = this.node.attributes[this.endpoint + "/30/0"];
|
|
55
|
+
|
|
56
|
+
return html`
|
|
57
|
+
<md-list>
|
|
58
|
+
<md-list-item>
|
|
59
|
+
<div slot="headline">
|
|
60
|
+
<b>Node ${this.node.node_id} ${this.node.nodeLabel}</b>
|
|
61
|
+
${this.node.available ? nothing : html`<span class="status">OFFLINE</span>`}
|
|
62
|
+
</div>
|
|
63
|
+
</md-list-item>
|
|
64
|
+
<md-list-item>
|
|
65
|
+
<div slot="supporting-text"><span class="left">VendorName: </span>${this.node.vendorName}</div>
|
|
66
|
+
<div slot="supporting-text"><span class="left">productName: </span>${this.node.productName}</div>
|
|
67
|
+
<div slot="supporting-text">
|
|
68
|
+
<span class="left">Commissioned: </span>${this.node.date_commissioned}
|
|
69
|
+
</div>
|
|
70
|
+
<div slot="supporting-text">
|
|
71
|
+
<span class="left">Last interviewed: </span>${this.node.last_interview}
|
|
72
|
+
</div>
|
|
73
|
+
<div slot="supporting-text"><span class="left">Is bridge: </span>${this.node.is_bridge}</div>
|
|
74
|
+
<div slot="supporting-text"><span class="left">Serialnumber: </span>${this.node.serialNumber}</div>
|
|
75
|
+
${this.node.is_bridge
|
|
76
|
+
? ""
|
|
77
|
+
: html` <div slot="supporting-text">
|
|
78
|
+
<span class="left">All device types: </span>${getNodeDeviceTypes(this.node)
|
|
79
|
+
.map(deviceType => {
|
|
80
|
+
return deviceType.label;
|
|
81
|
+
})
|
|
82
|
+
.join(" / ")}
|
|
83
|
+
</div>`}
|
|
84
|
+
</md-list-item>
|
|
85
|
+
<md-list-item class="btn">
|
|
86
|
+
<md-outlined-button @click=${this._reinterview}
|
|
87
|
+
>Interview<ha-svg-icon slot="icon" .path=${mdiChatProcessing}></ha-svg-icon
|
|
88
|
+
></md-outlined-button>
|
|
89
|
+
${this._updateInitiated || (this.node.updateState || 0) > 1
|
|
90
|
+
? html` <md-outlined-button disabled
|
|
91
|
+
>Update in progress (${this.node.updateStateProgress || 0}%)<ha-svg-icon
|
|
92
|
+
slot="icon"
|
|
93
|
+
.path=${mdiUpdate}
|
|
94
|
+
></ha-svg-icon
|
|
95
|
+
></md-outlined-button>`
|
|
96
|
+
: html`<md-outlined-button @click=${this._searchUpdate}
|
|
97
|
+
>Update<ha-svg-icon slot="icon" .path=${mdiUpdate}></ha-svg-icon
|
|
98
|
+
></md-outlined-button>`}
|
|
99
|
+
${bindings
|
|
100
|
+
? html`
|
|
101
|
+
<md-outlined-button @click=${this._binding}>
|
|
102
|
+
Binding
|
|
103
|
+
<ha-svg-icon slot="icon" .path=${mdiLink}></ha-svg-icon>
|
|
104
|
+
</md-outlined-button>
|
|
105
|
+
`
|
|
106
|
+
: nothing}
|
|
107
|
+
|
|
108
|
+
<md-outlined-button @click=${this._openCommissioningWindow}
|
|
109
|
+
>Share<ha-svg-icon slot="icon" .path=${mdiShareVariant}></ha-svg-icon
|
|
110
|
+
></md-outlined-button>
|
|
111
|
+
<md-outlined-button @click=${this._remove}
|
|
112
|
+
>Remove<ha-svg-icon slot="icon" .path=${mdiTrashCan}></ha-svg-icon
|
|
113
|
+
></md-outlined-button>
|
|
114
|
+
</md-list-item>
|
|
115
|
+
</md-list>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async _reinterview() {
|
|
120
|
+
if (
|
|
121
|
+
!(await showPromptDialog({
|
|
122
|
+
title: "Reinterview",
|
|
123
|
+
text: "Are you sure you want to reinterview this node?",
|
|
124
|
+
confirmText: "Reinterview",
|
|
125
|
+
}))
|
|
126
|
+
) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await this.client.interviewNode(this.node!.node_id);
|
|
131
|
+
showAlertDialog({
|
|
132
|
+
title: "Reinterview node",
|
|
133
|
+
text: "Success!",
|
|
134
|
+
});
|
|
135
|
+
location.reload();
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
showAlertDialog({
|
|
138
|
+
title: "Failed to reinterview node",
|
|
139
|
+
text: err.message,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async _remove() {
|
|
145
|
+
if (
|
|
146
|
+
!(await showPromptDialog({
|
|
147
|
+
title: "Remove",
|
|
148
|
+
text: "Are you sure you want to remove this node?",
|
|
149
|
+
confirmText: "Remove",
|
|
150
|
+
}))
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
await this.client.removeNode(this.node!.node_id);
|
|
156
|
+
// make sure to navigate back to the root if node details was opened
|
|
157
|
+
location.replace("#");
|
|
158
|
+
} catch (err: any) {
|
|
159
|
+
showAlertDialog({
|
|
160
|
+
title: "Failed to remove node",
|
|
161
|
+
text: err.message,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async _binding() {
|
|
167
|
+
try {
|
|
168
|
+
showNodeBindingDialog(this.client, this.node!, this.endpoint);
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
console.log(err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async _searchUpdate() {
|
|
175
|
+
const nodeUpdate = await this.client.checkNodeUpdate(this.node!.node_id);
|
|
176
|
+
if (!nodeUpdate) {
|
|
177
|
+
showAlertDialog({
|
|
178
|
+
title: "No update available",
|
|
179
|
+
text: "No update available for this node",
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (
|
|
184
|
+
!(await showPromptDialog({
|
|
185
|
+
title: "Firmware update available",
|
|
186
|
+
text: `Found a firmware update for this node on ${nodeUpdate.update_source}.
|
|
187
|
+
Do you want to update this node to version ${nodeUpdate.software_version_string}?
|
|
188
|
+
Note that updating firmware is at your own risk and may cause the device to
|
|
189
|
+
malfunction or needs additional handling such as power cycling it and/or recommisisoning it.
|
|
190
|
+
Use with care.\n${nodeUpdate.firmware_information}`,
|
|
191
|
+
confirmText: "Start Update",
|
|
192
|
+
}))
|
|
193
|
+
) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
this._updateInitiated = true;
|
|
198
|
+
await this.client.updateNode(this.node!.node_id, nodeUpdate.software_version);
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
showAlertDialog({
|
|
201
|
+
title: "Failed to update node",
|
|
202
|
+
text: err.message,
|
|
203
|
+
});
|
|
204
|
+
} finally {
|
|
205
|
+
this._updateInitiated = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async _openCommissioningWindow() {
|
|
210
|
+
if (
|
|
211
|
+
!(await showPromptDialog({
|
|
212
|
+
title: "Share device",
|
|
213
|
+
text: "Do you want to share this device with another Matter controller (open commissioning window)?",
|
|
214
|
+
confirmText: "Share",
|
|
215
|
+
}))
|
|
216
|
+
) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const shareCode = await this.client.openCommissioningWindow(this.node!.node_id);
|
|
221
|
+
showAlertDialog({
|
|
222
|
+
title: "Share device",
|
|
223
|
+
text: `Setup code: ${shareCode.setup_manual_code}`,
|
|
224
|
+
});
|
|
225
|
+
} catch (err: any) {
|
|
226
|
+
showAlertDialog({
|
|
227
|
+
title: "Failed to open commissioning window on node",
|
|
228
|
+
text: err.message,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static override styles = css`
|
|
234
|
+
.btn {
|
|
235
|
+
--md-outlined-button-container-shape: 0px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.left {
|
|
239
|
+
width: 30%;
|
|
240
|
+
display: inline-table;
|
|
241
|
+
}
|
|
242
|
+
.whitespace {
|
|
243
|
+
height: 15px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.status {
|
|
247
|
+
color: var(--danger-color);
|
|
248
|
+
font-weight: bold;
|
|
249
|
+
font-size: 0.8em;
|
|
250
|
+
}
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import "@material/web/button/filled-button";
|
|
8
|
+
import "@material/web/button/outlined-button";
|
|
9
|
+
import "@material/web/button/text-button";
|
|
10
|
+
import "@material/web/divider/divider";
|
|
11
|
+
import "@material/web/iconbutton/icon-button";
|
|
12
|
+
import "@material/web/list/list";
|
|
13
|
+
import "@material/web/list/list-item";
|
|
14
|
+
import { MatterClient } from "@matter-server/ws-client";
|
|
15
|
+
import { mdiFile, mdiPlus } from "@mdi/js";
|
|
16
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
17
|
+
import { customElement } from "lit/decorators.js";
|
|
18
|
+
import { showAlertDialog, showPromptDialog } from "../../components/dialog-box/show-dialog-box.js";
|
|
19
|
+
import { showCommissionNodeDialog } from "../../components/dialogs/commission-node-dialog/show-commission-node-dialog.js";
|
|
20
|
+
import "../../components/ha-svg-icon";
|
|
21
|
+
|
|
22
|
+
@customElement("server-details")
|
|
23
|
+
export class ServerDetails extends LitElement {
|
|
24
|
+
public client?: MatterClient;
|
|
25
|
+
|
|
26
|
+
protected override render() {
|
|
27
|
+
if (!this.client) return html``;
|
|
28
|
+
|
|
29
|
+
return html`
|
|
30
|
+
<md-list>
|
|
31
|
+
<md-list-item>
|
|
32
|
+
<div slot="headline">
|
|
33
|
+
<b>Open Home Foundation Matter Server ${this.client.isProduction ? "" : `(${this.client.serverBaseAddress})`}</b>
|
|
34
|
+
${this.client.connection.connected ? nothing : html`<span class="status">OFFLINE</span>`}
|
|
35
|
+
</div>
|
|
36
|
+
</md-list-item>
|
|
37
|
+
<md-list-item>
|
|
38
|
+
<div slot="supporting-text">
|
|
39
|
+
<div class="left">FabricId: </div>${this.client.serverInfo.fabric_id}
|
|
40
|
+
</div>
|
|
41
|
+
<div slot="supporting-text">
|
|
42
|
+
<div class="left">Compressed FabricId: </div>${this.client.serverInfo.compressed_fabric_id}
|
|
43
|
+
</div>
|
|
44
|
+
<div slot="supporting-text">
|
|
45
|
+
<div class="left">SDK Wheels Version: </div>${this.client.serverInfo.sdk_version}
|
|
46
|
+
</div>
|
|
47
|
+
<div slot="supporting-text">
|
|
48
|
+
<div class="left">Schema Version: </div>${this.client.serverInfo.schema_version}
|
|
49
|
+
</div>
|
|
50
|
+
<div slot="supporting-text">
|
|
51
|
+
<div class="left">Node count: </div>${Object.keys(this.client.nodes).length}
|
|
52
|
+
</div>
|
|
53
|
+
</md-list-item>
|
|
54
|
+
<md-list-item class="btn">
|
|
55
|
+
<span>
|
|
56
|
+
<md-outlined-button @click=${this._commissionNode}>Commission node<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon></md-outlined-button>
|
|
57
|
+
<md-outlined-button @click=${this._uploadDiagnosticsDumpFile}>Import node<ha-svg-icon slot="icon" .path=${mdiFile}></ha-svg-icon></md-outlined-button>
|
|
58
|
+
</md-list-item>
|
|
59
|
+
</md-list>
|
|
60
|
+
<!-- hidden file element for the upload diagnostics -->
|
|
61
|
+
<input
|
|
62
|
+
@change=${this._onFileInput}
|
|
63
|
+
type="file"
|
|
64
|
+
id="fileElem"
|
|
65
|
+
accept=".json"
|
|
66
|
+
style="display:none" />
|
|
67
|
+
</div>
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private _commissionNode() {
|
|
72
|
+
console.log(this.client);
|
|
73
|
+
showCommissionNodeDialog(this.client!);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async _uploadDiagnosticsDumpFile() {
|
|
77
|
+
if (
|
|
78
|
+
!(await showPromptDialog({
|
|
79
|
+
title: "Add test node",
|
|
80
|
+
text: "Do you want to add a test node from a diagnostics dump ?",
|
|
81
|
+
confirmText: "Select file",
|
|
82
|
+
}))
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// @ts-expect-error why?
|
|
87
|
+
const fileElem = this.renderRoot.getElementById("fileElem") as HTMLInputElement;
|
|
88
|
+
fileElem.click();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private _onFileInput = (event: Event) => {
|
|
92
|
+
const fileElem = event.target as HTMLInputElement;
|
|
93
|
+
if (fileElem.files!.length > 0) {
|
|
94
|
+
const selectedFile = fileElem.files![0];
|
|
95
|
+
const reader = new FileReader();
|
|
96
|
+
reader.readAsText(selectedFile, "UTF-8");
|
|
97
|
+
reader.onload = async () => {
|
|
98
|
+
try {
|
|
99
|
+
await this.client!.importTestNode(reader.result?.toString() || "");
|
|
100
|
+
} catch (err: any) {
|
|
101
|
+
showAlertDialog({
|
|
102
|
+
title: "Failed to import test node",
|
|
103
|
+
text: err.message,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
static override styles = css`
|
|
112
|
+
.btn {
|
|
113
|
+
--md-outlined-button-container-shape: 0px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.left {
|
|
117
|
+
width: 30%;
|
|
118
|
+
display: inline-table;
|
|
119
|
+
}
|
|
120
|
+
.whitespace {
|
|
121
|
+
height: 15px;
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { provide } from "@lit/context";
|
|
8
|
+
import "@material/web/divider/divider";
|
|
9
|
+
import "@material/web/iconbutton/icon-button";
|
|
10
|
+
import "@material/web/list/list";
|
|
11
|
+
import "@material/web/list/list-item";
|
|
12
|
+
import { MatterClient, MatterNode, toBigIntAwareJson } from "@matter-server/ws-client";
|
|
13
|
+
import { LitElement, css, html } from "lit";
|
|
14
|
+
import { customElement, property } from "lit/decorators.js";
|
|
15
|
+
import { clusters } from "../client/models/descriptions.js";
|
|
16
|
+
import { showAlertDialog } from "../components/dialog-box/show-dialog-box.js";
|
|
17
|
+
import "../components/ha-svg-icon";
|
|
18
|
+
import "../pages/components/node-details";
|
|
19
|
+
import { bindingContext } from "./components/context.js";
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface HTMLElementTagNameMap {
|
|
23
|
+
"matter-cluster-view": MatterClusterView;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clusterAttributes(attributes: { [key: string]: any }, endpoint: number, cluster: number) {
|
|
28
|
+
// extract unique clusters from the node attributes, as (sorted) array
|
|
29
|
+
return Object.keys(attributes)
|
|
30
|
+
.filter(key => key.startsWith(`${endpoint}/${cluster}`))
|
|
31
|
+
.map(key => {
|
|
32
|
+
const attributeKey = Number(key.split("/")[2]);
|
|
33
|
+
return { key: attributeKey, value: attributes[key] };
|
|
34
|
+
}, []);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@customElement("matter-cluster-view")
|
|
38
|
+
class MatterClusterView extends LitElement {
|
|
39
|
+
public client!: MatterClient;
|
|
40
|
+
|
|
41
|
+
@property()
|
|
42
|
+
public node?: MatterNode;
|
|
43
|
+
|
|
44
|
+
@provide({ context: bindingContext })
|
|
45
|
+
@property()
|
|
46
|
+
public endpoint!: number;
|
|
47
|
+
|
|
48
|
+
@property()
|
|
49
|
+
public cluster?: number;
|
|
50
|
+
|
|
51
|
+
override render() {
|
|
52
|
+
if (!this.node || this.endpoint == undefined || this.cluster == undefined) {
|
|
53
|
+
return html`
|
|
54
|
+
<p>Node, endpoint or cluster not found!</p>
|
|
55
|
+
<button @click=${this._goBack}>Back</button>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return html`
|
|
60
|
+
<dashboard-header
|
|
61
|
+
.title=${`Node ${this.node.node_id} | Endpoint ${this.endpoint} | Cluster ${this.cluster}`}
|
|
62
|
+
.backButton=${`#node/${this.node.node_id}/${this.endpoint}`}
|
|
63
|
+
.client=${this.client}
|
|
64
|
+
></dashboard-header>
|
|
65
|
+
|
|
66
|
+
<!-- node details section -->
|
|
67
|
+
<div class="container">
|
|
68
|
+
<node-details .node=${this.node} .client=${this.client}></node-details>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Cluster attributes listing -->
|
|
72
|
+
<div class="container">
|
|
73
|
+
<md-list>
|
|
74
|
+
<md-list-item>
|
|
75
|
+
<div slot="headline">
|
|
76
|
+
<b
|
|
77
|
+
>Attributes of ${clusters[this.cluster]?.label || "Custom/Unknown Cluster"} Cluster on
|
|
78
|
+
Endpoint ${this.endpoint}</b
|
|
79
|
+
>
|
|
80
|
+
</div>
|
|
81
|
+
<div slot="supporting-text">ClusterId ${this.cluster} (0x00${this.cluster.toString(16)})</div>
|
|
82
|
+
</md-list-item>
|
|
83
|
+
${clusterAttributes(this.node.attributes, this.endpoint, this.cluster).map(attribute => {
|
|
84
|
+
return html`
|
|
85
|
+
<md-list-item>
|
|
86
|
+
<div slot="headline">
|
|
87
|
+
${clusters[this.cluster!]?.attributes[attribute.key]?.label ||
|
|
88
|
+
"Custom/Unknown Attribute"}
|
|
89
|
+
</div>
|
|
90
|
+
<div slot="supporting-text">
|
|
91
|
+
AttributeId: ${attribute.key} (0x00${attribute.key.toString(16)}) - Value type:
|
|
92
|
+
${clusters[this.cluster!]?.attributes[attribute.key]?.type || "unknown"}
|
|
93
|
+
</div>
|
|
94
|
+
<div slot="end">
|
|
95
|
+
${toBigIntAwareJson(attribute.value).length > 20
|
|
96
|
+
? html`<button
|
|
97
|
+
@click=${() => {
|
|
98
|
+
this._showAttributeValue(attribute.value);
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
Show value
|
|
102
|
+
</button>`
|
|
103
|
+
: toBigIntAwareJson(attribute.value)}
|
|
104
|
+
</div>
|
|
105
|
+
</md-list-item>
|
|
106
|
+
<md-divider />
|
|
107
|
+
`;
|
|
108
|
+
})}
|
|
109
|
+
</md-list>
|
|
110
|
+
</div>
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async _showAttributeValue(value: any) {
|
|
115
|
+
showAlertDialog({
|
|
116
|
+
title: "Attribute value",
|
|
117
|
+
text: toBigIntAwareJson(value),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private _goBack() {
|
|
122
|
+
history.back();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static override styles = css`
|
|
126
|
+
:host {
|
|
127
|
+
display: block;
|
|
128
|
+
background-color: var(--md-sys-color-background);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.header {
|
|
132
|
+
background-color: var(--md-sys-color-primary);
|
|
133
|
+
color: var(--md-sys-color-on-primary);
|
|
134
|
+
--icon-primary-color: var(--md-sys-color-on-primary);
|
|
135
|
+
font-weight: 400;
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
padding-right: 8px;
|
|
139
|
+
height: 48px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
md-icon-button {
|
|
143
|
+
margin-right: 8px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.flex {
|
|
147
|
+
flex: 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.container {
|
|
151
|
+
padding: 16px;
|
|
152
|
+
max-width: 95%;
|
|
153
|
+
margin: 0 auto;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.status {
|
|
157
|
+
color: var(--danger-color);
|
|
158
|
+
font-weight: bold;
|
|
159
|
+
font-size: 0.8em;
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ContextProvider } from "@lit/context";
|
|
8
|
+
import { MatterClient, MatterError } from "@matter-server/ws-client";
|
|
9
|
+
import { LitElement, PropertyValueMap, html } from "lit";
|
|
10
|
+
import { customElement, state } from "lit/decorators.js";
|
|
11
|
+
import { clientContext } from "../client/client-context.js";
|
|
12
|
+
import { clone } from "../util/clone_class.js";
|
|
13
|
+
import type { Route } from "../util/routing.js";
|
|
14
|
+
import "./matter-cluster-view";
|
|
15
|
+
import "./matter-endpoint-view";
|
|
16
|
+
import "./matter-node-view";
|
|
17
|
+
import "./matter-server-view";
|
|
18
|
+
|
|
19
|
+
declare global {
|
|
20
|
+
interface HTMLElementTagNameMap {
|
|
21
|
+
"matter-dashboard-app": MatterDashboardApp;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@customElement("matter-dashboard-app")
|
|
26
|
+
class MatterDashboardApp extends LitElement {
|
|
27
|
+
@state() private _route: Route = {
|
|
28
|
+
prefix: "",
|
|
29
|
+
path: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
public client!: MatterClient;
|
|
33
|
+
|
|
34
|
+
@state()
|
|
35
|
+
private _state: "connecting" | "connected" | "error" | "disconnected" = "connecting";
|
|
36
|
+
|
|
37
|
+
private _error: string | undefined;
|
|
38
|
+
|
|
39
|
+
private provider = new ContextProvider(this, { context: clientContext, initialValue: this.client });
|
|
40
|
+
|
|
41
|
+
protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
|
42
|
+
super.firstUpdated(_changedProperties);
|
|
43
|
+
this.client.startListening().then(
|
|
44
|
+
() => {
|
|
45
|
+
this._state = "connected";
|
|
46
|
+
this.client.addEventListener("nodes_changed", () => {
|
|
47
|
+
this.requestUpdate();
|
|
48
|
+
this.provider.setValue(clone(this.client));
|
|
49
|
+
});
|
|
50
|
+
this.client.addEventListener("server_info_updated", () => {
|
|
51
|
+
this.provider.setValue(clone(this.client));
|
|
52
|
+
});
|
|
53
|
+
this.client.addEventListener("connection_lost", () => {
|
|
54
|
+
this._state = "disconnected";
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
(err: MatterError) => {
|
|
58
|
+
this._state = "error";
|
|
59
|
+
this._error = err.message;
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Handle history changes
|
|
64
|
+
const updateRoute = () => {
|
|
65
|
+
const pathParts = location.hash.substring(1).split("/");
|
|
66
|
+
this._route = {
|
|
67
|
+
prefix: pathParts.length == 1 ? "" : pathParts[0],
|
|
68
|
+
path: pathParts.length == 1 ? pathParts : pathParts.slice(1),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener("hashchange", updateRoute);
|
|
72
|
+
updateRoute();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override render() {
|
|
76
|
+
if (this._state === "connecting") {
|
|
77
|
+
return html`<p>Connecting...</p>`;
|
|
78
|
+
}
|
|
79
|
+
if (this._state === "disconnected") {
|
|
80
|
+
return html`<p>Connection lost</p>`;
|
|
81
|
+
}
|
|
82
|
+
if (this._state === "error") {
|
|
83
|
+
return html`
|
|
84
|
+
<p>Error: ${this._error}</p>
|
|
85
|
+
<button @click=${this.client.disconnect}>Clear stored URL</button>
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
if (this._route.prefix === "node" && this._route.path.length == 3) {
|
|
89
|
+
// cluster level
|
|
90
|
+
return html`
|
|
91
|
+
<matter-cluster-view
|
|
92
|
+
.client=${this.client}
|
|
93
|
+
.node=${this.client.nodes[this._route.path[0]]}
|
|
94
|
+
.endpoint=${parseInt(this._route.path[1], 10)}
|
|
95
|
+
.cluster=${parseInt(this._route.path[2], 10)}
|
|
96
|
+
></matter-cluster-view>
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
if (this._route.prefix === "node" && this._route.path.length == 2) {
|
|
100
|
+
// endpoint level
|
|
101
|
+
return html`
|
|
102
|
+
<matter-endpoint-view
|
|
103
|
+
.client=${this.client}
|
|
104
|
+
.node=${this.client.nodes[this._route.path[0]]}
|
|
105
|
+
.endpoint=${parseInt(this._route.path[1], 10)}
|
|
106
|
+
></matter-endpoint-view>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
if (this._route.prefix === "node") {
|
|
110
|
+
// node level
|
|
111
|
+
return html`
|
|
112
|
+
<matter-node-view
|
|
113
|
+
.client=${this.client}
|
|
114
|
+
.node=${this.client.nodes[this._route.path[0]]}
|
|
115
|
+
></matter-node-view>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
// root level: server overview
|
|
119
|
+
return html`<matter-server-view
|
|
120
|
+
.client=${this.client}
|
|
121
|
+
.nodes=${this.client.nodes}
|
|
122
|
+
.route=${this._route}
|
|
123
|
+
></matter-server-view>`;
|
|
124
|
+
}
|
|
125
|
+
}
|