src.nth.io/

summaryrefslogtreecommitdiff
path: root/src/controller.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/controller.ts')
-rw-r--r--src/controller.ts102
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]!;
+}