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
97
98
99
100
101
|
import { ManualPairingCodeCodec, QrPairingCodeCodec } from "@matter/main/types";
import { GeneralCommissioning } from "@matter/main/clusters/general-commissioning";
import type { CommissioningController, NodeCommissioningOptions } from "@project-chip/matter.js";
import type { Config } from "./config.js";
import { Log } from "./logging.js";
import { serviceStatePath, updateNodeState } from "./state.js";
const log = new Log("commissioning");
export interface CommissioningResult {
nodeId: bigint;
}
interface ParsedPairingCode {
passcode: number;
shortDiscriminator?: number;
longDiscriminator?: number;
}
/**
* Parses an Apple-generated Matter multi-admin pairing code. Accepts the
* 11/21-digit manual code (with or without hyphens) or an "MT:" QR payload.
* The code is used only in memory and never logged or stored.
*/
export function parsePairingCode(code: string): ParsedPairingCode {
const trimmed = code.trim();
if (trimmed.length === 0) {
throw new Error("Pairing code is empty");
}
if (trimmed.toUpperCase().startsWith("MT:")) {
const [payload] = QrPairingCodeCodec.decode(trimmed);
if (payload === undefined) {
throw new Error("QR pairing code contains no payload");
}
return { passcode: payload.passcode, longDiscriminator: payload.discriminator };
}
const digits = trimmed.replace(/[-\s]/g, "");
if (!/^\d{11}$|^\d{21}$/.test(digits)) {
throw new Error("Manual pairing code must be 11 or 21 digits");
}
const decoded = ManualPairingCodeCodec.decode(digits);
return {
passcode: decoded.passcode,
shortDiscriminator: decoded.shortDiscriminator,
longDiscriminator: decoded.discriminator,
};
}
/**
* Commissions a device as an additional Matter administrator using on-network
* (IP) discovery only. The device stays joined to its existing primary fabric
* (e.g. Apple Home); no Thread credentials are involved.
*
* Any number of devices may be commissioned, one pairing code each; a device
* that is already on this fabric rejects the attempt itself (fabric
* conflict), so duplicates cannot occur.
*/
export async function commissionDevice(
controller: CommissioningController,
config: Config,
pairingCode: string,
): Promise<CommissioningResult> {
const existing = controller.getCommissionedNodes();
if (existing.length > 0) {
log.info(
`${existing.length} node${existing.length === 1 ? "" : "s"} already commissioned; ` +
`adding another device to the same fabric.`,
);
}
const parsed = parsePairingCode(pairingCode);
const identifierData =
parsed.longDiscriminator !== undefined
? { longDiscriminator: parsed.longDiscriminator }
: parsed.shortDiscriminator !== undefined
? { shortDiscriminator: parsed.shortDiscriminator }
: {};
log.info("Discovering commissionable device on the IP network (_matterc._udp)...");
const options: NodeCommissioningOptions = {
commissioning: {
regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
regulatoryCountryCode: "XX",
},
discovery: {
identifierData,
discoveryCapabilities: { onIpNetwork: true },
},
passcode: parsed.passcode,
};
const nodeId = await controller.commissionNode(options, { connectNodeAfterCommissioning: false });
const nodeIdBigint = BigInt(nodeId);
updateNodeState(serviceStatePath(config.storagePath), nodeIdBigint, {});
log.info(`Commissioning complete: node ${nodeIdBigint}`);
return { nodeId: nodeIdBigint };
}
|