From 547377ae0c1958e0b1276112e13a869f14758dad Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 8 May 2024 11:28:04 +0200 Subject: [PATCH 001/120] feat: zniffer --- api/app.ts | 38 ++++++++++ api/config/store.ts | 2 + api/lib/SocketEvents.ts | 3 + api/lib/ZnifferManager.ts | 147 ++++++++++++++++++++++++++++++++++++++ api/lib/ZwaveClient.ts | 53 +------------- api/lib/utils.ts | 63 +++++++++++++++- src/stores/base.js | 21 +++++- 7 files changed, 274 insertions(+), 53 deletions(-) create mode 100644 api/lib/ZnifferManager.ts diff --git a/api/app.ts b/api/app.ts index 806d510a91..71e3f4c0bd 100644 --- a/api/app.ts +++ b/api/app.ts @@ -48,6 +48,7 @@ import * as utils from './lib/utils' import backupManager from './lib/BackupManager' import { readFile, realpath } from 'fs/promises' import { generate } from 'selfsigned' +import ZnifferManager from './lib/ZnifferManager' const createCertificate = promisify(generate) @@ -158,6 +159,7 @@ socketManager.authMiddleware = function ( } let gw: Gateway // the gateway instance +let zniffer: ZnifferManager // the zniffer instance const plugins: CustomPlugin[] = [] let pluginsRouter: Router @@ -386,6 +388,10 @@ async function startGateway(settings: Settings) { zwave = new ZWaveClient(settings.zwave, socketManager.io) } + if (settings.zniffer) { + zniffer = new ZnifferManager(settings.zniffer, socketManager.io) + } + backupManager.init(zwave) gw = new Gateway(settings.gateway, zwave, mqtt) @@ -730,6 +736,38 @@ function setupSocket(server: HttpServer) { cb(result) }) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + socket.on(inboundEvents.zniffer, async (data, cb = noop) => { + logger.info(`Zniffer api call: ${data.api}`) + + let res: any, err: string + try { + switch (data.apiName) { + case 'start': + res = await zniffer.start() + break + case 'stop': + res = await zniffer.stop() + break + case 'capture': + res = await zniffer.saveCaptureToFile() + break + } + } catch (error) { + logger.error('Error while calling ZNIFFER api', error) + err = error.message + } + + const result = { + success: !err, + message: err || 'Success ZNIFFER api call', + result: res, + api: data.apiName, + } + + cb(result) + }) }) // emitted every time a new client connects/disconnects diff --git a/api/config/store.ts b/api/config/store.ts index eb6ae2492d..f27ce34f65 100644 --- a/api/config/store.ts +++ b/api/config/store.ts @@ -2,6 +2,7 @@ import { GatewayConfig } from '../lib/Gateway' import { MqttConfig } from '../lib/MqttClient' +import { ZnifferConfig } from '../lib/ZnifferManager' import { ZwaveConfig, deviceConfigPriorityDir } from '../lib/ZwaveClient' export type StoreKeys = 'settings' | 'scenes' | 'nodes' | 'users' @@ -21,6 +22,7 @@ export interface Settings { mqtt?: MqttConfig zwave?: ZwaveConfig gateway?: GatewayConfig + zniffer?: ZnifferConfig } const store: Record = { diff --git a/api/lib/SocketEvents.ts b/api/lib/SocketEvents.ts index ee4d183628..7a9e427cba 100644 --- a/api/lib/SocketEvents.ts +++ b/api/lib/SocketEvents.ts @@ -19,6 +19,8 @@ export enum socketEvents { grantSecurityClasses = 'GRANT_SECURITY_CLASSES', validateDSK = 'VALIDATE_DSK', inclusionAborted = 'INCLUSION_ABORTED', + znifferFrame = 'ZNIFFER_FRAME', + znifferError = 'ZNIFFER_ERROR', } // events from client ---> server @@ -27,4 +29,5 @@ export enum inboundEvents { zwave = 'ZWAVE_API', // call a zwave api hass = 'HASS_API', // call an hass api mqtt = 'MQTT_API', // call an mqtt api + zniffer = 'ZNIFFER_API', // call a zniffer api } diff --git a/api/lib/ZnifferManager.ts b/api/lib/ZnifferManager.ts new file mode 100644 index 0000000000..21716c66b6 --- /dev/null +++ b/api/lib/ZnifferManager.ts @@ -0,0 +1,147 @@ +import { CommandClass, Frame, Zniffer, ZnifferOptions } from 'zwave-js' +import { TypedEventEmitter } from './EventEmitter' +import { module } from './logger' +import { Server as SocketServer } from 'socket.io' +import { socketEvents } from './SocketEvents' +import { ZwaveConfig } from './ZwaveClient' +import { base, logsDir, storeDir } from '../config/app' +import { joinPath, parseSecurityKeys } from './utils' +import { isDocker } from '@zwave-js/shared' +import { basename } from 'path' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const loglevels = require('triple-beam').configs.npm.levels + +export type ZnifferConfig = Pick< + ZwaveConfig, + | 'securityKeys' + | 'securityKeysLongRange' + | 'maxFiles' + | 'logEnabled' + | 'logToFile' + | 'logLevel' +> & { + port: string + enabled: boolean + convertRSSI?: boolean + defaultFrequency?: number +} + +export interface ZnifferManagerEventCallbacks {} + +const logger = module('ZnifferManager') + +const ZNIFFER_LOG_FILE = joinPath(logsDir, 'zniffer_%DATE%.log') +const ZNIFFER_CAPTURE_FILE = joinPath(storeDir, 'zniffer_capture_%DATE%.zlf') + +export type SocketFrame = Frame & { parsedPayload?: string; corrupted: boolean } + +export default class ZnifferManager extends TypedEventEmitter { + private zniffer: Zniffer + + private config: ZnifferConfig + + private socket: SocketServer + + private error: string + + constructor(config: ZnifferConfig, socket: SocketServer) { + super() + + this.config = config + this.socket = socket + + if (!config.enabled) { + logger.info('Zniffer is DISABLED') + return + } + + const znifferOptions: 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, + }, + } + + parseSecurityKeys(config, znifferOptions) + + this.zniffer = new Zniffer(config.port, znifferOptions) + + logger.info('Initing Zniffer...') + this.zniffer.init().catch((error) => this.onError(error)) + } + + private onError(error: Error) { + logger.error('Zniffer error:', error) + this.error = error.message + this.socket.emit(socketEvents.znifferError, error) + } + + public async close() { + this.zniffer.removeAllListeners() + await this.stop() + } + + public async start() { + logger.info('Starting...') + await this.zniffer.start() + + logger.info('ZnifferManager started') + + this.zniffer.on('frame', (frame) => { + const socketFrame: SocketFrame = { ...frame, corrupted: false } + + // try parsing payload to something human-readable + const payload: CommandClass | Buffer = (frame as any).payload + + if (payload instanceof CommandClass) { + socketFrame.parsedPayload = payload.toLogEntry( + this.zniffer, + ).message + } + + this.socket.emit(socketEvents.znifferFrame, socketFrame) + }) + + this.zniffer.on('corrupted frame', (frame) => { + const socketFrame: SocketFrame = { ...frame, corrupted: true } + + this.socket.emit(socketEvents.znifferFrame, socketFrame) + }) + + this.zniffer.on('error', (error) => { + this.onError(error) + }) + + this.zniffer.on('ready', () => { + logger.info('Zniffer ready') + }) + } + + public async stop() { + logger.info('Stopping...') + await this.zniffer.stop() + + logger.info('ZnifferManager stopped') + } + + public async saveCaptureToFile() { + 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') + return { + path: filePath, + name: basename(filePath), + } + } +} diff --git a/api/lib/ZwaveClient.ts b/api/lib/ZwaveClient.ts index ff731d7b19..31576be00e 100644 --- a/api/lib/ZwaveClient.ts +++ b/api/lib/ZwaveClient.ts @@ -2236,64 +2236,15 @@ class ZwaveClient extends TypedEventEmitter { this.cfg.securityKeys = this.cfg.securityKeys || {} + // update settings to fix compatibility if (s0Key && !this.cfg.securityKeys.S0_Legacy) { this.cfg.securityKeys.S0_Legacy = s0Key const settings = jsonStore.get(store.settings) settings.zwave = this.cfg await jsonStore.put(store.settings, settings) - } else if (process.env.NETWORK_KEY) { - this.cfg.securityKeys.S0_Legacy = process.env.NETWORK_KEY } - const availableKeys = [ - 'S2_Unauthenticated', - 'S2_Authenticated', - 'S2_AccessControl', - 'S0_Legacy', - ] - - const envKeys = Object.keys(process.env) - .filter((k) => k?.startsWith('KEY_')) - .map((k) => k.substring(4)) - - // load security keys from env - for (const k of envKeys) { - if (availableKeys.includes(k)) { - this.cfg.securityKeys[k] = process.env[`KEY_${k}`] - } - } - - zwaveOptions.securityKeys = {} - zwaveOptions.securityKeysLongRange = {} - - // convert security keys to buffer - for (const key in this.cfg.securityKeys) { - if ( - availableKeys.includes(key) && - this.cfg.securityKeys[key].length === 32 - ) { - zwaveOptions.securityKeys[key] = Buffer.from( - this.cfg.securityKeys[key], - 'hex', - ) - } - } - - this.cfg.securityKeysLongRange = - this.cfg.securityKeysLongRange || {} - - // convert security keys to buffer - for (const key in this.cfg.securityKeysLongRange) { - if ( - availableKeys.includes(key) && - this.cfg.securityKeysLongRange[key].length === 32 - ) { - zwaveOptions.securityKeysLongRange[key] = Buffer.from( - this.cfg.securityKeysLongRange[key], - 'hex', - ) - } - } + utils.parseSecurityKeys(this.cfg, zwaveOptions) try { // init driver here because if connect fails the driver is destroyed diff --git a/api/lib/utils.ts b/api/lib/utils.ts index 099279cae4..2f00e96e5d 100644 --- a/api/lib/utils.ts +++ b/api/lib/utils.ts @@ -1,8 +1,9 @@ // eslint-disable-next-line one-var -import { ValueID } from 'zwave-js' +import { PartialZWaveOptions, ValueID, ZnifferOptions } from 'zwave-js' import path, { resolve } from 'path' import crypto from 'crypto' import { readFileSync } from 'fs' +import type { ZwaveConfig } from './ZwaveClient' // don't use import here, it will break the build // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -311,3 +312,63 @@ export function parseJSON(str: string): any { return v }) } + +export function parseSecurityKeys( + config: ZwaveConfig, + options: PartialZWaveOptions | ZnifferOptions, +): void { + config.securityKeys = config.securityKeys || {} + + if (process.env.NETWORK_KEY) { + config.securityKeys.S0_Legacy = process.env.NETWORK_KEY + } + + const availableKeys = [ + 'S2_Unauthenticated', + 'S2_Authenticated', + 'S2_AccessControl', + 'S0_Legacy', + ] + + const envKeys = Object.keys(process.env) + .filter((k) => k?.startsWith('KEY_')) + .map((k) => k.substring(4)) + + // load security keys from env + for (const k of envKeys) { + if (availableKeys.includes(k)) { + config.securityKeys[k] = process.env[`KEY_${k}`] + } + } + + options.securityKeys = {} + options.securityKeysLongRange = {} + + // convert security keys to buffer + for (const key in config.securityKeys) { + if ( + availableKeys.includes(key) && + config.securityKeys[key].length === 32 + ) { + options.securityKeys[key] = Buffer.from( + config.securityKeys[key], + 'hex', + ) + } + } + + config.securityKeysLongRange = config.securityKeysLongRange || {} + + // convert security keys to buffer + for (const key in config.securityKeysLongRange) { + if ( + availableKeys.includes(key) && + config.securityKeysLongRange[key].length === 32 + ) { + options.securityKeysLongRange[key] = Buffer.from( + config.securityKeysLongRange[key], + 'hex', + ) + } + } +} diff --git a/src/stores/base.js b/src/stores/base.js index 5f0af0cd28..9ee1602bff 100644 --- a/src/stores/base.js +++ b/src/stores/base.js @@ -35,7 +35,6 @@ const useBaseStore = defineStore('base', { measured0dBm: undefined, }, }, - logEnabled: true, securityKeys: { S2_Unauthenticated: '', S2_Authenticated: '', @@ -47,6 +46,7 @@ const useBaseStore = defineStore('base', { S2_AccessControl: '', }, deviceConfigPriorityDir: '', + logEnabled: true, logToFile: true, maxFiles: 7, serverEnabled: false, @@ -81,6 +81,25 @@ const useBaseStore = defineStore('base', { username: undefined, password: undefined, }, + zniffer: { + enabled: false, + port: '', + logEnabled: true, + logToFile: true, + maxFiles: 7, + securityKeys: { + S2_Unauthenticated: '', + S2_Authenticated: '', + S2_AccessControl: '', + S0_Legacy: '', + }, + securityKeysLongRange: { + S2_Authenticated: '', + S2_AccessControl: '', + }, + convertRSSI: false, + defaultFrequency: undefined, + }, devices: [], gateway: { type: 0, From 85692379caa34ddf2097aa0d47bf3cd98435f82b Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 8 May 2024 14:04:41 +0200 Subject: [PATCH 002/120] fix: recursive frame parsing --- api/app.ts | 1 + api/lib/ZnifferManager.ts | 57 ++++-- src/stores/base.js | 1 + src/views/Settings.vue | 420 +++++++++++++++++++++++++++++++++++--- 4 files changed, 436 insertions(+), 43 deletions(-) diff --git a/api/app.ts b/api/app.ts index 71e3f4c0bd..a6bf00bb93 100644 --- a/api/app.ts +++ b/api/app.ts @@ -1115,6 +1115,7 @@ app.post( restarting = true await jsonStore.put(store.settings, settings) await gw.close() + await zniffer.close() await destroyPlugins() // reload loggers settings setupLogging(settings) diff --git a/api/lib/ZnifferManager.ts b/api/lib/ZnifferManager.ts index 21716c66b6..8659e83bd5 100644 --- a/api/lib/ZnifferManager.ts +++ b/api/lib/ZnifferManager.ts @@ -1,10 +1,18 @@ -import { CommandClass, Frame, Zniffer, ZnifferOptions } from 'zwave-js' +import { + CommandClass, + CorruptedFrame, + Frame, + isEncapsulatingCommandClass, + isMultiEncapsulatingCommandClass, + Zniffer, + ZnifferOptions, +} from 'zwave-js' import { TypedEventEmitter } from './EventEmitter' import { module } from './logger' import { Server as SocketServer } from 'socket.io' import { socketEvents } from './SocketEvents' import { ZwaveConfig } from './ZwaveClient' -import { base, logsDir, storeDir } from '../config/app' +import { logsDir, storeDir } from '../config/app' import { joinPath, parseSecurityKeys } from './utils' import { isDocker } from '@zwave-js/shared' import { basename } from 'path' @@ -20,6 +28,7 @@ export type ZnifferConfig = Pick< | 'logEnabled' | 'logToFile' | 'logLevel' + | 'nodeFilter' > & { port: string enabled: boolean @@ -34,7 +43,10 @@ const logger = module('ZnifferManager') const ZNIFFER_LOG_FILE = joinPath(logsDir, 'zniffer_%DATE%.log') const ZNIFFER_CAPTURE_FILE = joinPath(storeDir, 'zniffer_capture_%DATE%.zlf') -export type SocketFrame = Frame & { parsedPayload?: string; corrupted: boolean } +export type SocketFrame = (Frame | CorruptedFrame) & { + parsedPayload?: Record + corrupted: boolean +} export default class ZnifferManager extends TypedEventEmitter { private zniffer: Zniffer @@ -65,7 +77,11 @@ export default class ZnifferManager extends TypedEventEmitter 0 + ? config.nodeFilter.map((n) => parseInt(n)) + : undefined, }, } @@ -83,9 +99,29 @@ export default class ZnifferManager extends TypedEventEmitter { + const parsed: Record = commandClass.toLogEntry( + this.zniffer as any, + ).message + + if (isEncapsulatingCommandClass(commandClass)) { + parsed.encapsulated = [ + this.ccToLogRecord(commandClass.encapsulated), + ] + } else if (isMultiEncapsulatingCommandClass(commandClass)) { + parsed.encapsulated = [ + commandClass.encapsulated.map((cc) => this.ccToLogRecord(cc)), + ] + } + + return parsed + } + public async close() { - this.zniffer.removeAllListeners() - await this.stop() + if (this.zniffer) { + this.zniffer.removeAllListeners() + await this.stop() + } } public async start() { @@ -97,13 +133,8 @@ export default class ZnifferManager extends TypedEventEmitter { const socketFrame: SocketFrame = { ...frame, corrupted: false } - // try parsing payload to something human-readable - const payload: CommandClass | Buffer = (frame as any).payload - - if (payload instanceof CommandClass) { - socketFrame.parsedPayload = payload.toLogEntry( - this.zniffer, - ).message + if ('payload' in frame && frame.payload instanceof CommandClass) { + socketFrame.parsedPayload = this.ccToLogRecord(frame.payload) } this.socket.emit(socketEvents.znifferFrame, socketFrame) diff --git a/src/stores/base.js b/src/stores/base.js index 9ee1602bff..331d961274 100644 --- a/src/stores/base.js +++ b/src/stores/base.js @@ -485,6 +485,7 @@ const useBaseStore = defineStore('base', { this.zwave.rf.txPower = {} } Object.assign(this.mqtt, conf.mqtt || {}) + Object.assign(this.zniffer, conf.zniffer || {}) Object.assign(this.gateway, conf.gateway || {}) Object.assign(this.backup, conf.backup || {}) Object.assign(this.ui, conf.ui || {}) diff --git a/src/views/Settings.vue b/src/views/Settings.vue index acf108765c..38196086ff 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -51,7 +51,7 @@ - + General @@ -476,7 +476,7 @@ - + Z-Wave @@ -542,18 +542,22 @@ fixKey( $event, 'S2_Unauthenticated', + newZwave.securityKeys, ) " :rules="[ rules.validKey, rules.validLength, - differentKeys(), + differentKeys( + newZwave.securityKeys, + ), ]" persistent-hint append-outer-icon="wifi_protected_setup" @click:append-outer=" randomKey( 'S2_Unauthenticated', + newZwave.securityKeys, ) " > @@ -568,6 +572,7 @@ fixKey( $event, 'S2_Authenticated', + newZwave.securityKeys, ) " prepend-icon="vpn_key" @@ -576,12 +581,15 @@ :rules="[ rules.validKey, rules.validLength, - differentKeys(), + differentKeys( + newZwave.securityKeys, + ), ]" append-outer-icon="wifi_protected_setup" @click:append-outer=" randomKey( 'S2_Authenticated', + newZwave.securityKeys, ) " > @@ -596,6 +604,7 @@ fixKey( $event, 'S2_AccessControl', + newZwave.securityKeys, ) " prepend-icon="vpn_key" @@ -603,12 +612,15 @@ :rules="[ rules.validKey, rules.validLength, - differentKeys(), + differentKeys( + newZwave.securityKeys, + ), ]" append-outer-icon="wifi_protected_setup" @click:append-outer=" randomKey( 'S2_AccessControl', + newZwave.securityKeys, ) " > @@ -627,11 +639,16 @@ :rules="[ rules.validKey, rules.validLength, - differentKeys(), + differentKeys( + newZwave.securityKeys, + ), ]" append-outer-icon="wifi_protected_setup" @click:append-outer=" - randomKey('S0_Legacy') + randomKey( + 'S0_Legacy', + newZwave.securityKeys, + ) " > @@ -658,7 +675,7 @@ fixKey( $event, 'S2_Authenticated', - true, + newZwave.securityKeysLongRange, ) " prepend-icon="vpn_key" @@ -667,13 +684,15 @@ :rules="[ rules.validKey, rules.validLength, - differentKeys(true), + differentKeys( + newZwave.securityKeysLongRange, + ), ]" append-outer-icon="wifi_protected_setup" @click:append-outer=" randomKey( 'S2_Authenticated', - true, + newZwave.securityKeysLongRange, ) " > @@ -689,7 +708,7 @@ fixKey( $event, 'S2_AccessControl', - true, + newZwave.securityKeysLongRange, ) " prepend-icon="vpn_key" @@ -697,13 +716,15 @@ :rules="[ rules.validKey, rules.validLength, - differentKeys(true), + differentKeys( + newZwave.securityKeysLongRange, + ), ]" append-outer-icon="wifi_protected_setup" @click:append-outer=" randomKey( 'S2_AccessControl', - true, + newZwave.securityKeysLongRange, ) " > @@ -964,6 +985,351 @@ + + + + + + + Zniffer + + + Docs + launch + + + + + + + + + + + + + + + + + + Security Keys + + + + + + + + + + + + + + + + + + + Security Keys (Long Range) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1443,6 +1809,7 @@ :devices="devices" /> + { - const keys = isLongRange - ? this.newZwave.securityKeysLongRange - : this.newZwave.securityKeys - const values = Object.values(keys) + const values = Object.values(obj) // ensure there are no duplicates return ( @@ -1810,16 +2176,12 @@ export default { ) } }, - fixKey(event, key, isLongRange = false) { + fixKey(event, key, obj) { let data = event.clipboardData?.getData('Text') - const keys = isLongRange - ? this.newZwave.securityKeysLongRange - : this.newZwave.securityKeys - if (data) { data = data.replace(/0x|,|\s/gi, '') - this.$set(keys, key, data) + this.$set(obj, key, data) event.preventDefault() } }, @@ -1844,11 +2206,7 @@ export default { return item } }, - randomKey(k, isLongRange = false) { - const keys = isLongRange - ? this.newZwave.securityKeysLongRange - : this.newZwave.securityKeys - + randomKey(k, obj) { let key = '' while (key.length < 32) { @@ -1858,7 +2216,7 @@ export default { key += x.length === 2 ? x : '0' + x } - this.$set(keys, k, key) + this.$set(obj, k, key) }, readFile(file, callback) { const reader = new FileReader() @@ -1900,6 +2258,7 @@ export default { gateway: this.newGateway, zwave: this.newZwave, backup: this.newBackup, + zniffer: this.newZniffer, ui: this.ui, } }, @@ -2065,6 +2424,7 @@ export default { resetConfig() { this.newGateway = copy(this.gateway) this.newZwave = copy(this.zwave) + this.newZniffer = copy(this.zniffer) this.newMqtt = copy(this.mqtt) this.newBackup = copy(this.backup) }, From 80e8fa04c1583144cca59662c815358eb0a36dcc Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 8 May 2024 14:09:03 +0200 Subject: [PATCH 003/120] fix: build issues --- api/lib/SocketManager.ts | 1 + api/lib/ZnifferManager.ts | 2 +- src/lib/SocketEvents.js | 11 +++++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/api/lib/SocketManager.ts b/api/lib/SocketManager.ts index 069bafb5aa..e768b40ccf 100644 --- a/api/lib/SocketManager.ts +++ b/api/lib/SocketManager.ts @@ -13,6 +13,7 @@ export interface SocketManagerEventCallbacks { [inboundEvents.zwave]: (socket: Socket, data: any) => void [inboundEvents.hass]: (socket: Socket, data: any) => void [inboundEvents.mqtt]: (socket: Socket, data: any) => void + [inboundEvents.zniffer]: (socket: Socket, data: any) => void clients: ( event: 'connection' | 'disconnect', sockets: Map, diff --git a/api/lib/ZnifferManager.ts b/api/lib/ZnifferManager.ts index 8659e83bd5..341ad4d7b8 100644 --- a/api/lib/ZnifferManager.ts +++ b/api/lib/ZnifferManager.ts @@ -102,7 +102,7 @@ export default class ZnifferManager extends TypedEventEmitter { const parsed: Record = commandClass.toLogEntry( this.zniffer as any, - ).message + ) if (isEncapsulatingCommandClass(commandClass)) { parsed.encapsulated = [ diff --git a/src/lib/SocketEvents.js b/src/lib/SocketEvents.js index 262fd4661a..67e6a82e8a 100644 --- a/src/lib/SocketEvents.js +++ b/src/lib/SocketEvents.js @@ -1,4 +1,4 @@ -export const socketEvents = { +export const socketEvents = Object.freeze({ init: 'INIT', // automatically sent when a new client connects to the socket controller: 'CONTROLLER_CMD', // controller status updates connected: 'CONNECTED', // socket status @@ -19,12 +19,15 @@ export const socketEvents = { grantSecurityClasses: 'GRANT_SECURITY_CLASSES', validateDSK: 'VALIDATE_DSK', inclusionAborted: 'INCLUSION_ABORTED', -} + znifferFrame: 'ZNIFFER_FRAME', + znifferError: 'ZNIFFER_ERROR', +}) // events from client ---> server -export const inboundEvents = { +export const inboundEvents = Object.freeze({ init: 'INITED', // get all nodes zwave: 'ZWAVE_API', // call a zwave api hass: 'HASS_API', // call an hass api mqtt: 'MQTT_API', // call an mqtt api -} + zniffer: 'ZNIFFER_API', // call a zniffer api +}) From cbe088c45bac6f2843b4ce4e174b21518e824401 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 8 May 2024 14:31:15 +0200 Subject: [PATCH 004/120] feat: start with ui implementation --- api/lib/ZnifferManager.ts | 13 +++- src/App.vue | 5 ++ src/router/index.js | 11 +++ src/views/Zniffer.vue | 149 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 src/views/Zniffer.vue diff --git a/api/lib/ZnifferManager.ts b/api/lib/ZnifferManager.ts index 341ad4d7b8..8e739d77c5 100644 --- a/api/lib/ZnifferManager.ts +++ b/api/lib/ZnifferManager.ts @@ -46,6 +46,7 @@ const ZNIFFER_CAPTURE_FILE = joinPath(storeDir, 'zniffer_capture_%DATE%.zlf') export type SocketFrame = (Frame | CorruptedFrame) & { parsedPayload?: Record corrupted: boolean + timestamp: number } export default class ZnifferManager extends TypedEventEmitter { @@ -131,7 +132,11 @@ export default class ZnifferManager extends TypedEventEmitter { - const socketFrame: SocketFrame = { ...frame, corrupted: false } + const socketFrame: SocketFrame = { + ...frame, + corrupted: false, + timestamp: Date.now(), + } if ('payload' in frame && frame.payload instanceof CommandClass) { socketFrame.parsedPayload = this.ccToLogRecord(frame.payload) @@ -141,7 +146,11 @@ export default class ZnifferManager extends TypedEventEmitter { - const socketFrame: SocketFrame = { ...frame, corrupted: true } + const socketFrame: SocketFrame = { + ...frame, + corrupted: true, + timestamp: Date.now(), + } this.socket.emit(socketEvents.znifferFrame, socketFrame) }) diff --git a/src/App.vue b/src/App.vue index 635abfb542..ef4ffd5a6e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -562,6 +562,11 @@ export default { title: 'Control Panel', path: Routes.controlPanel, }, + { + icon: 'preview', + title: 'Zniffer', + path: Routes.zniffer, + }, { icon: 'qr_code_scanner', title: 'Smart Start', diff --git a/src/router/index.js b/src/router/index.js index e1709120a1..814c6bb074 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -12,6 +12,7 @@ const Login = () => import('@/views/Login.vue') const ErrorPage = () => import('@/views/ErrorPage.vue') const SmartStart = () => import('@/views/SmartStart.vue') const ControllerChart = () => import('@/views/ControllerChart.vue') +const Zniffer = () => import('@/views/Zniffer.vue') import ConfigApis from '../apis/ConfigApis' import useBaseStore from '../stores/base' @@ -29,6 +30,7 @@ export const Routes = { mesh: '/mesh', smartStart: '/smart-start', controllerChart: '/controller-chart', + zniffer: '/zniffer', } Routes.main = Routes.controlPanel @@ -55,6 +57,15 @@ const router = new Router({ requiresAuth: true, }, }, + { + path: Routes.zniffer, + name: 'Zniffer', + component: Zniffer, + props: true, + meta: { + requiresAuth: true, + }, + }, { path: Routes.smartStart, name: 'Smart Start', diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue new file mode 100644 index 0000000000..482cc3f156 --- /dev/null +++ b/src/views/Zniffer.vue @@ -0,0 +1,149 @@ + + From 1e37d204db6781350c512528ff74a82a063d2701 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 8 May 2024 15:23:30 +0200 Subject: [PATCH 005/120] feat: add cc tree view --- api/lib/ZnifferManager.ts | 8 +++ src/components/custom/CCTreeView.vue | 88 ++++++++++++++++++++++++++++ src/views/Zniffer.vue | 25 ++++++-- 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/components/custom/CCTreeView.vue diff --git a/api/lib/ZnifferManager.ts b/api/lib/ZnifferManager.ts index 8e739d77c5..8ffa05299e 100644 --- a/api/lib/ZnifferManager.ts +++ b/api/lib/ZnifferManager.ts @@ -49,6 +49,14 @@ export type SocketFrame = (Frame | CorruptedFrame) & { timestamp: number } +export interface FrameCCLogEntry { + tags: string[] + message?: { + encapsulated?: FrameCCLogEntry[] + [key: string]: string | number | boolean | FrameCCLogEntry[] + } +} + export default class ZnifferManager extends TypedEventEmitter { private zniffer: Zniffer diff --git a/src/components/custom/CCTreeView.vue b/src/components/custom/CCTreeView.vue new file mode 100644 index 0000000000..84df250e52 --- /dev/null +++ b/src/components/custom/CCTreeView.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index 482cc3f156..00413de2c1 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -32,9 +32,11 @@ + + + + + + + + + + @@ -55,12 +75,20 @@ + + diff --git a/src/components/custom/MultipaneResizer.vue b/src/components/custom/MultipaneResizer.vue new file mode 100644 index 0000000000..7413ccf73b --- /dev/null +++ b/src/components/custom/MultipaneResizer.vue @@ -0,0 +1,3 @@ + diff --git a/src/lib/SocketEvents.js b/src/lib/SocketEvents.js index 67e6a82e8a..9641d9f806 100644 --- a/src/lib/SocketEvents.js +++ b/src/lib/SocketEvents.js @@ -20,7 +20,7 @@ export const socketEvents = Object.freeze({ validateDSK: 'VALIDATE_DSK', inclusionAborted: 'INCLUSION_ABORTED', znifferFrame: 'ZNIFFER_FRAME', - znifferError: 'ZNIFFER_ERROR', + znifferState: 'ZNIFFER_STATE', }) // events from client ---> server diff --git a/src/stores/base.js b/src/stores/base.js index 7cf51fa105..e515d91910 100644 --- a/src/stores/base.js +++ b/src/stores/base.js @@ -127,6 +127,10 @@ const useBaseStore = defineStore('base', { controllerStatus: 'Unknown', newConfigVersion: undefined, }, + znifferState: { + error: '', + started: false, + }, ui: { darkMode: settings.load('dark', false), navTabs: settings.load('navTabs', false), @@ -189,6 +193,10 @@ const useBaseStore = defineStore('base', { this.appInfo.serverVersion = data.serverVersion this.appInfo.newConfigVersion = data.newConfigVersion }, + setZnifferState(data) { + this.znifferState.error = data?.error || '' + this.znifferState.started = data?.started || false + }, setValue(valueId) { const toReplace = this.getValue(valueId) const node = this.getNode(valueId.nodeId) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index 3915e87932..78a7eef6c5 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -1,77 +1,144 @@ - this.socket.on(socketEvents.znifferFrame, (data) => { - this.frames.push(data) + From 2fde83fd958a1ffb63ad223cbe76e5eb53e630ba Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Fri, 10 May 2024 16:32:51 +0200 Subject: [PATCH 012/120] fix: improvements on scroll --- src/views/Zniffer.vue | 56 ++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index 78a7eef6c5..818c1f2cb8 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -38,6 +38,7 @@ :headers="headers" :items="framesLimited" dense + v-scroll.self="onScroll" :style="{ height: `${topPaneHeight - 80}px`, maxHeight: `${topPaneHeight - 80}px`, @@ -95,27 +96,33 @@ @@ -175,6 +182,13 @@ export default { totalFrames() { return this.frames.length }, + startHeight() { + return this.start * this.rowHeight + }, + endHeight() { + const lastIndex = this.start + this.perPage + return this.rowHeight * (this.totalFrames - lastIndex + 1) + }, }, mounted() { this.socket.on(socketEvents.znifferFrame, (data) => { @@ -218,7 +232,6 @@ export default { data() { return { start: 0, - endIndex: 22, busy: false, rowHeight: 32, perPage: 22, @@ -251,9 +264,6 @@ export default { const el = this.$refs.framesTable?.$el if (el) { el.scrollTo(0, el.scrollHeight) - this.$nextTick(() => { - this.loadMore(null, null, true) - }) } }, scrollToRow(index) { @@ -283,31 +293,7 @@ export default { e.target.scrollTop = scrollTop }, 10) }, - loadMore(entries, observer, isIntersecting) { - if (isIntersecting) { - const indexesLeft = this.totalFrames - this.endIndex - if (indexesLeft < this.perPage) { - this.start = this.endIndex + indexesLeft - this.perPage - this.endIndex = this.totalFrames - } else { - this.inventoryStartIndex += this.perPage - this.endIndex += this.perPage - } - this.scrollToRow(this.perPage) - } - }, - loadLess(entries, observer, isIntersecting) { - if (isIntersecting) { - if (this.start < this.perPage) { - this.start = 0 - this.endIndex = this.perPage - } else { - this.start -= this.perPage - this.endIndex -= this.perPage - } - this.scrollToRow(5) - } - }, + getRegion(region) { return ( rfRegions.find((r) => r.value === region)?.text || From f057acee8cf455e66bc82b9315727efa735e2301 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Mon, 13 May 2024 16:37:19 +0200 Subject: [PATCH 013/120] fix: sticky header --- src/views/Zniffer.vue | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index 818c1f2cb8..f5b1d7fa2a 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -37,8 +37,9 @@ - {{ getRegion(item.region) }} - - @@ -87,15 +83,11 @@ >

