@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.
Files changed (172) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/dist/esm/client/client-context.d.ts +10 -0
  4. package/dist/esm/client/client-context.d.ts.map +1 -0
  5. package/dist/esm/client/client-context.js +11 -0
  6. package/dist/esm/client/client-context.js.map +6 -0
  7. package/dist/esm/client/models/descriptions.d.ts +20 -0
  8. package/dist/esm/client/models/descriptions.d.ts.map +1 -0
  9. package/dist/esm/client/models/descriptions.js +10929 -0
  10. package/dist/esm/client/models/descriptions.js.map +6 -0
  11. package/dist/esm/components/dialog-box/dialog-box.d.ts +25 -0
  12. package/dist/esm/components/dialog-box/dialog-box.d.ts.map +1 -0
  13. package/dist/esm/components/dialog-box/dialog-box.js +66 -0
  14. package/dist/esm/components/dialog-box/dialog-box.js.map +6 -0
  15. package/dist/esm/components/dialog-box/show-dialog-box.d.ts +18 -0
  16. package/dist/esm/components/dialog-box/show-dialog-box.d.ts.map +1 -0
  17. package/dist/esm/components/dialog-box/show-dialog-box.js +22 -0
  18. package/dist/esm/components/dialog-box/show-dialog-box.js.map +6 -0
  19. package/dist/esm/components/dialogs/acl/model.d.ts +33 -0
  20. package/dist/esm/components/dialogs/acl/model.d.ts.map +1 -0
  21. package/dist/esm/components/dialogs/acl/model.js +79 -0
  22. package/dist/esm/components/dialogs/acl/model.js.map +6 -0
  23. package/dist/esm/components/dialogs/binding/model.d.ts +20 -0
  24. package/dist/esm/components/dialogs/binding/model.d.ts.map +1 -0
  25. package/dist/esm/components/dialogs/binding/model.js +45 -0
  26. package/dist/esm/components/dialogs/binding/model.js.map +6 -0
  27. package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts +49 -0
  28. package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -0
  29. package/dist/esm/components/dialogs/binding/node-binding-dialog.js +357 -0
  30. package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +6 -0
  31. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts +8 -0
  32. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.d.ts.map +1 -0
  33. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js +17 -0
  34. package/dist/esm/components/dialogs/binding/show-node-binding-dialog.js.map +6 -0
  35. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts +31 -0
  36. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.d.ts.map +1 -0
  37. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js +94 -0
  38. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-dialog.js.map +6 -0
  39. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.d.ts +17 -0
  40. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.d.ts.map +1 -0
  41. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.js +64 -0
  42. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.js.map +6 -0
  43. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.d.ts +19 -0
  44. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.d.ts.map +1 -0
  45. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js +91 -0
  46. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js.map +6 -0
  47. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.d.ts +20 -0
  48. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.d.ts.map +1 -0
  49. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js +106 -0
  50. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js.map +6 -0
  51. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts +8 -0
  52. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.d.ts.map +1 -0
  53. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js +15 -0
  54. package/dist/esm/components/dialogs/commission-node-dialog/show-commission-node-dialog.js.map +6 -0
  55. package/dist/esm/components/ha-svg-icon.d.ts +19 -0
  56. package/dist/esm/components/ha-svg-icon.d.ts.map +1 -0
  57. package/dist/esm/components/ha-svg-icon.js +77 -0
  58. package/dist/esm/components/ha-svg-icon.js.map +6 -0
  59. package/dist/esm/entrypoint/main.d.ts +7 -0
  60. package/dist/esm/entrypoint/main.d.ts.map +1 -0
  61. package/dist/esm/entrypoint/main.js +45 -0
  62. package/dist/esm/entrypoint/main.js.map +6 -0
  63. package/dist/esm/package.json +3 -0
  64. package/dist/esm/pages/components/context.d.ts +9 -0
  65. package/dist/esm/pages/components/context.d.ts.map +1 -0
  66. package/dist/esm/pages/components/context.js +11 -0
  67. package/dist/esm/pages/components/context.js.map +6 -0
  68. package/dist/esm/pages/components/footer.d.ts +11 -0
  69. package/dist/esm/pages/components/footer.d.ts.map +1 -0
  70. package/dist/esm/pages/components/footer.js +52 -0
  71. package/dist/esm/pages/components/footer.js.map +6 -0
  72. package/dist/esm/pages/components/header.d.ts +27 -0
  73. package/dist/esm/pages/components/header.d.ts.map +1 -0
  74. package/dist/esm/pages/components/header.js +90 -0
  75. package/dist/esm/pages/components/header.js.map +6 -0
  76. package/dist/esm/pages/components/node-details.d.ts +29 -0
  77. package/dist/esm/pages/components/node-details.d.ts.map +1 -0
  78. package/dist/esm/pages/components/node-details.js +241 -0
  79. package/dist/esm/pages/components/node-details.js.map +6 -0
  80. package/dist/esm/pages/components/server-details.d.ts +24 -0
  81. package/dist/esm/pages/components/server-details.d.ts.map +1 -0
  82. package/dist/esm/pages/components/server-details.js +130 -0
  83. package/dist/esm/pages/components/server-details.js.map +6 -0
  84. package/dist/esm/pages/matter-cluster-view.d.ts +30 -0
  85. package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -0
  86. package/dist/esm/pages/matter-cluster-view.js +154 -0
  87. package/dist/esm/pages/matter-cluster-view.js.map +6 -0
  88. package/dist/esm/pages/matter-dashboard-app.d.ts +27 -0
  89. package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -0
  90. package/dist/esm/pages/matter-dashboard-app.js +122 -0
  91. package/dist/esm/pages/matter-dashboard-app.js.map +6 -0
  92. package/dist/esm/pages/matter-endpoint-view.d.ts +29 -0
  93. package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -0
  94. package/dist/esm/pages/matter-endpoint-view.js +149 -0
  95. package/dist/esm/pages/matter-endpoint-view.js.map +6 -0
  96. package/dist/esm/pages/matter-node-view.d.ts +28 -0
  97. package/dist/esm/pages/matter-node-view.d.ts.map +1 -0
  98. package/dist/esm/pages/matter-node-view.js +122 -0
  99. package/dist/esm/pages/matter-node-view.js.map +6 -0
  100. package/dist/esm/pages/matter-server-view.d.ts +31 -0
  101. package/dist/esm/pages/matter-server-view.d.ts.map +1 -0
  102. package/dist/esm/pages/matter-server-view.js +113 -0
  103. package/dist/esm/pages/matter-server-view.js.map +6 -0
  104. package/dist/esm/util/clone_class.d.ts +7 -0
  105. package/dist/esm/util/clone_class.d.ts.map +1 -0
  106. package/dist/esm/util/clone_class.js +10 -0
  107. package/dist/esm/util/clone_class.js.map +6 -0
  108. package/dist/esm/util/fire_event.d.ts +34 -0
  109. package/dist/esm/util/fire_event.d.ts.map +1 -0
  110. package/dist/esm/util/fire_event.js +21 -0
  111. package/dist/esm/util/fire_event.js.map +6 -0
  112. package/dist/esm/util/prevent_default.d.ts +7 -0
  113. package/dist/esm/util/prevent_default.d.ts.map +1 -0
  114. package/dist/esm/util/prevent_default.js +10 -0
  115. package/dist/esm/util/prevent_default.js.map +6 -0
  116. package/dist/esm/util/routing.d.ts +10 -0
  117. package/dist/esm/util/routing.d.ts.map +1 -0
  118. package/dist/esm/util/routing.js +6 -0
  119. package/dist/esm/util/routing.js.map +6 -0
  120. package/dist/web/index.html +40 -0
  121. package/dist/web/js/commission-node-dialog-BJsfA4IV.js +78 -0
  122. package/dist/web/js/commission-node-dialog-DEZ3EqYO.js +78 -0
  123. package/dist/web/js/commission-node-existing-CzRtUgBm.js +50 -0
  124. package/dist/web/js/commission-node-existing-OK1ybPFI.js +50 -0
  125. package/dist/web/js/commission-node-thread-DLmclivF.js +75 -0
  126. package/dist/web/js/commission-node-thread-FcLFz84I.js +75 -0
  127. package/dist/web/js/commission-node-wifi-C8ho-UYb.js +88 -0
  128. package/dist/web/js/commission-node-wifi-C8iGfy7c.js +88 -0
  129. package/dist/web/js/dialog-box-BPz-oO3d.js +52 -0
  130. package/dist/web/js/dialog-box-DN32sjfR.js +52 -0
  131. package/dist/web/js/fire_event-BERTqZpV.js +169 -0
  132. package/dist/web/js/fire_event-BlsbXpOL.js +169 -0
  133. package/dist/web/js/main.js +547 -0
  134. package/dist/web/js/matter-dashboard-app-5UjO1Ik8.js +16068 -0
  135. package/dist/web/js/matter-dashboard-app-BazvuIIi.js +16068 -0
  136. package/dist/web/js/node-binding-dialog-2yitVn0R.js +443 -0
  137. package/dist/web/js/node-binding-dialog-Cw6QEmL3.js +443 -0
  138. package/dist/web/js/outlined-text-field-BMLYwwlc.js +2086 -0
  139. package/dist/web/js/outlined-text-field-Sqd4JHxo.js +2086 -0
  140. package/dist/web/js/prevent_default-BsT53c0u.js +814 -0
  141. package/dist/web/js/prevent_default-D4GG_QeD.js +814 -0
  142. package/package.json +54 -0
  143. package/src/client/client-context.ts +10 -0
  144. package/src/client/models/descriptions.ts +10948 -0
  145. package/src/components/dialog-box/dialog-box.ts +62 -0
  146. package/src/components/dialog-box/show-dialog-box.ts +32 -0
  147. package/src/components/dialogs/acl/model.ts +105 -0
  148. package/src/components/dialogs/binding/model.ts +58 -0
  149. package/src/components/dialogs/binding/node-binding-dialog.ts +419 -0
  150. package/src/components/dialogs/binding/show-node-binding-dialog.ts +16 -0
  151. package/src/components/dialogs/commission-node-dialog/commission-node-dialog.ts +102 -0
  152. package/src/components/dialogs/commission-node-dialog/commission-node-existing.ts +49 -0
  153. package/src/components/dialogs/commission-node-dialog/commission-node-thread.ts +76 -0
  154. package/src/components/dialogs/commission-node-dialog/commission-node-wifi.ts +90 -0
  155. package/src/components/dialogs/commission-node-dialog/show-commission-node-dialog.ts +14 -0
  156. package/src/components/ha-svg-icon.ts +66 -0
  157. package/src/entrypoint/main.ts +60 -0
  158. package/src/pages/components/context.ts +10 -0
  159. package/src/pages/components/footer.ts +39 -0
  160. package/src/pages/components/header.ts +87 -0
  161. package/src/pages/components/node-details.ts +252 -0
  162. package/src/pages/components/server-details.ts +124 -0
  163. package/src/pages/matter-cluster-view.ts +162 -0
  164. package/src/pages/matter-dashboard-app.ts +125 -0
  165. package/src/pages/matter-endpoint-view.ts +152 -0
  166. package/src/pages/matter-node-view.ts +126 -0
  167. package/src/pages/matter-server-view.ts +117 -0
  168. package/src/tsconfig.json +16 -0
  169. package/src/util/clone_class.ts +7 -0
  170. package/src/util/fire_event.ts +83 -0
  171. package/src/util/prevent_default.ts +7 -0
  172. 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
+ }