zwave-js-ui
Version:
Z-Wave Control Panel and MQTT Gateway
230 lines (229 loc) • 8.23 kB
JavaScript
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,
};
}
}