@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,62 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-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 type { MdDialog } from "@material/web/dialog/dialog.js";
10
+ import { html, LitElement } from "lit";
11
+ import { customElement, property } from "lit/decorators.js";
12
+ import { preventDefault } from "../../util/prevent_default.js";
13
+ import type { PromptDialogBoxParams } from "./show-dialog-box.js";
14
+ @customElement("dialox-box")
15
+ export class DialogBox extends LitElement {
16
+ @property({ attribute: false }) public params!: PromptDialogBoxParams;
17
+
18
+ @property({ attribute: false }) public dialogResult!: (result: boolean) => void;
19
+
20
+ @property() public type!: "alert" | "prompt";
21
+
22
+ protected override render() {
23
+ const params = this.params;
24
+ return html`
25
+ <md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
26
+ ${params.title ? html`<div slot="headline">${params.title}</div>` : ""}
27
+ ${params.text ? html`<div slot="content">${params.text}</div>` : ""}
28
+ <div slot="actions">
29
+ ${this.type === "prompt"
30
+ ? html`
31
+ <md-text-button @click=${this._cancel}> ${params.cancelText || "Cancel"} </md-text-button>
32
+ `
33
+ : ""}
34
+ <md-text-button @click=${this._confirm}> ${params.confirmText || "OK"} </md-text-button>
35
+ </div>
36
+ </md-dialog>
37
+ `;
38
+ }
39
+
40
+ private _cancel() {
41
+ this._setResult(false);
42
+ }
43
+
44
+ private _confirm() {
45
+ this._setResult(true);
46
+ }
47
+
48
+ _setResult(result: boolean) {
49
+ this.dialogResult(result);
50
+ this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close();
51
+ }
52
+
53
+ private _handleClosed() {
54
+ this.parentElement!.removeChild(this);
55
+ }
56
+ }
57
+
58
+ declare global {
59
+ interface HTMLElementTagNameMap {
60
+ "dialox-box": DialogBox;
61
+ }
62
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type { TemplateResult } from "lit";
8
+
9
+ interface BaseDialogBoxParams {
10
+ confirmText?: string;
11
+ text: string | TemplateResult;
12
+ title: string;
13
+ }
14
+
15
+ export interface PromptDialogBoxParams extends BaseDialogBoxParams {
16
+ cancelText?: string;
17
+ }
18
+
19
+ const showDialogBox = async (type: "alert" | "prompt", dialogParams: PromptDialogBoxParams) => {
20
+ await import("./dialog-box.js");
21
+ return new Promise<boolean>(resolve => {
22
+ const dialog = document.createElement("dialox-box");
23
+ dialog.params = dialogParams;
24
+ dialog.dialogResult = resolve;
25
+ dialog.type = type;
26
+ document.body.appendChild(dialog);
27
+ });
28
+ };
29
+
30
+ export const showAlertDialog = (dialogParams: BaseDialogBoxParams) => showDialogBox("alert", dialogParams);
31
+
32
+ export const showPromptDialog = (dialogParams: BaseDialogBoxParams) => showDialogBox("prompt", dialogParams);
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export type AccessControlTargetStruct = {
8
+ cluster: number | undefined;
9
+ endpoint: number | undefined;
10
+ deviceType: number | undefined;
11
+ };
12
+
13
+ export type AccessControlEntryRawInput = {
14
+ "1": number;
15
+ "2": number;
16
+ "3": number[];
17
+ "4": AccessControlTargetStruct[] | undefined;
18
+ "254": number;
19
+ };
20
+
21
+ export type AccessControlEntryStruct = {
22
+ privilege: number;
23
+ authMode: number;
24
+ subjects: (number | bigint)[];
25
+ targets: AccessControlTargetStruct[] | undefined;
26
+ fabricIndex: number;
27
+ };
28
+
29
+ export class AccessControlTargetTransformer {
30
+ private static readonly KEY_MAPPING: {
31
+ [inputKey: string]: keyof AccessControlTargetStruct;
32
+ } = {
33
+ "0": "cluster",
34
+ "1": "endpoint",
35
+ "2": "deviceType",
36
+ };
37
+
38
+ public static transform(input: any): AccessControlTargetStruct {
39
+ if (!input || typeof input !== "object") {
40
+ throw new Error("Invalid input: expected an object");
41
+ }
42
+
43
+ const result: Partial<AccessControlTargetStruct> = {};
44
+ const keyMapping = AccessControlTargetTransformer.KEY_MAPPING;
45
+
46
+ for (const key in input) {
47
+ if (key in keyMapping) {
48
+ const mappedKey = keyMapping[key];
49
+ if (mappedKey) {
50
+ const value = input[key];
51
+ if (value === undefined) continue;
52
+ result[mappedKey] = value;
53
+ }
54
+ }
55
+ }
56
+ return result as AccessControlTargetStruct;
57
+ }
58
+ }
59
+
60
+ export class AccessControlEntryDataTransformer {
61
+ private static readonly KEY_MAPPING: {
62
+ [inputKey: string]: keyof AccessControlEntryStruct;
63
+ } = {
64
+ "1": "privilege",
65
+ "2": "authMode",
66
+ "3": "subjects",
67
+ "4": "targets",
68
+ "254": "fabricIndex",
69
+ };
70
+
71
+ public static transform(input: any): AccessControlEntryStruct {
72
+ if (!input || typeof input !== "object") {
73
+ throw new Error("Invalid input: expected an object");
74
+ }
75
+
76
+ const result: Partial<AccessControlEntryStruct> = {};
77
+ const keyMapping = AccessControlEntryDataTransformer.KEY_MAPPING;
78
+
79
+ for (const key in input) {
80
+ if (key in keyMapping) {
81
+ const mappedKey = keyMapping[key];
82
+ if (mappedKey) {
83
+ const value = input[key];
84
+ if (value === undefined) continue;
85
+ if (mappedKey === "subjects") {
86
+ result[mappedKey] = Array.isArray(value) ? value : undefined;
87
+ } else if (mappedKey === "targets") {
88
+ if (Array.isArray(value)) {
89
+ const _targets = Object.values(value).map(val =>
90
+ AccessControlTargetTransformer.transform(val),
91
+ );
92
+ result[mappedKey] = _targets;
93
+ } else {
94
+ result[mappedKey] = undefined;
95
+ }
96
+ } else {
97
+ result[mappedKey] = value;
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return result as AccessControlEntryStruct;
104
+ }
105
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export type InputType = {
8
+ [key: string]: number | number[] | undefined;
9
+ };
10
+
11
+ export interface BindingEntryStruct {
12
+ node: number | undefined;
13
+ group: number | undefined;
14
+ endpoint: number | undefined;
15
+ cluster: number | undefined;
16
+ fabricIndex: number | undefined;
17
+ }
18
+
19
+ export class BindingEntryDataTransformer {
20
+ private static readonly KEY_MAPPING: {
21
+ [inputKey: string]: keyof BindingEntryStruct;
22
+ } = {
23
+ "1": "node",
24
+ "3": "endpoint",
25
+ "4": "cluster",
26
+ "254": "fabricIndex",
27
+ };
28
+
29
+ public static transform(input: any): BindingEntryStruct {
30
+ if (!input || typeof input !== "object") {
31
+ throw new Error("Invalid input: expected an object");
32
+ }
33
+
34
+ const result: Partial<BindingEntryStruct> = {};
35
+ const keyMapping = BindingEntryDataTransformer.KEY_MAPPING;
36
+
37
+ for (const key in input) {
38
+ if (key in keyMapping) {
39
+ const mappedKey = keyMapping[key];
40
+ if (mappedKey) {
41
+ const value = input[key];
42
+ if (value === undefined) {
43
+ continue;
44
+ }
45
+ if (mappedKey === "fabricIndex") {
46
+ result[mappedKey] = value === undefined ? undefined : Number(value);
47
+ } else if (mappedKey === "node" || mappedKey === "endpoint") {
48
+ result[mappedKey] = Number(value);
49
+ } else {
50
+ result[mappedKey] = value as BindingEntryStruct[typeof mappedKey];
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ return result as BindingEntryStruct;
57
+ }
58
+ }
@@ -0,0 +1,419 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-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 type { MdDialog } from "@material/web/dialog/dialog.js";
10
+ import "@material/web/list/list";
11
+ import "@material/web/list/list-item";
12
+ import "@material/web/textfield/outlined-text-field";
13
+ import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field.js";
14
+ import "../../../components/ha-svg-icon";
15
+
16
+ import { AccessControlEntry, BindingTarget, MatterClient, MatterNode } from "@matter-server/ws-client";
17
+ import { css, html, LitElement, nothing } from "lit";
18
+ import { customElement, property, query } from "lit/decorators.js";
19
+ import { preventDefault } from "../../../util/prevent_default.js";
20
+ import { BindingEntryDataTransformer, BindingEntryStruct, InputType } from "./model.js";
21
+
22
+ import {
23
+ AccessControlEntryDataTransformer,
24
+ AccessControlEntryStruct,
25
+ AccessControlTargetStruct,
26
+ } from "../acl/model.js";
27
+
28
+ import { consume } from "@lit/context";
29
+ import { clientContext } from "../../../client/client-context.js";
30
+
31
+ @customElement("node-binding-dialog")
32
+ export class NodeBindingDialog extends LitElement {
33
+ @consume({ context: clientContext, subscribe: true })
34
+ @property({ attribute: false })
35
+ public client!: MatterClient;
36
+
37
+ @property()
38
+ public node?: MatterNode;
39
+
40
+ @property({ attribute: false })
41
+ endpoint!: number;
42
+
43
+ @query("md-outlined-text-field[name='NodeId']")
44
+ private _targetNodeId!: MdOutlinedTextField;
45
+
46
+ @query("md-outlined-text-field[name='Endpoint']")
47
+ private _targetEndpoint!: MdOutlinedTextField;
48
+
49
+ @query("md-outlined-text-field[name='Cluster']")
50
+ private _targetCluster!: MdOutlinedTextField;
51
+
52
+ private fetchBindingEntry(): BindingEntryStruct[] {
53
+ const bindings_raw = this.node!.attributes[this.endpoint + "/30/0"] as InputType[] | undefined;
54
+ if (!bindings_raw) return [];
55
+ return Object.values(bindings_raw).map(value => BindingEntryDataTransformer.transform(value));
56
+ }
57
+
58
+ private fetchACLEntry(targetNodeId: number): AccessControlEntryStruct[] {
59
+ const acl_cluster_raw = this.client.nodes[targetNodeId]?.attributes["0/31/0"] as InputType[] | undefined;
60
+ if (!acl_cluster_raw) return [];
61
+ return Object.values(acl_cluster_raw).map((value: InputType) =>
62
+ AccessControlEntryDataTransformer.transform(value),
63
+ );
64
+ }
65
+
66
+ private async deleteBindingHandler(index: number): Promise<void> {
67
+ const rawBindings = this.fetchBindingEntry();
68
+ try {
69
+ const targetNodeId = rawBindings[index].node;
70
+ const endpoint = rawBindings[index].endpoint;
71
+ if (targetNodeId === undefined || endpoint === undefined) return;
72
+ await this.removeNodeAtACLEntry(this.getNodeIdAsNumber(), endpoint, targetNodeId);
73
+ const updatedBindings = this.removeBindingAtIndex(rawBindings, index);
74
+ await this.syncBindingUpdates(updatedBindings, index);
75
+ } catch (error) {
76
+ this.handleBindingDeletionError(error);
77
+ }
78
+ }
79
+
80
+ /** Helper to convert node_id (number | bigint) to number for API calls */
81
+ private getNodeIdAsNumber(): number {
82
+ const nodeId = this.node!.node_id;
83
+ return typeof nodeId === "bigint" ? Number(nodeId) : nodeId;
84
+ }
85
+
86
+ private async removeNodeAtACLEntry(
87
+ sourceNodeId: number,
88
+ sourceEndpoint: number,
89
+ targetNodeId: number,
90
+ ): Promise<void> {
91
+ const aclEntries = this.fetchACLEntry(targetNodeId);
92
+
93
+ const updatedACLEntries = aclEntries
94
+ .map(entry => this.removeEntryAtACL(sourceNodeId, sourceEndpoint, entry))
95
+ .filter((entry): entry is AccessControlEntryStruct => entry !== undefined);
96
+
97
+ // Convert to API format (without fabricIndex - server handles it)
98
+ const apiEntries = updatedACLEntries.map(e => this.toAccessControlEntry(e));
99
+ await this.client.setACLEntry(targetNodeId, apiEntries);
100
+ }
101
+
102
+ private removeEntryAtACL(
103
+ nodeId: number,
104
+ sourceEndpoint: number,
105
+ entry: AccessControlEntryStruct,
106
+ ): AccessControlEntryStruct | undefined {
107
+ const hasSubject = entry.subjects.includes(nodeId);
108
+ if (!hasSubject) return entry;
109
+
110
+ const hasTarget = entry.targets!.filter(item => item.endpoint === sourceEndpoint);
111
+ return hasTarget.length > 0 ? undefined : entry;
112
+ }
113
+
114
+ private removeBindingAtIndex(bindings: BindingEntryStruct[], index: number): BindingEntryStruct[] {
115
+ return [...bindings.slice(0, index), ...bindings.slice(index + 1)];
116
+ }
117
+
118
+ private async syncBindingUpdates(updatedBindings: BindingEntryStruct[], index: number): Promise<void> {
119
+ // Convert to API format (without fabricIndex - server handles it)
120
+ const apiBindings = updatedBindings.map(b => this.toBindingTarget(b));
121
+ await this.client.setNodeBinding(this.getNodeIdAsNumber(), this.endpoint, apiBindings);
122
+
123
+ const attributePath = `${this.endpoint}/30/0`;
124
+ const currentBindings = this.node!.attributes[attributePath] as BindingEntryStruct[] | undefined;
125
+ const updatedAttributes = {
126
+ ...this.node!.attributes,
127
+ [attributePath]: currentBindings ? this.removeBindingAtIndex(currentBindings, index) : [],
128
+ };
129
+
130
+ this.node!.attributes = updatedAttributes;
131
+ this.requestUpdate();
132
+ }
133
+
134
+ private handleBindingDeletionError(error: unknown): void {
135
+ const errorMessage = error instanceof Error ? error.message : String(error);
136
+ console.error(`Binding deletion failed: ${errorMessage}`);
137
+ }
138
+
139
+ private async add_target_acl(targetNodeId: number, entry: AccessControlEntryStruct) {
140
+ try {
141
+ // Fetch existing ACL entries and transform to local struct format
142
+ const rawEntries = this.client.nodes[targetNodeId]?.attributes["0/31/0"] as InputType[] | undefined;
143
+ const entries = rawEntries
144
+ ? Object.values(rawEntries).map(v => AccessControlEntryDataTransformer.transform(v))
145
+ : [];
146
+ entries.push(entry);
147
+
148
+ // Convert to API format (without fabricIndex - server handles it)
149
+ const apiEntries = entries.map(e => this.toAccessControlEntry(e));
150
+ const result = await this.client.setACLEntry(targetNodeId, apiEntries);
151
+ // Check first result status if available
152
+ if (result && result.length > 0) {
153
+ return result[0].status === 0;
154
+ }
155
+ return true; // Assume success if no error thrown
156
+ } catch (err) {
157
+ console.error("add acl error:", err);
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /** Convert local BindingEntryStruct to API BindingTarget (without fabricIndex) */
163
+ private toBindingTarget(entry: BindingEntryStruct): BindingTarget {
164
+ return {
165
+ node: entry.node ?? null,
166
+ group: entry.group ?? null,
167
+ endpoint: entry.endpoint ?? null,
168
+ cluster: entry.cluster ?? null,
169
+ };
170
+ }
171
+
172
+ /** Convert local AccessControlEntryStruct to API AccessControlEntry (without fabricIndex) */
173
+ private toAccessControlEntry(entry: AccessControlEntryStruct): AccessControlEntry {
174
+ return {
175
+ privilege: entry.privilege,
176
+ auth_mode: entry.authMode,
177
+ subjects: entry.subjects ?? null,
178
+ targets:
179
+ entry.targets?.map(t => ({
180
+ cluster: t.cluster ?? null,
181
+ endpoint: t.endpoint ?? null,
182
+ device_type: t.deviceType ?? null,
183
+ })) ?? null,
184
+ };
185
+ }
186
+
187
+ private async add_bindings(endpoint: number, bindingEntry: BindingEntryStruct) {
188
+ const bindings = this.fetchBindingEntry();
189
+ bindings.push(bindingEntry);
190
+ try {
191
+ // Convert to API format (without fabricIndex - server handles it)
192
+ const apiBindings = bindings.map(b => this.toBindingTarget(b));
193
+ const result = await this.client.setNodeBinding(this.getNodeIdAsNumber(), endpoint, apiBindings);
194
+ // Check first result status if available
195
+ if (result && result.length > 0) {
196
+ return result[0].status === 0;
197
+ }
198
+ return true; // Assume success if no error thrown
199
+ } catch (err) {
200
+ console.log("add bindings error:", err);
201
+ return false;
202
+ }
203
+ }
204
+
205
+ async addBindingHandler() {
206
+ const targetNodeId = this._targetNodeId.value ? parseInt(this._targetNodeId.value, 10) : undefined;
207
+ const targetEndpoint = this._targetEndpoint.value ? parseInt(this._targetEndpoint.value, 10) : undefined;
208
+ const targetCluster = this._targetCluster.value ? parseInt(this._targetCluster.value, 10) : undefined;
209
+
210
+ // Matter Server does not use random NodeIds, so this is ok for now, but needs to be adjusted later
211
+ if (targetNodeId === undefined || targetNodeId <= 0 || targetNodeId > 65535) {
212
+ alert("Please enter a valid target node ID");
213
+ return;
214
+ }
215
+
216
+ if (targetEndpoint === undefined || targetEndpoint <= 0 || targetEndpoint > 0xfffe) {
217
+ alert("Please enter a valid target endpoint");
218
+ return;
219
+ }
220
+
221
+ // cluster optional
222
+ if (targetCluster !== undefined) {
223
+ // We ignore vendor specific clusters for now
224
+ if (targetCluster < 0 || targetCluster > 0x7fff) {
225
+ alert("Please enter a valid target cluster");
226
+ return;
227
+ }
228
+ }
229
+
230
+ const targets: AccessControlTargetStruct = {
231
+ endpoint: targetEndpoint,
232
+ cluster: targetCluster,
233
+ deviceType: undefined,
234
+ };
235
+
236
+ // Note: fabricIndex is assigned by the server based on the device's fabric table
237
+ const acl_entry: AccessControlEntryStruct = {
238
+ privilege: 5,
239
+ authMode: 2,
240
+ subjects: [this.getNodeIdAsNumber()],
241
+ targets: [targets],
242
+ fabricIndex: 0, // Placeholder - server will use correct fabric index
243
+ };
244
+
245
+ const result_acl = await this.add_target_acl(targetNodeId, acl_entry);
246
+ if (!result_acl) {
247
+ alert("add target acl error!");
248
+ return;
249
+ }
250
+
251
+ const endpoint = this.endpoint;
252
+ // Note: fabricIndex is assigned by the server based on the device's fabric table
253
+ const bindingEntry: BindingEntryStruct = {
254
+ node: targetNodeId,
255
+ endpoint: targetEndpoint,
256
+ group: undefined,
257
+ cluster: targetCluster,
258
+ fabricIndex: undefined, // Server will use correct fabric index
259
+ };
260
+
261
+ const result_binding = await this.add_bindings(endpoint, bindingEntry);
262
+
263
+ if (result_binding) {
264
+ this._targetNodeId.value = "";
265
+ this._targetEndpoint.value = "";
266
+ this._targetCluster.value = "";
267
+ this.requestUpdate();
268
+ }
269
+ }
270
+
271
+ private _close() {
272
+ this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close();
273
+ }
274
+
275
+ private _handleClosed() {
276
+ this.parentNode!.removeChild(this);
277
+ }
278
+
279
+ private onChange(e: Event) {
280
+ const textfield = e.target as MdOutlinedTextField;
281
+ const value = parseInt(textfield.value, 10);
282
+
283
+ if (parseInt(textfield.max, 10) < value || value < parseInt(textfield.min, 10)) {
284
+ textfield.error = true;
285
+ textfield.errorText = "value error";
286
+ } else {
287
+ textfield.error = false;
288
+ }
289
+
290
+ // console.log(`value: ${value} error: ${textfield.error}`);
291
+ }
292
+
293
+ protected override render() {
294
+ const rawBindings = this.node!.attributes[this.endpoint + "/30/0"] as InputType[] | undefined;
295
+ const bindings = rawBindings
296
+ ? Object.values(rawBindings).map(entry => BindingEntryDataTransformer.transform(entry))
297
+ : [];
298
+
299
+ return html`
300
+ <md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
301
+ <div slot="headline">
302
+ <div>Binding</div>
303
+ </div>
304
+ <div slot="content">
305
+ <div>
306
+ <md-list style="padding-bottom:18px;">
307
+ ${Object.values(bindings).map(
308
+ (entry, index) => html`
309
+ <md-list-item style="background:cornsilk;">
310
+ <div style="display:flex;gap:10px;">
311
+ <div>node:${entry["node"]}</div>
312
+ <div>endpoint:${entry["endpoint"]}</div>
313
+ ${entry["cluster"] ? html` <div>cluster:${entry["cluster"]}</div> ` : nothing}
314
+ </div>
315
+ <div slot="end">
316
+ <md-text-button
317
+ @click=${() => this.deleteBindingHandler(index)}
318
+ >delete</md-text-button
319
+ </div>
320
+ </md-list-item>
321
+ `,
322
+ )}
323
+ </md-list>
324
+ <div class="inline-group">
325
+ <div class="group-label">target</div>
326
+ <div class="group-input">
327
+ <md-outlined-text-field
328
+ label="node id"
329
+ name="NodeId"
330
+ type="number"
331
+ min="0"
332
+ max="65535"
333
+ class="target-item"
334
+ @change=${this.onChange}
335
+ supporting-text="required"
336
+ ></md-outlined-text-field>
337
+ <md-outlined-text-field
338
+ label="endpoint"
339
+ name="Endpoint"
340
+ type="number"
341
+ min="0"
342
+ max="65534"
343
+ @change=${this.onChange}
344
+ class="target-item"
345
+ supporting-text="required"
346
+ ></md-outlined-text-field>
347
+ <md-outlined-text-field
348
+ label="cluster"
349
+ name="Cluster"
350
+ type="number"
351
+ min="0"
352
+ max="32767"
353
+ @change=${this.onChange}
354
+ class="target-item"
355
+ supporting-text="optional"
356
+ ></md-outlined-text-field>
357
+ </div>
358
+ </div>
359
+ <div style="margin:8px;">
360
+ <Text style="font-size: 10px;font-style: italic;font-weight: bold;">
361
+ Note: The Cluster ID field is optional according to the Matter specification. If you
362
+ leave it blank, the binding applies to all eligible clusters on the target endpoint.
363
+ However, some devices may require a specific cluster to be set in order for the binding
364
+ to function correctly. If you experience unexpected behavior, try specifying the cluster
365
+ explicitly.
366
+ </Text>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ <div slot="actions">
371
+ <md-text-button @click=${this.addBindingHandler}>Add</md-text-button>
372
+ <md-text-button @click=${this._close}>Cancel</md-text-button>
373
+ </div>
374
+ </md-dialog>
375
+ `;
376
+ }
377
+
378
+ static override styles = css`
379
+ .inline-group {
380
+ display: flex;
381
+ border: 2px solid #673ab7;
382
+ padding: 1px;
383
+ border-radius: 8px;
384
+ position: relative;
385
+ margin: 8px;
386
+ }
387
+
388
+ .group-input {
389
+ display: flex;
390
+ width: -webkit-fill-available;
391
+ }
392
+
393
+ .target-item {
394
+ display: inline-block;
395
+ padding: 20px 10px 10px 10px;
396
+ border-radius: 4px;
397
+ vertical-align: middle;
398
+ min-width: 80px;
399
+ text-align: center;
400
+ width: -webkit-fill-available;
401
+ }
402
+
403
+ .group-label {
404
+ position: absolute;
405
+ left: 15px;
406
+ top: -12px;
407
+ background: #673ab7;
408
+ color: white;
409
+ padding: 3px 15px;
410
+ border-radius: 4px;
411
+ }
412
+ `;
413
+ }
414
+
415
+ declare global {
416
+ interface HTMLElementTagNameMap {
417
+ "node-binding-dialog": NodeBindingDialog;
418
+ }
419
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-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 showNodeBindingDialog = async (client: MatterClient, node: MatterNode, endpoint: number) => {
10
+ await import("./node-binding-dialog.js");
11
+ const dialog = document.createElement("node-binding-dialog");
12
+ dialog.client = client;
13
+ dialog.node = node;
14
+ dialog.endpoint = endpoint;
15
+ document.querySelector("matter-dashboard-app")?.renderRoot.appendChild(dialog);
16
+ };