import { mkdirSync } from "node:fs"; import { Environment, StorageService } from "@matter/main"; import { CommissioningController } from "@project-chip/matter.js"; import type { Config } from "./config.js"; import { Log, type LogLevel } from "./logging.js"; const log = new Log("controller"); /** * Fabric label shown to other Matter administrators (e.g. in Apple Home's * connected-services list). Maximum 32 characters. */ const FABRIC_LABEL = "mattertimesync"; /** Storage namespace under the storage path; also the controller's unique environment id. */ const CONTROLLER_ID = "mattertimesync"; /** * matter.js library log level derived from the service log level. At service * level "info" the library is kept at "warn" so protocol internals (discovery, * session management) only appear when debugging. */ function matterLogLevel(serviceLevel: LogLevel): string { return serviceLevel === "debug" ? "debug" : serviceLevel === "info" ? "warn" : serviceLevel; } /** * Creates and starts the Matter controller with persistent storage rooted at * the configured storage path. The fabric identity (root certificate, keys, * node allocation state) persists across restarts; a fabric is only generated * on the very first start. */ export async function startController(config: Config): Promise { mkdirSync(config.storagePath, { recursive: true, mode: 0o700 }); const environment = Environment.default; environment.vars.set("storage.path", config.storagePath); environment.vars.set("log.level", matterLogLevel(config.logLevel)); const storage = environment.get(StorageService); log.debug(`Matter storage location: ${storage.location}`); const controller = new CommissioningController({ environment: { environment, id: CONTROLLER_ID }, autoConnect: false, adminFabricLabel: FABRIC_LABEL, }); await controller.start(); const commissioned = controller.getCommissionedNodes(); log.info( `Controller started (fabric ${controller.fabric.fabricId}, ${commissioned.length} commissioned node${ commissioned.length === 1 ? "" : "s" })`, ); return controller; } /** All commissioned node IDs in controller storage. */ export function commissionedNodeIds(controller: CommissioningController): bigint[] { return controller.getCommissionedNodes().map(id => BigInt(id)); } /** * Resolves the set of nodes a command operates on: the explicitly requested * one, or every commissioned node. */ export function resolveTargetNodes(controller: CommissioningController, requested?: bigint): bigint[] { const commissioned = commissionedNodeIds(controller); if (commissioned.length === 0) { throw new Error('No device is commissioned yet. Run "commission" first.'); } if (requested === undefined) return commissioned; if (!commissioned.includes(requested)) { throw new Error( `Node ${requested} is not commissioned on this controller ` + `(known nodes: ${commissioned.join(", ")}). Run "nodes" to list them.`, ); } return [requested]; } /** * Resolves exactly one node for commands that cannot meaningfully operate on * several (inspect, fabrics, decommission). --node is required once more than * one device is commissioned. */ export function resolveSingleNode(controller: CommissioningController, requested?: bigint): bigint { const targets = resolveTargetNodes(controller, requested); if (targets.length > 1) { throw new Error( `Multiple nodes are commissioned (${targets.join(", ")}); pass --node to choose one.`, ); } return targets[0]!; }