diff options
Diffstat (limited to 'src/controller.ts')
| -rw-r--r-- | src/controller.ts | 102 |
1 files changed, 102 insertions, 0 deletions
diff --git a/src/controller.ts b/src/controller.ts new file mode 100644 index 0000000..ee65d82 --- /dev/null +++ b/src/controller.ts @@ -0,0 +1,102 @@ +import { mkdirSync } from "node:fs"; +import { Environment, LogFormat, Logger, StorageService } from "@matter/main"; +import { CommissioningController } from "@project-chip/matter.js"; +import type { Config } from "./config.js"; +import { Log, type LogLevel } from "./logging.js"; + +// matter.js library logs go to stderr, like the service's own logs, so +// stdout carries only command output (in particular clean --json). Plain +// format: the default ANSI escapes would land in the systemd journal. +Logger.format = LogFormat.PLAIN; +Logger.destinations["default"]!.write = text => process.stderr.write(text + "\n"); + +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<CommissioningController> { + 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 <id> to choose one.`, + ); + } + return targets[0]!; +} |
