import { ManualPairingCodeCodec, QrPairingCodeCodec } from "@matter/main/types"; import { GeneralCommissioning } from "@matter/main/clusters/general-commissioning"; import type { CommissioningController, NodeCommissioningOptions } from "@project-chip/matter.js"; import type { Config } from "./config.js"; import { Log } from "./logging.js"; import { serviceStatePath, updateNodeState } from "./state.js"; const log = new Log("commissioning"); export interface CommissioningResult { nodeId: bigint; } interface ParsedPairingCode { passcode: number; shortDiscriminator?: number; longDiscriminator?: number; } /** * Parses an Apple-generated Matter multi-admin pairing code. Accepts the * 11/21-digit manual code (with or without hyphens) or an "MT:" QR payload. * The code is used only in memory and never logged or stored. */ export function parsePairingCode(code: string): ParsedPairingCode { const trimmed = code.trim(); if (trimmed.length === 0) { throw new Error("Pairing code is empty"); } if (trimmed.toUpperCase().startsWith("MT:")) { const [payload] = QrPairingCodeCodec.decode(trimmed); if (payload === undefined) { throw new Error("QR pairing code contains no payload"); } return { passcode: payload.passcode, longDiscriminator: payload.discriminator }; } const digits = trimmed.replace(/[-\s]/g, ""); if (!/^\d{11}$|^\d{21}$/.test(digits)) { throw new Error("Manual pairing code must be 11 or 21 digits"); } const decoded = ManualPairingCodeCodec.decode(digits); return { passcode: decoded.passcode, shortDiscriminator: decoded.shortDiscriminator, longDiscriminator: decoded.discriminator, }; } /** * Commissions a device as an additional Matter administrator using on-network * (IP) discovery only. The device stays joined to its existing primary fabric * (e.g. Apple Home); no Thread credentials are involved. * * Any number of devices may be commissioned, one pairing code each; a device * that is already on this fabric rejects the attempt itself (fabric * conflict), so duplicates cannot occur. */ export async function commissionDevice( controller: CommissioningController, config: Config, pairingCode: string, ): Promise { const existing = controller.getCommissionedNodes(); if (existing.length > 0) { log.info( `${existing.length} node${existing.length === 1 ? "" : "s"} already commissioned; ` + `adding another device to the same fabric.`, ); } const parsed = parsePairingCode(pairingCode); const identifierData = parsed.longDiscriminator !== undefined ? { longDiscriminator: parsed.longDiscriminator } : parsed.shortDiscriminator !== undefined ? { shortDiscriminator: parsed.shortDiscriminator } : {}; log.info("Discovering commissionable device on the IP network (_matterc._udp)..."); const options: NodeCommissioningOptions = { commissioning: { regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor, regulatoryCountryCode: "XX", }, discovery: { identifierData, discoveryCapabilities: { onIpNetwork: true }, }, passcode: parsed.passcode, }; const nodeId = await controller.commissionNode(options, { connectNodeAfterCommissioning: false }); const nodeIdBigint = BigInt(nodeId); updateNodeState(serviceStatePath(config.storagePath), nodeIdBigint, {}); log.info(`Commissioning complete: node ${nodeIdBigint}`); return { nodeId: nodeIdBigint }; }