- {{ item.payload.data }} + {{ item.payload }}

---

- - diff --git a/src/lib/utils.js b/src/lib/utils.js index af7fb5dd00..91cf018074 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -223,6 +223,7 @@ export function getRegion(item) { } export function getRepeaters(item) { const repRSSI = item.repeaterRSSI || [] + const dir = item.direction === 'inbound' ? '←' : '→' const repeatersString = item.repeaters?.length > 0 ? item.repeaters @@ -234,12 +235,12 @@ export function getRepeaters(item) { : '' }`, ) - .join(' > ') + .join(` ${dir} `) : '' - return `${item.sourceNodeId} >${ - repeatersString ? ' ' + repeatersString + ' ' : '' - }> ${item.destinationNodeId}` + return `${item.sourceNodeId} ${ + repeatersString ? ` ${dir} ${repeatersString} ${dir}` : dir + } ${item.destinationNodeId}` } export function getType(item) { return getEnumMemberName(ZWaveFrameType, item.type) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index bdf4da83a0..f7bb655f4f 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -1,5 +1,5 @@ + + @@ -192,7 +196,7 @@ import { inboundEvents as socketActions } from '@server/lib/SocketEvents' import { znifferRegions } from '../lib/items.js' import { uuid, - getRepeaters, + getRoute, getType, getRssi, getProtocolDataRate, @@ -322,8 +326,7 @@ export default { { text: 'RSSI', value: 'rssi' }, { text: 'Ch', value: 'channel' }, { text: 'Home Id', value: 'homeId' }, - { text: 'Src', value: 'sourceNodeId' }, - { text: 'Dest', value: 'destinationNodeId' }, + { text: 'Route', value: 'sourceNodeId' }, { text: 'Type', value: 'type' }, { text: 'Payload', value: 'payload' }, ], @@ -478,7 +481,7 @@ export default { .toString() .padEnd(3, '0')}` }, - getRepeaters, + getRoute, getType, getRssi, getProtocolDataRate, From bd5f9848484e345ab8f8993c281e2cb8fbeaf2e9 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 15 May 2024 09:03:12 +0200 Subject: [PATCH 054/120] fix: remove hint from frequency --- src/views/Zniffer.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index bb1a277b96..9006c0a449 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -62,9 +62,8 @@ Date: Wed, 15 May 2024 09:18:03 +0200 Subject: [PATCH 055/120] fix: clarify expression --- src/views/Zniffer.vue | 60 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index 9006c0a449..d1e24ccf2b 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -17,7 +17,7 @@ clearable flat persistent-hint - hint="Search expression. Valid values are: homeId, ch, src, dest, protocolDataRate. Ex: src === 1 && dest === 2" + hint="Search expression using JS. Click on info button for more info" :error="searchError" :error-messages=" searchError ? ['Invalid search'] : [] @@ -27,7 +27,57 @@ class="ma-2" prepend-inner-icon="search" label="Search" - > + > + + Date: Wed, 15 May 2024 09:19:19 +0200 Subject: [PATCH 056/120] fix: reset error on expression --- src/views/Zniffer.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index d1e24ccf2b..75a000e866 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -386,6 +386,7 @@ export default { filterFrames(search) { if (!search || search.trim() === '') { this.framesFiltered = this.frames + this.searchError = false return } From eed858cdc9472309d30804510aef92687b60f760 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 15 May 2024 09:32:16 +0200 Subject: [PATCH 057/120] fix: highlight hop --- src/lib/utils.js | 51 +++++++++++++++++++++++++------------------ src/views/Zniffer.vue | 19 +++++++++++----- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/lib/utils.js b/src/lib/utils.js index 0887fbf1e1..b8f14e30f4 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -225,27 +225,36 @@ export function getRoute(item) { const repRSSI = item.repeaterRSSI || [] const dir = item.direction === 'inbound' ? '←' : '→' const hop = item.hop !== undefined ? item.hop : -1 - const repeatersString = - item.repeaters?.length > 0 - ? item.repeaters - .map( - (r, i) => - `${ - hop === i - ? '' - : '' - }${r}${ - repRSSI[i] && !isRssiError(repRSSI[i]) - ? ` (${rssiToString(repRSSI[i])})` - : '' - }${hop === i ? '' : ''}`, - ) - .join(` ${dir} `) - : '' - - return `${item.sourceNodeId} ${ - repeatersString ? ` ${dir} ${repeatersString} ${dir}` : dir - } ${item.destinationNodeId}` + const route = [ + item.sourceNodeId, + ...(item.repeaters || []), + item.destinationNodeId, + ].map( + (r, i) => + `${r}${ + repRSSI[i] && !isRssiError(repRSSI[i - 1]) + ? ` (${rssiToString(repRSSI[i - 1])})` + : '' + }`, + ) + + let routeString = '' + + if (hop >= 0) { + // highlight the hop + for (let i = 0; i < route.length; i++) { + routeString += route[i] + if (i < route.length - 1) { + routeString += ` ${ + hop === i ? '' : '' + }${dir}${hop === i ? '' : ''} ` + } + } + } else { + routeString = route.join(` ${dir} `) + } + + return routeString } export function getType(item) { return getEnumMemberName(ZWaveFrameType, item.type) diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index 75a000e866..05e161dad0 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -38,10 +38,11 @@

