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]!;
}
|