UNPKG

matterbridge

Version:
942 lines 143 kB
/** * This file contains the class Matterbridge. * * @file matterbridge.ts * @author Luca Liguori * @date 2023-12-29 * @version 1.5.2 * * 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'; // 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 } from './utils/export.js'; import { logInterfaces, getGlobalNodeModules } from './utils/network.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'; // @matter import { DeviceTypeId, Endpoint as EndpointNode, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode } from '@matter/main'; import { DeviceCommissioner, FabricAction, MdnsService, PaseClient } from '@matter/main/protocol'; import { AggregatorEndpoint } from '@matter/main/endpoints'; import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information'; import { BasicInformationServer } from '@matter/main/behaviors/basic-information'; // Default colors const plg = '\u001B[38;5;33m'; const dev = '\u001B[38;5;79m'; const typ = '\u001B[38;5;207m'; /** * 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: '', matterbridgeQrPairingCode: undefined, matterbridgeManualPairingCode: undefined, matterbridgeFabricInformations: [], matterbridgeSessionInformations: [], matterbridgePaired: false, matterbridgeAdvertise: false, bridgeMode: '', restartMode: '', 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 = ''; 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; sigintHandler; sigtermHandler; exceptionHandler; rejectionHandler; // Matter environment environment = Environment.default; // Matter storage matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''); matterStorageService; matterStorageManager; matterbridgeContext; mattercontrollerContext; // 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 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. * @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 initialize - Whether to initialize the Matterbridge instance after loading. * @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(). * @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); } } // 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`); } // Wait for the cleanup to finish await new Promise((resolve) => { setTimeout(resolve, 1000); }); } /** * 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() { // Set the restart mode if (hasParameter('service')) this.restartMode = 'service'; if (hasParameter('docker')) this.restartMode = 'docker'; // Set the matterbridge directory this.homeDirectory = getParameter('homedir') ?? os.homedir(); this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge'); // 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); // Create the matterbridge logger this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ }); // 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(); 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.defaultLogLevel = MatterLogLevel.DEBUG; } else if (level === 'info') { Logger.defaultLogLevel = MatterLogLevel.INFO; } else if (level === 'notice') { Logger.defaultLogLevel = MatterLogLevel.NOTICE; } else if (level === 'warn') { Logger.defaultLogLevel = MatterLogLevel.WARN; } else if (level === 'error') { Logger.defaultLogLevel = MatterLogLevel.ERROR; } else if (level === 'fatal') { Logger.defaultLogLevel = MatterLogLevel.FATAL; } else { this.log.warn(`Invalid matter.js logger level: ${level}. Using default level "info".`); Logger.defaultLogLevel = MatterLogLevel.INFO; } } else { Logger.defaultLogLevel = 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; } 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; } } // 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; } } // 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 this.spawnCommand('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(); 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) - matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info) - 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 - ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional)) - 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 versions this.checkUpdateTimeout = setTimeout(async () => { const { checkUpdates } = await import('./update.js'); checkUpdates(this); }, 30 * 1000).unref(); // Check each 24 hours the latest versions this.checkUpdateInterval = setInterval(async () => { const { checkUpdates } = await import('./update.js'); checkUpdates(this); }, 24 * 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 staarting all enabled plugins. * It ensures that each plugin is properly loaded and started before the bridge starts. * * @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started. */ async startPlugins() { // Check, load and start the plugins for (const plugin of this.plugins) { plugin.configJson = await this.plugins.loadConfig(plugin); plugin.schemaJson = await this.plugins.loadSchema(plugin); // Check if the plugin is available if (!(await this.plugins.resolve(plugin.path))) { this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`); plugin.enabled = false; plugin.error = true; continue; } if (!plugin.enabled) { this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`); continue; } plugin.error = false; plugin.locked = false; plugin.loaded = false; plugin.started = false; plugin.configured = false; plugin.registeredDevices = undefined; plugin.addedDevices = undefined; plugin.qrPairingCode = undefined; plugin.manualPairingCode = undefined; this.plugins.load(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously } this.frontend.wssSendRefreshRequired('plugins'); } /** * Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM. * When either of these signals are received, the cleanup method is called with an appropriate message. */ registerProcessHandlers() { this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`); process.removeAllListeners('uncaughtException'); process.removeAllListeners('unhandledRejection'); this.exceptionHandler = async (error) => { this.log.error('Unhandled Exception detected at:', error.stack || error, rs); // await this.cleanup('Unhandled Exception detected, cleaning up...'); }; process.on('uncaughtException', this.exceptionHandler); this.rejectionHandler = async (reason, promise) => { this.log.error('Unhandled Rejection detected at:', promise, 'reason:', reason instanceof Error ? reason.stack : reason, rs); // await this.cleanup('Unhandled Rejection detected, cleaning up...'); }; process.on('unhandledRejection', this.rejectionHandler); this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`); this.sigintHandler = async () => { await this.cleanup('SIGINT received, cleaning up...'); }; process.on('SIGINT', this.sigintHandler); this.sigtermHandler = async () => { await this.cleanup('SIGTERM received, cleaning up...'); }; process.on('SIGTERM', this.sigtermHandler); } /** * Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers. */ deregisterProcesslHandlers() { this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`); if (this.exceptionHandler) process.off('uncaughtException', this.exceptionHandler); this.exceptionHandler = undefined; if (this.rejectionHandler) process.off('unhandledRejection', this.rejectionHandler); this.rejectionHandler = undefined; this.log.debug(`Deregistering SIGINT and SIGTERM signal handlers...`); if (this.sigintHandler) process.off('SIGINT', this.sigintHandler); this.sigintHandler = undefined; if (this.sigtermHandler) process.off('SIGTERM', this.sigtermHandler); this.sigtermHandler = undefined; } /** * Logs the node and system information. */ async logNodeAndSystemInfo() { // IP address information const networkInterfaces = os.networkInterfaces(); this.systemInformation.interfaceName = ''; this.systemInformation.ipv4Address = ''; this.systemInformation.ipv6Address = ''; for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) { // this.log.debug(`Checking interface: '${interfaceName}' for '${this.mdnsInterface}'`); if (this.mdnsInterface && interfaceName !== this.mdnsInterface) continue; if (!interfaceDetails) { break; } for (const detail of interfaceDetails) { if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === '') { this.systemInformation.interfaceName = interfaceName; this.systemInformation.ipv4Address = detail.address; this.systemInformation.macAddress = detail.mac; } else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === '') { this.systemInformation.interfaceName = interfaceName; this.systemInformation.ipv6Address = detail.address; this.systemInformation.macAddress = detail.mac; } } if (this.systemInformation.ipv4Address !== '' || this.systemInformation.ipv6Address !== '') { this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`); this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`); this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`); this.log.debug(`- with IPv6 address: '${this.systemInformation.ipv6Address}'`); break; } } // Node information this.systemInformation.nodeVersion = process.versions.node; const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]); const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]); const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]); // Host system information this.systemInformation.hostname = os.hostname(); this.systemInformation.user = os.userInfo().username; this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc. this.systemInformation.osRelease = os.release(); // Kernel version this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc. this.systemInformation.osArch = os.arch(); // "x64", "arm", etc. this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours // Log the system information this.log.debug('Host System Information:'); this.log.debug(`- Hostname: ${this.systemInformation.hostname}`); this.log.debug(`- User: ${t