UNPKG

matterbridge

Version:
909 lines 145 kB
/** * This file contains the class Matterbridge. * * @file matterbridge.ts * @author Luca Liguori * @date 2023-12-29 * @version 1.6.0 * * Copyright 2023, 2024, 2025 Luca Liguori. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // Node.js modules import os from 'node:os'; import path from 'node:path'; import { promises as fs } from 'node:fs'; import EventEmitter from 'node:events'; import { inspect } from 'node:util'; // AnsiLogger module import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN } from './logger/export.js'; // NodeStorage module import { NodeStorageManager } from './storage/export.js'; // Matterbridge import { getParameter, getIntParameter, hasParameter, copyDirectory, withTimeout, waiter, isValidString, parseVersionString, isValidNumber, createDirectory } from './utils/export.js'; import { logInterfaces, getGlobalNodeModules } from './utils/network.js'; import { dev, plg, typ } from './matterbridgeTypes.js'; import { PluginManager } from './pluginManager.js'; import { DeviceManager } from './deviceManager.js'; import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js'; import { bridge } from './matterbridgeDeviceTypes.js'; import { Frontend } from './frontend.js'; import { addVirtualDevices } from './helpers.js'; import spawn from './utils/spawn.js'; // @matter import { DeviceTypeId, Endpoint, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode, UINT32_MAX, UINT16_MAX, } from '@matter/main'; import { DeviceCommissioner, FabricAction, MdnsService, PaseClient } from '@matter/main/protocol'; import { AggregatorEndpoint } from '@matter/main/endpoints'; import { BasicInformationServer } from '@matter/main/behaviors/basic-information'; import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information'; /** * Represents the Matterbridge application. */ export class Matterbridge extends EventEmitter { systemInformation = { interfaceName: '', macAddress: '', ipv4Address: '', ipv6Address: '', nodeVersion: '', hostname: '', user: '', osType: '', osRelease: '', osPlatform: '', osArch: '', totalMemory: '', freeMemory: '', systemUptime: '', processUptime: '', cpuUsage: '', rss: '', heapTotal: '', heapUsed: '', }; matterbridgeInformation = { homeDirectory: '', rootDirectory: '', matterbridgeDirectory: '', matterbridgePluginDirectory: '', matterbridgeCertDirectory: '', globalModulesDirectory: '', matterbridgeVersion: '', matterbridgeLatestVersion: '', matterbridgeDevVersion: '', matterbridgeSerialNumber: '', matterbridgeQrPairingCode: undefined, matterbridgeManualPairingCode: undefined, matterbridgeFabricInformations: [], matterbridgeSessionInformations: [], matterbridgePaired: false, matterbridgeAdvertise: false, bridgeMode: '', restartMode: '', virtualMode: 'outlet', readOnly: hasParameter('readonly') || hasParameter('shelly'), shellyBoard: hasParameter('shelly'), shellySysUpdate: false, shellyMainUpdate: false, profile: getParameter('profile'), loggerLevel: "info" /* LogLevel.INFO */, fileLogger: false, matterLoggerLevel: MatterLogLevel.INFO, matterFileLogger: false, mattermdnsinterface: undefined, matteripv4address: undefined, matteripv6address: undefined, matterPort: 5540, matterDiscriminator: undefined, matterPasscode: undefined, restartRequired: false, updateRequired: false, }; homeDirectory = ''; rootDirectory = ''; matterbridgeDirectory = ''; matterbridgePluginDirectory = ''; matterbridgeCertDirectory = ''; globalModulesDirectory = ''; matterbridgeVersion = ''; matterbridgeLatestVersion = ''; matterbridgeDevVersion = ''; matterbridgeQrPairingCode = undefined; matterbridgeManualPairingCode = undefined; matterbridgeFabricInformations = undefined; matterbridgeSessionInformations = undefined; matterbridgePaired = undefined; bridgeMode = ''; restartMode = ''; profile = getParameter('profile'); shutdown = false; edge = true; failCountLimit = hasParameter('shelly') ? 600 : 120; log; matterbrideLoggerFile = 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log'; matterLoggerFile = 'matter' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log'; plugins; devices; frontend = new Frontend(this); // Matterbridge storage nodeStorage; nodeContext; nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''); // Cleanup hasCleanupStarted = false; initialized = false; execRunningCount = 0; startMatterInterval; checkUpdateInterval; checkUpdateTimeout; configureTimeout; reachabilityTimeout; endAdvertiseTimeout; sigintHandler; sigtermHandler; exceptionHandler; rejectionHandler; // Matter environment environment = Environment.default; // Matter storage matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''); matterStorageService; matterStorageManager; matterbridgeContext; controllerContext; // Matter parameters mdnsInterface; // matter server node mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi' ipv4address; // matter server node listeningAddressIpv4 ipv6address; // matter server node listeningAddressIpv6 port; // first server node port passcode; // first server node passcode discriminator; // first server node discriminator certification; // device certification serverNode; aggregatorNode; aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1); aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge'; aggregatorProductId = getIntParameter('productId') ?? 0x8000; aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator'; static instance; // We load asyncronously so is private constructor() { super(); } /** * Emits an event of the specified type with the provided arguments. * * @template K - The type of the event. * @param {K} eventName - The name of the event to emit. * @param {...MatterbridgeEvent[K]} args - The arguments to pass to the event listeners. * @returns {boolean} - Returns true if the event had listeners, false otherwise. */ emit(eventName, ...args) { return super.emit(eventName, ...args); } /** * Registers an event listener for the specified event type. * * @template K - The type of the event. * @param {K} eventName - The name of the event to listen for. * @param {(...args: MatterbridgeEvent[K]) => void} listener - The callback function to invoke when the event is emitted. * @returns {this} - Returns the instance of the Matterbridge class. */ on(eventName, listener) { return super.on(eventName, listener); } /** * Retrieves the list of Matterbridge devices. * @returns {MatterbridgeEndpoint[]} An array of MatterbridgeDevice objects. */ getDevices() { return this.devices.array(); } /** * Retrieves the list of registered plugins. * @returns {RegisteredPlugin[]} An array of RegisteredPlugin objects. */ getPlugins() { return this.plugins.array(); } /** * Set the logger logLevel for the Matterbridge classes and call onChangeLoggerLevel() for each plugin. * @param {LogLevel} logLevel The logger logLevel to set. */ async setLogLevel(logLevel) { if (this.log) this.log.logLevel = logLevel; this.matterbridgeInformation.loggerLevel = logLevel; this.frontend.logLevel = logLevel; MatterbridgeEndpoint.logLevel = logLevel; if (this.devices) this.devices.logLevel = logLevel; if (this.plugins) this.plugins.logLevel = logLevel; for (const plugin of this.plugins) { if (!plugin.platform || !plugin.platform.log || !plugin.platform.config) continue; plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel; await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel); } // Set the global logger callback for the WebSocketServer to the common minimum logLevel let callbackLogLevel = "notice" /* LogLevel.NOTICE */; if (this.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO) callbackLogLevel = "info" /* LogLevel.INFO */; if (this.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG) callbackLogLevel = "debug" /* LogLevel.DEBUG */; AnsiLogger.setGlobalCallback(this.frontend.wssSendMessage.bind(this.frontend), callbackLogLevel); this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`); } /** ***********************************************************************************************************************************/ /** loadInstance() and cleanup() methods */ /** ***********************************************************************************************************************************/ /** * Loads an instance of the Matterbridge class. * If an instance already exists, return that instance. * * @param {boolean} initialize - Whether to initialize the Matterbridge instance after loading. Defaults to false. * @returns The loaded Matterbridge instance. */ static async loadInstance(initialize = false) { if (!Matterbridge.instance) { // eslint-disable-next-line no-console if (hasParameter('debug')) console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs); Matterbridge.instance = new Matterbridge(); if (initialize) await Matterbridge.instance.initialize(); } return Matterbridge.instance; } /** * Call cleanup() and dispose MdnsService. * * @deprecated This method is deprecated and is ONLY used for jest tests. */ async destroyInstance() { this.log.info(`Destroy instance...`); // Save server nodes to close const servers = []; if (this.bridgeMode === 'bridge') { if (this.serverNode) servers.push(this.serverNode); } if (this.bridgeMode === 'childbridge') { for (const plugin of this.plugins.array()) { if (plugin.serverNode) servers.push(plugin.serverNode); } } // Let any already‐queued microtasks run first await Promise.resolve(); // Cleanup await this.cleanup('destroying instance...', false); // Close servers mdns service this.log.info(`Dispose ${servers.length} MdnsService...`); for (const server of servers) { await server.env.get(MdnsService)[Symbol.asyncDispose](); this.log.info(`Closed ${server.id} MdnsService`); } // Let any already‐queued microtasks run first await Promise.resolve(); // Wait for the cleanup to finish await new Promise((resolve) => { setTimeout(resolve, 500); }); } /** * Initializes the Matterbridge application. * * @remarks * This method performs the necessary setup and initialization steps for the Matterbridge application. * It displays the help information if the 'help' parameter is provided, sets up the logger, checks the * node version, registers signal handlers, initializes storage, and parses the command line. * * @returns A Promise that resolves when the initialization is complete. */ async initialize() { // Emit the initialize_started event this.emit('initialize_started'); // Create the matterbridge logger this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ }); // Set the restart mode if (hasParameter('service')) this.restartMode = 'service'; if (hasParameter('docker')) this.restartMode = 'docker'; // Set the matterbridge home directory this.homeDirectory = getParameter('homedir') ?? os.homedir(); this.matterbridgeInformation.homeDirectory = this.homeDirectory; await createDirectory(this.homeDirectory, 'Matterbridge Home Directory', this.log); // Set the matterbridge directory this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge'); this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory; await createDirectory(this.matterbridgeDirectory, 'Matterbridge Directory', this.log); await createDirectory(path.join(this.matterbridgeDirectory, 'certs'), 'Matterbridge Frontend Certificate Directory', this.log); await createDirectory(path.join(this.matterbridgeDirectory, 'uploads'), 'Matterbridge Frontend Uploads Directory', this.log); // Set the matterbridge plugin directory this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge'); this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory; await createDirectory(this.matterbridgePluginDirectory, 'Matterbridge Plugin Directory', this.log); // Set the matterbridge cert directory this.matterbridgeCertDirectory = path.join(this.homeDirectory, '.mattercert'); this.matterbridgeInformation.matterbridgeCertDirectory = this.matterbridgeCertDirectory; await createDirectory(this.matterbridgeCertDirectory, 'Matterbridge Matter Certificate Directory', this.log); // Set the matterbridge root directory const { fileURLToPath } = await import('node:url'); const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url)); this.rootDirectory = path.resolve(currentFileDirectory, '../'); this.matterbridgeInformation.rootDirectory = this.rootDirectory; // Setup the matter environment this.environment.vars.set('log.level', MatterLogLevel.INFO); this.environment.vars.set('log.format', MatterLogFormat.ANSI); this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, this.matterStorageName)); this.environment.vars.set('runtime.signals', false); this.environment.vars.set('runtime.exitcode', false); // Register process handlers this.registerProcessHandlers(); // Initialize nodeStorage and nodeContext try { this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`); this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false }); this.log.debug('Creating node storage context for matterbridge'); this.nodeContext = await this.nodeStorage.createStorage('matterbridge'); // TODO: Remove this code when node-persist-manager is updated // eslint-disable-next-line @typescript-eslint/no-explicit-any const keys = (await this.nodeStorage?.storage.keys()); for (const key of keys) { this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`); // eslint-disable-next-line @typescript-eslint/no-explicit-any await this.nodeStorage?.storage.get(key); } const storages = await this.nodeStorage.getStorageNames(); for (const storage of storages) { this.log.debug(`Checking storage: ${CYAN}${storage}${db}`); const nodeContext = await this.nodeStorage?.createStorage(storage); // TODO: Remove this code when node-persist-manager is updated // eslint-disable-next-line @typescript-eslint/no-explicit-any const keys = (await nodeContext?.storage.keys()); keys.forEach(async (key) => { this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`); await nodeContext?.get(key); }); } // Creating a backup of the node storage since it is not corrupted this.log.debug('Creating node storage backup...'); await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName), path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup')); this.log.debug('Created node storage backup'); } catch (error) { // Restoring the backup of the node storage since it is corrupted this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`); if (hasParameter('norestore')) { this.log.fatal(`The matterbridge node storage is corrupted. Parameter -norestore found: exiting...`); await this.cleanup('Fatal error creating node storage manager and context for matterbridge'); return; } this.log.notice(`The matterbridge storage is corrupted. Restoring it with backup...`); await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'), path.join(this.matterbridgeDirectory, this.nodeStorageName)); this.log.notice(`The matterbridge storage has been restored with backup`); } if (!this.nodeStorage || !this.nodeContext) { this.log.fatal('Fatal error creating node storage manager and context for matterbridge'); throw new Error('Fatal error creating node storage manager and context for matterbridge'); } // Set the first port to use for the commissioning server (will be incremented in childbridge mode) this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540; // Set the first passcode to use for the commissioning server (will be incremented in childbridge mode) this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode(); // Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode) this.discriminator = getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator(); // Certificate management const pairingFilePath = path.join(this.matterbridgeCertDirectory, 'pairing.json'); try { await fs.access(pairingFilePath, fs.constants.R_OK); const pairingFileContent = await fs.readFile(pairingFilePath, 'utf8'); const pairingFileJson = JSON.parse(pairingFileContent); // Set the vendorId, vendorName, productId and productName if they are present in the pairing file if (isValidNumber(pairingFileJson.vendorId)) this.aggregatorVendorId = VendorId(pairingFileJson.vendorId); if (isValidString(pairingFileJson.vendorName, 3)) this.aggregatorVendorName = pairingFileJson.vendorName; if (isValidNumber(pairingFileJson.productId)) this.aggregatorProductId = VendorId(pairingFileJson.productId); if (isValidString(pairingFileJson.productName, 3)) this.aggregatorProductName = pairingFileJson.productName; // Override the passcode and discriminator if they are present in the pairing file if (isValidNumber(pairingFileJson.passcode) && isValidNumber(pairingFileJson.discriminator)) { this.passcode = pairingFileJson.passcode; this.discriminator = pairingFileJson.discriminator; this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using passcode ${CYAN}${this.passcode}${nf} and discriminator ${CYAN}${this.discriminator}${nf} from pairing file.`); } // Set the certification if it is present in the pairing file if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) { const hexStringToUint8Array = (hexString) => { const matches = hexString.match(/.{1,2}/g); return matches ? new Uint8Array(matches.map((byte) => parseInt(byte, 16))) : new Uint8Array(); }; // const hexString = Buffer.from('Test string', 'utf-8').toString('hex'); // console.log(hexString, Buffer.from(hexStringToUint8Array(hexString)).toString('utf-8')); this.certification = { privateKey: hexStringToUint8Array(pairingFileJson.privateKey), certificate: hexStringToUint8Array(pairingFileJson.certificate), intermediateCertificate: hexStringToUint8Array(pairingFileJson.intermediateCertificate), declaration: hexStringToUint8Array(pairingFileJson.declaration), }; this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using privateKey, certificate, intermediateCertificate and declaration from pairing file.`); } } catch (error) { this.log.debug(`Pairing file ${CYAN}${pairingFilePath}${db} not found: ${error instanceof Error ? error.message : error}`); } // Store the passcode, discriminator and port in the node context await this.nodeContext.set('matterport', this.port); await this.nodeContext.set('matterpasscode', this.passcode); await this.nodeContext.set('matterdiscriminator', this.discriminator); this.log.debug(`Initializing server node for Matterbridge on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`); // Set matterbridge logger level (context: matterbridgeLogLevel) if (hasParameter('logger')) { const level = getParameter('logger'); if (level === 'debug') { this.log.logLevel = "debug" /* LogLevel.DEBUG */; } else if (level === 'info') { this.log.logLevel = "info" /* LogLevel.INFO */; } else if (level === 'notice') { this.log.logLevel = "notice" /* LogLevel.NOTICE */; } else if (level === 'warn') { this.log.logLevel = "warn" /* LogLevel.WARN */; } else if (level === 'error') { this.log.logLevel = "error" /* LogLevel.ERROR */; } else if (level === 'fatal') { this.log.logLevel = "fatal" /* LogLevel.FATAL */; } else { this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`); this.log.logLevel = "info" /* LogLevel.INFO */; } } else { this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.matterbridgeInformation.shellyBoard ? "notice" /* LogLevel.NOTICE */ : "info" /* LogLevel.INFO */); } this.frontend.logLevel = this.log.logLevel; MatterbridgeEndpoint.logLevel = this.log.logLevel; this.matterbridgeInformation.loggerLevel = this.log.logLevel; // Create the file logger for matterbridge (context: matterbridgeFileLog) if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) { AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), this.log.logLevel, true); this.matterbridgeInformation.fileLogger = true; } this.log.notice('Matterbridge is starting...'); this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.matterbridgeInformation.fileLogger}.`); if (this.profile !== undefined) this.log.debug(`Matterbridge profile: ${this.profile}.`); // Set matter.js logger level, format and logger (context: matterLogLevel) if (hasParameter('matterlogger')) { const level = getParameter('matterlogger'); if (level === 'debug') { Logger.level = MatterLogLevel.DEBUG; } else if (level === 'info') { Logger.level = MatterLogLevel.INFO; } else if (level === 'notice') { Logger.level = MatterLogLevel.NOTICE; } else if (level === 'warn') { Logger.level = MatterLogLevel.WARN; } else if (level === 'error') { Logger.level = MatterLogLevel.ERROR; } else if (level === 'fatal') { Logger.level = MatterLogLevel.FATAL; } else { this.log.warn(`Invalid matter.js logger level: ${level}. Using default level "info".`); Logger.level = MatterLogLevel.INFO; } } else { Logger.level = (await this.nodeContext.get('matterLogLevel', this.matterbridgeInformation.shellyBoard ? MatterLogLevel.NOTICE : MatterLogLevel.INFO)); } Logger.format = MatterLogFormat.ANSI; Logger.setLogger('default', this.createMatterLogger()); this.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel; // Create the file logger for matter.js (context: matterFileLog) if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) { this.matterbridgeInformation.matterFileLogger = true; Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), { defaultLogLevel: Logger.defaultLogLevel, logFormat: MatterLogFormat.PLAIN, }); } this.log.debug(`Matter logLevel: ${Logger.defaultLogLevel} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`); // Log network interfaces const networkInterfaces = os.networkInterfaces(); const availableAddresses = Object.entries(networkInterfaces); const availableInterfaces = Object.keys(networkInterfaces); for (const [ifaceName, ifaces] of availableAddresses) { if (ifaces && ifaces.length > 0) { this.log.debug(`Network interface: ${CYAN}${ifaceName}${db}:`); ifaces.forEach((iface) => { this.log.debug(`- ${CYAN}${iface.family}${db} address ${CYAN}${iface.address}${db} netmask ${CYAN}${iface.netmask}${db} mac ${CYAN}${iface.mac}${db} scopeid ${CYAN}${iface.scopeid}${db} ${iface.internal ? 'internal' : 'external'}`); }); } } // Set the interface to use for matter server node mdnsInterface if (hasParameter('mdnsinterface')) { this.mdnsInterface = getParameter('mdnsinterface'); } else { this.mdnsInterface = await this.nodeContext.get('mattermdnsinterface', undefined); if (this.mdnsInterface === '') this.mdnsInterface = undefined; } // Validate mdnsInterface if (this.mdnsInterface) { if (!availableInterfaces.includes(this.mdnsInterface)) { this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`); this.mdnsInterface = undefined; await this.nodeContext.remove('mattermdnsinterface'); } else { this.log.info(`Using mdnsInterface ${CYAN}${this.mdnsInterface}${nf} for the Matter MdnsBroadcaster.`); } } if (this.mdnsInterface) this.environment.vars.set('mdns.networkInterface', this.mdnsInterface); // Set the listeningAddressIpv4 for the matter commissioning server if (hasParameter('ipv4address')) { this.ipv4address = getParameter('ipv4address'); } else { this.ipv4address = await this.nodeContext.get('matteripv4address', undefined); if (this.ipv4address === '') this.ipv4address = undefined; } // Validate ipv4address if (this.ipv4address) { let isValid = false; for (const [ifaceName, ifaces] of availableAddresses) { if (ifaces && ifaces.find((iface) => iface.address === this.ipv4address)) { this.log.info(`Using ipv4address ${CYAN}${this.ipv4address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`); isValid = true; break; } } if (!isValid) { this.log.error(`Invalid ipv4address: ${this.ipv4address}. Using all available addresses.`); this.ipv4address = undefined; await this.nodeContext.remove('matteripv4address'); } } // Set the listeningAddressIpv6 for the matter commissioning server if (hasParameter('ipv6address')) { this.ipv6address = getParameter('ipv6address'); } else { this.ipv6address = await this.nodeContext?.get('matteripv6address', undefined); if (this.ipv6address === '') this.ipv6address = undefined; } // Validate ipv6address if (this.ipv6address) { let isValid = false; for (const [ifaceName, ifaces] of availableAddresses) { if (ifaces && ifaces.find((iface) => (iface.scopeid === undefined || iface.scopeid === 0) && iface.address === this.ipv6address)) { this.log.info(`Using ipv6address ${CYAN}${this.ipv6address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`); isValid = true; break; } if (ifaces && ifaces.find((iface) => iface.scopeid && iface.scopeid > 0 && iface.address + '%' + (process.platform === 'win32' ? iface.scopeid : ifaceName) === this.ipv6address)) { this.log.info(`Using ipv6address ${CYAN}${this.ipv6address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`); isValid = true; break; } } if (!isValid) { this.log.error(`Invalid ipv6address: ${this.ipv6address}. Using all available addresses.`); this.ipv6address = undefined; await this.nodeContext.remove('matteripv6address'); } } // Initialize the virtual mode if (hasParameter('novirtual')) { this.matterbridgeInformation.virtualMode = 'disabled'; await this.nodeContext.set('virtualmode', 'disabled'); } else { this.matterbridgeInformation.virtualMode = (await this.nodeContext.get('virtualmode', 'outlet')); } this.log.debug(`Virtual mode ${this.matterbridgeInformation.virtualMode}.`); // Initialize PluginManager this.plugins = new PluginManager(this); await this.plugins.loadFromStorage(); this.plugins.logLevel = this.log.logLevel; // Initialize DeviceManager this.devices = new DeviceManager(this, this.nodeContext); this.devices.logLevel = this.log.logLevel; // Get the plugins from node storage and create the plugins node storage contexts for (const plugin of this.plugins) { const packageJson = await this.plugins.parse(plugin); if (packageJson === null && !hasParameter('add') && !hasParameter('remove') && !hasParameter('enable') && !hasParameter('disable') && !hasParameter('reset') && !hasParameter('factoryreset')) { // Try to reinstall the plugin from npm (for Docker pull and external plugins) // We don't do this when the add and other parameters are set because we shut down the process after adding the plugin this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`); try { await spawn.spawnCommand(this, 'npm', ['install', '-g', plugin.name, '--omit=dev', '--verbose']); this.log.info(`Plugin ${plg}${plugin.name}${nf} reinstalled.`); plugin.error = false; } catch (error) { plugin.error = true; plugin.enabled = false; this.log.error(`Error installing plugin ${plg}${plugin.name}${er}. The plugin is disabled.`, error instanceof Error ? error.message : error); } } this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`); plugin.nodeContext = await this.nodeStorage.createStorage(plugin.name); await plugin.nodeContext.set('name', plugin.name); await plugin.nodeContext.set('type', plugin.type); await plugin.nodeContext.set('path', plugin.path); await plugin.nodeContext.set('version', plugin.version); await plugin.nodeContext.set('description', plugin.description); await plugin.nodeContext.set('author', plugin.author); } // Log system info and create .matterbridge directory await this.logNodeAndSystemInfo(); this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` + `${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` + `${hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge') ? 'mode childbridge ' : ''}` + `${hasParameter('controller') ? 'mode controller ' : ''}` + `${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` + `running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`); // Check node version and throw error const minNodeVersion = 18; const nodeVersion = process.versions.node; const versionMajor = parseInt(nodeVersion.split('.')[0]); if (versionMajor < minNodeVersion) { this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`); throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`); } // Parse command line await this.parseCommandLine(); // Emit the initialize_completed event this.emit('initialize_completed'); this.initialized = true; } /** * Parses the command line arguments and performs the corresponding actions. * @private * @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits. */ async parseCommandLine() { if (hasParameter('help')) { this.log.info(`\nUsage: matterbridge [options]\n Options: - help: show the help - bridge: start Matterbridge in bridge mode - childbridge: start Matterbridge in childbridge mode - port [port]: start the commissioning server on the given port (default 5540) - mdnsinterface [name]: set the interface to use for the matter server mdnsInterface (default all interfaces) - ipv4address [address]: set the ipv4 interface address to use for the matter listener (default all interfaces) - ipv6address [address]: set the ipv6 interface address to use for the matter listener (default all interfaces) - frontend [port]: start the frontend on the given port (default 8283) - logger: set the matterbridge logger level: debug | info | notice | warn | error | fatal (default info) - filelogger enable the matterbridge file logger (matterbridge.log) - matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info) - matterfilelogger enable the matter.js file logger (matter.log) - reset: remove the commissioning for Matterbridge (bridge mode). Shutdown Matterbridge before using it! - factoryreset: remove all commissioning information and reset all internal storages. Shutdown Matterbridge before using it! - list: list the registered plugins - loginterfaces: log the network interfaces (usefull for finding the name of the interface to use with -mdnsinterface option) - logstorage: log the node storage - sudo: force the use of sudo to install or update packages if the internal logic fails - nosudo: force not to use sudo to install or update packages if the internal logic fails - norestore: force not to automatically restore the matterbridge node storage and the matter storage from backup if it is corrupted - novirtual: disable the creation of the virtual devices Restart, Update and Reboot Matterbridge - ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional)) - vendorId: override the default vendorId 0xfff1 - vendorName: override the default vendorName "Matterbridge" - productId: override the default productId 0x8000 - productName: override the default productName "Matterbridge aggregator" - service: enable the service mode (used in the systemctl configuration file) - docker: enable the docker mode (used in the docker image) - homedir: override the home directory (default: os.homedir()) - add [plugin path]: register the plugin from the given absolute or relative path - add [plugin name]: register the globally installed plugin with the given name - remove [plugin path]: remove the plugin from the given absolute or relative path - remove [plugin name]: remove the globally installed plugin with the given name - enable [plugin path]: enable the plugin from the given absolute or relative path - enable [plugin name]: enable the globally installed plugin with the given name - disable [plugin path]: disable the plugin from the given absolute or relative path - disable [plugin name]: disable the globally installed plugin with the given name - reset [plugin path]: remove the commissioning for the plugin from the given absolute or relative path (childbridge mode). Shutdown Matterbridge before using it! - reset [plugin name]: remove the commissioning for the globally installed plugin (childbridge mode). Shutdown Matterbridge before using it!${rs}`); this.shutdown = true; return; } if (hasParameter('list')) { this.log.info(`│ Registered plugins (${this.plugins.length})`); let index = 0; for (const plugin of this.plugins) { if (index !== this.plugins.length - 1) { this.log.info(`├─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`); this.log.info(`│ └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`); } else { this.log.info(`└─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`); this.log.info(` └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`); } index++; } const serializedRegisteredDevices = await this.nodeContext?.get('devices', []); this.log.info(`│ Registered devices (${serializedRegisteredDevices?.length})`); serializedRegisteredDevices?.forEach((device, index) => { if (index !== serializedRegisteredDevices.length - 1) { this.log.info(`├─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`); this.log.info(`│ └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`); } else { this.log.info(`└─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`); this.log.info(` └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`); } }); this.shutdown = true; return; } if (hasParameter('logstorage')) { this.log.info(`${plg}Matterbridge${nf} storage log`); await this.nodeContext?.logStorage(); for (const plugin of this.plugins) { this.log.info(`${plg}${plugin.name}${nf} storage log`); await plugin.nodeContext?.logStorage(); } this.shutdown = true; return; } if (hasParameter('loginterfaces')) { this.log.info(`${plg}Matterbridge${nf} network interfaces log`); logInterfaces(); this.shutdown = true; return; } if (getParameter('add')) { this.log.debug(`Adding plugin ${getParameter('add')}`); await this.plugins.add(getParameter('add')); this.shutdown = true; return; } if (getParameter('remove')) { this.log.debug(`Removing plugin ${getParameter('remove')}`); await this.plugins.remove(getParameter('remove')); this.shutdown = true; return; } if (getParameter('enable')) { this.log.debug(`Enabling plugin ${getParameter('enable')}`); await this.plugins.enable(getParameter('enable')); this.shutdown = true; return; } if (getParameter('disable')) { this.log.debug(`Disabling plugin ${getParameter('disable')}`); await this.plugins.disable(getParameter('disable')); this.shutdown = true; return; } if (hasParameter('factoryreset')) { this.initialized = true; await this.shutdownProcessAndFactoryReset(); this.shutdown = true; return; } // Start the matter storage and create the matterbridge context try { await this.startMatterStorage(); } catch (error) { this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`); throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`); } // Clear the matterbridge context if the reset parameter is set if (hasParameter('reset') && getParameter('reset') === undefined) { this.initialized = true; await this.shutdownProcessAndReset(); this.shutdown = true; return; } // Clear matterbridge plugin context if the reset parameter is set if (hasParameter('reset') && getParameter('reset') !== undefined) { this.log.debug(`Reset plugin ${getParameter('reset')}`); const plugin = this.plugins.get(getParameter('reset')); if (plugin) { const matterStorageManager = await this.matterStorageService?.open(plugin.name); if (!matterStorageManager) { this.log.error(`Plugin ${plg}${plugin.name}${er} storageManager not found`); } else { await matterStorageManager.createContext('events')?.clearAll(); await matterStorageManager.createContext('fabrics')?.clearAll(); await matterStorageManager.createContext('root')?.clearAll(); await matterStorageManager.createContext('sessions')?.clearAll(); await matterStorageManager.createContext('persist')?.clearAll(); this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`); } } else { this.log.warn(`Plugin ${plg}${getParameter('reset')}${wr} not registerd in matterbridge`); } await this.stopMatterStorage(); this.shutdown = true; return; } // Initialize frontend if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined) await this.frontend.start(getIntParameter('frontend')); // Check in 30 seconds the latest and dev versions of matterbridge and the plugins this.checkUpdateTimeout = setTimeout(async () => { const { checkUpdates } = await import('./update.js'); checkUpdates(this); }, 30 * 1000).unref(); // Check each 12 hours the latest and dev versions of matterbridge and the plugins this.checkUpdateInterval = setInterval(async () => { const { checkUpdates } = await import('./update.js'); checkUpdates(this); }, 12 * 60 * 60 * 1000).unref(); // Start the matterbridge in mode test if (hasParameter('test')) { this.bridgeMode = 'bridge'; MatterbridgeEndpoint.bridgeMode = 'bridge'; return; } // Start the matterbridge in mode controller if (hasParameter('controller')) { this.bridgeMode = 'controller'; await this.startController(); return; } // Check if the bridge mode is set and start matterbridge in bridge mode if not set if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') { this.log.info('Setting default matterbridge start mode to bridge'); await this.nodeContext?.set('bridgeMode', 'bridge'); } // Start matterbridge in bridge mode if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) { this.bridgeMode = 'bridge'; MatterbridgeEndpoint.bridgeMode = 'bridge'; this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`); await this.startBridge(); return; } // Start matterbridge in childbridge mode if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) { this.bridgeMode = 'childbridge'; MatterbridgeEndpoint.bridgeMode = 'childbridge'; this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`); await this.startChildbridge(); return; } } /** * Asynchronously loads and starts the registered plugins. * * This method is responsible for initializing and starting all enabled plugins. * It ensures that each plugin i