UNPKG

@pmouli/isy-matter-server

Version:

Service to expose an ISY device as a Matter Border router

248 lines 11 kB
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