- Search expression. Valid values - are: homeId, ch, src, dest, + Write a custom filter function + in JS. Function arguments are: + homeId, ch, src, dest, protocolDataRate, hop, dir - (direction). + (direction), repeaters.

Examples:
    @@ -72,6 +73,12 @@ >dir === 'inbound' +
  • + repeaters.length > + 0 +
@@ -392,7 +399,7 @@ export default { try { const fn = new Function( - 'homeId, ch, src, dest, protocolDataRate, hop, dir', + 'homeId, ch, src, dest, protocolDataRate, hop, dir, repeaters', `return ${search.replace(/\\/g, '\\\\')}`, ) @@ -405,15 +412,17 @@ export default { protocolDataRate, hop, direction, + repeaters, } = frame return fn( - homeId?.toString(16), + homeId?.toString(16) || '', channel, sourceNodeId, destinationNodeId, protocolDataRate, hop, direction, + repeaters || [], ) }) From c4b6e80fe7eda9063b867e3b80612990c5fc69f9 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 15 May 2024 09:33:02 +0200 Subject: [PATCH 058/120] fix: rssi error --- src/lib/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils.js b/src/lib/utils.js index b8f14e30f4..4eb827eebc 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -232,7 +232,7 @@ export function getRoute(item) { ].map( (r, i) => `${r}${ - repRSSI[i] && !isRssiError(repRSSI[i - 1]) + repRSSI[i - 1] && !isRssiError(repRSSI[i - 1]) ? ` (${rssiToString(repRSSI[i - 1])})` : '' }`, From f0af637fce3bd30eeb9496faabb323d55eb68f42 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 15 May 2024 09:34:58 +0200 Subject: [PATCH 059/120] fix: hide rssi from table --- src/components/custom/FrameDetails.vue | 2 +- src/lib/utils.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/custom/FrameDetails.vue b/src/components/custom/FrameDetails.vue index 596c7c20c9..bcfb2c7e7c 100644 --- a/src/components/custom/FrameDetails.vue +++ b/src/components/custom/FrameDetails.vue @@ -48,7 +48,7 @@ Route - + Ack Requested diff --git a/src/lib/utils.js b/src/lib/utils.js index 4eb827eebc..c89895be3b 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -221,7 +221,7 @@ export function getRegion(item) { `Unknown region ${item?.region}` ) } -export function getRoute(item) { +export function getRoute(item, withRssi = false) { const repRSSI = item.repeaterRSSI || [] const dir = item.direction === 'inbound' ? '←' : '→' const hop = item.hop !== undefined ? item.hop : -1 @@ -232,7 +232,7 @@ export function getRoute(item) { ].map( (r, i) => `${r}${ - repRSSI[i - 1] && !isRssiError(repRSSI[i - 1]) + withRssi && repRSSI[i - 1] && !isRssiError(repRSSI[i - 1]) ? ` (${rssiToString(repRSSI[i - 1])})` : '' }`, From c9730bc2dc0eab37a6d5984f315e7880c63deb8f Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 15 May 2024 09:41:56 +0200 Subject: [PATCH 060/120] fix: with speed modified --- src/components/custom/FrameDetails.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/custom/FrameDetails.vue b/src/components/custom/FrameDetails.vue index bcfb2c7e7c..9f7e714a3f 100644 --- a/src/components/custom/FrameDetails.vue +++ b/src/components/custom/FrameDetails.vue @@ -30,7 +30,14 @@ Protocol Data Rate - {{ getProtocolDataRate(value) }} + + {{ + getProtocolDataRate(value) + + (value.speedModified + ? ' (speed modified)' + : '') + }} + Sequence Number From 3e57de07cc7c74824bec938e8c1a3efc6655572b Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 15 May 2024 10:53:29 +0200 Subject: [PATCH 061/120] fix: improve search filter by passing frame --- src/components/custom/FrameDetails.vue | 18 +++++++++----- src/views/Zniffer.vue | 33 +++++++++++--------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/components/custom/FrameDetails.vue b/src/components/custom/FrameDetails.vue index 9f7e714a3f..adfb65563c 100644 --- a/src/components/custom/FrameDetails.vue +++ b/src/components/custom/FrameDetails.vue @@ -12,10 +12,6 @@ Protocol {{ getProtocol(value) }} - - Payload - {{ value.payload }} - Channel {{ value.channel }} @@ -73,8 +69,18 @@
- - + + + diff --git a/src/views/Zniffer.vue b/src/views/Zniffer.vue index 05e161dad0..0918825c67 100644 --- a/src/views/Zniffer.vue +++ b/src/views/Zniffer.vue @@ -40,12 +40,16 @@

Write a custom filter function in JS. Function arguments are: + frame (the full frame object), homeId, ch, src, dest, protocolDataRate, hop, dir (direction), repeaters.

Examples:
    +
  • + frame.corrupted +
  • src === 1 && dest === @@ -399,30 +403,21 @@ export default { try { const fn = new Function( - 'homeId, ch, src, dest, protocolDataRate, hop, dir, repeaters', + 'frame, homeId, ch, src, dest, protocolDataRate, hop, dir, repeaters', `return ${search.replace(/\\/g, '\\\\')}`, ) this.framesFiltered = this.frames.filter((frame) => { - const { - homeId, - channel, - sourceNodeId, - destinationNodeId, - protocolDataRate, - hop, - direction, - repeaters, - } = frame return fn( - homeId?.toString(16) || '', - channel, - sourceNodeId, - destinationNodeId, - protocolDataRate, - hop, - direction, - repeaters || [], + frame, + frame.homeId?.toString(16) || '', + frame.channel, + frame.sourceNodeId, + frame.destinationNodeId, + frame.protocolDataRate, + frame.hop, + frame.direction, + frame.repeaters || [], ) }) From a159cf6c6b6714acb6f8a6d2ee76d001a61ac725 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 15 May 2024 11:13:23 +0200 Subject: [PATCH 062/120] fix: parsed payload not shown --- src/components/custom/FrameDetails.vue | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/components/custom/FrameDetails.vue b/src/components/custom/FrameDetails.vue index adfb65563c..7596d901a1 100644 --- a/src/components/custom/FrameDetails.vue +++ b/src/components/custom/FrameDetails.vue @@ -1,7 +1,7 @@