node-red-contrib-symi-mesh 1.8.10 → 1.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -704,6 +704,16 @@ node-red-contrib-symi-mesh/
704
704
 
705
705
  ## 更新日志
706
706
 
707
+ ### v1.8.11 (2026-01-16)
708
+
709
+ **核心稳定性修复**:
710
+ - **僵尸节点彻底清除**:修复了在删除或修改 KNX 场景映射并重新部署后,旧的配置逻辑仍在后台运行的问题。
711
+ - **原因分析**:旧节点实例销毁时,未能完全解绑网关事件监听器,导致“僵尸节点”继续响应事件。
712
+ - **解决方案**:引入 `node.isClosed` 标志位,强制拦截销毁后的所有逻辑执行;同时修复了网关连接状态监听器的内存泄漏问题。
713
+ - **内存泄漏修复**:将所有匿名事件监听器改为具名函数,确保在节点关闭时能被正确移除,防止多次部署后的内存累积。
714
+ - **空指针异常防护**:在 `symi-gateway` 中增加了多处连接状态检查,防止网关断开后后台任务访问已销毁的客户端对象,彻底消除 `Cannot read properties of null (reading 'sendFrame')` 报错刷屏。
715
+ - **UI 显示优化**:修复了 KNX 开关类型在配置列表中错误显示“扩展”列数据的问题,现在开关类型的扩展列将正确显示为“-”。
716
+
707
717
  ### v1.8.10 (2026-01-16)
708
718
 
709
719
  **新增功能**:
@@ -495,6 +495,10 @@ module.exports = function(RED) {
495
495
  }
496
496
 
