src.nth.io/

summaryrefslogtreecommitdiff
path: root/src/controller.ts
blob: b4d27f3af3bd4e99fcc2cba356b2a23fc5b177f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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<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]!;
}