UNPKG

matterbridge

Version:
916 lines • 109 kB
/** * This file contains the class Frontend. * * @file frontend.ts * @author Luca Liguori * @date 2025-01-13 * @version 1.0.2 * * Copyright 2025, 2026, 2027 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. * */ // @matter import { EndpointServer, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main'; // Node modules import { createServer } from 'node:http'; import os from 'node:os'; import path from 'node:path'; import { promises as fs } from 'node:fs'; import https from 'https'; import express from 'express'; import WebSocket, { WebSocketServer } from 'ws'; import multer from 'multer'; // AnsiLogger module import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW, nt } from './logger/export.js'; // Matterbridge import { createZip, deepCopy, isValidArray, isValidNumber, isValidObject, isValidString } from './utils/export.js'; import { plg } from './matterbridgeTypes.js'; import { hasParameter } from './utils/export.js'; import { BridgedDeviceBasicInformation } from '@matter/main/clusters'; /** * Websocket message ID for logging. * @constant {number} */ export const WS_ID_LOG = 0; /** * Websocket message ID indicating a refresh is needed. * @constant {number} */ export const WS_ID_REFRESH_NEEDED = 1; /** * Websocket message ID indicating a restart is needed. * @constant {number} */ export const WS_ID_RESTART_NEEDED = 2; /** * Websocket message ID indicating a cpu update. * @constant {number} */ export const WS_ID_CPU_UPDATE = 3; /** * Websocket message ID indicating a memory update. * @constant {number} */ export const WS_ID_MEMORY_UPDATE = 4; /** * Websocket message ID indicating an uptime update. * @constant {number} */ export const WS_ID_UPTIME_UPDATE = 5; /** * Websocket message ID indicating a snackbar message. * @constant {number} */ export const WS_ID_SNACKBAR = 6; /** * Websocket message ID indicating matterbridge has un update available. * @constant {number} */ export const WS_ID_UPDATE_NEEDED = 7; /** * Websocket message ID indicating a state update. * @constant {number} */ export const WS_ID_STATEUPDATE = 8; /** * Websocket message ID indicating a shelly system update. * check: * curl -k http://127.0.0.1:8101/api/updates/sys/check * perform: * curl -k http://127.0.0.1:8101/api/updates/sys/perform * @constant {number} */ export const WS_ID_SHELLY_SYS_UPDATE = 100; /** * Websocket message ID indicating a shelly main update. * check: * curl -k http://127.0.0.1:8101/api/updates/main/check * perform: * curl -k http://127.0.0.1:8101/api/updates/main/perform * @constant {number} */ export const WS_ID_SHELLY_MAIN_UPDATE = 101; export class Frontend { matterbridge; log; port = 8283; initializeError = false; expressApp; httpServer; httpsServer; webSocketServer; prevCpus = deepCopy(os.cpus()); lastCpuUsage = 0; memoryData = []; memoryInterval; memoryTimeout; constructor(matterbridge) { this.matterbridge = matterbridge; this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ }); } set logLevel(logLevel) { this.log.logLevel = logLevel; } async start(port = 8283) { this.port = port; this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`); // Initialize multer with the upload directory const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); await fs.mkdir(uploadDir, { recursive: true }); const upload = multer({ dest: uploadDir }); // Create the express app that serves the frontend this.expressApp = express(); // Log all requests to the server for debugging /* this.expressApp.use((req, res, next) => { this.log.debug(`Received request on expressApp: ${req.method} ${req.url}`); next(); }); */ // Serve static files from '/static' endpoint this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build'))); if (!hasParameter('ssl')) { // Create an HTTP server and attach the express app this.httpServer = createServer(this.expressApp); // Listen on the specified port if (hasParameter('ingress')) { this.httpServer.listen(this.port, '0.0.0.0', () => { this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`); }); } else { this.httpServer.listen(this.port, () => { if (this.matterbridge.systemInformation.ipv4Address !== '') this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`); if (this.matterbridge.systemInformation.ipv6Address !== '') this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`); }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any this.httpServer.on('error', (error) => { this.log.error(`Frontend http server error listening on ${this.port}`); switch (error.code) { case 'EACCES': this.log.error(`Port ${this.port} requires elevated privileges`); break; case 'EADDRINUSE': this.log.error(`Port ${this.port} is already in use`); break; } this.initializeError = true; return; }); } else { // Load the SSL certificate, the private key and optionally the CA certificate let cert; try { cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8'); this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`); } catch (error) { this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`); return; } let key; try { key = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem'), 'utf8'); this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}`); } catch (error) { this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`); return; } let ca; try { ca = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem'), 'utf8'); this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')}`); } catch (error) { this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`); } const serverOptions = { cert, key, ca }; // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app this.httpsServer = https.createServer(serverOptions, this.expressApp); // Listen on the specified port if (hasParameter('ingress')) { this.httpsServer.listen(this.port, '0.0.0.0', () => { this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`); }); } else { this.httpsServer.listen(this.port, () => { if (this.matterbridge.systemInformation.ipv4Address !== '') this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`); if (this.matterbridge.systemInformation.ipv6Address !== '') this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`); }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any this.httpsServer.on('error', (error) => { this.log.error(`Frontend https server error listening on ${this.port}`); switch (error.code) { case 'EACCES': this.log.error(`Port ${this.port} requires elevated privileges`); break; case 'EADDRINUSE': this.log.error(`Port ${this.port} is already in use`); break; } this.initializeError = true; return; }); } if (this.initializeError) return; // Create a WebSocket server and attach it to the http or https server const wssPort = this.port; const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`; this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer }); this.webSocketServer.on('connection', (ws, request) => { const clientIp = request.socket.remoteAddress; // Set the global logger callback for the WebSocketServer let callbackLogLevel = "notice" /* LogLevel.NOTICE */; if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO) callbackLogLevel = "info" /* LogLevel.INFO */; if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG) callbackLogLevel = "debug" /* LogLevel.DEBUG */; AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel); this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`); this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`); ws.on('message', (message) => { this.wsMessageHandler(ws, message); }); ws.on('ping', () => { this.log.debug('WebSocket client ping'); ws.pong(); }); ws.on('pong', () => { this.log.debug('WebSocket client pong'); }); ws.on('close', () => { this.log.info('WebSocket client disconnected'); if (this.webSocketServer?.clients.size === 0) { AnsiLogger.setGlobalCallback(undefined); this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed'); } }); ws.on('error', (error) => { this.log.error(`WebSocket client error: ${error}`); }); }); this.webSocketServer.on('close', () => { this.log.debug(`WebSocketServer closed`); }); this.webSocketServer.on('listening', () => { this.log.info(`The WebSocketServer is listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`); }); this.webSocketServer.on('error', (ws, error) => { this.log.error(`WebSocketServer error: ${error}`); }); // Subscribe to cli events const { cliEmitter } = await import('./cli.js'); cliEmitter.removeAllListeners(); cliEmitter.on('uptime', (systemUptime, processUptime) => { this.wssSendUptimeUpdate(systemUptime, processUptime); }); cliEmitter.on('memory', (totalMememory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) => { this.wssSendMemoryUpdate(totalMememory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers); }); cliEmitter.on('cpu', (cpuUsage) => { this.wssSendCpuUpdate(cpuUsage); }); // Endpoint to validate login code this.expressApp.post('/api/login', express.json(), async (req, res) => { const { password } = req.body; this.log.debug('The frontend sent /api/login', password); if (!this.matterbridge.nodeContext) { this.log.error('/api/login nodeContext not found'); res.json({ valid: false }); return; } try { const storedPassword = await this.matterbridge.nodeContext.get('password', ''); if (storedPassword === '' || password === storedPassword) { this.log.debug('/api/login password valid'); res.json({ valid: true }); } else { this.log.warn('/api/login error wrong password'); res.json({ valid: false }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { this.log.error('/api/login error getting password'); res.json({ valid: false }); } }); // Endpoint to provide health check this.expressApp.get('/health', (req, res) => { this.log.debug('Express received /health'); const healthStatus = { status: 'ok', // Indicate service is healthy uptime: process.uptime(), // Server uptime in seconds timestamp: new Date().toISOString(), // Current timestamp }; res.status(200).json(healthStatus); }); // Endpoint to provide memory usage details this.expressApp.get('/memory', async (req, res) => { this.log.debug('Express received /memory'); // Memory usage from process const memoryUsageRaw = process.memoryUsage(); const memoryUsage = { rss: this.formatMemoryUsage(memoryUsageRaw.rss), heapTotal: this.formatMemoryUsage(memoryUsageRaw.heapTotal), heapUsed: this.formatMemoryUsage(memoryUsageRaw.heapUsed), external: this.formatMemoryUsage(memoryUsageRaw.external), arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers), }; // V8 heap statistics const { default: v8 } = await import('node:v8'); const heapStatsRaw = v8.getHeapStatistics(); const heapSpacesRaw = v8.getHeapSpaceStatistics(); // Format heapStats const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)])); // Format heapSpaces const heapSpaces = heapSpacesRaw.map((space) => ({ ...space, space_size: this.formatMemoryUsage(space.space_size), space_used_size: this.formatMemoryUsage(space.space_used_size), space_available_size: this.formatMemoryUsage(space.space_available_size), physical_space_size: this.formatMemoryUsage(space.physical_space_size), })); const { default: module } = await import('node:module'); const loadedModules = module._cache ? Object.keys(module._cache).sort() : []; const memoryReport = { memoryUsage, heapStats, heapSpaces, loadedModules, }; res.status(200).json(memoryReport); }); // Endpoint to start advertising the server node this.expressApp.get('/api/advertise', express.json(), async (req, res) => { const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode); if (pairingCodes) { const { manualPairingCode, qrPairingCode } = pairingCodes; res.json({ manualPairingCode, qrPairingCode: 'https://project-chip.github.io/connectedhomeip/qrcode.html?data=' + qrPairingCode }); } else { res.status(500).json({ error: 'Failed to generate pairing codes' }); } }); // Endpoint to provide settings this.expressApp.get('/api/settings', express.json(), async (req, res) => { this.log.debug('The frontend sent /api/settings'); res.json(await this.getApiSettings()); }); // Endpoint to provide plugins this.expressApp.get('/api/plugins', async (req, res) => { this.log.debug('The frontend sent /api/plugins'); res.json(this.getBaseRegisteredPlugins()); }); // Endpoint to provide devices this.expressApp.get('/api/devices', (req, res) => { this.log.debug('The frontend sent /api/devices'); const devices = []; this.matterbridge.devices.forEach(async (device) => { // Check if the device has the required properties if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady) return; const cluster = this.getClusterTextFromDevice(device); devices.push({ pluginName: device.plugin, type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')', endpoint: device.number, name: device.deviceName, serial: device.serialNumber, productUrl: device.productUrl, configUrl: device.configUrl, uniqueId: device.uniqueId, reachable: this.getReachability(device), cluster: cluster, }); }); res.json(devices); }); // Endpoint to provide the cluster servers of the devices this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => { const selectedPluginName = req.params.selectedPluginName; const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10); this.log.debug(`The frontend sent /api/devices_clusters plugin:${selectedPluginName} endpoint:${selectedDeviceEndpoint}`); if (selectedPluginName === 'none') { res.json([]); return; } const data = []; this.matterbridge.devices.forEach(async (device) => { const pluginName = device.plugin; if (pluginName === selectedPluginName && device.number === selectedDeviceEndpoint) { const endpointServer = EndpointServer.forEndpoint(device); const clusterServers = endpointServer.getAllClusterServers(); clusterServers.forEach((clusterServer) => { Object.entries(clusterServer.attributes).forEach(([key, value]) => { if (clusterServer.name === 'EveHistory') return; let attributeValue; try { if (typeof value.getLocal() === 'object') attributeValue = stringify(value.getLocal()); else attributeValue = value.getLocal().toString(); } catch (error) { attributeValue = 'Fabric-Scoped'; this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`); } data.push({ endpoint: device.number ? device.number.toString() : '...', clusterName: clusterServer.name, clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'), attributeName: key, attributeId: '0x' + value.id.toString(16).padStart(2, '0'), attributeValue, }); }); }); endpointServer.getChildEndpoints().forEach((childEndpoint) => { const name = childEndpoint.name; const clusterServers = childEndpoint.getAllClusterServers(); clusterServers.forEach((clusterServer) => { Object.entries(clusterServer.attributes).forEach(([key, value]) => { if (clusterServer.name === 'EveHistory') return; let attributeValue; try { if (typeof value.getLocal() === 'object') attributeValue = stringify(value.getLocal()); else attributeValue = value.getLocal().toString(); } catch (error) { attributeValue = 'Fabric-Scoped'; this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`); } data.push({ endpoint: (childEndpoint.number ? childEndpoint.number.toString() : '...') + (name ? ' (' + name + ')' : ''), clusterName: clusterServer.name, clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'), attributeName: key, attributeId: '0x' + value.id.toString(16).padStart(2, '0'), attributeValue, }); }); }); }); } }); res.json(data); }); // Endpoint to view the log this.expressApp.get('/api/view-log', async (req, res) => { this.log.debug('The frontend sent /api/log'); try { const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8'); res.type('text/plain'); res.send(data); } catch (error) { this.log.error(`Error reading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error reading log file'); } }); // Endpoint to download the matterbridge log this.expressApp.get('/api/download-mblog', async (req, res) => { this.log.debug('The frontend sent /api/download-mblog'); try { await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger'); } res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => { if (error) { this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error downloading the matterbridge log file'); } }); }); // Endpoint to download the matter log this.expressApp.get('/api/download-mjlog', async (req, res) => { this.log.debug('The frontend sent /api/download-mjlog'); try { await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger'); } res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'matter.log', (error) => { if (error) { this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error downloading the matter log file'); } }); }); // Endpoint to download the matter log this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => { this.log.debug('The frontend sent /api/shellydownloadsystemlog'); try { await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'Create the Shelly system log before downloading it.'); } res.download(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'shelly.log', (error) => { if (error) { this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error downloading Shelly system log file'); } }); }); // Endpoint to download the matter storage file this.expressApp.get('/api/download-mjstorage', async (req, res) => { this.log.debug('The frontend sent /api/download-mjstorage'); await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName)); res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), `matterbridge.${this.matterbridge.matterStorageName}.zip`, (error) => { if (error) { this.log.error(`Error downloading the matter storage matterbridge.${this.matterbridge.matterStorageName}.zip: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error downloading the matter storage zip file'); } }); }); // Endpoint to download the matterbridge storage directory this.expressApp.get('/api/download-mbstorage', async (req, res) => { this.log.debug('The frontend sent /api/download-mbstorage'); await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName)); res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => { if (error) { this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error downloading the matterbridge storage file'); } }); }); // Endpoint to download the matterbridge plugin directory this.expressApp.get('/api/download-pluginstorage', async (req, res) => { this.log.debug('The frontend sent /api/download-pluginstorage'); await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory); res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => { if (error) { this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error downloading the matterbridge plugin storage file'); } }); }); // Endpoint to download the matterbridge plugin config files this.expressApp.get('/api/download-pluginconfig', async (req, res) => { this.log.debug('The frontend sent /api/download-pluginconfig'); await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json'))); // await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, 'certs', '*.*')), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json'))); res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => { if (error) { this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`); res.status(500).send('Error downloading the matterbridge plugin storage file'); } }); }); // Endpoint to download the matterbridge plugin config files this.expressApp.get('/api/download-backup', async (req, res) => { this.log.debug('The frontend sent /api/download-backup'); res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => { if (error) { this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`); res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`); } }); }); // Endpoint to receive commands this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => { const command = req.params.command; let param = req.params.param; this.log.debug(`The frontend sent /api/command/${command}/${param}`); if (!command) { res.status(400).json({ error: 'No command provided' }); return; } this.log.debug(`Received frontend command: ${command}:${param}`); // Handle the command setpassword from Settings if (command === 'setpassword') { const password = param.slice(1, -1); // Remove the first and last characters this.log.debug('setpassword', param, password); await this.matterbridge.nodeContext?.set('password', password); res.json({ message: 'Command received' }); return; } // Handle the command setbridgemode from Settings if (command === 'setbridgemode') { this.log.debug(`setbridgemode: ${param}`); this.wssSendRestartRequired(); await this.matterbridge.nodeContext?.set('bridgeMode', param); res.json({ message: 'Command received' }); return; } // Handle the command backup from Settings if (command === 'backup') { this.log.notice(`Prepairing the backup...`); await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory)); this.log.notice(`Backup ready to be downloaded.`); this.wssSendSnackbarMessage('Backup ready to be downloaded', 10); res.json({ message: 'Command received' }); return; } // Handle the command setmbloglevel from Settings if (command === 'setmbloglevel') { this.log.debug('Matterbridge log level:', param); if (param === 'Debug') { this.log.logLevel = "debug" /* LogLevel.DEBUG */; } else if (param === 'Info') { this.log.logLevel = "info" /* LogLevel.INFO */; } else if (param === 'Notice') { this.log.logLevel = "notice" /* LogLevel.NOTICE */; } else if (param === 'Warn') { this.log.logLevel = "warn" /* LogLevel.WARN */; } else if (param === 'Error') { this.log.logLevel = "error" /* LogLevel.ERROR */; } else if (param === 'Fatal') { this.log.logLevel = "fatal" /* LogLevel.FATAL */; } await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel); await this.matterbridge.setLogLevel(this.log.logLevel); res.json({ message: 'Command received' }); return; } // Handle the command setmbloglevel from Settings if (command === 'setmjloglevel') { this.log.debug('Matter.js log level:', param); if (param === 'Debug') { Logger.defaultLogLevel = MatterLogLevel.DEBUG; } else if (param === 'Info') { Logger.defaultLogLevel = MatterLogLevel.INFO; } else if (param === 'Notice') { Logger.defaultLogLevel = MatterLogLevel.NOTICE; } else if (param === 'Warn') { Logger.defaultLogLevel = MatterLogLevel.WARN; } else if (param === 'Error') { Logger.defaultLogLevel = MatterLogLevel.ERROR; } else if (param === 'Fatal') { Logger.defaultLogLevel = MatterLogLevel.FATAL; } await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.defaultLogLevel); res.json({ message: 'Command received' }); return; } // Handle the command setmdnsinterface from Settings if (command === 'setmdnsinterface') { if (param === 'json' && isValidString(req.body.value)) { this.matterbridge.matterbridgeInformation.mattermdnsinterface = req.body.value; this.log.debug(`Matter.js mdns interface: ${req.body.value === '' ? 'all interfaces' : req.body.value}`); await this.matterbridge.nodeContext?.set('mattermdnsinterface', req.body.value); } res.json({ message: 'Command received' }); return; } // Handle the command setipv4address from Settings if (command === 'setipv4address') { if (param === 'json' && isValidString(req.body.value)) { this.log.debug(`Matter.js ipv4 address: ${req.body.value === '' ? 'all ipv4 addresses' : req.body.value}`); this.matterbridge.matterbridgeInformation.matteripv4address = req.body.value; await this.matterbridge.nodeContext?.set('matteripv4address', req.body.value); } res.json({ message: 'Command received' }); return; } // Handle the command setipv6address from Settings if (command === 'setipv6address') { if (param === 'json' && isValidString(req.body.value)) { this.log.debug(`Matter.js ipv6 address: ${req.body.value === '' ? 'all ipv6 addresses' : req.body.value}`); this.matterbridge.matterbridgeInformation.matteripv6address = req.body.value; await this.matterbridge.nodeContext?.set('matteripv6address', req.body.value); } res.json({ message: 'Command received' }); return; } // Handle the command setmatterport from Settings if (command === 'setmatterport') { const port = Math.min(Math.max(parseInt(req.body.value), 5540), 5560); this.matterbridge.matterbridgeInformation.matterPort = port; this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`); await this.matterbridge.nodeContext?.set('matterport', port); res.json({ message: 'Command received' }); return; } // Handle the command setmatterdiscriminator from Settings if (command === 'setmatterdiscriminator') { const discriminator = Math.min(Math.max(parseInt(req.body.value), 1000), 4095); this.matterbridge.matterbridgeInformation.matterDiscriminator = discriminator; this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`); await this.matterbridge.nodeContext?.set('matterdiscriminator', discriminator); res.json({ message: 'Command received' }); return; } // Handle the command setmatterpasscode from Settings if (command === 'setmatterpasscode') { const passcode = Math.min(Math.max(parseInt(req.body.value), 10000000), 90000000); this.matterbridge.matterbridgeInformation.matterPasscode = passcode; this.log.debug(`Set matter commissioning passcode to ${CYAN}${passcode}${db}`); await this.matterbridge.nodeContext?.set('matterpasscode', passcode); res.json({ message: 'Command received' }); return; } // Handle the command setmbloglevel from Settings if (command === 'setmblogfile') { this.log.debug('Matterbridge file log:', param); this.matterbridge.matterbridgeInformation.fileLogger = param === 'true'; await this.matterbridge.nodeContext?.set('matterbridgeFileLog', param === 'true'); // Create the file logger for matterbridge if (param === 'true') AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug" /* LogLevel.DEBUG */, true); else AnsiLogger.setGlobalLogfile(undefined); res.json({ message: 'Command received' }); return; } // Handle the command setmbloglevel from Settings if (command === 'setmjlogfile') { this.log.debug('Matter file log:', param); this.matterbridge.matterbridgeInformation.matterFileLogger = param === 'true'; await this.matterbridge.nodeContext?.set('matterFileLog', param === 'true'); if (param === 'true') { try { Logger.addLogger('matterfilelogger', await this.matterbridge.createMatterFileLogger(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), true), { defaultLogLevel: MatterLogLevel.DEBUG, logFormat: MatterLogFormat.PLAIN, }); } catch (error) { this.log.debug(`Error adding the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`); } } else { try { Logger.removeLogger('matterfilelogger'); } catch (error) { this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`); } } res.json({ message: 'Command received' }); return; } // Handle the command unregister from Settings if (command === 'unregister') { await this.matterbridge.unregisterAndShutdownProcess(); res.json({ message: 'Command received' }); return; } // Handle the command reset from Settings if (command === 'reset') { await this.matterbridge.shutdownProcessAndReset(); res.json({ message: 'Command received' }); return; } // Handle the command factoryreset from Settings if (command === 'factoryreset') { await this.matterbridge.shutdownProcessAndFactoryReset(); res.json({ message: 'Command received' }); return; } // Handle the command shutdown from Header if (command === 'shutdown') { await this.matterbridge.shutdownProcess(); res.json({ message: 'Command received' }); return; } // Handle the command restart from Header if (command === 'restart') { await this.matterbridge.restartProcess(); res.json({ message: 'Command received' }); return; } // Handle the command update from Header if (command === 'update') { await this.matterbridge.updateProcess(); this.wssSendRestartRequired(); res.json({ message: 'Command received' }); return; } // Handle the command saveconfig from Home if (command === 'saveconfig') { param = param.replace(/\*/g, '\\'); this.log.info(`Saving config for plugin ${plg}${param}${nf}...`); // console.log('Req.body:', JSON.stringify(req.body, null, 2)); if (!this.matterbridge.plugins.has(param)) { this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`); } else { const plugin = this.matterbridge.plugins.get(param); if (!plugin) return; this.matterbridge.plugins.saveConfigFromJson(plugin, req.body); this.wssSendSnackbarMessage(`Saved config for plugin ${param}`); this.wssSendRestartRequired(); } res.json({ message: 'Command received' }); return; } // Handle the command installplugin from Home if (command === 'installplugin') { param = param.replace(/\*/g, '\\'); this.log.info(`Installing plugin ${plg}${param}${nf}...`); this.wssSendSnackbarMessage(`Installing package ${param}. Please wait...`); try { await this.matterbridge.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']); this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`); this.wssSendSnackbarMessage(`Installed package ${param}`, 10, 'success'); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { this.log.error(`Error installing plugin ${plg}${param}${er}`); this.wssSendSnackbarMessage(`Package ${param} not installed`, 10, 'error'); } this.wssSendRestartRequired(); param = param.split('@')[0]; // Also add the plugin to matterbridge so no return! if (param === 'matterbridge') { // If we used the command installplugin to install a dev or a specific version of matterbridge we don't want to add it to matterbridge res.json({ message: 'Command received' }); return; } } // Handle the command addplugin from Home if (command === 'addplugin' || command === 'installplugin') { param = param.replace(/\*/g, '\\'); const plugin = await this.matterbridge.plugins.add(param); if (plugin) { this.wssSendSnackbarMessage(`Added plugin ${param}`); if (this.matterbridge.bridgeMode === 'childbridge') { // We don't know now if the plugin is a dynamic platform or an accessory platform so we create the server node and the aggregator node // eslint-disable-next-line @typescript-eslint/no-explicit-any this.matterbridge.createDynamicPlugin(plugin, true); } this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true).then(() => { this.wssSendRefreshRequired('plugins'); }); } res.json({ message: 'Command received' }); return; } // Handle the command removeplugin from Home if (command === 'removeplugin') { if (!this.matterbridge.plugins.has(param)) { this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`); } else { const plugin = this.matterbridge.plugins.get(param); await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true); // This will also close the server node in childbridge mode await this.matterbridge.plugins.remove(param); this.wssSendSnackbarMessage(`Removed plugin ${param}`); this.wssSendRefreshRequired('plugins'); } res.json({ message: 'Command received' }); return; } // Handle the command enableplugin from Home if (command === 'enableplugin') { if (!this.matterbridge.plugins.has(param)) { this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`); } else { const plugin = this.matterbridge.plugins.get(param); if (plugin && !plugin.enabled) { plugin.locked = undefined; plugin.error = undefined; plugin.loaded = undefined; plugin.started = undefined; plugin.configured = undefined; plugin.platform = undefined; plugin.registeredDevices = undefined; plugin.addedDevices = undefined; await this.matterbridge.plugins.enable(param); this.wssSendSnackbarMessage