@project-chip/matter.js
Version:
Matter protocol in pure js
848 lines (749 loc) • 33.3 kB
text/typescript
/**
* @license
* Copyright 2022-2025 Matter.js Authors
* SPDX-License-Identifier: Apache-2.0
*/
import { OperationalCredentials } from "#clusters";
import { ControllerStore } from "#ControllerStore.js";
import {
ClassExtends,
Crypto,
Environment,
ImplementationError,
InternalError,
Logger,
NetInterfaceSet,
Network,
NoAddressAvailableError,
NoProviderError,
StorageContext,
SyncStorage,
UdpInterface,
UnexpectedDataError,
} from "#general";
import { LegacyControllerStore } from "#LegacyControllerStore.js";
import {
Ble,
CommissionableDevice,
CommissionableDeviceIdentifiers,
ControllerCommissioningFlow,
ControllerDiscovery,
DecodedAttributeReportValue,
DiscoveryAndCommissioningOptions,
DiscoveryData,
InteractionClient,
MdnsBroadcaster,
MdnsScanner,
MdnsService,
MessageChannel,
NodeDiscoveryType,
ScannerSet,
} from "#protocol";
import {
CaseAuthenticatedTag,
DiscoveryCapabilitiesBitmap,
FabricId,
FabricIndex,
NodeId,
TypeFromPartialBitSchema,
VendorId,
} from "#types";
import { CertificateAuthority, Fabric, MdnsScannerTargetCriteria } from "@matter/protocol";
import { CommissioningControllerNodeOptions, NodeStates, PairedNode } from "./device/PairedNode.js";
import { MatterController } from "./MatterController.js";
const logger = new Logger("CommissioningController");
// TODO how to enhance "getting devices" as API? Or is getDevices() enough?
// TODO decline using setRoot*Cluster
// TODO Decline cluster access after announced/paired
export type ControllerEnvironmentOptions = {
/**
* Environment to register the node with on start()
*/
readonly environment: Environment;
/**
* Unique id to register to node.
*/
readonly id: string;
};
/**
* Constructor options for the CommissioningController class
*/
export type CommissioningControllerOptions = CommissioningControllerNodeOptions & {
/**
* Local port number to use for the UDP interface. By default, a random port number will be generated
* (strongly recommended!).
*/
readonly localPort?: number;
/** Listening address for IPv4. By default, the interface will listen on all IPv4 addresses. */
readonly listeningAddressIpv4?: string;
/** Listening address for IPv6. By default, the interface will listen on all IPv6 addresses. */
readonly listeningAddressIpv6?: string;
/**
* If set to false, the controller will not connect to any device on startup. You need to use connectNode() or
* connect() to connect to the relevant nodes in this case. Else all nodes are connected on startup.
* */
readonly autoConnect?: boolean;
/** Admin Vendor ID used for all commissioning operations. Cannot be changed afterward. Default: 0xFFF1 */
readonly adminVendorId?: VendorId;
/**
* Controller own Fabric ID used to initialize the Controller the first time and to generate the Root certificate.
* Cannot be changed afterward.
* Default: 1
*/
readonly adminFabricId?: FabricId;
/**
* Fabric Index used to initialize the Controller the first time. Cannot be changed afterward.
* Default: 1
*/
readonly adminFabricIndex?: FabricIndex;
/**
* CASE Authenticated Tags used to initialize the Controller the first time. Cannot be changed afterward.
* Maximum 3 tags are supported.
*/
readonly caseAuthenticatedTags?: CaseAuthenticatedTag[];
/**
* The Fabric Label to set for the commissioned devices. The #label is used for users to identify the admin.
* The maximum length are 32 characters!
* The value will automatically be checked on connection to a node and updated if necessary.
*/
readonly adminFabricLabel: string;
/**
* When used with the new API Environment set the environment here and the CommissioningServer will self-register
* on the environment when you call start().
*/
readonly environment?: ControllerEnvironmentOptions;
/**
* The NodeId of the root node to use for the controller. This is only needed if a special NodeId needs to be used
* but certificates should be self-generated. By default, a random operational ID is generated.
*/
readonly rootNodeId?: NodeId;
/**
* If provided this Certificate Authority instance is used to fetch or get all relevant certificates for the
* Controller. If not provided a new Certificate Authority instance is created and certificates will be self-generated.
*/
readonly rootCertificateAuthority?: CertificateAuthority;
/**
* If provided this Fabric instance is used for this controller. The instance need to be in sync with the provided
* or stored certificate authority. If provided then rootFabricId, rootFabricIndex and rootFabricLabel are ignored.
*/
readonly rootFabric?: Fabric;
};
/** Options needed to commission a new node */
export type NodeCommissioningOptions = CommissioningControllerNodeOptions & {
commissioning: Omit<DiscoveryAndCommissioningOptions, "fabric" | "discovery" | "passcode">;
discovery: DiscoveryAndCommissioningOptions["discovery"];
passcode: number;
};
/** Controller class to commission and connect multiple nodes into one fabric. */
export class CommissioningController {
#crypto: Crypto;
#started = false;
#ipv4Disabled?: boolean;
readonly #listeningAddressIpv4?: string;
readonly #listeningAddressIpv6?: string;
readonly #options: CommissioningControllerOptions;
#environment?: Environment; // Set when new API was initialized correctly
#storage?: StorageContext;
#mdnsScanner?: MdnsScanner;
#mdnsBroadcaster?: MdnsBroadcaster;
#controllerInstance?: MatterController;
readonly #initializedNodes = new Map<NodeId, PairedNode>();
readonly #nodeUpdateLabelHandlers = new Map<NodeId, (nodeState: NodeStates) => Promise<void>>();
readonly #sessionDisconnectedHandler = new Map<NodeId, () => Promise<void>>();
#mdnsTargetCriteria?: MdnsScannerTargetCriteria;
/**
* Creates a new CommissioningController instance
*
* @param options The options for the CommissioningController
*/
constructor(options: CommissioningControllerOptions) {
this.#options = options;
this.#crypto = (options.environment?.environment ?? Environment.default).get(Crypto);
this.#crypto.reportUsage();
}
get crypto() {
return this.#crypto;
}
get nodeId() {
return this.#controllerInstance?.nodeId;
}
/** Returns the configuration data needed to create a PASE commissioner, e.g. in a mobile app. */
get paseCommissionerConfig() {
const controller = this.#assertControllerIsStarted(
"The CommissioningController needs to be started to get the PASE commissioner data.",
);
const { caConfig, fabricConfig: fabricData } = controller;
return {
caConfig,
fabricData,
};
}
#assertIsAddedToMatterServer() {
if (this.#mdnsScanner === undefined || (this.#storage === undefined && this.#environment === undefined)) {
throw new ImplementationError("Add the node to the Matter instance before.");
}
if (!this.#started) {
throw new ImplementationError("The node needs to be started before interacting with the controller.");
}
return { mdnsScanner: this.#mdnsScanner, storage: this.#storage, environment: this.#environment };
}
#assertControllerIsStarted(errorText?: string) {
if (this.#controllerInstance === undefined) {
throw new ImplementationError(
errorText ?? "Controller instance not yet started. Please call start() first.",
);
}
return this.#controllerInstance;
}
/** Internal method to initialize a MatterController instance. */
async #initializeController() {
const { mdnsScanner, storage, environment } = this.#assertIsAddedToMatterServer();
if (this.#controllerInstance !== undefined) {
return this.#controllerInstance;
}
const {
localPort,
adminFabricId,
adminVendorId,
adminFabricIndex,
caseAuthenticatedTags,
adminFabricLabel,
rootNodeId,
rootCertificateAuthority,
rootFabric,
} = this.#options;
if (environment === undefined && storage === undefined) {
throw new ImplementationError("Storage not initialized correctly.");
}
// Initialize the Storage in a compatible way for the legacy API and new style for new API
// TODO: clean this up when we really implement ControllerNode/ClientNode concepts in new API
const controllerStore = environment?.has(ControllerStore)
? environment.get(ControllerStore)
: new LegacyControllerStore(storage!);
const { netInterfaces, scanners, port } = await configureNetwork({
network: environment?.has(Network) ? environment.get(Network) : Environment.default.get(Network),
ipv4Disabled: this.#ipv4Disabled,
mdnsScanner,
localPort,
listeningAddressIpv4: this.#listeningAddressIpv4,
listeningAddressIpv6: this.#listeningAddressIpv6,
});
const controller = await MatterController.create({
controllerStore,
scanners,
netInterfaces,
sessionClosedCallback: peerNodeId => {
logger.info(`Session for peer node ${peerNodeId} disconnected ...`);
const handler = this.#sessionDisconnectedHandler.get(peerNodeId);
if (handler !== undefined) {
handler().catch(error => logger.warn(`Error while handling session disconnect: ${error}`));
}
},
adminVendorId,
adminFabricId,
adminFabricIndex,
caseAuthenticatedTags,
adminFabricLabel,
rootNodeId,
rootCertificateAuthority,
rootFabric,
});
if (this.#mdnsBroadcaster) {
controller.addBroadcaster(this.#mdnsBroadcaster.createInstanceBroadcaster(port));
}
return controller;
}
/**
* Commissions/Pairs a new device into the controller fabric. The method returns the NodeId of the commissioned
* node on success.
*/
async commissionNode(
nodeOptions: NodeCommissioningOptions,
commissionOptions?: {
connectNodeAfterCommissioning?: boolean;
commissioningFlowImpl?: ClassExtends<ControllerCommissioningFlow>;
},
) {
this.#assertIsAddedToMatterServer();
const controller = this.#assertControllerIsStarted();
const { connectNodeAfterCommissioning = true, commissioningFlowImpl } = commissionOptions ?? {};
const nodeId = await controller.commission(nodeOptions, { commissioningFlowImpl });
if (connectNodeAfterCommissioning) {
const node = await this.connectNode(nodeId, {
...nodeOptions,
autoSubscribe: nodeOptions.autoSubscribe ?? this.#options.autoSubscribe,
subscribeMinIntervalFloorSeconds:
nodeOptions.subscribeMinIntervalFloorSeconds ?? this.#options.subscribeMinIntervalFloorSeconds,
subscribeMaxIntervalCeilingSeconds:
nodeOptions.subscribeMaxIntervalCeilingSeconds ?? this.#options.subscribeMaxIntervalCeilingSeconds,
});
await node.events.initialized;
}
return nodeId;
}
connectPaseChannel(nodeOptions: NodeCommissioningOptions) {
this.#assertIsAddedToMatterServer();
const controller = this.#assertControllerIsStarted();
return controller.connectPaseChannel(nodeOptions);
}
/**
* Completes the commissioning process for a node when the initial commissioning process was done by a PASE
* commissioner. This method should be called to discover the device operational and complete the commissioning
* process.
*/
completeCommissioningForNode(peerNodeId: NodeId, discoveryData?: DiscoveryData) {
this.#assertIsAddedToMatterServer();
const controller = this.#assertControllerIsStarted();
return controller.completeCommissioning(peerNodeId, discoveryData);
}
/** Check if a given node id is commissioned on this controller. */
isNodeCommissioned(nodeId: NodeId) {
const controller = this.#assertControllerIsStarted();
return controller.getCommissionedNodes().includes(nodeId) ?? false;
}
/**
* Remove a Node id from the controller. This method should only be used if the decommission method on the
* PairedNode instance returns an error. By default, it tries to decommission the node from the controller but will
* remove it also in case of an error during decommissioning. Ideally try to decommission the node before and only
* use this in case of an error as last option.
* If this method is used the state of the PairedNode instance might be out of sync, so the PairedNode instance
* should be disconnected first.
*/
async removeNode(nodeId: NodeId, tryDecommissioning = true) {
const controller = this.#assertControllerIsStarted();
const node = this.#initializedNodes.get(nodeId);
let decommissionSuccess = false;
if (tryDecommissioning) {
try {
if (node === undefined) {
throw new ImplementationError(`Node ${nodeId} is not initialized.`);
}
await node.decommission();
decommissionSuccess = true;
} catch (error) {
logger.warn(`Decommissioning node ${nodeId} failed with error, remove node anyway: ${error}`);
}
}
if (node !== undefined) {
node.close(!decommissionSuccess);
}
await controller.removeNode(nodeId);
this.#initializedNodes.delete(nodeId);
}
/** @deprecated Use PairedNode.disconnect() instead */
async disconnectNode(nodeId: NodeId, force = false) {
const node = this.#initializedNodes.get(nodeId);
if (node === undefined && !force) {
throw new ImplementationError(`Node ${nodeId} is not connected!`);
}
await this.#controllerInstance?.disconnect(nodeId);
}
/**
* Returns the PairedNode instance for a given NodeId. The instance is initialized without auto connect if not yet
* created.
*/
async getNode(nodeId: NodeId, allowUnknownNode = false) {
const existingNode = this.#initializedNodes.get(nodeId);
if (existingNode !== undefined) {
return existingNode;
}
return await this.connectNode(nodeId, { autoConnect: false }, allowUnknownNode);
}
/**
* Connect to an already paired Node.
* After connection the endpoint data of the device is analyzed and an object structure is created.
* This call is not blocking and returns an initialized PairedNode instance. The connection or reconnection
* happens in the background. Please monitor the state of the node to see if the connection was successful.
*
* @deprecated Use getNode() instead and call PairedNode.connect() or PairedNode.disconnect() as needed.
*/
async connectNode(nodeId: NodeId, connectOptions?: CommissioningControllerNodeOptions, allowUnknownNode = false) {
const controller = this.#assertControllerIsStarted();
logger.info(`Connecting to node ${nodeId}...`);
const nodeIsCommissioned = controller.getCommissionedNodes().includes(nodeId);
if (!nodeIsCommissioned && !allowUnknownNode) {
throw new ImplementationError(`Node ${nodeId} is not commissioned!`);
}
const existingNode = this.#initializedNodes.get(nodeId);
if (existingNode !== undefined) {
if (!existingNode.initialized) {
existingNode.connect(connectOptions);
}
return existingNode;
}
const pairedNode = await PairedNode.create(
nodeId,
this,
connectOptions,
nodeIsCommissioned ? (this.#controllerInstance?.getCommissionedNodeDetails(nodeId)?.deviceData ?? {}) : {},
await this.createInteractionClient(nodeId, NodeDiscoveryType.None, { forcedConnection: false }), // First connect without discovery to last known address
async (discoveryType?: NodeDiscoveryType) => void (await controller.connect(nodeId, { discoveryType })),
handler => this.#sessionDisconnectedHandler.set(nodeId, handler),
controller.sessions,
this.#crypto,
await this.#collectStoredAttributeData(nodeId),
);
this.#initializedNodes.set(nodeId, pairedNode);
pairedNode.events.initializedFromRemote.on(
async deviceData => await controller.enhanceCommissionedNodeDetails(nodeId, deviceData),
);
return pairedNode;
}
async #collectStoredAttributeData(nodeId: NodeId): Promise<DecodedAttributeReportValue<any>[]> {
const controller = this.#assertControllerIsStarted();
const storedDataVersions = await controller.getStoredClusterDataVersions(nodeId);
const result = new Array<DecodedAttributeReportValue<any>>();
for (const { endpointId, clusterId } of storedDataVersions) {
result.push(...(await controller.retrieveStoredAttributes(nodeId, endpointId, clusterId)));
}
return result;
}
/**
* Connects to all paired nodes.
* After connection the endpoint data of the device is analyzed and an object structure is created.
*
* @deprecated Use getCommissionedNodes() to get the list of nodes and getNode(nodeId) instead and call PairedNode.connect() or PairedNode.disconnect() as needed.
*/
async connect(connectOptions?: CommissioningControllerNodeOptions) {
const controller = this.#assertControllerIsStarted();
if (!controller.isCommissioned()) {
throw new ImplementationError(
"Controller instance not yet paired with any device, so nothing to connect to.",
);
}
for (const nodeId of controller.getCommissionedNodes()) {
await this.connectNode(nodeId, connectOptions);
}
return Array.from(this.#initializedNodes.values());
}
/**
* Set the MDNS Scanner instance. Should be only used internally
*
* @param mdnsScanner MdnsScanner instance
* @private
*/
setMdnsScanner(mdnsScanner: MdnsScanner) {
this.#mdnsScanner = mdnsScanner;
}
/**
* Set the MDNS Broadcaster instance. Should be only used internally
*
* @param mdnsBroadcaster MdnsBroadcaster instance
* @private
*/
setMdnsBroadcaster(mdnsBroadcaster: MdnsBroadcaster) {
this.#mdnsBroadcaster = mdnsBroadcaster;
}
/**
* Set the Storage instance. Should be only used internally
*
* @param storage storage context to use
* @private
*/
setStorage(storage: StorageContext<SyncStorage>) {
this.#storage = storage;
this.#environment = undefined;
}
/** Returns true if t least one node is commissioned/paired with this controller instance. */
isCommissioned() {
const controller = this.#assertControllerIsStarted();
return controller.isCommissioned();
}
/**
* Creates and Return a new InteractionClient to communicate with a node. This is mainly used internally and should
* not be used directly. See the PairedNode class for the public API.
*/
async createInteractionClient(
nodeIdOrChannel: NodeId | MessageChannel,
discoveryType?: NodeDiscoveryType,
options?: {
forcedConnection?: boolean;
},
): Promise<InteractionClient> {
const controller = this.#assertControllerIsStarted();
const { forcedConnection } = options ?? {};
if (nodeIdOrChannel instanceof MessageChannel || !forcedConnection) {
return controller.createInteractionClient(nodeIdOrChannel, { discoveryType });
}
return controller.connect(nodeIdOrChannel, { discoveryType }, forcedConnection);
}
/**
* Returns the PairedNode instance for a given node id, if this node is connected.
* @deprecated Use getNode() instead
*/
getPairedNode(nodeId: NodeId) {
return this.#initializedNodes.get(nodeId);
}
/** Returns an array with the NodeIds of all commissioned nodes. */
getCommissionedNodes() {
const controller = this.#assertControllerIsStarted();
return controller.getCommissionedNodes() ?? [];
}
/** Returns an arra with all commissioned NodeIds and their metadata. */
getCommissionedNodesDetails() {
const controller = this.#assertControllerIsStarted();
return controller.getCommissionedNodesDetails() ?? [];
}
/**
* Disconnects all connected nodes and closes the network connections and other resources of the controller.
* You can use "start()" to restart the controller after closing it.
*/
async close() {
for (const node of this.#initializedNodes.values()) {
node.close();
}
await this.#controllerInstance?.close();
if (this.#mdnsScanner !== undefined && this.#mdnsTargetCriteria !== undefined) {
this.#mdnsScanner.targetCriteriaProviders.delete(this.#mdnsTargetCriteria);
}
this.#controllerInstance = undefined;
this.#initializedNodes.clear();
this.#ipv4Disabled = undefined;
this.#started = false;
}
/** Return the port used by the controller for the UDP interface. */
getPort(): number | undefined {
return this.#options.localPort;
}
/** @private */
initialize(ipv4Disabled: boolean) {
if (this.#started) {
throw new ImplementationError("Controller instance already started.");
}
if (this.#ipv4Disabled !== undefined && this.#ipv4Disabled !== ipv4Disabled) {
throw new ImplementationError(
"Changing the IPv4 disabled flag after starting the controller is not supported.",
);
}
this.#ipv4Disabled = ipv4Disabled;
}
/** @private */
async initializeControllerStore() {
// This can only happen if "MatterServer" approach is not used
if (this.#options.environment === undefined) {
throw new ImplementationError("Initialization not done. Add the controller to the MatterServer first.");
}
const { environment, id } = this.#options.environment;
const controllerStore = await ControllerStore.create(id, environment);
environment.set(ControllerStore, controllerStore);
}
/**
* Initialize the controller and initialize and connect to all commissioned nodes if autoConnect is not set to false.
*/
async start() {
if (this.#ipv4Disabled === undefined) {
if (this.#options.environment === undefined) {
throw new ImplementationError("Initialization not done. Add the controller to the MatterServer first.");
}
const { environment: env } = this.#options.environment;
if (!env.has(ControllerStore)) {
await this.initializeControllerStore();
}
// Load the MDNS service from the environment and set onto the controller
const mdnsService = await env.load(MdnsService);
this.#ipv4Disabled = !mdnsService.enableIpv4;
this.setMdnsBroadcaster(mdnsService.broadcaster);
this.setMdnsScanner(mdnsService.scanner);
this.#environment = env;
const runtime = env.runtime;
runtime.add(this);
}
this.#started = true;
if (this.#controllerInstance === undefined) {
this.#controllerInstance = await this.#initializeController();
}
this.#mdnsTargetCriteria = {
commissionable: true,
operationalTargets: [
{
operationalId: this.#controllerInstance.fabricConfig.operationalId,
},
],
};
this.#mdnsScanner?.targetCriteriaProviders.add(this.#mdnsTargetCriteria);
await this.#controllerInstance.announce();
if (this.#options.autoConnect !== false && this.#controllerInstance.isCommissioned()) {
await this.connect();
}
}
/**
* Cancels the discovery process for commissionable devices started with discoverCommissionableDevices().
*/
cancelCommissionableDeviceDiscovery(
identifierData: CommissionableDeviceIdentifiers,
discoveryCapabilities?: TypeFromPartialBitSchema<typeof DiscoveryCapabilitiesBitmap>,
) {
this.#assertIsAddedToMatterServer();
const controller = this.#assertControllerIsStarted();
controller
.collectScanners(discoveryCapabilities)
.forEach(scanner => ControllerDiscovery.cancelCommissionableDeviceDiscovery(scanner, identifierData));
}
/**
* Starts to discover commissionable devices.
* The promise will be fulfilled after the provided timeout or when the discovery is stopped via
* cancelCommissionableDeviceDiscovery(). The discoveredCallback will be called for each discovered device.
*/
async discoverCommissionableDevices(
identifierData: CommissionableDeviceIdentifiers,
discoveryCapabilities?: TypeFromPartialBitSchema<typeof DiscoveryCapabilitiesBitmap>,
discoveredCallback?: (device: CommissionableDevice) => void,
timeoutSeconds = 900,
) {
this.#assertIsAddedToMatterServer();
const controller = this.#assertControllerIsStarted();
return await ControllerDiscovery.discoverCommissionableDevices(
controller.collectScanners(discoveryCapabilities),
timeoutSeconds,
identifierData,
discoveredCallback,
);
}
/**
* Use this method to reset the Controller storage. The method can only be called if the controller is stopped and
* will remove all commissioning data and paired nodes from the controller.
*/
async resetStorage() {
this.#assertControllerIsStarted(
"Storage cannot be reset while the controller is operating! Please close the controller first.",
);
const { storage, environment } = this.#assertIsAddedToMatterServer();
if (environment !== undefined) {
const controllerStore = environment.get(ControllerStore);
await controllerStore.erase();
} else if (storage !== undefined) {
await storage.clearAll();
} else {
throw new InternalError("Storage not initialized correctly."); // Should not happen
}
}
/** Returns active session information for all connected nodes. */
getActiveSessionInformation() {
return this.#controllerInstance?.getActiveSessionInformation() ?? [];
}
/** @private */
async validateAndUpdateFabricLabel(nodeId: NodeId) {
const controller = this.#assertControllerIsStarted();
const node = this.#initializedNodes.get(nodeId);
if (node === undefined) {
throw new ImplementationError(`Node ${nodeId} is not connected!`);
}
const operationalCredentialsCluster = node.getRootClusterClient(OperationalCredentials.Cluster);
if (operationalCredentialsCluster === undefined) {
throw new UnexpectedDataError(`Node ${nodeId}: Operational Credentials Cluster not available!`);
}
const fabrics = await operationalCredentialsCluster.getFabricsAttribute(false, true);
if (fabrics.length !== 1) {
logger.info(`Invalid fabrics returned from node ${nodeId}.`, fabrics);
return;
}
const label = controller.fabricConfig.label;
const fabric = fabrics[0];
if (fabric.label !== label) {
logger.info(
`Node ${nodeId}: Fabric label "${fabric.label}" does not match requested admin fabric Label "${label}". Updating...`,
);
await operationalCredentialsCluster.updateFabricLabel({
label,
fabricIndex: fabric.fabricIndex,
});
}
}
/**
* Updates the fabric label for the controller and all connected nodes.
* The label is used to identify the controller and all connected nodes in the fabric.
*/
async updateFabricLabel(label: string) {
const controller = this.#assertControllerIsStarted();
if (controller.fabricConfig.label === label) {
return;
}
await controller.updateFabricLabel(label);
for (const node of this.#initializedNodes.values()) {
if (node.isConnected) {
// When Node is connected, update the fabric label on the node directly
try {
await this.validateAndUpdateFabricLabel(node.nodeId);
return;
} catch (error) {
logger.warn(`Error updating fabric label on node ${node.nodeId}:`, error);
}
}
if (!node.remoteInitializationDone) {
// Node not online and was also not yet initialized, means update happens
// automatically when node connects
logger.info(`Node ${node.nodeId} is offline. Fabric label will be updated on next connection.`);
return;
}
logger.info(
`Node ${node.nodeId} is reconnecting. Delaying fabric label update to when node is back online.`,
);
// If no update handler is registered, register one
// TODO: Convert this next to a task system for node tasks and also better handle error cases
if (!this.#nodeUpdateLabelHandlers.has(node.nodeId)) {
const updateOnReconnect = (nodeState: NodeStates) => {
if (nodeState === NodeStates.Connected) {
this.validateAndUpdateFabricLabel(node.nodeId)
.catch(error => logger.warn(`Error updating fabric label on node ${node.nodeId}:`, error))
.finally(() => {
node.events.stateChanged.off(updateOnReconnect);
this.#nodeUpdateLabelHandlers.delete(node.nodeId);
});
}
};
node.events.stateChanged.on(updateOnReconnect);
}
}
}
get groups() {
if (this.#controllerInstance === undefined) {
throw new ImplementationError("Controller instance not yet started. Please call start() first.");
}
return this.#controllerInstance.getFabrics()[0].groups;
}
}
export async function configureNetwork(options: {
network: Network;
ipv4Disabled?: boolean;
mdnsScanner?: MdnsScanner;
localPort?: number;
listeningAddressIpv6?: string;
listeningAddressIpv4?: string;
}) {
const { network, ipv4Disabled, mdnsScanner, localPort, listeningAddressIpv6, listeningAddressIpv4 } = options;
const netInterfaces = new NetInterfaceSet();
const scanners = new ScannerSet();
let udpInterface: UdpInterface;
try {
udpInterface = await UdpInterface.create(network, "udp6", localPort, listeningAddressIpv6);
netInterfaces.add(udpInterface);
} catch (error) {
NoAddressAvailableError.accept(error);
logger.info(`IPv6 UDP interface not created because IPv6 is not available, but required my Matter.`);
throw error;
}
if (!ipv4Disabled) {
// TODO: Add option to transport different ports to broadcaster
try {
netInterfaces.add(await UdpInterface.create(network, "udp4", udpInterface.port, listeningAddressIpv4));
} catch (error) {
NoAddressAvailableError.accept(error);
logger.info(`IPv4 UDP interface not created because IPv4 is not available`);
}
}
if (mdnsScanner) {
scanners.add(mdnsScanner);
}
try {
const ble = Ble.get();
netInterfaces.add(ble.getBleCentralInterface());
scanners.add(ble.getBleScanner());
} catch (e) {
if (e instanceof NoProviderError) {
logger.warn("BLE is not supported on this platform");
} else {
logger.error("Disabling BLE due to initialization error:", e);
}
}
return { netInterfaces, scanners, port: udpInterface.port };
}