/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { capitalize, ChannelType, decamelize, Diagnostic, ServerAddress } from "@matter/general"; import { ClientNode, CommissioningClient, NetworkClient, SoftwareUpdateManager } from "@matter/node"; import { PeerAddress, PeerSet } from "@matter/protocol"; import { FabricIndex, NodeId, VendorId } from "@matter/types"; import { CommissioningControllerNodeOptions, NodeStateInformation } from "@project-chip/matter.js/device"; import type { Argv } from "yargs"; import { MatterNode } from "../MatterNode.js"; export function createDiagnosticCallbacks(): Partial { return { attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) => console.log( `attributeChangedCallback ${peerNodeId}: Attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Diagnostic.json( value, )}`, ), eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) => console.log( `eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Diagnostic.json( events, )}`, ), stateInformationCallback: (peerNodeId, info) => { switch (info) { case NodeStateInformation.Connected: console.log(`stateInformationCallback Node ${peerNodeId} connected`); break; case NodeStateInformation.Disconnected: console.log(`stateInformationCallback Node ${peerNodeId} disconnected`); break; case NodeStateInformation.Reconnecting: console.log(`stateInformationCallback Node ${peerNodeId} reconnecting`); break; case NodeStateInformation.WaitingForDeviceDiscovery: console.log( `stateInformationCallback Node ${peerNodeId} waiting that device gets discovered again`, ); break; case NodeStateInformation.StructureChanged: console.log(`stateInformationCallback Node ${peerNodeId} structure changed`); break; case NodeStateInformation.Decommissioned: console.log(`stateInformationCallback Node ${peerNodeId} decommissioned`); break; } }, }; } /** Parse a `udp://host:port` / `tcp://host:port` URL (IPv6 host in brackets) into a {@link ServerAddress}. */ function parseFallbackAddress(input: string): ServerAddress { const match = /^(udp|tcp):\/\/(.+)$/i.exec(input); if (!match) { throw new Error(`Invalid address "${input}". Expected udp://: or tcp://:`); } const type = match[1].toLowerCase() === "tcp" ? "tcp" : "udp"; const rest = match[2]; let ip: string; let portStr: string; if (rest.startsWith("[")) { const end = rest.indexOf("]"); if (end === -1 || rest[end + 1] !== ":") { throw new Error(`Invalid IPv6 address "${input}". Expected ${type}://[]:`); } ip = rest.slice(1, end); portStr = rest.slice(end + 2); } else { const idx = rest.lastIndexOf(":"); if (idx === -1) { throw new Error(`Missing port in "${input}". Expected ${type}://:`); } ip = rest.slice(0, idx); portStr = rest.slice(idx + 1); } const port = Number(portStr); if (!ip.length || !Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`Invalid host/port in "${input}". Expected ${type}://: with port 1-65535`); } return { ip, port, type }; } export default function commands(theNode: MatterNode) { return { command: ["nodes", "node"], describe: "Manage nodes", builder: (yargs: Argv) => yargs // Pair .command( ["*", "list [status]"], "List all commissioned nodes", yargs => { return yargs.positional("status", { describe: "status", options: ["commissioned", "connected"] as const, default: "commissioned", type: "string", }); }, async argv => { const { status } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } switch (status) { case "commissioned": { const details = theNode.commissioningController.getCommissionedNodesDetails(); details .map(detail => ({ ...detail, nodeId: detail.nodeId.toString(), })) .forEach(detail => { console.log(detail); }); break; } case "connected": { const nodeIds = theNode.commissioningController .getCommissionedNodes() .filter(nodeId => !!theNode.commissioningController?.getPairedNode(nodeId)); console.log(nodeIds.map(nodeId => nodeId.toString())); break; } } }, ) .command( "log [node-id]", "Log the Structure of one node", yargs => { return yargs.positional("node-id", { describe: "node id to log - if omitted the first node is logged.", default: undefined, type: "string", }); }, async argv => { const { nodeId } = argv; const node = (await theNode.connectAndGetNodes(nodeId))[0]; console.log("Logging structure of Node ", node.nodeId.toString()); node.logStructure(); }, ) .command( "descriptor ", "Show peer descriptor and transport details for a node", yargs => { return yargs.positional("node-id", { describe: "node id", type: "string", demandOption: true, }); }, async argv => { const { nodeId: nodeIdStr } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } const nodeId = NodeId(BigInt(nodeIdStr)); const peerAddress = theNode.commissioningController.fabric.addressOf(nodeId); const peerSet = theNode.node.env.get(PeerSet); const peer = peerSet.for(peerAddress); if (!peer) { console.log(`Peer ${nodeIdStr} not found`); return; } const desc = peer.descriptor; console.log(`\nPeer Descriptor for node ${nodeIdStr}:`); console.log(` Address: ${desc.address}`); console.log( ` Operational Address: ${desc.operationalAddress ? ServerAddress.urlFor(desc.operationalAddress) : "(unknown)"}`, ); console.log( ` Transport Preference: ${peer.transportPreference === ChannelType.TCP ? "TCP" : "UDP (default)"}`, ); if (desc.discoveryData) { const dd = desc.discoveryData; console.log(` Discovery Data:`); if (dd.DN) console.log(` Device Name: ${dd.DN}`); if (dd.VP) console.log(` Vendor/Prod: ${dd.VP}`); if (dd.DT !== undefined) console.log(` Device Type: ${dd.DT}`); if (dd.T !== undefined) { console.log(` TCP Support: client=${dd.T.tcpClient}, server=${dd.T.tcpServer}`); } else { console.log(` TCP Support: not advertised`); } if (dd.SII) console.log(` Idle Interval: ${dd.SII}ms`); if (dd.SAI) console.log(` Active Interval: ${dd.SAI}ms`); } if (desc.sessionParameters) { const sp = desc.sessionParameters; console.log(` Session Parameters:`); console.log(` Supported Transports: ${Diagnostic.json(sp.supportedTransports)}`); if (sp.maxTcpMessageSize !== undefined) { console.log(` Max TCP Message Size: ${sp.maxTcpMessageSize}`); } } const sessions = [...peer.sessions]; if (sessions.length) { console.log(` Active Sessions: ${sessions.length}`); for (const session of sessions) { console.log(` ${session.via} (${session.channel.transportChannel.type})`); } } else { console.log(` Active Sessions: none`); } console.log(); }, ) .command( "connect [node-id] [min-subscription-interval] [max-subscription-interval]", "Connects to one or all commissioned nodes", yargs => { return yargs .positional("node-id", { describe: "node id to connect. Use 'all' to connect to all nodes.", default: "all", type: "string", demandOption: true, }) .positional("min-subscription-interval", { describe: "Minimum subscription interval in seconds. If set then the node is subscribed to all attributes and events.", type: "number", }) .positional("max-subscription-interval", { describe: "Maximum subscription interval in seconds. If minimum interval is set and this not it will be determined automatically.", type: "number", }); }, async argv => { const { nodeId: nodeIdStr, maxSubscriptionInterval, minSubscriptionInterval } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } let nodeIds = theNode.commissioningController.getCommissionedNodes(); if (nodeIdStr !== "all") { const cmdNodeId = NodeId(BigInt(nodeIdStr)); nodeIds = nodeIds.filter(nodeId => nodeId === cmdNodeId); if (!nodeIds.length) { throw new Error(`Node ${nodeIdStr} not commissioned`); } } const autoSubscribe = minSubscriptionInterval !== undefined; for (const nodeIdToProcess of nodeIds) { const node = await theNode.commissioningController.getNode(nodeIdToProcess); node.connect({ autoSubscribe, subscribeMinIntervalFloorSeconds: autoSubscribe ? minSubscriptionInterval : undefined, subscribeMaxIntervalCeilingSeconds: autoSubscribe ? maxSubscriptionInterval : undefined, ...createDiagnosticCallbacks(), }); } }, ) .command( "disconnect [node-id]", "Disconnects from one or all nodes", yargs => { return yargs.positional("node-id", { describe: "node id to disconnect. Use 'all' to disconnect from all nodes.", default: "all", type: "string", }); }, async argv => { const { nodeId: nodeIdStr } = argv; if (theNode.commissioningController === undefined) { console.log("Controller not initialized, nothing to disconnect."); return; } let nodeIds = theNode.commissioningController.getCommissionedNodes(); if (nodeIdStr !== "all") { const cmdNodeId = NodeId(BigInt(nodeIdStr)); nodeIds = nodeIds.filter(nodeId => nodeId === cmdNodeId); if (!nodeIds.length) { throw new Error(`Node ${nodeIdStr} not commissioned`); } } for (const nodeIdToProcess of nodeIds) { const node = theNode.commissioningController.getPairedNode(nodeIdToProcess); if (node === undefined) { console.log(`Node ${nodeIdToProcess} not connected`); continue; } await node.disconnect(); } }, ) .command( "status [node-ids]", "Logs the connection status for all or specified nodes", yargs => { return yargs.positional("node-ids", { describe: "node ids to connect (comma separated list allowed). Use 'all' to log status for all nodes.", default: "all", type: "string", }); }, async argv => { const { nodeIds: nodeIdStr } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } let nodeIds = theNode.commissioningController.getCommissionedNodes(); if (nodeIdStr !== "all") { const nodeIdList = nodeIdStr.split(",").map(nodeId => NodeId(BigInt(nodeId))); nodeIds = nodeIds.filter(nodeId => nodeIdList.includes(nodeId)); if (!nodeIds.length) { throw new Error(`Node ${nodeIdStr} not commissioned`); } } const nodeDetails = theNode.commissioningController.getCommissionedNodesDetails(); for (const nodeIdToProcess of nodeIds) { const node = theNode.commissioningController.getPairedNode(nodeIdToProcess); if (node === undefined) { const details = nodeDetails.find(nd => nd.nodeId === nodeIdToProcess); console.log( `Node ${nodeIdToProcess}: Not initialized${details?.deviceData?.basicInformation !== undefined ? ` (${details.deviceData.basicInformation.vendorName} ${details.deviceData.basicInformation.productName})` : ""}`, ); } else { const basicInfo = node.basicInformation; console.log( `Node ${nodeIdToProcess}: Node Status: ${capitalize(decamelize(NodeStateInformation[node.connectionState], " "))}${basicInfo !== undefined ? ` (${basicInfo.vendorName} ${basicInfo.productName})` : ""}`, ); } } }, ) .command( "tcp ", "Set TCP transport preference for a node", yargs => { return yargs .positional("node-id", { describe: "node id", type: "string", demandOption: true, }) .positional("preference", { describe: "tcp preference: on (prefer TCP) / off (prefer UDP) / clear (inherit controller default)", choices: ["on", "off", "clear"], demandOption: true, type: "string", }); }, async argv => { const { nodeId: nodeIdStr, preference } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } const nodeId = NodeId(BigInt(nodeIdStr)); const node = await theNode.commissioningController.getNode(nodeId); const pref = preference === "on" ? "tcp" : preference === "off" ? "udp" : undefined; await node.node.setStateOf(NetworkClient, { transportPreference: pref }); // Also update the protocol-level peer preference const peer = theNode.node.env .get(PeerSet) .for(theNode.commissioningController.fabric.addressOf(nodeId)); if (peer) { peer.transportPreference = pref === "tcp" ? ChannelType.TCP : undefined; } console.log( `Transport preference for node ${nodeIdStr} set to ${pref?.toUpperCase() ?? "CLEARED (inherit controller default)"}. Reconnect to the node to take effect.`, ); }, ) .command( "fallback", "Get or set the fallback (commissioning) addresses used to reach a node", yargs => yargs .command( "get ", "Print the fallback addresses currently stored for a node", yargs => { return yargs.positional("node-id", { describe: "node id", type: "string", demandOption: true, }); }, async argv => { const { nodeId: nodeIdStr } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } const nodeId = NodeId(BigInt(nodeIdStr)); const node = await theNode.commissioningController.getNode(nodeId); const addresses = node.node.state.commissioning.addresses; if (!addresses?.length) { console.log(`Node ${nodeIdStr} has no fallback addresses stored`); return; } console.log(`Fallback addresses for node ${nodeIdStr}:`); for (const address of addresses) { console.log(` ${ServerAddress.urlFor(address)}`); } }, ) .command( "set
", "Set or drop the fallback address for a node (udp://host:port, tcp://host:port, or 'drop')", yargs => { return yargs .positional("node-id", { describe: "node id", type: "string", demandOption: true, }) .positional("address", { describe: "udp://:, tcp://:, or 'drop' to remove", type: "string", demandOption: true, }); }, async argv => { const { nodeId: nodeIdStr, address: addressStr } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } const nodeId = NodeId(BigInt(nodeIdStr)); const node = await theNode.commissioningController.getNode(nodeId); if (addressStr === "drop") { await node.node.setStateOf(CommissioningClient, { addresses: undefined }); console.log(`Fallback address for node ${nodeIdStr} dropped`); return; } const address = parseFallbackAddress(addressStr); await node.node.setStateOf(CommissioningClient, { addresses: [address] }); console.log( `Fallback address for node ${nodeIdStr} set to ${ServerAddress.urlFor(address)}`, ); }, ) .demandCommand(1, "Please specify 'get' or 'set'"), async (argv: any) => { argv.unhandled = true; }, ) .command( "add [node-id]", "Adds a node without commissioning and connects to it (means need to exist in the fabric and commissioned otherwise)", yargs => { return yargs .positional("node-id", { describe: "node id to connect.", default: "all", type: "string", demandOption: true, }) .positional("min-subscription-interval", { describe: "Minimum subscription interval in seconds. If set then the node is subscribed to all attributes and events.", type: "number", }) .positional("max-subscription-interval", { describe: "Maximum subscription interval in seconds. If minimum interval is set and this not it will be determined automatically.", type: "number", }); }, async argv => { const { nodeId: nodeIdStr, maxSubscriptionInterval, minSubscriptionInterval } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } let nodeIds = theNode.commissioningController.getCommissionedNodes(); const cmdNodeId = NodeId(BigInt(nodeIdStr)); nodeIds = nodeIds.filter(nodeId => nodeId === cmdNodeId); if (nodeIds.length) { throw new Error(`Node ${nodeIdStr} already known`); } await theNode.commissioningController.node.peers.forAddress( theNode.commissioningController.fabric.addressOf(cmdNodeId), ); const autoSubscribe = minSubscriptionInterval !== undefined; const node = await theNode.commissioningController.getNode(cmdNodeId); node.connect({ autoSubscribe, subscribeMinIntervalFloorSeconds: autoSubscribe ? minSubscriptionInterval : undefined, subscribeMaxIntervalCeilingSeconds: autoSubscribe ? maxSubscriptionInterval : undefined, ...createDiagnosticCallbacks(), }); }, ) .command( "ota", "OTA update operations for nodes", yargs => yargs .command( "known [node-id]", "List which OTA updates are known to be available for commissioned nodes. Only nodes that are connected and subscribed are considered.", yargs => { return yargs .positional("node-id", { describe: "Node ID to check for updates", type: "string", default: undefined, }) .option("local", { describe: "include local update files", type: "boolean", default: false, }); }, async argv => { const { nodeId: nodeIdStr, local } = argv; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } let peerToCheck: ClientNode | undefined = undefined; if (nodeIdStr !== undefined) { const nodeId = NodeId(BigInt(nodeIdStr)); peerToCheck = (await theNode.commissioningController.getNode(nodeId))?.node; } const updatesAvailable = await theNode.commissioningController.otaProvider.act( agent => agent .get(SoftwareUpdateManager) .queryUpdates({ peerToCheck, includeStoredUpdates: local }), ); if (updatesAvailable.length) { console.log(`OTA updates available for ${updatesAvailable.length} nodes:`); for (const { peerAddress, info } of updatesAvailable) { console.log( peerAddress.toString(), `: new Version: ${info.softwareVersion} (${info.softwareVersionString})`, ); } } else { console.log("No OTA updates available."); } }, ) .command( "check ", "Check for OTA updates for a commissioned node", yargs => { return yargs .positional("node-id", { describe: "Node ID to check for updates", type: "string", demandOption: true, }) .option("mode", { describe: "DCL mode (prod or test)", type: "string", choices: ["prod", "test", "both"], default: "prod", }) .option("local", { describe: "include local update files", type: "boolean", default: false, }); }, async argv => { const { nodeId: nodeIdStr, mode, local } = argv; const isProduction = mode === "prod" ? true : mode === "test" ? false : undefined; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } const nodeId = NodeId(BigInt(nodeIdStr)); const nodeDetails = theNode.commissioningController .getCommissionedNodesDetails() .find(nd => nd.nodeId === nodeId); const basicInfo = nodeDetails?.deviceData?.basicInformation; if (!basicInfo) { throw new Error(`Node ${nodeIdStr} has no basic information available`); } if ( basicInfo.vendorId === undefined || basicInfo.productId === undefined || basicInfo.softwareVersion === undefined ) { throw new Error( `Node ${nodeIdStr} is missing required basic information for OTA check`, ); } console.log(`Checking for OTA updates for node ${nodeIdStr}...`); console.log( ` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId as VendorId, 4).toUpperCase()}`, ); console.log( ` Product ID: ${Diagnostic.hex(basicInfo.productId as number, 4).toUpperCase()}`, ); console.log( ` Current Software Version: ${basicInfo.softwareVersion} (${basicInfo.softwareVersionString})`, ); console.log(` DCL Mode: ${mode}\n`); const updateInfo = await ( await theNode.otaService() ).checkForUpdate({ vendorId: basicInfo.vendorId as VendorId, productId: basicInfo.productId as number, currentSoftwareVersion: basicInfo.softwareVersion as number, includeStoredUpdates: local, isProduction, }); if (updateInfo) { console.log("✓ Update available!"); console.log( ` New Version: ${updateInfo.softwareVersion} (${updateInfo.softwareVersionString})`, ); console.log(` OTA URL: ${updateInfo.otaUrl}`); if (updateInfo.otaFileSize) { const sizeKB = Number(updateInfo.otaFileSize) / 1024; console.log(` File Size: ${sizeKB.toFixed(2)} KB`); } if (updateInfo.releaseNotesUrl) { console.log(` Release Notes: ${updateInfo.releaseNotesUrl}`); } console.log( `\nRun "nodes ota download ${nodeIdStr}${mode === "test" ? " --mode test" : ""}" to download this update.`, ); } else { console.log("✓ No updates available. Device is up to date."); } }, ) .command( "download ", "Download OTA update for a commissioned node", yargs => { return yargs .positional("node-id", { describe: "Node ID to download update for", type: "string", demandOption: true, }) .option("mode", { describe: "DCL mode (prod or test)", type: "string", choices: ["prod", "test", "both"], default: "prod", }) .option("force", { describe: "Force download even if update is already stored locally", type: "boolean", default: false, }) .option("local", { describe: "include local update files", type: "boolean", default: false, }); }, async argv => { const { nodeId: nodeIdStr, mode, force, local } = argv; const isProduction = mode === "prod" ? true : mode === "test" ? false : undefined; const forceDownload = force === true; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } const nodeId = NodeId(BigInt(nodeIdStr)); const nodeDetails = theNode.commissioningController .getCommissionedNodesDetails() .find(nd => nd.nodeId === nodeId); const basicInfo = nodeDetails?.deviceData?.basicInformation; if (!basicInfo) { throw new Error(`Node ${nodeIdStr} has no basic information available`); } if ( basicInfo.vendorId === undefined || basicInfo.productId === undefined || basicInfo.softwareVersion === undefined ) { throw new Error( `Node ${nodeIdStr} is missing required basic information for OTA check`, ); } console.log(`Checking for OTA updates for node ${nodeIdStr}...`); console.log( ` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId as VendorId, 4).toUpperCase()}`, ); console.log( ` Product ID: ${Diagnostic.hex(basicInfo.productId as number, 4).toUpperCase()}`, ); console.log( ` Current Software Version: ${basicInfo.softwareVersion} (${basicInfo.softwareVersionString})`, ); console.log(` DCL Mode: ${mode}\n`); const updateInfo = await ( await theNode.otaService() ).checkForUpdate({ vendorId: basicInfo.vendorId as VendorId, productId: basicInfo.productId as number, currentSoftwareVersion: basicInfo.softwareVersion as number, includeStoredUpdates: local, isProduction, }); if (!updateInfo) { console.log("No updates available. Device is up to date."); return; } console.log("Update found:"); console.log( ` New Version: ${updateInfo.softwareVersion} (${updateInfo.softwareVersionString})`, ); console.log(` OTA URL: ${updateInfo.otaUrl}`); console.log(` Source: ${updateInfo.source}`); if (updateInfo.otaFileSize) { const sizeKB = Number(updateInfo.otaFileSize) / 1024; console.log(` File Size: ${sizeKB.toFixed(2)} KB`); } console.log("\nDownloading update..."); const fd = await ( await theNode.otaService() ).downloadUpdate(updateInfo, forceDownload); console.log(`✓ Update downloaded and stored successfully: ${fd.text}`); console.log( `\nYou can now apply this update to the device using your device's OTA mechanism.`, ); }, ) .command( "apply ", "Apply OTA update for a commissioned node", yargs => { return yargs .positional("node-id", { describe: "Node ID to download update for", type: "string", demandOption: true, }) .option("mode", { describe: "DCL mode (prod or test)", type: "string", choices: ["prod", "test", "both"], default: "prod", }) .option("force", { describe: "Force download even if update is already stored locally", type: "boolean", default: false, }) .option("local", { describe: "Apply update from local file", type: "boolean", default: false, }); }, async argv => { const { nodeId: nodeIdStr, mode, force, local } = argv; const isProduction = mode === "prod" ? true : mode === "test" ? false : undefined; const forceDownload = force === true; await theNode.start(); if (theNode.commissioningController === undefined) { throw new Error("CommissioningController not initialized"); } const nodeId = NodeId(BigInt(nodeIdStr)); const nodeDetails = theNode.commissioningController .getCommissionedNodesDetails() .find(nd => nd.nodeId === nodeId); const basicInfo = nodeDetails?.deviceData?.basicInformation; if (!basicInfo) { throw new Error(`Node ${nodeIdStr} has no basic information available`); } if ( basicInfo.vendorId === undefined || basicInfo.productId === undefined || basicInfo.softwareVersion === undefined ) { throw new Error( `Node ${nodeIdStr} is missing required basic information for OTA check`, ); } console.log(`Checking for OTA updates for node ${nodeIdStr}...`); console.log( ` Vendor ID: ${Diagnostic.hex(basicInfo.vendorId as VendorId, 4).toUpperCase()}`, ); console.log( ` Product ID: ${Diagnostic.hex(basicInfo.productId as number, 4).toUpperCase()}`, ); console.log( ` Current Software Version: ${basicInfo.softwareVersion} (${basicInfo.softwareVersionString})`, ); console.log(` DCL Mode: ${mode}\n`); const localUpdates = await ( await theNode.otaService() ).find({ vendorId: basicInfo.vendorId as VendorId, productId: basicInfo.productId as number, currentVersion: basicInfo.softwareVersion as number, }); if (local && !localUpdates.length) { console.log("No applicable updates available in local storage."); return; } const updateInfo = await ( await theNode.otaService() ).checkForUpdate({ vendorId: basicInfo.vendorId as VendorId, productId: basicInfo.productId as number, currentSoftwareVersion: basicInfo.softwareVersion as number, includeStoredUpdates: local, isProduction, }); let updateVersion: number; if (!updateInfo && !local) { console.log("No updates available in DCL. Device is up to date."); return; } else if (updateInfo) { console.log("Update found:"); console.log( ` New Version: ${updateInfo.softwareVersion} (${updateInfo.softwareVersionString})`, ); console.log(` OTA URL: ${updateInfo.otaUrl}`); if (updateInfo.otaFileSize) { const sizeKB = Number(updateInfo.otaFileSize) / 1024; console.log(` File Size: ${sizeKB.toFixed(2)} KB`); } console.log("\nDownloading update..."); const fd = await ( await theNode.otaService() ).downloadUpdate(updateInfo, forceDownload); updateVersion = updateInfo.softwareVersion; console.log( `✓ Update to version ${updateVersion} (${updateInfo.softwareVersionString}) downloaded and stored successfully: ${fd.text}`, ); } else { updateVersion = localUpdates[0].softwareVersion; console.log( `Update to version ${updateVersion} (${localUpdates[0].softwareVersionString}) found in local storage: ${localUpdates[0].filename}`, ); } const node = theNode.commissioningController.getPairedNode(nodeId); if (node === undefined) { throw new Error(`Node ${nodeIdStr} not connected`); } await theNode.commissioningController.otaProvider.act(agent => { return agent .get(SoftwareUpdateManager) .forceUpdate( PeerAddress({ nodeId, fabricIndex: FabricIndex(1) }), basicInfo.vendorId as VendorId, basicInfo.productId as number, updateVersion, ); }); }, ) .demandCommand(1, "Please specify an OTA subcommand"), async (argv: any) => { argv.unhandled = true; }, ), handler: async (argv: any) => { argv.unhandled = true; }, }; }