@pmouli/isy-matter-server
Version:
Service to expose an ISY device as a Matter Border router
248 lines • 11 kB
JavaScript
import winston, { format } from 'winston';
import chalk from 'chalk';
import { Command } from 'commander';
import { config } from 'dotenv';
import { expand } from 'dotenv-expand';
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import { logStringify, updateConfig, writeDebugFile, writeFile } from 'isy-nodejs/Utils';
import { Socket } from 'net';
import path from 'path';
import process from 'process';
import TransportStream from 'winston-transport';
export let isyConfig;
export let matterConfig;
export let serverConfig;
export let options = { autoStart: false, dependencies: 'static', env: '.env', requireAuth: true, openSocket: false, reset: false, devices: [], filter: '', subprocess: false, factoryReset: false };
export function right(numChars) {
var l = this.length;
return this.substring(length - numChars);
}
export function left(numChars) {
return this.substring(0, numChars - 1);
}
export function rightWithToken(maxNumChars, token = ' ') {
var s = this.split(token);
var sb = s.pop();
var sp = s.pop();
while (sp !== undefined && sb.length + sp.length + token.length <= maxNumChars) {
sb = sp + token + sb;
sp = s.pop();
}
return sb;
}
export function leftWithToken(maxNumChars, token = ' ') {
var s = this.split(token).reverse();
var sb = s.pop();
var sp = s.pop();
while (sp !== undefined && sb.length + sp?.length + token.length <= maxNumChars) {
sb = sb + token + sp;
sp = s.pop();
}
return sb;
}
export function remove(searchValue) {
return this.replace(searchValue, '');
}
export function removeAll(searchValue) {
return this.replaceAll(searchValue, '');
}
String.prototype.remove = remove;
String.prototype.removeAll = removeAll;
String.prototype.left = left;
String.prototype.right = right;
String.prototype.leftWithToken = leftWithToken;
String.prototype.rightWithToken = rightWithToken;
export const myFormat = format.combine(format.splat(), winston.format.printf((info) => {
const d = new Date();
const dStr = d.getFullYear() + '-' + zPad2(d.getMonth() + 1) + '-' + zPad2(d.getDate()) + ' ' + zPad2(d.getHours()) + ':' + zPad2(d.getMinutes()) + ':' + zPad2(d.getSeconds());
return `${dStr} ${info.level}: ${info.label}: ${info.message}`;
}), format.colorize({ all: true }));
export function zPad2(str) {
return str.toString().padStart(2, '0');
}
export async function loadConfigs(options) {
let isyConfig = {
host: process.env.ISY_HOST_URL ?? 'eisy.local',
password: process.env.ISY_PASSWORD,
port: process.env.ISY_HOST_PORT ?? 8080,
protocol: process.env.ISY_HOST_PROTOCOL ?? 'http',
username: process.env.ISY_USERNAME ?? 'admin'
};
let matterConfig = {
passcode: Number(process.env.MATTER_PASSCODE),
discriminator: Number(process.env.MATTER_DISCRIMINATOR),
port: process.env.MATTER_PORT,
productId: Number.parseInt(process.env.MATTER_PRODUCTID),
vendorId: Number.parseInt(process.env.MATTER_VENDORID),
logLevels: {
'iox-matter': process.env.IOX_MATTER_LOG_LEVEL ?? process.env.MATTER_LOG_LEVEL ?? process.env.LOG_LEVEL ?? 'debug',
'matter.js': process.env.MATTER_JS_LOG_LEVEL ?? process.env.MATTER_LOG_LEVEL ?? process.env.LOG_LEVEL ?? 'info',
isy: process.env.ISY_LOG_LEVEL ?? process.env.MATTER_LOG_LEVEL ?? process.env.LOG_LEVEL ?? 'debug'
},
excludeDevicesByDefault: options.devices.length > 0 ? true : false,
deviceConfig: options.filter && options.filter != '' ?
[
{
applyTo: {
predicate: (p) => /slave/.test(p.label.toLowerCase()) || new RegExp(options.filter, 'i').test(p.label)
},
options: { exclude: true }
}
]
: [
{
applyTo: {
predicate: (p) => /slave/.test(p.label.toLowerCase())
},
options: { exclude: true }
}
]
};
let serverConfig = {
logLevel: process.env.LOG_LEVEL ?? `debug`,
logPath: process.env.LOG_PATH ?? process.cwd() + '/matter_server.log',
workingDir: process.env.WORKING_DIR ?? process.cwd(),
subprocess: Boolean(process.env.SUBPROCESS ?? options.subprocess)
};
overrides = {};
if (existsSync('overrides.json')) {
console.log('Loading overrides.json');
try {
overrides = JSON.parse(await readFile('overrides.json', 'utf8'));
}
catch (e) {
console.error('Error loading overrides.json: ' + e.message);
}
}
({ isyConfig, matterConfig, serverConfig } = updateConfig({ isyConfig, matterConfig, serverConfig }, overrides));
writeDebugFile(logStringify({ isyConfig, matterConfig, serverConfig }), 'initialConfigs.json', logger, serverConfig.workingDir);
return { isyConfig, matterConfig, serverConfig };
}
export function createLogger() {
return winston.loggers.add('server', {
format: winston.format.label({ label: chalk.gray.bold('server') + (process.send ? chalk.blue(` (child: ${process.pid})`) : '') }),
transports: [new winston.transports.Console({ level: 'info', format: myFormat }), new winston.transports.File({ filename: serverConfig.logPath, level: 'debug', format: myFormat, zippedArchive: true, maxFiles: 5, maxsize: 1000000 })],
exitOnError: false,
levels: winston.config.cli.levels,
level: 'debug'
});
}
export let logger;
export const tagFormat = format((info) => {
info.type = 'log';
//let r = { type: 'log', ...info };
//if (typeof info.message === 'string') r.message = info.message.replace(/[\u001b\u009b][[0-?9;]*[mK]/g, '');
return info;
})();
export let overrides = {};
export async function updateConfigs(config) {
overrides = updateConfig(overrides, config);
if (config.isyConfig)
isyConfig = updateConfig(isyConfig, config.isyConfig);
if (config.matterConfig)
matterConfig = updateConfig(matterConfig, config.matterConfig);
if (config.serverConfig) {
serverConfig = updateConfig(serverConfig, config.serverConfig);
}
await writeDebugFile(JSON.stringify({ isyConfig, matterConfig, serverConfig }), 'effectiveConfigs.json', logger, serverConfig.workingDir);
await writeFile(JSON.stringify(config), 'overrides.json', serverConfig.workingDir);
}
let initialized = false;
export async function initialize() {
if (initialized) {
logger?.info('Already initialized');
return;
}
const program = new Command();
program.option('-a, --autoStart', 'Start matter bridge server on startup', false).option('-l, --dependencies', 'Load dependencies - static (from local node_modules), plugin (from plugin node_modules)', 'static').option('-e, --env [PATH]', 'Path to environment file', '.env').option('-d, --devices <DEVICES>', 'list of device (addresses) to expose', []).option('--filter [FILTER]', 'devices names to exclude', '').option('-r, --requireAuth', 'Require authentication to start bridge server', false).option('-s, --openSocket', 'Open socket to receive requests from plugin/client', false).option('-f, --factoryReset', 'Reset bridge server to initial state', false).option('-p --subprocess', 'running inside a subprocess', false);
program.parse();
options = program.opts();
let envPath = path.resolve(process.cwd(), options.env);
let env = expand(config({ path: envPath }));
if (options.autoStart) {
options.requireAuth = false; /* Since we are starting the bridge server immediately, nothing to authenticate */
}
else {
options.openSocket = true; /* If we are not auto starting, we need to open the socket to receive commands */
}
console.log(`Environment variables loaded from ${path}: ${logStringify(env)}`);
({ isyConfig, matterConfig, serverConfig } = await loadConfigs(options));
logger = createLogger();
logger.info(`Options: ${logStringify(options)}`);
logger.debug(`All environment variables: ${logStringify(process.env)}`);
logger.info(`LOGNAME: ${process.env.LOGNAME}, USER: ${process.env.USER}`);
if (process.env.LOGNAME == 'root' || process.env.LOGNAME == 'polyglot' || process.env.USER == 'polyglot' || process.env.LOGNAME == 'isy' || process.env.USER == 'isy') {
logger.info(`Running as ${process.env.LOGNAME}. Enabling connection to IoX over domain socket`);
isyConfig.socketPath = '/tmp/ns2isy182652';
}
else if (options.autoStart && !isyConfig.password) {
logger.error('Auto start requires ISY password to be configured or running as polyglot or isy');
process.exit(1);
}
logger.info(`Loaded configs: ${logStringify({ isyConfig, matterConfig, serverConfig })}`);
initialized = true;
}
export let clientLogTransport;
export async function removeClientLogTransport() {
try {
logger.remove(clientLogTransport);
if (clientLogTransport) {
clientLogTransport.close();
clientLogTransport = null;
}
}
catch (e) {
console.error('Error closing client log transport: ' + e.message);
}
}
export class ProcessTransport extends TransportStream {
proc;
constructor(proc = process, options = {}) {
super(options);
proc = process;
}
log(info, callback) {
try {
process.send(info, null, { swallowErrors: true }, (err) => {
if (err) {
console.error('Error sending message to parent process: ' + err.message);
}
callback();
});
}
catch (e) {
console.error('Error sending message to parent process: ' + e.message);
}
}
}
export function addClientLogTransport(socket) {
if (socket instanceof Socket) {
clientLogTransport = new winston.transports.Stream({ stream: socket, level: 'info', format: format.combine(tagFormat, format.json()), handleExceptions: true });
}
else {
clientLogTransport = new ProcessTransport(socket, { level: 'info', format: format.combine(tagFormat, format.json()) });
}
logger.add(clientLogTransport);
}
export function handleExit(...tasks) {
return async (signal) => {
process.once(signal, () => {
logger?.info(`Received signal again ${signal}.`);
});
logger.info(`Received ${signal}. Shutting down.`);
for (const task of tasks) {
await task();
}
logger.once('close', () => {
console.log('logger closed');
process.exit(0);
});
logger.info('Cleanup completed. Will exit.').close();
setTimeout(() => {
logger.info('Timeout waiting for logger to close. Exiting.');
process.exit(0);
}, 5000);
};
}
//# sourceMappingURL=utils.js.map