497
497
  for (const attr of queryAttrs) {
498
+ if (!this.client) {
499
+ this.error(`查询设备${device.name}失败: 网关未连接 (Client is null)`);
500
+ break;
501
+ }
498
502
  const frame = this.protocolHandler.buildDeviceStatusQueryFrame(device.networkAddress, attr);
499
503
  await this.client.sendFrame(frame, 2);
500
504
  await this.sleep(150); // 增加延迟确保设备有时间响应
@@ -522,6 +526,7 @@ module.exports = function(RED) {
522
526
  this.log(`[三合一检测] 查询 ${device.name} 完整状态...`);
523
527
  const threeInOneAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D, 0x6A, 0x6C];
524
528
  for (const attr of threeInOneAttrs) {
529
+ if (!this.client) break;
525
530
  const frame = this.protocolHandler.buildDeviceStatusQueryFrame(device.networkAddress, attr);
526
531
  await this.client.sendFrame(frame, 2);
527
532
  await this.sleep(150);
@@ -545,6 +550,7 @@ module.exports = function(RED) {
545
550
  // 查询温控器状态
546
551
  const tempCtrlAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D];
547
552
  for (const attr of tempCtrlAttrs) {
553
+ if (!this.client) break;
548
554
  const frame = this.protocolHandler.buildDeviceStatusQueryFrame(device.networkAddress, attr);
549
555
  await this.client.sendFrame(frame, 2);
550
556
  await this.sleep(150);
@@ -567,6 +573,10 @@ module.exports = function(RED) {
567
573
  this.log('启用设备状态上报功能...');
568
574
  for (const device of devices) {
569
575
  try {
576
+ if (!this.client) {
577
+ this.error('网关连接断开,停止启用状态上报');
578
+ break;
579
+ }
570
580
  // 启用状态上报:msg_type=0x10, param=0x01
571
581
  const frame = this.protocolHandler.buildDeviceControlFrame(device.networkAddress, 0x10, Buffer.from([0x01]));
572
582
  await this.client.sendFrame(frame, 2);
@@ -79,7 +79,7 @@
79
79
  if (!knxEntities.length) { c.html('<div class="tips">点击"导入"或"添加"管理KNX实体</div>'); return; }
80
80
  let h = '<table class="tbl"><tr><th>名称</th><th>类型</th><th>命令</th><th>状态</th><th>扩展</th><th style="width:60px">操作</th></tr>';
81
81
  knxEntities.forEach((e,i) => {
82
- const ext = [e.ext1,e.ext2,e.ext3].filter(x=>x).join(',');
82
+ const ext = (e.type === 'switch') ? '-' : [e.ext1,e.ext2,e.ext3].filter(x=>x).join(',');
83
83
  const inv = e.type==='cover' ? '<input type="checkbox" class="e-inv" title="位置反转"'+(e.invert?' checked':'')+'>' : '';
84
84
  h += '<tr data-ei="'+i+'"><td>'+e.name+'</td><td>'+(typeLabels[e.type]||e.type)+inv+'</td><td>'+e.cmdAddr+'</td><td>'+(e.statusAddr||'-')+'</td><td>'+(ext||'-')+'</td><td><button class="red-ui-button red-ui-button-small e-edit" title="编辑"><i class="fa fa-pencil"></i></button> <button class="red-ui-button red-ui-button-small e-del" title="删除"><i class="fa fa-times"></i></button></td></tr>';
85
85
  });
@@ -178,6 +178,7 @@ module.exports = function(RED) {
178
178
 
179
179
  // 初始化标记
180
180
  node.initializing = true;
181
+ node.isClosed = false; // 防止僵尸节点执行逻辑
181
182
  node.initTimer = setTimeout(() => {
182
183
  node.initializing = false;
183
184
  node.log('[KNX Bridge] 初始化完成,开始同步');
@@ -261,6 +262,9 @@ module.exports = function(RED) {
261
262
 
262
263
  // ========== Mesh设备状态变化处理 ==========
263
264
  const handleMeshStateChange = (eventData) => {
265
+ // 防止僵尸节点执行
266
+ if (node.isClosed) return;
267
+
264
268
  // 只检查initializing,不检查syncLock
265
269
  // syncLock会导致队列处理期间丢失事件,改用per-device时间戳防回环
266
270
  if (node.initializing) return;
@@ -1198,13 +1202,17 @@ module.exports = function(RED) {
1198
1202
  // ========== 事件监听 ==========
1199
1203
 
1200
1204
  // 监听网关连接状态
1201
- node.gateway.on('gateway-connected', () => {
1205
+ const handleGatewayConnected = () => {
1206
+ if (node.isClosed) return;
1202
1207
  node.status({ fill: 'green', shape: 'ring', text: `网关已连接 ${node.mappings.length}个映射` });
1203
- });
1208
+ };
1209
+ node.gateway.on('gateway-connected', handleGatewayConnected);
1204
1210
 
1205
- node.gateway.on('gateway-disconnected', () => {
1211
+ const handleGatewayDisconnected = () => {
1212
+ if (node.isClosed) return;
1206
1213
  node.status({ fill: 'red', shape: 'ring', text: '网关断开' });
1207
- });
1214
+ };
1215
+ node.gateway.on('gateway-disconnected', handleGatewayDisconnected);
1208
1216
 
1209
1217
  // 监听Mesh设备状态变化
1210
1218
  node.gateway.on('device-state-changed', handleMeshStateChange);
@@ -1212,6 +1220,9 @@ module.exports = function(RED) {
1212
1220
  // ========== 场景执行事件处理 ==========
1213
1221
  // 当收到场景执行通知时,查询所有已映射设备的状态
1214
1222
  const handleSceneExecuted = (eventData) => {
1223
+ // 防止僵尸节点执行
1224
+ if (node.isClosed) return;
1225
+
1215
1226
  if (node.initializing) return;
1216
1227
 
1217
1228
  node.log(`[场景执行] 检测到场景${eventData.sceneId}执行,延迟查询设备状态`);
@@ -1253,6 +1264,9 @@ module.exports = function(RED) {
1253
1264
 
1254
1265
  // ========== 清理 ==========
1255
1266
  node.on('close', function(done) {
1267
+ // 标记为已关闭,阻止任何后续事件处理
1268
+ node.isClosed = true;
1269
+
1256
1270
  // 清除初始化定时器
1257
1271
  if (node.initTimer) {
1258
1272
  clearTimeout(node.initTimer);
@@ -1262,6 +1276,8 @@ module.exports = function(RED) {
1262
1276
  if (node.gateway) {
1263
1277
  node.gateway.removeListener('device-state-changed', handleMeshStateChange);
1264
1278
  node.gateway.removeListener('scene-executed', handleSceneExecuted);
1279
+ node.gateway.removeListener('gateway-connected', handleGatewayConnected);
1280
+ node.gateway.removeListener('gateway-disconnected', handleGatewayDisconnected);
1265
1281
  }
1266
1282
 
1267
1283
  // 销毁 SyncUtils 实例,清理资源
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.8.10",
3
+ "version": "1.8.11",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {