From de73ec8a5ed033db94eac017538c49951e0a5755 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 16 Jul 2024 11:21:19 +0200 Subject: [PATCH 1/4] extend client --- dashboard/src/client/client.ts | 25 +++++++++++++++---------- dashboard/src/client/models/model.ts | 8 +++++++- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/dashboard/src/client/client.ts b/dashboard/src/client/client.ts index ae306426..3baf0757 100644 --- a/dashboard/src/client/client.ts +++ b/dashboard/src/client/client.ts @@ -5,6 +5,7 @@ import { APICommands, ErrorResultMessage, EventMessage, + NodePingResult, SuccessResultMessage, } from "./models/model"; @@ -40,20 +41,16 @@ export class MatterClient { }; } - async commissionWithCode(code: string, networkOnly: boolean) { - console.log("TODO"); - } - - async commissionOnNetwork(setup_pin_code: number, ip_addr: string) { - console.log("TODO"); + async commissionWithCode(code: string, networkOnly: boolean): Promise { + return await this.sendCommand("commission_with_code", 0, { code: code, network_only: networkOnly }) as MatterNode; } async setWifiCredentials(ssid: string, credentials: string) { - console.log("TODO"); + await this.sendCommand("set_wifi_credentials", 0, { ssid, credentials }) } async setThreadOperationalDataset(dataset: string) { - console.log("TODO"); + await this.sendCommand("set_thread_dataset", 0, { dataset }) } async openCommissioningWindow( @@ -78,8 +75,12 @@ export class MatterClient { console.log("TODO"); } - async pingNode(nodeId: number) { - await this.sendCommand("ping_node", 0, { node_id: nodeId }); + async pingNode(nodeId: number): Promise { + return await this.sendCommand("ping_node", 0, { node_id: nodeId }); + } + + async getNodeIPAddresses(nodeId: number, preferCache = false, scoped = false): Promise { + return await this.sendCommand("get_node_ip_addresses", 0, { node_id: nodeId, prefer_cache: preferCache, scoped: scoped }); } async removeNode(nodeId: number) { @@ -94,6 +95,10 @@ export class MatterClient { await this.sendCommand("import_test_node", 0, { dump }); } + async writeAttribute(nodeId: number, attributePath: string, value: any) { + await this.sendCommand("write_attribute", 0, { node_id: nodeId, attribute_path: attributePath, value: value }); + } + async sendCommand( command: T, require_schema: number | undefined = undefined, diff --git a/dashboard/src/client/models/model.ts b/dashboard/src/client/models/model.ts index f45efe80..497c3d09 100644 --- a/dashboard/src/client/models/model.ts +++ b/dashboard/src/client/models/model.ts @@ -75,12 +75,16 @@ export interface APICommands { }; ping_node: { requestArgs: {}; - response: {}; + response: NodePingResult; }; import_test_node: { requestArgs: { dump: string; }; response: null; }; + get_node_ip_addresses: { + requestArgs: { node_id: number, prefer_cache: boolean, scoped: boolean }; + response: string[]; + }; } export interface CommandMessage { @@ -97,6 +101,7 @@ export interface ServerInfoMessage { sdk_version: string; wifi_credentials_set: boolean; thread_credentials_set: boolean; + bluetooth_enabled: boolean; } interface ServerEventNodeAdded { @@ -156,3 +161,4 @@ export interface WebSocketConfig { } export type NotificationType = "success" | "info" | "warning" | "error"; +export type NodePingResult = Record; From fae771df529515e0b3311a1842cfc8d949fef253 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 16 Jul 2024 11:54:55 +0200 Subject: [PATCH 2/4] some more missing methods --- dashboard/src/client/client.ts | 47 +++++++++++++++++----- dashboard/src/client/models/model.ts | 59 ++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/dashboard/src/client/client.ts b/dashboard/src/client/client.ts index 3baf0757..481831af 100644 --- a/dashboard/src/client/client.ts +++ b/dashboard/src/client/client.ts @@ -3,8 +3,10 @@ import { Connection } from "./connection"; import { InvalidServerVersion } from "./exceptions"; import { APICommands, + CommissioningParameters, ErrorResultMessage, EventMessage, + MatterSoftwareVersion, NodePingResult, SuccessResultMessage, } from "./models/model"; @@ -42,25 +44,33 @@ export class MatterClient { } async commissionWithCode(code: string, networkOnly: boolean): Promise { + // Commission a device using a QR Code or Manual Pairing Code. + // code: The QR Code or Manual Pairing Code for device commissioning. + // network_only: If True, restricts device discovery to network only. + // Returns: The NodeInfo of the commissioned device. return await this.sendCommand("commission_with_code", 0, { code: code, network_only: networkOnly }) as MatterNode; } async setWifiCredentials(ssid: string, credentials: string) { + // Set WiFi credentials for commissioning to a (new) device. await this.sendCommand("set_wifi_credentials", 0, { ssid, credentials }) } async setThreadOperationalDataset(dataset: string) { + // Set Thread Operational dataset in the stack. await this.sendCommand("set_thread_dataset", 0, { dataset }) } async openCommissioningWindow( nodeId: number, - timeout = 300, - iteration = 1000, - option = 1, - distriminator: number | undefined = undefined - ) { - console.log("TODO"); + timeout?: number, + iteration?: number, + option?: number, + distriminator?: number + ): Promise { + // Open a commissioning window to commission a device present on this controller to another. + // Returns code to use as discriminator. + return await this.sendCommand("open_commissioning_window", 0, { node_id: nodeId, timeout, iteration, option, distriminator }) as CommissioningParameters; } async discoverCommissionableNodes() { @@ -76,11 +86,13 @@ export class MatterClient { } async pingNode(nodeId: number): Promise { - return await this.sendCommand("ping_node", 0, { node_id: nodeId }); + // Ping node on the currently known IP-address(es). + return await this.sendCommand("ping_node", 0, { node_id: nodeId }) as NodePingResult; } - async getNodeIPAddresses(nodeId: number, preferCache = false, scoped = false): Promise { - return await this.sendCommand("get_node_ip_addresses", 0, { node_id: nodeId, prefer_cache: preferCache, scoped: scoped }); + async getNodeIPAddresses(nodeId: number, preferCache?: boolean, scoped?: boolean): Promise { + // Return the currently known (scoped) IP-address(es). + return await this.sendCommand("get_node_ip_addresses", 8, { node_id: nodeId, prefer_cache: preferCache, scoped: scoped }) as string[]; } async removeNode(nodeId: number) { @@ -99,6 +111,23 @@ export class MatterClient { await this.sendCommand("write_attribute", 0, { node_id: nodeId, attribute_path: attributePath, value: value }); } + async checkNodeUpdate(nodeId: number): Promise { + // Check if there is an update for a particular node. + // Reads the current software version and checks the DCL if there is an update + // available. If there is an update available, the command returns the version + // information of the latest update available. + return await this.sendCommand("check_node_update", 10, { node_id: nodeId }); + } + + async updateNode(nodeId: number, softwareVersion: number | string) { + // Update a node to a new software version. + // This command checks if the requested software version is indeed still available + // and if so, it will start the update process. The update process will be handled + // by the built-in OTA provider. The OTA provider will download the update and + // notify the node about the new update. + await this.sendCommand("update_node", 10, { node_id: nodeId, software_version: softwareVersion }); + } + async sendCommand( command: T, require_schema: number | undefined = undefined, diff --git a/dashboard/src/client/models/model.ts b/dashboard/src/client/models/model.ts index 497c3d09..f15ed780 100644 --- a/dashboard/src/client/models/model.ts +++ b/dashboard/src/client/models/model.ts @@ -75,15 +75,23 @@ export interface APICommands { }; ping_node: { requestArgs: {}; - response: NodePingResult; + response: {}; }; import_test_node: { - requestArgs: { dump: string; }; + requestArgs: {}; response: null; }; get_node_ip_addresses: { - requestArgs: { node_id: number, prefer_cache: boolean, scoped: boolean }; - response: string[]; + requestArgs: {}; + response: {}; + }; + check_node_update: { + requestArgs: {}; + response: MatterSoftwareVersion | null; + }; + update_node: { + requestArgs: {}; + response: MatterSoftwareVersion | null; }; } @@ -160,5 +168,48 @@ export interface WebSocketConfig { path: string; } +export enum UpdateSource { + MAIN_NET_DCL = "main-net-dcl", + TEST_NET_DCL = "test-net-dcl", + LOCAL = "local" +} + +export interface MatterSoftwareVersion { + vid: number + pid: number + software_version: number + software_version_string: string + firmware_information?: string + min_applicable_software_version: number + max_applicable_software_version: number + release_notes_url?: string + update_source: UpdateSource +} + +export interface CommissioningParameters { + setup_pin_code: number + setup_manual_code: string + setup_qr_code: string +} + +export interface CommissionableNodeData { + instance_name?: string + host_name?: string + port?: number + long_discriminator?: number + vendor_id?: number + product_id?: number + commissioning_mode?: number + device_type?: number + device_name?: string + pairing_instruction?: string + pairing_hint?: number + mrp_retry_interval_idle?: number + mrp_retry_interval_active?: number + supports_tcp?: boolean + addresses?: string[] + rotating_id?: string +} + export type NotificationType = "success" | "info" | "warning" | "error"; export type NodePingResult = Record; From abc0c917aaf8f58923517fa782292ab55b88db81 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 16 Jul 2024 12:14:55 +0200 Subject: [PATCH 3/4] add remaining implementations to client and models --- dashboard/src/client/client.ts | 27 +++++++++++++++++++++------ dashboard/src/client/models/model.ts | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/dashboard/src/client/client.ts b/dashboard/src/client/client.ts index 481831af..5ef1472a 100644 --- a/dashboard/src/client/client.ts +++ b/dashboard/src/client/client.ts @@ -3,9 +3,11 @@ import { Connection } from "./connection"; import { InvalidServerVersion } from "./exceptions"; import { APICommands, + CommissionableNodeData, CommissioningParameters, ErrorResultMessage, EventMessage, + MatterFabricData, MatterSoftwareVersion, NodePingResult, SuccessResultMessage, @@ -73,16 +75,20 @@ export class MatterClient { return await this.sendCommand("open_commissioning_window", 0, { node_id: nodeId, timeout, iteration, option, distriminator }) as CommissioningParameters; } - async discoverCommissionableNodes() { - console.log("TODO"); + async discoverCommissionableNodes(): Promise { + // Discover Commissionable Nodes (discovered on BLE or mDNS). + return await this.sendCommand("discover_commissionable_nodes", 0, {}) as CommissionableNodeData[]; } - async getMatterFabrics(nodeId: number) { - console.log("TODO"); + async getMatterFabrics(nodeId: number): Promise { + // Get Matter fabrics from a device. + // Returns a list of MatterFabricData objects. + return await this.sendCommand("get_matter_fabrics", 3, {}) as MatterFabricData[]; } - async removeMatterFabric(nodeId: number, fabricId: number) { - console.log("TODO"); + async removeMatterFabric(nodeId: number, fabricIndex: number) { + // Remove a Matter fabric from a device. + await this.sendCommand("remove_matter_fabric", 3, { node_id: nodeId, fabric_index: fabricIndex }); } async pingNode(nodeId: number): Promise { @@ -96,18 +102,27 @@ export class MatterClient { } async removeNode(nodeId: number) { + // Remove a Matter node/device from the fabric. await this.sendCommand("remove_node", 0, { node_id: nodeId }); } async interviewNode(nodeId: number) { + // Interview a node. await this.sendCommand("interview_node", 0, { node_id: nodeId }); } async importTestNode(dump: string) { + // Import test node(s) from a HA or Matter server diagnostics dump. await this.sendCommand("import_test_node", 0, { dump }); } + async readAttribute(nodeId: number, attributePath: string | string[]): Promise> { + // Read one or more attribute(s) on a node by specifying an attributepath. + return await this.sendCommand("read_attribute", 0, { node_id: nodeId, attribute_path: attributePath }); + } + async writeAttribute(nodeId: number, attributePath: string, value: any) { + // Write an attribute(value) on a target node. await this.sendCommand("write_attribute", 0, { node_id: nodeId, attribute_path: attributePath, value: value }); } diff --git a/dashboard/src/client/models/model.ts b/dashboard/src/client/models/model.ts index f15ed780..438005d1 100644 --- a/dashboard/src/client/models/model.ts +++ b/dashboard/src/client/models/model.ts @@ -93,6 +93,18 @@ export interface APICommands { requestArgs: {}; response: MatterSoftwareVersion | null; }; + discover_commissionable_nodes: { + requestArgs: {}; + response: {}; + }; + get_matter_fabrics: { + requestArgs: {}; + response: {}; + }; + remove_matter_fabric: { + requestArgs: {}; + response: {}; + }; } export interface CommandMessage { @@ -211,5 +223,15 @@ export interface CommissionableNodeData { rotating_id?: string } +export interface MatterFabricData { + fabric_id?: number + vendor_id?: number + fabric_index?: number + fabric_label?: string + vendor_name?: string +} + + + export type NotificationType = "success" | "info" | "warning" | "error"; export type NodePingResult = Record; From 78f826488ef8562e9bbf54831785c3a8c684e811 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 16 Jul 2024 13:58:33 +0200 Subject: [PATCH 4/4] update serverinfo when setting credentials --- dashboard/src/client/client.ts | 6 ++++++ dashboard/src/client/models/model.ts | 6 +++++- matter_server/common/models.py | 1 + matter_server/server/device_controller.py | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/dashboard/src/client/client.ts b/dashboard/src/client/client.ts index 5ef1472a..3bffe338 100644 --- a/dashboard/src/client/client.ts +++ b/dashboard/src/client/client.ts @@ -267,6 +267,12 @@ export class MatterClient { this.fireEvent("nodes_changed"); return; } + + if (event.event === "server_info_updated") { + this.connection.serverInfo = event.data; + this.fireEvent("server_info_updated"); + return; + } } private fireEvent(event: string, data?: any) { diff --git a/dashboard/src/client/models/model.ts b/dashboard/src/client/models/model.ts index 438005d1..a791c3fe 100644 --- a/dashboard/src/client/models/model.ts +++ b/dashboard/src/client/models/model.ts @@ -156,8 +156,12 @@ interface ServerEventEndpointRemoved { event: "endpoint_removed"; data: {}; } +interface ServerEvenInfoUpdated { + event: "server_info_updated"; + data: ServerInfoMessage; +} -export type EventMessage = ServerEventNodeAdded | ServerEventNodeUpdated | ServerEventNodeRemoved | ServerEventNodeEvent | ServerEventAttributeUpdated | ServerEventServerShutdown | ServerEventEndpointAdded | ServerEventEndpointRemoved +export type EventMessage = ServerEventNodeAdded | ServerEventNodeUpdated | ServerEventNodeRemoved | ServerEventNodeEvent | ServerEventAttributeUpdated | ServerEventServerShutdown | ServerEventEndpointAdded | ServerEventEndpointRemoved | ServerEvenInfoUpdated export interface ResultMessageBase { diff --git a/matter_server/common/models.py b/matter_server/common/models.py index f049782d..4c291840 100644 --- a/matter_server/common/models.py +++ b/matter_server/common/models.py @@ -20,6 +20,7 @@ class EventType(Enum): NODE_EVENT = "node_event" ATTRIBUTE_UPDATED = "attribute_updated" SERVER_SHUTDOWN = "server_shutdown" + SERVER_INFO_UPDATED = "server_info_updated" ENDPOINT_ADDED = "endpoint_added" ENDPOINT_REMOVED = "endpoint_removed" diff --git a/matter_server/server/device_controller.py b/matter_server/server/device_controller.py index 0ae9a299..9d4d48f8 100644 --- a/matter_server/server/device_controller.py +++ b/matter_server/server/device_controller.py @@ -429,6 +429,7 @@ async def set_wifi_credentials(self, ssid: str, credentials: str) -> None: await self._chip_device_controller.set_wifi_credentials(ssid, credentials) self._wifi_credentials_set = True + self.server.signal_event(EventType.SERVER_INFO_UPDATED, self.server.get_info()) @api_command(APICommand.SET_THREAD_DATASET) async def set_thread_operational_dataset(self, dataset: str) -> None: @@ -436,6 +437,7 @@ async def set_thread_operational_dataset(self, dataset: str) -> None: await self._chip_device_controller.set_thread_operational_dataset(dataset) self._thread_credentials_set = True + self.server.signal_event(EventType.SERVER_INFO_UPDATED, self.server.get_info()) @api_command(APICommand.OPEN_COMMISSIONING_WINDOW) async def open_commissioning_window(