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