UNPKG

matterbridge

Version:
887 lines 150 kB
/** * This file contains the class Matterbridge. * * @file matterbridge.ts * @author Luca Liguori * @created 2023-12-29 * @version 1.6.0 * @license Apache-2.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, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN, nt, BLUE } from 'node-ansi-logger'; // NodeStorage module import { NodeStorageManager } from 'node-persist-manager'; // @matter import { DeviceTypeId, Endpoint, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode, UINT32_MAX, UINT16_MAX, Crypto, } 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'; // Matterbridge import { getParameter, getIntParameter, hasParameter, copyDirectory, isValidString, parseVersionString, isValidNumber, createDirectory } from './utils/export.js'; import { withTimeout, waiter, wait } from './utils/wait.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'; /** * 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, matterbridgeEndAdvertise: 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, fixedRestartRequired: false, updateRequired: false, }; homeDirectory = ''; rootDirectory = ''; matterbridgeDirectory = ''; matterbridgePluginDirectory = ''; matterbridgeCertDirectory = ''; globalModulesDirectory = ''; matterbridgeVersion = ''; matterbridgeLatestVersion = ''; matterbridgeDevVersion = ''; bridgeMode = ''; restartMode = ''; profile = getParameter('profile'); shutdown = false; failCountLimit = hasParameter('shelly') ? 600 : 120; // Matterbridge log files log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ }); matterbridgeLoggerFile = 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log'; matterLoggerFile = 'matter' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log'; plugins = new PluginManager(this); devices = new DeviceManager(this); frontend = new Frontend(this); // Matterbridge storage nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''); nodeStorage; nodeContext; // Cleanup hasCleanupStarted = false; initialized = false; execRunningCount = 0; startMatterInterval; startMatterIntervalMs = 1000; 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 // Matter nodes serverNode; aggregatorNode; aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1); aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge'; aggregatorProductId = getIntParameter('productId') ?? 0x8000; aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator'; aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code); aggregatorSerialNumber = getParameter('serialNumber'); aggregatorUniqueId = getParameter('uniqueId'); static instance; // We load asyncronously so is private constructor() { super(); } /** * 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 {Matterbridge} A promise that resolves to the 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. * * @param {number} [timeout] - The timeout duration to wait for the cleanup to complete in milliseconds. Default is 1000. * @param {number} [pause] - The pause duration after the cleanup in milliseconds. Default is 250. * * @deprecated This method is deprecated and is ONLY used for jest tests. */ async destroyInstance(timeout = 1000, pause = 250) { 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' && this.plugins !== undefined) { for (const plugin of this.plugins.array()) { if (plugin.serverNode) servers.push(plugin.serverNode); } } if (this.devices !== undefined) { for (const device of this.devices.array()) { if (device.mode === 'server' && device.serverNode) servers.push(device.serverNode); } } // Let any already‐queued microtasks run first await Promise.resolve(); // Wait for the cleanup to finish await wait(pause, 'destroyInstance start', true); // Cleanup await this.cleanup('destroying instance...', false, timeout); // 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 wait(pause, 'destroyInstance stop', true); } /** * 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 {Promise<void>} A Promise that resolves when the initialization is complete. */ async initialize() { // Emit the initialize_started event this.emit('initialize_started'); // 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 storage is corrupted. Found -norestore parameter: exiting...`); } else { 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) { 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(this.environment.get(Crypto)); // 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.environment.get(Crypto)); // 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, productName, deviceType, serialNumber, uniqueId if they are present in the pairing file if (isValidNumber(pairingFileJson.vendorId)) { this.aggregatorVendorId = VendorId(pairingFileJson.vendorId); this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorId ${CYAN}${this.aggregatorVendorId}${nf} from pairing file.`); } if (isValidString(pairingFileJson.vendorName, 3)) { this.aggregatorVendorName = pairingFileJson.vendorName; this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorName ${CYAN}${this.aggregatorVendorName}${nf} from pairing file.`); } if (isValidNumber(pairingFileJson.productId)) { this.aggregatorProductId = pairingFileJson.productId; this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productId ${CYAN}${this.aggregatorProductId}${nf} from pairing file.`); } if (isValidString(pairingFileJson.productName, 3)) { this.aggregatorProductName = pairingFileJson.productName; this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productName ${CYAN}${this.aggregatorProductName}${nf} from pairing file.`); } if (isValidNumber(pairingFileJson.deviceType)) { this.aggregatorDeviceType = DeviceTypeId(pairingFileJson.deviceType); this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using deviceType ${CYAN}${this.aggregatorDeviceType}(0x${this.aggregatorDeviceType.toString(16).padStart(4, '0')})${nf} from pairing file.`); } if (isValidString(pairingFileJson.serialNumber, 3)) { this.aggregatorSerialNumber = pairingFileJson.serialNumber; this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using serialNumber ${CYAN}${this.aggregatorSerialNumber}${nf} from pairing file.`); } if (isValidString(pairingFileJson.uniqueId, 3)) { this.aggregatorUniqueId = pairingFileJson.uniqueId; this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using uniqueId ${CYAN}${this.aggregatorUniqueId}${nf} from pairing file.`); } // 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 for matter.js if it is present in the pairing file if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) { const { hexToBuffer } = await import('./utils/hex.js'); this.certification = { privateKey: hexToBuffer(pairingFileJson.privateKey), certificate: hexToBuffer(pairingFileJson.certificate), intermediateCertificate: hexToBuffer(pairingFileJson.intermediateCertificate), declaration: hexToBuffer(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.matterbridgeLoggerFile), 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()); // Logger.destinations.default.write = this.createMatterLogger(); this.matterbridgeInformation.matterLoggerLevel = Logger.level; // 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.level, logFormat: MatterLogFormat.PLAIN, }); } this.log.debug(`Matter logLevel: ${Logger.level} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`); // Log network interfaces const networkInterfaces = os.networkInterfaces(); // console.log(`Network interfaces:`, 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 ${BLUE}${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}` + `${iface.scopeid ? ` scopeid ${CYAN}${iface.scopeid}${db}` : ''}${iface.cidr ? ` cidr ${CYAN}${iface.cidr}${db}` : ''} ${CYAN}${iface.internal ? 'internal' : 'external'}${db}`); }); } } // 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; } /* istanbul ignore next */ 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.logLevel = this.log.logLevel; await this.plugins.loadFromStorage(); // Initialize DeviceManager 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 { const { spawnCommand } = await import('./utils/spawn.js'); await 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 the WebSocketServer (the server will use the certificates and switch to https) - mtls: enable mTLS for the frontend and the WebSocketServer (both server and client will use and require the certificates and switch to https) - 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${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${nf}`); this.log.info(` └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`); } index++; } /* const serializedRegisteredDevices = await this.nodeContext?.get<SerializedMatterbridgeEndpoint[]>('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')) { const { logInterfaces } = await import('./utils/network.js'); 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(); if (this.aggregatorSerialNumber && this.aggregatorUniqueId && this.matterStorageService) { const storageManager = await this.matterStorageService.open('Matterbridge'); const storageContext = storageManager?.createContext('persist'); if (this.aggregatorSerialNumber) await storageContext?.set('serialNumber', this.aggregatorSerialNumber); if (this.aggregatorUniqueId) await storageContext?.set('uniqueId', this.aggregatorUniqueId); this.matterbridgeInformation.matterbridgeSerialNumber = this.aggregatorSerialNumber; } } 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) { /* istanbul ignore next */ 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.notice(`Reset commissioning for plugin ${plg}${plugin.name}${nt} 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 clearTimeout(this.checkUpdateTimeout); 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 matterb