matterbridge
Version:
Matterbridge plugin manager for Matter
942 lines • 143 kB
JavaScript
/**
* 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