UNPKG

zwave-js-ui

Version:

Z-Wave Control Panel and MQTT Gateway

230 lines (229 loc) 8.23 kB
import { CommandClass, isEncapsulatingCommandClass, isMultiEncapsulatingCommandClass, Zniffer, } from 'zwave-js'; import { TypedEventEmitter } from "./EventEmitter.js"; import { module } from "./logger.js"; import { socketEvents } from "./SocketEvents.js"; import { logsDir, storeDir } from "../config/app.js"; import { buffer2hex, joinPath, parseSecurityKeys } from "./utils.js"; import { isDocker } from "./utils.js"; import { basename } from 'node:path'; import { readFile } from 'node:fs/promises'; import tripleBeam from 'triple-beam'; const loglevels = tripleBeam.configs.npm.levels; const logger = module('ZnifferManager'); const ZNIFFER_LOG_FILE = joinPath(logsDir, 'zniffer_%DATE%.log'); const ZNIFFER_CAPTURE_FILE = joinPath(storeDir, 'zniffer_capture_%DATE%.zlf'); export default class ZnifferManager extends TypedEventEmitter { zniffer; config; socket; error; restartTimeout; get started() { return !!this.zniffer?.active; } constructor(config, socket) { super(); this.config = config; this.socket = socket; if (!config.enabled) { logger.info('Zniffer is DISABLED'); return; } const znifferOptions = { convertRSSI: config.convertRSSI, defaultFrequency: config.defaultFrequency, logConfig: { enabled: config.logEnabled, level: config.logLevel ? loglevels[config.logLevel] : 'info', logToFile: config.logToFile, filename: ZNIFFER_LOG_FILE, forceConsole: isDocker() ? !config.logToFile : false, maxFiles: config.maxFiles || 7, nodeFilter: config.nodeFilter && config.nodeFilter.length > 0 ? config.nodeFilter.map((n) => parseInt(n)) : undefined, }, }; parseSecurityKeys(config, znifferOptions); this.zniffer = new Zniffer(config.port, znifferOptions); this.zniffer.on('error', (error) => { this.onError(error); }); this.zniffer.on('frame', (frame, rawData) => { const socketFrame = this.parseFrame(frame, rawData); this.socket.emit(socketEvents.znifferFrame, socketFrame); }); this.zniffer.on('corrupted frame', (frame, rawData) => { const socketFrame = this.parseFrame(frame, rawData); this.socket.emit(socketEvents.znifferFrame, socketFrame); }); this.zniffer.on('ready', () => { logger.info('Zniffer ready'); }); logger.info('Initing Zniffer...'); this.init().catch(() => { }); } async init() { try { await this.zniffer.init(); } catch (error) { logger.info('Retrying in 5s...'); this.restartTimeout = setTimeout(() => { this.init().catch(() => { }); }, 5000); } this.onStateChange(); } parseFrame(frame, rawData, timestamp = Date.now()) { const socketFrame = { ...frame, corrupted: !('protocol' in frame), payload: '', timestamp, raw: buffer2hex(rawData), }; if ('payload' in frame) { if (frame.payload instanceof CommandClass) { socketFrame.parsedPayload = this.ccToLogRecord(frame.payload); } else { socketFrame.payload = buffer2hex(frame.payload); } } return socketFrame; } onError(error) { logger.error('Zniffer error:', error); this.error = error.message; this.onStateChange(); } onStateChange() { this.socket.emit(socketEvents.znifferState, this.status()); } checkReady() { if (!this.config.enabled || !this.zniffer) { throw new Error('Zniffer is not initialized'); } } status() { return { error: this.error, started: this.started, supportedFrequencies: Object.fromEntries(this.zniffer?.supportedFrequencies ?? []), frequency: this.zniffer?.currentFrequency, lrRegions: Array.from(this.zniffer?.lrRegions ?? []), supportedLRChannelConfigs: Object.fromEntries(this.zniffer?.supportedLRChannelConfigs ?? []), lrChannelConfig: this.zniffer?.currentLRChannelConfig, }; } getFrames() { this.checkReady(); return this.zniffer.capturedFrames.map((frame) => { return this.parseFrame(frame.parsedFrame, Buffer.from(frame.frameData), frame.timestamp.getTime()); }); } async setFrequency(frequency) { this.checkReady(); logger.info(`Setting Zniffer frequency to ${frequency}`); await this.zniffer.setFrequency(frequency); this.onStateChange(); logger.info(`Zniffer frequency set to ${frequency}`); } async setLRChannelConfig(channelConfig) { this.checkReady(); logger.info(`Setting Zniffer LR channel configuration to ${channelConfig}`); await this.zniffer.setLRChannelConfig(channelConfig); this.onStateChange(); logger.info(`Zniffer LR channel configuration set to ${channelConfig}`); } ccToLogRecord(commandClass) { try { const parsed = commandClass.toLogEntry(); if (isEncapsulatingCommandClass(commandClass)) { parsed.encapsulated = [ this.ccToLogRecord(commandClass.encapsulated), ]; } else if (isMultiEncapsulatingCommandClass(commandClass)) { parsed.encapsulated = [ commandClass.encapsulated.map((cc) => this.ccToLogRecord(cc)), ]; } return parsed; } catch (error) { logger.error('Error parsing command class:', error); return { error: error.message, }; } } async close() { if (this.restartTimeout) clearTimeout(this.restartTimeout); if (this.zniffer) { this.zniffer.removeAllListeners(); await this.stop(); await this.zniffer.destroy(); } } async start() { this.checkReady(); if (this.started) { logger.info('Zniffer already started'); return; } logger.info('Starting...'); await this.zniffer.start(); this.onStateChange(); logger.info('Started'); } async stop() { this.checkReady(); if (!this.started) { logger.info('Zniffer is already stopped'); return; } logger.info('Stopping...'); await this.zniffer.stop(); this.onStateChange(); logger.info('Stopped'); } clear() { this.checkReady(); logger.info('Clearing...'); this.zniffer.clearCapturedFrames(); logger.info('Frames cleared'); } async loadCaptureFromBuffer(buffer) { this.checkReady(); logger.info(`Loading capture from buffer (${buffer.length} bytes)`); try { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error await this.zniffer.loadCaptureFromBuffer(buffer); logger.info(`Successfully loaded capture`); } catch (error) { logger.error('Error loading capture:', error); return { error: `Failed to load capture: ${error.message}`, }; } } async saveCaptureToFile() { this.checkReady(); const filePath = ZNIFFER_CAPTURE_FILE.replace('%DATE%', new Date().toISOString()); logger.info(`Saving capture to ${filePath}`); await this.zniffer.saveCaptureToFile(filePath); logger.info('Capture saved'); // Read the saved file to return its content for download const data = await readFile(filePath); return { path: filePath, name: basename(filePath), data: data, }; } }