From 744962ec539476b44d2be7b98da28ee0b30e1275 Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Sat, 3 May 2025 11:23:33 +0800 Subject: [PATCH 01/10] feat: add binding and acl command. --- matter_server/common/models.py | 4 ++ matter_server/server/device_controller.py | 59 ++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/matter_server/common/models.py b/matter_server/common/models.py index 56d3fb1d..695464b8 100644 --- a/matter_server/common/models.py +++ b/matter_server/common/models.py @@ -51,6 +51,10 @@ class APICommand(str, Enum): CHECK_NODE_UPDATE = "check_node_update" UPDATE_NODE = "update_node" SET_DEFAULT_FABRIC_LABEL = "set_default_fabric_label" + GET_ACL_ENTRY = "get_acl_entry" + SET_ACL_ENTRY = "set_acl_entry" + GET_NODE_BINDINGS = "get_node_bindings" + SET_NODE_BINDING = "set_node_binding" EventCallBackType = Callable[[EventType, Any], None] diff --git a/matter_server/server/device_controller.py b/matter_server/server/device_controller.py index 0e344c84..d904d516 100644 --- a/matter_server/server/device_controller.py +++ b/matter_server/server/device_controller.py @@ -20,7 +20,7 @@ from chip.ChipDeviceCtrl import ChipDeviceController from chip.clusters import Attribute, Objects as Clusters -from chip.clusters.Attribute import ValueDecodeFailure +from chip.clusters.Attribute import SubscriptionTransaction, ValueDecodeFailure from chip.clusters.ClusterObjects import ALL_ATTRIBUTES, ALL_CLUSTERS, Cluster from chip.discovery import DiscoveryType from chip.exceptions import ChipStackError @@ -862,6 +862,63 @@ async def remove_node(self, node_id: int) -> None: except ChipStackError as err: LOGGER.warning("Removing current fabric from device failed: %s", err) + @api_command(APICommand.SET_ACL_ENTRY) + async def set_acl_entry( + self, + node_id: int, + entry: list[Clusters.AccessControl.Structs.AccessControlEntryStruct], + ): + """Set acl entry""" + return await self._chip_device_controller.write_attribute( + node_id, [(0, Clusters.AccessControl.Attributes.Acl(entry))] + ) + + @api_command(APICommand.GET_ACL_ENTRY) + async def get_acl_entry(self, node_id: int): + """Get acl entry""" + read_response: ( + SubscriptionTransaction | Attribute.AsyncReadTransaction.ReadResponse | None + ) = await self._chip_device_controller.read_attribute( + node_id, [(0, Clusters.AccessControl.Attributes.Acl)] + ) + acl_entities = [] + if isinstance(read_response, Attribute.AsyncReadTransaction.ReadResponse): + acl_entities: list[ + Clusters.AccessControl.Structs.AccessControlEntryStruct + ] = read_response.attributes[0][Clusters.AccessControl][ + Clusters.AccessControl.Attributes.Acl + ] + return acl_entities + + @api_command(APICommand.GET_NODE_BINDINGS) + async def get_node_bindings(self, node_id: int): + """Get node bindings""" + read_response: ( + SubscriptionTransaction | Attribute.AsyncReadTransaction.ReadResponse | None + ) = await self._chip_device_controller.read_attribute( + node_id, (Clusters.Binding.Attributes.Binding,) + ) + + bindings = [] + if isinstance(read_response, Attribute.AsyncReadTransaction.ReadResponse): + for k, v in read_response.attributes.items(): + bindings.append( + {k: v[Clusters.Binding][Clusters.Binding.Attributes.Binding]} + ) + return bindings + + @api_command(APICommand.SET_NODE_BINDING) + async def set_node_binding( + self, + node_id: int, + endpoint: int, + bindings: list[Clusters.Binding.Structs.TargetStruct], + ): + """Set node binding""" + return await self._chip_device_controller.write_attribute( + node_id, [(endpoint, Clusters.Binding.Attributes.Binding(bindings))] + ) + @api_command(APICommand.PING_NODE) async def ping_node(self, node_id: int, attempts: int = 1) -> NodePingResult: """Ping node on the currently known IP-address(es).""" From 808a2927740ea96348c6395e565fa826c9b97778 Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Sat, 3 May 2025 11:25:31 +0800 Subject: [PATCH 02/10] feat: add binding and acl command in dashboard. --- dashboard/src/client/client.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dashboard/src/client/client.ts b/dashboard/src/client/client.ts index 25625f68..19d0f5f6 100644 --- a/dashboard/src/client/client.ts +++ b/dashboard/src/client/client.ts @@ -143,6 +143,21 @@ export class MatterClient { await this.sendCommand("update_node", 10, { node_id: nodeId, software_version: softwareVersion }); } + async setACLEntry(nodeId: number, entry: any) { + return await this.sendCommand("set_acl_entry", 0, { + node_id: nodeId, + entry: entry, + }); + } + + async setNodeBinding(nodeId: number, endpoint: number, bindings: any) { + return await this.sendCommand("set_node_binding", 0, { + node_id: nodeId, + endpoint: endpoint, + bindings: bindings, + }); + } + async sendCommand( command: T, require_schema: number | undefined = undefined, From b5db47061f2ea7e08d8e6458e085785b6b24f1bb Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Sat, 3 May 2025 11:38:20 +0800 Subject: [PATCH 03/10] feat: add binding button in node-details.ts. --- .../binding/show-node-binding-dialog.ts | 17 +++++++++ dashboard/src/pages/components/context.ts | 4 +++ .../src/pages/components/node-details.ts | 36 ++++++++++++++++++- dashboard/src/pages/matter-cluster-view.ts | 11 ++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts create mode 100644 dashboard/src/pages/components/context.ts diff --git a/dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts new file mode 100644 index 00000000..8a396960 --- /dev/null +++ b/dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts @@ -0,0 +1,17 @@ +import { MatterClient } from "../../../client/client"; +import { MatterNode } from "../../../client/models/node"; + +export const showNodeBindingDialog = async ( + client: MatterClient, + node: MatterNode, + bindingPath: string +) => { + await import("./node-binding-dialog"); + const dialog = document.createElement("node-binding-dialog"); + dialog.client = client; + dialog.node = node; + dialog.bindingPath = bindingPath; + document + .querySelector("matter-dashboard-app") + ?.renderRoot.appendChild(dialog); +}; diff --git a/dashboard/src/pages/components/context.ts b/dashboard/src/pages/components/context.ts new file mode 100644 index 00000000..217c1e65 --- /dev/null +++ b/dashboard/src/pages/components/context.ts @@ -0,0 +1,4 @@ + +import { createContext } from "@lit/context"; + +export const bindingContext = createContext(""); diff --git a/dashboard/src/pages/components/node-details.ts b/dashboard/src/pages/components/node-details.ts index 437c9aeb..dba6c1d3 100644 --- a/dashboard/src/pages/components/node-details.ts +++ b/dashboard/src/pages/components/node-details.ts @@ -5,7 +5,15 @@ import "@material/web/divider/divider"; import "@material/web/iconbutton/icon-button"; import "@material/web/list/list"; import "@material/web/list/list-item"; -import { mdiChatProcessing, mdiShareVariant, mdiTrashCan, mdiUpdate } from "@mdi/js"; +import { + mdiChatProcessing, + mdiShareVariant, + mdiTrashCan, + mdiUpdate, + mdiLink, +} from "@mdi/js"; + +import { consume } from "@lit/context"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { MatterClient } from "../../client/client"; @@ -17,6 +25,8 @@ import { } from "../../components/dialog-box/show-dialog-box"; import "../../components/ha-svg-icon"; import { getEndpointDeviceTypes } from "../matter-endpoint-view"; +import { bindingContext } from "./context"; +import { showNodeBindingDialog } from "../../components/dialogs/binding/show-node-binding-dialog"; function getNodeDeviceTypes(node: MatterNode): DeviceType[] { const uniqueEndpoints = new Set( @@ -40,8 +50,15 @@ export class NodeDetails extends LitElement { @state() private _updateInitiated: boolean = false; + @consume({ context: bindingContext }) + @property({ attribute: false }) + bindingPath!: string; + protected render() { if (!this.node) return html``; + + const bindings = this.node.attributes[this.bindingPath]; + return html` @@ -90,6 +107,15 @@ export class NodeDetails extends LitElement { Update in progress (${this.node.updateStateProgress || 0}%)` : html`Update`} + ${bindings + ? html` + + Binding + + + ` + : nothing} + Share Remove @@ -144,6 +170,14 @@ export class NodeDetails extends LitElement { } } + private async _binding() { + try { + showNodeBindingDialog(this.client!, this.node!, this.bindingPath!); + } catch (err: any) { + console.log(err); + } + } + private async _searchUpdate() { const nodeUpdate = await this.client.checkNodeUpdate(this.node!.node_id); if (!nodeUpdate) { diff --git a/dashboard/src/pages/matter-cluster-view.ts b/dashboard/src/pages/matter-cluster-view.ts index f604531a..8b593d73 100644 --- a/dashboard/src/pages/matter-cluster-view.ts +++ b/dashboard/src/pages/matter-cluster-view.ts @@ -9,6 +9,9 @@ import { clusters } from "../client/models/descriptions"; import { MatterNode } from "../client/models/node"; import { showAlertDialog } from "../components/dialog-box/show-dialog-box"; import "../components/ha-svg-icon"; +import "../pages/components/node-details"; +import { provide } from "@lit/context"; +import { bindingContext } from "./components/context"; declare global { interface HTMLElementTagNameMap { @@ -43,6 +46,10 @@ class MatterClusterView extends LitElement { @property() public cluster?: number; + @provide({ context: bindingContext }) + @property({ attribute: false }) + bindingPath: string = ""; + render() { if (!this.node || this.endpoint == undefined || this.cluster == undefined) { return html` @@ -51,6 +58,10 @@ class MatterClusterView extends LitElement { `; } + if (this.cluster == 30) { + this.bindingPath = this.endpoint + "/30/0"; + } + return html` Date: Sat, 3 May 2025 11:40:13 +0800 Subject: [PATCH 04/10] feat: add node binding dialog. --- dashboard/src/client/models/model.ts | 8 + .../src/components/dialogs/binding/model.ts | 116 ++++++++ .../dialogs/binding/node-binding-dialog.ts | 251 ++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100644 dashboard/src/components/dialogs/binding/model.ts create mode 100644 dashboard/src/components/dialogs/binding/node-binding-dialog.ts diff --git a/dashboard/src/client/models/model.ts b/dashboard/src/client/models/model.ts index 2c04fe66..bb0ef517 100644 --- a/dashboard/src/client/models/model.ts +++ b/dashboard/src/client/models/model.ts @@ -105,6 +105,14 @@ export interface APICommands { requestArgs: {}; response: {}; }; + set_acl_entry: { + requestArgs: {}; + response: {}; + }; + set_node_binding: { + requestArgs: {}; + response: {}; + }; } export interface CommandMessage { diff --git a/dashboard/src/components/dialogs/binding/model.ts b/dashboard/src/components/dialogs/binding/model.ts new file mode 100644 index 00000000..dd83c413 --- /dev/null +++ b/dashboard/src/components/dialogs/binding/model.ts @@ -0,0 +1,116 @@ +export type InputType = { + [key: string]: number | number[] | undefined; +}; + +export interface BindingEntryStruct { + node: number; + group: number | undefined; + endpoint: number; + cluster: number | undefined; + fabricIndex: number | undefined; +} + +export type AccessControlEntryStruct = { + privilege: number; + authMode: number; + subjects: number[]; + targets: number[] | undefined; + fabricIndex: number; +}; + +export class AccessControlEntryDataTransformer { + private static readonly KEY_MAPPING: { + [inputKey: string]: keyof AccessControlEntryStruct; + } = { + "1": "privilege", + "2": "authMode", + "3": "subjects", + "4": "targets", + "254": "fabricIndex", + }; + + public static transform(input: any): AccessControlEntryStruct { + if (!input || typeof input !== "object") { + throw new Error("Invalid input: expected an object"); + } + + const result: Partial = {}; + const keyMapping = AccessControlEntryDataTransformer.KEY_MAPPING; + + for (const key in input) { + if (key in keyMapping) { + const mappedKey = keyMapping[key]; + if (mappedKey) { + const value = input[key]; + if (value === undefined) continue; + if (mappedKey === "subjects" || mappedKey === "targets") { + result[mappedKey] = Array.isArray(value) ? value : undefined; + } else { + result[mappedKey] = value; + } + } + } + } + + if ( + result.privilege === undefined || + result.authMode === undefined || + result.subjects === undefined || + result.fabricIndex === undefined + ) { + throw new Error("Missing required fields in AccessControlEntryStruct"); + } + + return result as AccessControlEntryStruct; + } +} + +export class BindingEntryDataTransformer { + private static readonly KEY_MAPPING: { + [inputKey: string]: keyof BindingEntryStruct; + } = { + "1": "node", + "3": "endpoint", + "4": "cluster", + "254": "fabricIndex", + }; + + public static transform(input: any): BindingEntryStruct { + if (!input || typeof input !== "object") { + throw new Error("Invalid input: expected an object"); + } + + const result: Partial = {}; + const keyMapping = BindingEntryDataTransformer.KEY_MAPPING; + + for (const key in input) { + if (key in keyMapping) { + const mappedKey = keyMapping[key]; + if (mappedKey) { + const value = input[key]; + if (value === undefined) { + continue; + } + if (mappedKey === "fabricIndex") { + result[mappedKey] = value === undefined ? undefined : Number(value); + } else if (mappedKey === "node" || mappedKey === "endpoint") { + result[mappedKey] = Number(value); + } else { + result[mappedKey] = value as BindingEntryStruct[typeof mappedKey]; + } + } + } + } + + // Validate required fields + if ( + result.node === undefined || + result.endpoint === undefined || + result.fabricIndex === undefined + ) { + throw new Error("Missing required fields in BindingEntryStruct"); + } + + return result as BindingEntryStruct; + } +} diff --git a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts new file mode 100644 index 00000000..704c1bdb --- /dev/null +++ b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts @@ -0,0 +1,251 @@ +import "@material/web/button/text-button"; +import "@material/web/dialog/dialog"; +import "@material/web/list/list"; +import "@material/web/list/list-item"; +import "../../../components/ha-svg-icon"; +import "@material/web/textfield/outlined-text-field"; +import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; + +import { html, LitElement } from "lit"; +import { customElement, property, query } from "lit/decorators.js"; +import { MatterNode } from "../../../client/models/node"; +import { preventDefault } from "../../../util/prevent_default"; +import { MatterClient } from "../../../client/client"; +import { + InputType, + AccessControlEntryStruct, + AccessControlEntryDataTransformer, + BindingEntryStruct, + BindingEntryDataTransformer, +} from "./model"; + +@customElement("node-binding-dialog") +export class NodeBindingDialog extends LitElement { + @property({ attribute: false }) public client!: MatterClient; + + @property() + public node?: MatterNode; + + @property({ attribute: false }) + bindingPath!: string; + + @query("md-outlined-text-field[label='target node id']") + private _targetNodeId!: MdOutlinedTextField; + + @query("md-outlined-text-field[label='target endpoint']") + private _targetEndpoint!: MdOutlinedTextField; + + private _transformBindingStruct(): BindingEntryStruct[] { + const bindings_raw: [] = this.node!.attributes[this.bindingPath]; + return Object.values(bindings_raw).map((value) => + BindingEntryDataTransformer.transform(value) + ); + } + + private _transformACLStruct( + targetNodeId: number + ): AccessControlEntryStruct[] { + const acl_cluster_raw: [InputType] = + this.client.nodes[targetNodeId].attributes["0/31/0"]; + return Object.values(acl_cluster_raw).map((value: InputType) => + AccessControlEntryDataTransformer.transform(value) + ); + } + + async _bindingDelete(index: number) { + const endpoint = this.bindingPath.split("/")[0]; + const bindings = this._transformBindingStruct(); + const targetNodeId = bindings[index].node; + + try { + const acl_cluster = this._transformACLStruct(targetNodeId); + const _acl_cluster = acl_cluster + .map((entry) => { + if (entry.subjects && entry.subjects.includes(this.node!.node_id)) { + entry.subjects = entry.subjects.filter( + (nodeId) => nodeId !== this.node!.node_id + ); + if (entry.subjects.length === 0) { + return null; + } + } + return entry; + }) + .filter((entry) => entry !== null); + this.client.setACLEntry(targetNodeId, _acl_cluster); + } catch (err) { + console.log(err); + } + + bindings.splice(index, 1); + try { + await this.client.setNodeBinding( + this.node!.node_id, + parseInt(endpoint, 10), + bindings + ); + this.node!.attributes[this.bindingPath].splice(index, 1); + this.requestUpdate(); + } catch (err) { + console.error("Failed to delete binding:", err); + } + } + + private async _updateEntry( + targetId: number, + path: string, + entry: T, + transformFn: (value: InputType) => T, + updateFn: (targetId: number, entries: T[]) => Promise + ) { + try { + const rawEntries: [InputType] = + this.client.nodes[targetId].attributes[path]; + const entries = Object.values(rawEntries).map(transformFn); + entries.push(entry); + return await updateFn(targetId, entries); + } catch (err) { + console.log(err); + } + } + + private async add_target_acl( + targetNodeId: number, + entry: AccessControlEntryStruct + ) { + try { + const result = (await this._updateEntry( + targetNodeId, + "0/31/0", + entry, + AccessControlEntryDataTransformer.transform, + this.client.setACLEntry.bind(this.client) + )) as { [key: string]: { Status: number } }; + return result["0"].Status === 0; + } catch (err) { + console.error("add acl error:", err); + return false; + } + } + + private async add_bindings( + endpoint: number, + bindingEntry: BindingEntryStruct + ) { + const bindings = this._transformBindingStruct(); + bindings.push(bindingEntry); + try { + const result = (await this.client.setNodeBinding( + this.node!.node_id, + endpoint, + bindings + )) as { [key: string]: { Status: number } }; + return result["0"].Status === 0; + } catch (err) { + console.log("add bindings error:", err); + return false; + } + } + + async _bindingAdd() { + const targetNodeId = parseInt(this._targetNodeId.value, 10); + const targetEndpoint = parseInt(this._targetEndpoint.value, 10); + + if (isNaN(targetNodeId) || targetNodeId <= 0) { + alert("Please enter a valid target node ID"); + return; + } + if (isNaN(targetEndpoint) || targetEndpoint < 0) { + alert("Please enter a valid target endpoint"); + return; + } + + const acl_entry: AccessControlEntryStruct = { + privilege: 5, + authMode: 2, + subjects: [this.node!.node_id], + targets: undefined, + fabricIndex: this.client.connection.serverInfo!.fabric_id, + }; + const result_acl = await this.add_target_acl(targetNodeId, acl_entry); + if (!result_acl) return; + + const endpoint = this.bindingPath.split("/")[0]; + const bindingEntry: BindingEntryStruct = { + node: targetNodeId, + endpoint: targetEndpoint, + group: undefined, + cluster: undefined, + fabricIndex: undefined, + }; + + const result_binding = await this.add_bindings( + parseInt(endpoint, 10), + bindingEntry + ); + + if (result_binding) { + this.requestUpdate(); + } + } + + private _close() { + this.shadowRoot!.querySelector("md-dialog")!.close(); + } + + private _handleClosed() { + this.parentNode!.removeChild(this); + } + + protected render() { + const bindings = this.node!.attributes[this.bindingPath]; + + return html` + +
+
Binding Add
+
+
+
+ + ${Object.values(bindings).map( + (entry, index) => html` + +
+ ${JSON.stringify( + BindingEntryDataTransformer.transform(entry) + )} +
+
+ this._bindingDelete(index)} + >delete + + ` + )} + +
+ + +
+
+
+
+ Add + Cancel +
+ + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "node-binding-dialog": NodeBindingDialog; + } +} From c275e132dba161bfe01b3c267425a14c56582986 Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Sat, 3 May 2025 23:03:15 +0800 Subject: [PATCH 05/10] fix: can not add binding entry to list after add button click. --- .../dialogs/binding/node-binding-dialog.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts index 704c1bdb..582fa0a0 100644 --- a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts +++ b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts @@ -18,10 +18,14 @@ import { BindingEntryStruct, BindingEntryDataTransformer, } from "./model"; +import { consume } from "@lit/context"; +import { clientContext } from "../../../client/client-context"; @customElement("node-binding-dialog") export class NodeBindingDialog extends LitElement { - @property({ attribute: false }) public client!: MatterClient; + @consume({ context: clientContext, subscribe: true }) + @property({ attribute: false }) + public client!: MatterClient; @property() public node?: MatterNode; @@ -38,17 +42,17 @@ export class NodeBindingDialog extends LitElement { private _transformBindingStruct(): BindingEntryStruct[] { const bindings_raw: [] = this.node!.attributes[this.bindingPath]; return Object.values(bindings_raw).map((value) => - BindingEntryDataTransformer.transform(value) + BindingEntryDataTransformer.transform(value), ); } private _transformACLStruct( - targetNodeId: number + targetNodeId: number, ): AccessControlEntryStruct[] { const acl_cluster_raw: [InputType] = this.client.nodes[targetNodeId].attributes["0/31/0"]; return Object.values(acl_cluster_raw).map((value: InputType) => - AccessControlEntryDataTransformer.transform(value) + AccessControlEntryDataTransformer.transform(value), ); } @@ -63,7 +67,7 @@ export class NodeBindingDialog extends LitElement { .map((entry) => { if (entry.subjects && entry.subjects.includes(this.node!.node_id)) { entry.subjects = entry.subjects.filter( - (nodeId) => nodeId !== this.node!.node_id + (nodeId) => nodeId !== this.node!.node_id, ); if (entry.subjects.length === 0) { return null; @@ -82,7 +86,7 @@ export class NodeBindingDialog extends LitElement { await this.client.setNodeBinding( this.node!.node_id, parseInt(endpoint, 10), - bindings + bindings, ); this.node!.attributes[this.bindingPath].splice(index, 1); this.requestUpdate(); @@ -96,7 +100,7 @@ export class NodeBindingDialog extends LitElement { path: string, entry: T, transformFn: (value: InputType) => T, - updateFn: (targetId: number, entries: T[]) => Promise + updateFn: (targetId: number, entries: T[]) => Promise, ) { try { const rawEntries: [InputType] = @@ -111,7 +115,7 @@ export class NodeBindingDialog extends LitElement { private async add_target_acl( targetNodeId: number, - entry: AccessControlEntryStruct + entry: AccessControlEntryStruct, ) { try { const result = (await this._updateEntry( @@ -119,7 +123,7 @@ export class NodeBindingDialog extends LitElement { "0/31/0", entry, AccessControlEntryDataTransformer.transform, - this.client.setACLEntry.bind(this.client) + this.client.setACLEntry.bind(this.client), )) as { [key: string]: { Status: number } }; return result["0"].Status === 0; } catch (err) { @@ -130,7 +134,7 @@ export class NodeBindingDialog extends LitElement { private async add_bindings( endpoint: number, - bindingEntry: BindingEntryStruct + bindingEntry: BindingEntryStruct, ) { const bindings = this._transformBindingStruct(); bindings.push(bindingEntry); @@ -138,7 +142,7 @@ export class NodeBindingDialog extends LitElement { const result = (await this.client.setNodeBinding( this.node!.node_id, endpoint, - bindings + bindings, )) as { [key: string]: { Status: number } }; return result["0"].Status === 0; } catch (err) { @@ -181,7 +185,7 @@ export class NodeBindingDialog extends LitElement { const result_binding = await this.add_bindings( parseInt(endpoint, 10), - bindingEntry + bindingEntry, ); if (result_binding) { @@ -213,7 +217,7 @@ export class NodeBindingDialog extends LitElement {
${JSON.stringify( - BindingEntryDataTransformer.transform(entry) + BindingEntryDataTransformer.transform(entry), )}
@@ -222,7 +226,7 @@ export class NodeBindingDialog extends LitElement { >delete - ` + `, )}
From b1214b807b980f9a5062ea5b8e0c2a97b3918118 Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Sat, 3 May 2025 23:34:32 +0800 Subject: [PATCH 06/10] refactor: use endpoint replace binding path. --- .../dialogs/binding/node-binding-dialog.ts | 23 +++++++------------ .../binding/show-node-binding-dialog.ts | 4 ++-- dashboard/src/pages/components/context.ts | 3 ++- .../src/pages/components/node-details.ts | 6 ++--- dashboard/src/pages/matter-cluster-view.ts | 11 ++------- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts index 582fa0a0..0f7d00b7 100644 --- a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts +++ b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts @@ -31,7 +31,7 @@ export class NodeBindingDialog extends LitElement { public node?: MatterNode; @property({ attribute: false }) - bindingPath!: string; + endpoint!: number; @query("md-outlined-text-field[label='target node id']") private _targetNodeId!: MdOutlinedTextField; @@ -40,7 +40,7 @@ export class NodeBindingDialog extends LitElement { private _targetEndpoint!: MdOutlinedTextField; private _transformBindingStruct(): BindingEntryStruct[] { - const bindings_raw: [] = this.node!.attributes[this.bindingPath]; + const bindings_raw: [] = this.node!.attributes[this.endpoint + "/30/0"]; return Object.values(bindings_raw).map((value) => BindingEntryDataTransformer.transform(value), ); @@ -57,7 +57,7 @@ export class NodeBindingDialog extends LitElement { } async _bindingDelete(index: number) { - const endpoint = this.bindingPath.split("/")[0]; + const endpoint = this.endpoint; const bindings = this._transformBindingStruct(); const targetNodeId = bindings[index].node; @@ -83,12 +83,8 @@ export class NodeBindingDialog extends LitElement { bindings.splice(index, 1); try { - await this.client.setNodeBinding( - this.node!.node_id, - parseInt(endpoint, 10), - bindings, - ); - this.node!.attributes[this.bindingPath].splice(index, 1); + await this.client.setNodeBinding(this.node!.node_id, endpoint, bindings); + this.node!.attributes[this.endpoint + "/30/0"].splice(index, 1); this.requestUpdate(); } catch (err) { console.error("Failed to delete binding:", err); @@ -174,7 +170,7 @@ export class NodeBindingDialog extends LitElement { const result_acl = await this.add_target_acl(targetNodeId, acl_entry); if (!result_acl) return; - const endpoint = this.bindingPath.split("/")[0]; + const endpoint = this.endpoint; const bindingEntry: BindingEntryStruct = { node: targetNodeId, endpoint: targetEndpoint, @@ -183,10 +179,7 @@ export class NodeBindingDialog extends LitElement { fabricIndex: undefined, }; - const result_binding = await this.add_bindings( - parseInt(endpoint, 10), - bindingEntry, - ); + const result_binding = await this.add_bindings(endpoint, bindingEntry); if (result_binding) { this.requestUpdate(); @@ -202,7 +195,7 @@ export class NodeBindingDialog extends LitElement { } protected render() { - const bindings = this.node!.attributes[this.bindingPath]; + const bindings = this.node!.attributes[this.endpoint + "/30/0"]; return html` diff --git a/dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts index 8a396960..22351d8b 100644 --- a/dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts +++ b/dashboard/src/components/dialogs/binding/show-node-binding-dialog.ts @@ -4,13 +4,13 @@ import { MatterNode } from "../../../client/models/node"; export const showNodeBindingDialog = async ( client: MatterClient, node: MatterNode, - bindingPath: string + endpoint: number, ) => { await import("./node-binding-dialog"); const dialog = document.createElement("node-binding-dialog"); dialog.client = client; dialog.node = node; - dialog.bindingPath = bindingPath; + dialog.endpoint = endpoint; document .querySelector("matter-dashboard-app") ?.renderRoot.appendChild(dialog); diff --git a/dashboard/src/pages/components/context.ts b/dashboard/src/pages/components/context.ts index 217c1e65..f0a0c7d6 100644 --- a/dashboard/src/pages/components/context.ts +++ b/dashboard/src/pages/components/context.ts @@ -1,4 +1,5 @@ import { createContext } from "@lit/context"; -export const bindingContext = createContext(""); +// export const bindingContext = createContext(""); +export const bindingContext = createContext("binding"); diff --git a/dashboard/src/pages/components/node-details.ts b/dashboard/src/pages/components/node-details.ts index dba6c1d3..a7cda4cb 100644 --- a/dashboard/src/pages/components/node-details.ts +++ b/dashboard/src/pages/components/node-details.ts @@ -52,12 +52,12 @@ export class NodeDetails extends LitElement { @consume({ context: bindingContext }) @property({ attribute: false }) - bindingPath!: string; + endpoint!: number; protected render() { if (!this.node) return html``; - const bindings = this.node.attributes[this.bindingPath]; + const bindings = this.node.attributes[this.endpoint + "/30/0"]; return html` @@ -172,7 +172,7 @@ export class NodeDetails extends LitElement { private async _binding() { try { - showNodeBindingDialog(this.client!, this.node!, this.bindingPath!); + showNodeBindingDialog(this.client!, this.node!, this.endpoint!); } catch (err: any) { console.log(err); } diff --git a/dashboard/src/pages/matter-cluster-view.ts b/dashboard/src/pages/matter-cluster-view.ts index 8b593d73..3ca3ea4a 100644 --- a/dashboard/src/pages/matter-cluster-view.ts +++ b/dashboard/src/pages/matter-cluster-view.ts @@ -40,16 +40,13 @@ class MatterClusterView extends LitElement { @property() public node?: MatterNode; + @provide({ context: bindingContext }) @property() - public endpoint?: number; + public endpoint!: number; @property() public cluster?: number; - @provide({ context: bindingContext }) - @property({ attribute: false }) - bindingPath: string = ""; - render() { if (!this.node || this.endpoint == undefined || this.cluster == undefined) { return html` @@ -58,10 +55,6 @@ class MatterClusterView extends LitElement { `; } - if (this.cluster == 30) { - this.bindingPath = this.endpoint + "/30/0"; - } - return html` Date: Sun, 4 May 2025 13:03:13 +0800 Subject: [PATCH 07/10] refactor: rewrite delete binding handler. --- .../dialogs/binding/node-binding-dialog.ts | 116 ++++++++++++------ 1 file changed, 78 insertions(+), 38 deletions(-) diff --git a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts index 0f7d00b7..0db952e7 100644 --- a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts +++ b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts @@ -39,16 +39,14 @@ export class NodeBindingDialog extends LitElement { @query("md-outlined-text-field[label='target endpoint']") private _targetEndpoint!: MdOutlinedTextField; - private _transformBindingStruct(): BindingEntryStruct[] { + private fetchBindingEntry(): BindingEntryStruct[] { const bindings_raw: [] = this.node!.attributes[this.endpoint + "/30/0"]; return Object.values(bindings_raw).map((value) => BindingEntryDataTransformer.transform(value), ); } - private _transformACLStruct( - targetNodeId: number, - ): AccessControlEntryStruct[] { + private fetchACLEntry(targetNodeId: number): AccessControlEntryStruct[] { const acl_cluster_raw: [InputType] = this.client.nodes[targetNodeId].attributes["0/31/0"]; return Object.values(acl_cluster_raw).map((value: InputType) => @@ -56,39 +54,81 @@ export class NodeBindingDialog extends LitElement { ); } - async _bindingDelete(index: number) { - const endpoint = this.endpoint; - const bindings = this._transformBindingStruct(); - const targetNodeId = bindings[index].node; - + private async deleteBindingHandler(index: number): Promise { + const rawBindings = this.fetchBindingEntry(); try { - const acl_cluster = this._transformACLStruct(targetNodeId); - const _acl_cluster = acl_cluster - .map((entry) => { - if (entry.subjects && entry.subjects.includes(this.node!.node_id)) { - entry.subjects = entry.subjects.filter( - (nodeId) => nodeId !== this.node!.node_id, - ); - if (entry.subjects.length === 0) { - return null; - } - } - return entry; - }) - .filter((entry) => entry !== null); - this.client.setACLEntry(targetNodeId, _acl_cluster); - } catch (err) { - console.log(err); + const targetNodeId = rawBindings[index].node; + await this.removeNodeAtACLEntry(this.node!.node_id, targetNodeId); + const updatedBindings = this.removeBindingAtIndex(rawBindings, index); + await this.syncBindingUpdates(updatedBindings, index); + } catch (error) { + this.handleBindingDeletionError(error); } + } - bindings.splice(index, 1); - try { - await this.client.setNodeBinding(this.node!.node_id, endpoint, bindings); - this.node!.attributes[this.endpoint + "/30/0"].splice(index, 1); - this.requestUpdate(); - } catch (err) { - console.error("Failed to delete binding:", err); - } + private async removeNodeAtACLEntry( + sourceNodeId: number, + targetNodeId: number, + ): Promise { + const aclEntries = this.fetchACLEntry(targetNodeId); + + const updatedACLEntries = aclEntries + .map((entry) => this.removeSubjectAtACL(sourceNodeId, entry)) + .filter((entry): entry is Exclude => entry !== null); + + await this.client.setACLEntry(targetNodeId, updatedACLEntries); + } + + private removeSubjectAtACL( + nodeId: number, + entry: AccessControlEntryStruct, + ): AccessControlEntryStruct | null { + const shouldRemoveSubject = entry.subjects?.includes(nodeId); + + if (!shouldRemoveSubject) return entry; + + const updatedSubjects = entry.subjects.filter( + (nodeId) => nodeId !== this.node!.node_id, + ); + + return updatedSubjects.length > 0 + ? { ...entry, subjects: updatedSubjects } + : null; + } + + private removeBindingAtIndex( + bindings: BindingEntryStruct[], + index: number, + ): BindingEntryStruct[] { + return [...bindings.slice(0, index), ...bindings.slice(index + 1)]; + } + + private async syncBindingUpdates( + updatedBindings: BindingEntryStruct[], + index: number, + ): Promise { + await this.client.setNodeBinding( + this.node!.node_id, + this.endpoint, + updatedBindings, + ); + + const attributePath = `${this.endpoint}/30/0`; + const updatedAttributes = { + ...this.node!.attributes, + [attributePath]: this.removeBindingAtIndex( + this.node!.attributes[attributePath], + index, + ), + }; + + this.node!.attributes = updatedAttributes; + this.requestUpdate(); + } + + private handleBindingDeletionError(error: unknown): void { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`Binding deletion failed: ${errorMessage}`); } private async _updateEntry( @@ -132,7 +172,7 @@ export class NodeBindingDialog extends LitElement { endpoint: number, bindingEntry: BindingEntryStruct, ) { - const bindings = this._transformBindingStruct(); + const bindings = this.fetchBindingEntry(); bindings.push(bindingEntry); try { const result = (await this.client.setNodeBinding( @@ -147,7 +187,7 @@ export class NodeBindingDialog extends LitElement { } } - async _bindingAdd() { + async addBindingHandler() { const targetNodeId = parseInt(this._targetNodeId.value, 10); const targetEndpoint = parseInt(this._targetEndpoint.value, 10); @@ -215,7 +255,7 @@ export class NodeBindingDialog extends LitElement {
this._bindingDelete(index)} + @click=${() => this.deleteBindingHandler(index)} >delete @@ -233,7 +273,7 @@ export class NodeBindingDialog extends LitElement {
- Add + Add Cancel
From 6abee40df58f537fbe0b3c5d3e9b2ab77090268c Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Mon, 19 May 2025 18:29:56 +0800 Subject: [PATCH 08/10] refactor: remove acl entry by target's endpoint. --- dashboard/src/components/dialogs/acl/model.ts | 109 +++++++++++++++++ .../src/components/dialogs/binding/model.ts | 55 --------- .../dialogs/binding/node-binding-dialog.ts | 111 +++++++++++++----- 3 files changed, 192 insertions(+), 83 deletions(-) create mode 100644 dashboard/src/components/dialogs/acl/model.ts diff --git a/dashboard/src/components/dialogs/acl/model.ts b/dashboard/src/components/dialogs/acl/model.ts new file mode 100644 index 00000000..88fa9816 --- /dev/null +++ b/dashboard/src/components/dialogs/acl/model.ts @@ -0,0 +1,109 @@ + +export type AccessControlEntryRawInput = { + "1": number; + "2": number; + "3": number[]; + "4": null; + "254": number; +}; + +export type AccessControlTargetStruct = { + cluster: number | undefined; + endpoint: number | undefined; + deviceType: number | undefined; +}; + +export type AccessControlEntryStruct = { + privilege: number; + authMode: number; + subjects: number[]; + targets: AccessControlTargetStruct[] | undefined; + fabricIndex: number; +}; + +export class AccessControlTargetTransformer { + private static readonly KEY_MAPPING: { + [inputKey: string]: keyof AccessControlTargetStruct; + } = { + "0": "cluster", + "1": "endpoint", + "2": "deviceType", + }; + + public static transform(input: any): AccessControlTargetStruct { + if (!input || typeof input !== "object") { + throw new Error("Invalid input: expected an object"); + } + + const result: Partial = {}; + const keyMapping = AccessControlTargetTransformer.KEY_MAPPING; + + for (const key in input) { + if (key in keyMapping) { + const mappedKey = keyMapping[key]; + if (mappedKey) { + const value = input[key]; + if (value === undefined) continue; + result[mappedKey] = value; + } + } + } + return result as AccessControlTargetStruct; + } +} + +export class AccessControlEntryDataTransformer { + private static readonly KEY_MAPPING: { + [inputKey: string]: keyof AccessControlEntryStruct; + } = { + "1": "privilege", + "2": "authMode", + "3": "subjects", + "4": "targets", + "254": "fabricIndex", + }; + + public static transform(input: any): AccessControlEntryStruct { + if (!input || typeof input !== "object") { + throw new Error("Invalid input: expected an object"); + } + + const result: Partial = {}; + const keyMapping = AccessControlEntryDataTransformer.KEY_MAPPING; + + for (const key in input) { + if (key in keyMapping) { + const mappedKey = keyMapping[key]; + if (mappedKey) { + const value = input[key]; + if (value === undefined) continue; + if (mappedKey === "subjects") { + result[mappedKey] = Array.isArray(value) ? value : undefined; + } else if (mappedKey === "targets") { + if (Array.isArray(value)) { + const _targets = Object.values(value).map((val) => + AccessControlTargetTransformer.transform(val), + ); + result[mappedKey] = _targets; + } else { + result[mappedKey] = undefined; + } + } else { + result[mappedKey] = value; + } + } + } + } + + if ( + result.privilege === undefined || + result.authMode === undefined || + result.subjects === undefined || + result.fabricIndex === undefined + ) { + throw new Error("Missing required fields in AccessControlEntryStruct"); + } + + return result as AccessControlEntryStruct; + } +} diff --git a/dashboard/src/components/dialogs/binding/model.ts b/dashboard/src/components/dialogs/binding/model.ts index dd83c413..e1b224d3 100644 --- a/dashboard/src/components/dialogs/binding/model.ts +++ b/dashboard/src/components/dialogs/binding/model.ts @@ -10,61 +10,6 @@ export interface BindingEntryStruct { fabricIndex: number | undefined; } -export type AccessControlEntryStruct = { - privilege: number; - authMode: number; - subjects: number[]; - targets: number[] | undefined; - fabricIndex: number; -}; - -export class AccessControlEntryDataTransformer { - private static readonly KEY_MAPPING: { - [inputKey: string]: keyof AccessControlEntryStruct; - } = { - "1": "privilege", - "2": "authMode", - "3": "subjects", - "4": "targets", - "254": "fabricIndex", - }; - - public static transform(input: any): AccessControlEntryStruct { - if (!input || typeof input !== "object") { - throw new Error("Invalid input: expected an object"); - } - - const result: Partial = {}; - const keyMapping = AccessControlEntryDataTransformer.KEY_MAPPING; - - for (const key in input) { - if (key in keyMapping) { - const mappedKey = keyMapping[key]; - if (mappedKey) { - const value = input[key]; - if (value === undefined) continue; - if (mappedKey === "subjects" || mappedKey === "targets") { - result[mappedKey] = Array.isArray(value) ? value : undefined; - } else { - result[mappedKey] = value; - } - } - } - } - - if ( - result.privilege === undefined || - result.authMode === undefined || - result.subjects === undefined || - result.fabricIndex === undefined - ) { - throw new Error("Missing required fields in AccessControlEntryStruct"); - } - - return result as AccessControlEntryStruct; - } -} - export class BindingEntryDataTransformer { private static readonly KEY_MAPPING: { [inputKey: string]: keyof BindingEntryStruct; diff --git a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts index 0db952e7..40304fd2 100644 --- a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts +++ b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts @@ -6,18 +6,23 @@ import "../../../components/ha-svg-icon"; import "@material/web/textfield/outlined-text-field"; import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; -import { html, LitElement } from "lit"; +import { html, LitElement, css } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import { MatterNode } from "../../../client/models/node"; import { preventDefault } from "../../../util/prevent_default"; import { MatterClient } from "../../../client/client"; import { InputType, - AccessControlEntryStruct, - AccessControlEntryDataTransformer, BindingEntryStruct, BindingEntryDataTransformer, } from "./model"; + +import { + AccessControlEntryDataTransformer, + AccessControlEntryStruct, + AccessControlTargetStruct, +} from "../acl/model"; + import { consume } from "@lit/context"; import { clientContext } from "../../../client/client-context"; @@ -33,10 +38,10 @@ export class NodeBindingDialog extends LitElement { @property({ attribute: false }) endpoint!: number; - @query("md-outlined-text-field[label='target node id']") + @query("md-outlined-text-field[label='node id']") private _targetNodeId!: MdOutlinedTextField; - @query("md-outlined-text-field[label='target endpoint']") + @query("md-outlined-text-field[label='endpoint']") private _targetEndpoint!: MdOutlinedTextField; private fetchBindingEntry(): BindingEntryStruct[] { @@ -49,6 +54,7 @@ export class NodeBindingDialog extends LitElement { private fetchACLEntry(targetNodeId: number): AccessControlEntryStruct[] { const acl_cluster_raw: [InputType] = this.client.nodes[targetNodeId].attributes["0/31/0"]; + return Object.values(acl_cluster_raw).map((value: InputType) => AccessControlEntryDataTransformer.transform(value), ); @@ -58,7 +64,11 @@ export class NodeBindingDialog extends LitElement { const rawBindings = this.fetchBindingEntry(); try { const targetNodeId = rawBindings[index].node; - await this.removeNodeAtACLEntry(this.node!.node_id, targetNodeId); + await this.removeNodeAtACLEntry( + this.node!.node_id, + this.endpoint, + targetNodeId, + ); const updatedBindings = this.removeBindingAtIndex(rawBindings, index); await this.syncBindingUpdates(updatedBindings, index); } catch (error) { @@ -68,32 +78,36 @@ export class NodeBindingDialog extends LitElement { private async removeNodeAtACLEntry( sourceNodeId: number, + sourceEndpoint: number, targetNodeId: number, ): Promise { const aclEntries = this.fetchACLEntry(targetNodeId); const updatedACLEntries = aclEntries - .map((entry) => this.removeSubjectAtACL(sourceNodeId, entry)) + .map((entry) => + this.removeEntryAtACL(sourceNodeId, sourceEndpoint, entry), + ) .filter((entry): entry is Exclude => entry !== null); + console.log(updatedACLEntries); await this.client.setACLEntry(targetNodeId, updatedACLEntries); } - private removeSubjectAtACL( + private removeEntryAtACL( nodeId: number, + sourceEndpoint: number, entry: AccessControlEntryStruct, ): AccessControlEntryStruct | null { - const shouldRemoveSubject = entry.subjects?.includes(nodeId); + const hasSubject = entry.subjects!.includes(nodeId); - if (!shouldRemoveSubject) return entry; + if (!hasSubject) return entry; - const updatedSubjects = entry.subjects.filter( - (nodeId) => nodeId !== this.node!.node_id, + const hasTarget = entry.targets!.filter( + (item) => item.endpoint === sourceEndpoint, ); - - return updatedSubjects.length > 0 - ? { ...entry, subjects: updatedSubjects } - : null; + return hasTarget.length > 0 + ? null + : entry; } private removeBindingAtIndex( @@ -200,11 +214,17 @@ export class NodeBindingDialog extends LitElement { return; } + const targets: AccessControlTargetStruct = { + endpoint: targetEndpoint, + cluster: undefined, + deviceType: undefined, + }; + const acl_entry: AccessControlEntryStruct = { privilege: 5, authMode: 2, subjects: [this.node!.node_id], - targets: undefined, + targets: [targets], fabricIndex: this.client.connection.serverInfo!.fabric_id, }; const result_acl = await this.add_target_acl(targetNodeId, acl_entry); @@ -235,23 +255,25 @@ export class NodeBindingDialog extends LitElement { } protected render() { - const bindings = this.node!.attributes[this.endpoint + "/30/0"]; + const bindings = Object.values( + this.node!.attributes[this.endpoint + "/30/0"], + ).map((entry) => BindingEntryDataTransformer.transform(entry)); return html`
-
Binding Add
+
Binding
- + ${Object.values(bindings).map( (entry, index) => html` - -
- ${JSON.stringify( - BindingEntryDataTransformer.transform(entry), - )} + +
+
node:${entry["node"]}
+
endpoint:${entry["endpoint"]}
+
fabricIndex:${entry["fabricIndex"]}
-
+
+
target
@@ -279,6 +304,36 @@ export class NodeBindingDialog extends LitElement { `; } + + static styles = css` + .inline-group { + display: flex; + border: 2px solid #673ab7; + padding: 1px; + border-radius: 8px; + position: relative; + margin: 8px; + } + + .target-item { + display: inline-block; + padding: 20px 10px 10px 10px; + border-radius: 4px; + vertical-align: middle; + min-width: 80px; + text-align: center; + } + + .group-label { + position: absolute; + left: 15px; + top: -12px; + background: #673ab7; + color: white; + padding: 3px 15px; + border-radius: 4px; + } + `; } declare global { From 58eb24701b34293af22c8b685218b799eba9ba7c Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Tue, 20 May 2025 17:39:19 +0800 Subject: [PATCH 09/10] refactor: remove unused commands. --- matter_server/common/models.py | 2 -- matter_server/server/device_controller.py | 40 ++--------------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/matter_server/common/models.py b/matter_server/common/models.py index 695464b8..fb897502 100644 --- a/matter_server/common/models.py +++ b/matter_server/common/models.py @@ -51,9 +51,7 @@ class APICommand(str, Enum): CHECK_NODE_UPDATE = "check_node_update" UPDATE_NODE = "update_node" SET_DEFAULT_FABRIC_LABEL = "set_default_fabric_label" - GET_ACL_ENTRY = "get_acl_entry" SET_ACL_ENTRY = "set_acl_entry" - GET_NODE_BINDINGS = "get_node_bindings" SET_NODE_BINDING = "set_node_binding" diff --git a/matter_server/server/device_controller.py b/matter_server/server/device_controller.py index d904d516..492c36b6 100644 --- a/matter_server/server/device_controller.py +++ b/matter_server/server/device_controller.py @@ -20,7 +20,7 @@ from chip.ChipDeviceCtrl import ChipDeviceController from chip.clusters import Attribute, Objects as Clusters -from chip.clusters.Attribute import SubscriptionTransaction, ValueDecodeFailure +from chip.clusters.Attribute import AttributeWriteResult, ValueDecodeFailure from chip.clusters.ClusterObjects import ALL_ATTRIBUTES, ALL_CLUSTERS, Cluster from chip.discovery import DiscoveryType from chip.exceptions import ChipStackError @@ -867,53 +867,19 @@ async def set_acl_entry( self, node_id: int, entry: list[Clusters.AccessControl.Structs.AccessControlEntryStruct], - ): + ) -> list[AttributeWriteResult] | None: """Set acl entry""" return await self._chip_device_controller.write_attribute( node_id, [(0, Clusters.AccessControl.Attributes.Acl(entry))] ) - @api_command(APICommand.GET_ACL_ENTRY) - async def get_acl_entry(self, node_id: int): - """Get acl entry""" - read_response: ( - SubscriptionTransaction | Attribute.AsyncReadTransaction.ReadResponse | None - ) = await self._chip_device_controller.read_attribute( - node_id, [(0, Clusters.AccessControl.Attributes.Acl)] - ) - acl_entities = [] - if isinstance(read_response, Attribute.AsyncReadTransaction.ReadResponse): - acl_entities: list[ - Clusters.AccessControl.Structs.AccessControlEntryStruct - ] = read_response.attributes[0][Clusters.AccessControl][ - Clusters.AccessControl.Attributes.Acl - ] - return acl_entities - - @api_command(APICommand.GET_NODE_BINDINGS) - async def get_node_bindings(self, node_id: int): - """Get node bindings""" - read_response: ( - SubscriptionTransaction | Attribute.AsyncReadTransaction.ReadResponse | None - ) = await self._chip_device_controller.read_attribute( - node_id, (Clusters.Binding.Attributes.Binding,) - ) - - bindings = [] - if isinstance(read_response, Attribute.AsyncReadTransaction.ReadResponse): - for k, v in read_response.attributes.items(): - bindings.append( - {k: v[Clusters.Binding][Clusters.Binding.Attributes.Binding]} - ) - return bindings - @api_command(APICommand.SET_NODE_BINDING) async def set_node_binding( self, node_id: int, endpoint: int, bindings: list[Clusters.Binding.Structs.TargetStruct], - ): + ) -> list[AttributeWriteResult] | None: """Set node binding""" return await self._chip_device_controller.write_attribute( node_id, [(endpoint, Clusters.Binding.Attributes.Binding(bindings))] From 4231e23a8e77c8e8cdf106252b4b16af15ef2614 Mon Sep 17 00:00:00 2001 From: luxni <307993459@qq.com> Date: Fri, 23 May 2025 14:38:41 +0800 Subject: [PATCH 10/10] refactor: add cluster to binding command. --- dashboard/note.md | 12 ++++++++ .../dialogs/binding/node-binding-dialog.ts | 29 ++++++++++++++----- .../src/pages/components/node-details.ts | 6 ++-- 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 dashboard/note.md diff --git a/dashboard/note.md b/dashboard/note.md new file mode 100644 index 00000000..12a6d1b2 --- /dev/null +++ b/dashboard/note.md @@ -0,0 +1,12 @@ +``` +
+
${"privilege:" + entry.privilege}
+
${"authMode:" + entry.authMode}
+
${"fabricIndex:" + entry.fabricIndex}
+
+
subjects:${JSON.stringify(entry.subjects)}
+
targets:${JSON.stringify(entry.targets)}
+ +``` diff --git a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts index 40304fd2..2d7dc6db 100644 --- a/dashboard/src/components/dialogs/binding/node-binding-dialog.ts +++ b/dashboard/src/components/dialogs/binding/node-binding-dialog.ts @@ -6,7 +6,7 @@ import "../../../components/ha-svg-icon"; import "@material/web/textfield/outlined-text-field"; import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; -import { html, LitElement, css } from "lit"; +import { html, LitElement, css, nothing } from "lit"; import { customElement, property, query } from "lit/decorators.js"; import { MatterNode } from "../../../client/models/node"; import { preventDefault } from "../../../util/prevent_default"; @@ -44,6 +44,9 @@ export class NodeBindingDialog extends LitElement { @query("md-outlined-text-field[label='endpoint']") private _targetEndpoint!: MdOutlinedTextField; + @query("md-outlined-text-field[label='cluster']") + private _targetCluster!: MdOutlinedTextField; + private fetchBindingEntry(): BindingEntryStruct[] { const bindings_raw: [] = this.node!.attributes[this.endpoint + "/30/0"]; return Object.values(bindings_raw).map((value) => @@ -105,9 +108,7 @@ export class NodeBindingDialog extends LitElement { const hasTarget = entry.targets!.filter( (item) => item.endpoint === sourceEndpoint, ); - return hasTarget.length > 0 - ? null - : entry; + return hasTarget.length > 0 ? null : entry; } private removeBindingAtIndex( @@ -204,6 +205,7 @@ export class NodeBindingDialog extends LitElement { async addBindingHandler() { const targetNodeId = parseInt(this._targetNodeId.value, 10); const targetEndpoint = parseInt(this._targetEndpoint.value, 10); + const targetCluster = parseInt(this._targetCluster.value, 10); if (isNaN(targetNodeId) || targetNodeId <= 0) { alert("Please enter a valid target node ID"); @@ -213,10 +215,14 @@ export class NodeBindingDialog extends LitElement { alert("Please enter a valid target endpoint"); return; } + if (isNaN(targetCluster) || targetCluster < 0) { + alert("Please enter a valid target endpoint"); + return; + } const targets: AccessControlTargetStruct = { endpoint: targetEndpoint, - cluster: undefined, + cluster: targetCluster, deviceType: undefined, }; @@ -235,13 +241,16 @@ export class NodeBindingDialog extends LitElement { node: targetNodeId, endpoint: targetEndpoint, group: undefined, - cluster: undefined, - fabricIndex: undefined, + cluster: targetCluster, + fabricIndex: this.client.connection.serverInfo!.fabric_id, }; const result_binding = await this.add_bindings(endpoint, bindingEntry); if (result_binding) { + this._targetNodeId.value = ""; + this._targetEndpoint.value = ""; + this._targetCluster.value = ""; this.requestUpdate(); } } @@ -273,7 +282,7 @@ export class NodeBindingDialog extends LitElement {
node:${entry["node"]}
endpoint:${entry["endpoint"]}
-
fabricIndex:${entry["fabricIndex"]}
+ ${entry["cluster"] ? html`
cluster:${entry["cluster"]}
` : nothing}
+
diff --git a/dashboard/src/pages/components/node-details.ts b/dashboard/src/pages/components/node-details.ts index a7cda4cb..8cea5ce0 100644 --- a/dashboard/src/pages/components/node-details.ts +++ b/dashboard/src/pages/components/node-details.ts @@ -108,9 +108,9 @@ export class NodeDetails extends LitElement { : html`Update`} ${bindings - ? html` - - Binding + ? html` + + Binding `