node-red-contrib-symi-mesh 1.3.1 → 1.6.1

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.
@@ -19,7 +19,7 @@ module.exports = function(RED) {
19
19
  this.baudRate = parseInt(config.baudRate) || 115200;
20
20
 
21
21
  this.client = null;
22
- this.deviceManager = new DeviceManager(this.context(), this);
22
+ this.deviceManager = new DeviceManager(this.context(), this, this.id);
23
23
  this.protocolHandler = new ProtocolHandler();
24
24
  this.connected = false;
25
25
  this.deviceListComplete = false;
@@ -27,6 +27,7 @@ module.exports = function(RED) {
27
27
 
28
28
  // 状态事件处理队列 - 确保场景执行后的大量状态更新能被正确处理
29
29
  this.stateEventQueue = [];
30
+ this.maxQueueSize = 100; // 最大队列大小,防止内存泄漏
30
31
  this.isProcessingStateEvent = false;
31
32
  this.sceneExecutionInProgress = false; // 场景执行中标志
32
33
  this.sceneExecutionTimer = null; // 场景执行超时定时器
@@ -131,6 +132,11 @@ module.exports = function(RED) {
131
132
  // 清空队列
132
133
  this.stateEventQueue = [];
133
134
 
135
+ // 清理设备管理器资源
136
+ if (this.deviceManager) {
137
+ this.deviceManager.cleanup();
138
+ }
139
+
134
140
  done();
135
141
  });
136
142
  };
@@ -151,9 +157,15 @@ module.exports = function(RED) {
151
157
  if (frame.opcode === OP_RESP_DEVICE_LIST && frame.status === 0x00) {
152
158
  this.parseDeviceListFrame(frame);
153
159
  } else if (frame.isDeviceStatusEvent()) {
154
- // 将状态事件加入队列处理
155
- this.stateEventQueue.push(frame);
156
- this.processStateEventQueue();
160
+ // 将状态事件加入队列处理,限制队列大小防止内存泄漏
161
+ if (this.stateEventQueue.length < this.maxQueueSize) {
162
+ this.stateEventQueue.push(frame);
163
+ this.processStateEventQueue();
164
+ } else {
165
+ this.debug(`状态事件队列已满(${this.maxQueueSize}),丢弃旧事件`);
166
+ this.stateEventQueue.shift(); // 移除最旧的事件
167
+ this.stateEventQueue.push(frame);
168
+ }
157
169
  } else if (frame.opcode === 0xB0) {
158
170
  // 控制命令响应
159
171
  this.debug(`[控制响应] 0xB0: ${frameHex} ${frame.status === 0 ? '(成功)' : '(失败)'}`)
@@ -214,12 +226,42 @@ module.exports = function(RED) {
214
226
  this.debug(`${logPrefix} 地址=0x${event.networkAddress.toString(16).toUpperCase()}, 消息类型=0x${event.attrType.toString(16).toUpperCase()}, 参数=[${Array.from(event.parameters).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
215
227
  }
216
228
 
229
+ // 获取更新前的三合一状态
230
+ const deviceBefore = this.deviceManager.getDeviceByAddress(event.networkAddress);
231
+ const wasThreeInOne = deviceBefore ? deviceBefore.isThreeInOne : false;
232
+
217
233
  const device = this.deviceManager.updateDeviceState(
218
234
  event.networkAddress,
219
235
  event.attrType,
220
236
  event.parameters
221
237
  );
222
238
 
239
+ // 检测到新的三合一设备时记录日志
240
+ if (device && device.isThreeInOne && !wasThreeInOne) {
241
+ const attrName = event.attrType === 0x68 ? '新风' : event.attrType === 0x6B ? '地暖' : '0x94';
242
+ this.log(`[三合一检测] 收到 ${device.name} 的${attrName}响应 (0x${event.attrType.toString(16).toUpperCase()}),确认为三合一面板`);
243
+ }
244
+
245
+ // 检查是否是开关状态变化,且需要触发场景
246
+ if (device && event.attrType === 0x02 && !this.isQueryingStates && !this.sceneExecutionInProgress) {
247
+ // 检查是否有按键状态变化
248
+ if (device.lastChangedButtons && device.lastChangedButtons.length > 0) {
249
+ for (const change of device.lastChangedButtons) {
250
+ const sceneInfo = device.getButtonSceneId(change.button);
251
+ if (sceneInfo) {
252
+ this.log(`[按键场景] 检测到按键${change.button}(${sceneInfo.name})状态变化: ${change.oldState ? '开' : '关'} → ${change.newState ? '开' : '关'}, 触发场景${sceneInfo.sceneId}`);
253
+
254
+ // 发送场景控制命令
255
+ this.sendScene(sceneInfo.sceneId).then(() => {
256
+ this.log(`[按键场景] 场景${sceneInfo.sceneId}(${sceneInfo.name})控制命令已发送`);
257
+ }).catch(err => {
258
+ this.error(`[按键场景] 场景${sceneInfo.sceneId}控制命令发送失败: ${err.message}`);
259
+ });
260
+ }
261
+ }
262
+ }
263
+ }
264
+
223
265
  // 只在非查询状态期间才发送state-changed事件到MQTT
224
266
  // 查询状态期间的事件只用于更新设备状态,不触发MQTT发布
225
267
  if (device && !this.isQueryingStates) {
@@ -237,7 +279,8 @@ module.exports = function(RED) {
237
279
  await new Promise(resolve => setImmediate(resolve));
238
280
  }
239
281
  } catch (error) {
240
- this.error(`Error parsing status event: ${error.message}`);
282
+ // 状态事件解析错误改为debug级别,避免干扰正常日志(通常是网络噪音或不完整数据包)
283
+ this.debug(`Error parsing status event: ${error.message}`);
241
284
  }
242
285
  }
243
286
 
@@ -265,24 +308,37 @@ module.exports = function(RED) {
265
308
 
266
309
  this.log(`Device ${index + 1}/${maxDevices}: ${device.name} @ 0x${networkAddress.toString(16).toUpperCase()}`);
267
310
 
268
- // 对于温控器类型(0x0A),需要通过查询0x94来判断是否是三合一
311
+ // 对于温控器类型(deviceType=10),需要通过查询新风/地暖来判断是否是三合一
312
+ // 空调温控器和三合一都属于同一品类,只有通过主动查询才能区分
269
313
  if (deviceType === 10) {
270
- device.needsThreeInOneCheck = true;
271
- this.log(`温控器设备待检测,将通过0x94查询确认类型: ${device.name}`);
314
+ // 如果设备已经被确认为三合一或普通温控器,跳过检测
315
+ if (device.isThreeInOne) {
316
+ this.log(`[三合一检测] ${device.name} 已确认为三合一面板(从缓存恢复)`);
317
+ } else if (device.thermostatConfirmed) {
318
+ this.log(`[三合一检测] ${device.name} 已确认为普通温控器(从缓存恢复)`);
319
+ } else {
320
+ device.needsThreeInOneCheck = true;
321
+ this.log(`[三合一检测] 发现温控器类型设备: ${device.name},将通过查询新风/地暖确认类型`);
322
+ }
272
323
  }
273
324
 
274
325
  if (index === maxDevices - 1) {
275
326
  this.log(`Device discovery complete: ${maxDevices} devices`);
276
-
277
- // 先查询设备状态,完成后再触发device-list-complete
327
+
328
+ // 立即标记设备列表完成,让设备在HA中可用
329
+ this.deviceListComplete = true;
330
+ this.emit('device-list-complete', this.deviceManager.getAllDevices());
331
+
332
+ // 在后台异步查询设备状态和确认三合一设备
278
333
  setTimeout(async () => {
279
- this.isQueryingStates = true; // 开始查询状态
334
+ this.log('开始后台查询设备状态...');
335
+ this.isQueryingStates = true;
280
336
  await this.queryAllDeviceStates();
281
- this.isQueryingStates = false; // 查询完成
282
-
283
- this.deviceListComplete = true;
284
- // 状态查询完成后才触发device-list-complete,确保设备类型已确认
285
- this.emit('device-list-complete', this.deviceManager.getAllDevices());
337
+ this.isQueryingStates = false;
338
+ this.log('后台状态查询完成');
339
+
340
+ // 状态查询完成后,如果有设备类型变化(如三合一确认),重新发布MQTT Discovery
341
+ this.emit('device-states-synced', this.deviceManager.getAllDevices());
286
342
  }, 500);
287
343
  }
288
344
  };
@@ -296,9 +352,11 @@ module.exports = function(RED) {
296
352
  let queryAttrs = [];
297
353
 
298
354
  // 根据设备类型查询不同属性
299
- if ([1, 2, 3].includes(device.deviceType)) {
300
- queryAttrs = [0x02]; // 开关状态
301
- this.log(`查询开关设备: ${device.name} (地址=0x${device.networkAddress.toString(16).toUpperCase()}, 路数=${device.channels})`);
355
+ if ([1, 2, 3, 39].includes(device.deviceType)) {
356
+ // 6-8路开关使用0x45,1-4路使用0x02
357
+ const msgType = (device.channels >= 6) ? 0x45 : 0x02;
358
+ queryAttrs = [msgType];
359
+ this.log(`查询开关设备: ${device.name} (地址=0x${device.networkAddress.toString(16).toUpperCase()}, 路数=${device.channels}, msgType=0x${msgType.toString(16).toUpperCase()})`);
302
360
  } else if (device.deviceType === 9) {
303
361
  queryAttrs = [0x02, 0x0E]; // 插卡取电:开关状态、插卡状态
304
362
  } else if (device.deviceType === 4 || device.deviceType === 0x18) {
@@ -306,14 +364,16 @@ module.exports = function(RED) {
306
364
  } else if (device.deviceType === 5) {
307
365
  queryAttrs = [0x05, 0x06]; // 窗帘:运行状态、位置
308
366
  } else if (device.deviceType === 10) {
309
- // 温控器类型:先查询三合一专用消息(0x68/0x6B)判断是否是三合一
367
+ // 温控器类型:通过查询新风(0x68)和地暖(0x6B)来识别是否是三合一
368
+ // 研发说明:空调温控器和三合一都是同一品类(deviceType=10)
369
+ // 区分方法:主动查询新风/地暖状态,有反馈的是三合一,无反馈的是普通温控器
310
370
  if (device.needsThreeInOneCheck) {
311
- this.log(`检测设备${device.name}是否为三合一面板...`);
312
- // 先查询0x68(新风开关),如果有响应就是三合一
313
- queryAttrs = [0x68];
371
+ this.log(`[三合一检测] 开始检测设备 ${device.name} (0x${device.networkAddress.toString(16).toUpperCase()})`);
372
+ // 同时查询新风(0x68)和地暖(0x6B),任意一个有响应就确认是三合一
373
+ queryAttrs = [0x68, 0x6B];
314
374
  } else if (device.isThreeInOne) {
315
375
  // 已确认是三合一,查询完整状态
316
- queryAttrs = [0x16, 0x1B, 0x1C, 0x1D, 0x68, 0x6A, 0x6B, 0x6C];
376
+ queryAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D, 0x68, 0x6A, 0x6B, 0x6C];
317
377
  } else {
318
378
  // 普通温控器
319
379
  queryAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D];
@@ -328,25 +388,34 @@ module.exports = function(RED) {
328
388
 
329
389
  // 等待设备响应,检查是否被标记为三合一
330
390
  if (device.needsThreeInOneCheck) {
331
- await this.sleep(800); // 增加到800ms确保响应到达
391
+ // 等待1.5秒确保响应到达(网络延迟可能较大)
392
+ await this.sleep(1500);
393
+
394
+ // 处理状态事件队列,确保所有响应都被处理
395
+ await this.processStateEventQueue();
396
+
332
397
  if (device.isThreeInOne) {
333
- this.log(`设备${device.name}确认为三合一面板,继续查询完整状态`);
398
+ this.log(`[三合一检测] ${device.name} 确认为三合一面板`);
334
399
  device.needsThreeInOneCheck = false;
335
400
  this.deviceManager.saveDevices();
336
401
 
337
- // 查询三合一的其他状态
338
- const threeInOneAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D, 0x6A, 0x6B, 0x6C];
402
+ // 查询三合一的完整状态
403
+ this.log(`[三合一检测] 查询 ${device.name} 完整状态...`);
404
+ const threeInOneAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D, 0x6A, 0x6C];
339
405
  for (const attr of threeInOneAttrs) {
340
406
  const frame = this.protocolHandler.buildDeviceStatusQueryFrame(device.networkAddress, attr);
341
407
  await this.client.sendFrame(frame, 2);
342
408
  await this.sleep(150);
343
409
  }
344
410
  } else {
345
- this.log(`设备${device.name}确认为普通温控器`);
411
+ this.log(`[三合一检测] ${device.name} 确认为普通温控器`);
346
412
  device.needsThreeInOneCheck = false;
347
- device.thermostatConfirmed = true; // 标记为已确认的温控器
413
+ device.thermostatConfirmed = true;
348
414
  this.deviceManager.saveDevices();
349
-
415
+
416
+ // 触发温控器确认事件
417
+ this.emit('thermostat-confirmed', device);
418
+
350
419
  // 查询温控器状态
351
420
  const tempCtrlAttrs = [0x02, 0x16, 0x1B, 0x1C, 0x1D];
352
421
  for (const attr of tempCtrlAttrs) {
@@ -422,8 +491,18 @@ module.exports = function(RED) {
422
491
  try {
423
492
  // 从设备管理器获取当前状态
424
493
  const device = this.deviceManager.getDeviceByAddress(networkAddr);
494
+
495
+ // 根据路数选择正确的msgType
496
+ // 1-4路使用0x02 (TYPE_ON_OFF)
497
+ // 6-8路使用0x45 (VD_GROUP_MSG_TYPE_1_8_ON_OFF)
498
+ let actualMsgType = attrType;
499
+ if (channels >= 6) {
500
+ actualMsgType = 0x45; // VD_GROUP_MSG_TYPE_1_8_ON_OFF
501
+ this.debug(`[开关控制] 6-8路开关使用msgType=0x45`);
502
+ }
503
+
425
504
  let currentState = null;
426
-
505
+
427
506
  if (device && typeof device.getCurrentSwitchState === 'function') {
428
507
  currentState = device.getCurrentSwitchState();
429
508
  this.debug(`使用缓存状态: 0x${currentState.toString(16).toUpperCase()}`);
@@ -433,37 +512,40 @@ module.exports = function(RED) {
433
512
  } else {
434
513
  // 如果没有当前状态,查询一次
435
514
  this.debug(`查询开关状态: 地址0x${networkAddr.toString(16).toUpperCase()}`);
436
- const queryFrame = this.protocolHandler.buildDeviceStatusQueryFrame(networkAddr, 0x02);
515
+ const queryFrame = this.protocolHandler.buildDeviceStatusQueryFrame(networkAddr, actualMsgType);
437
516
  await this.client.sendFrame(queryFrame, 2);
438
517
  await this.sleep(200);
439
-
518
+
440
519
  if (device && typeof device.getCurrentSwitchState === 'function') {
441
520
  currentState = device.getCurrentSwitchState();
442
521
  } else {
443
522
  // 使用默认全关状态
444
- const defaultStates = { 2: 0x05, 3: 0x15, 4: 0x55, 6: 0x5555 };
523
+ const defaultStates = { 2: 0x05, 3: 0x15, 4: 0x55, 6: 0x5555, 8: 0x5555 };
445
524
  currentState = defaultStates[channels] || 0x55;
446
525
  this.debug(`使用默认状态: 0x${currentState.toString(16).toUpperCase()}`);
447
526
  }
448
527
  }
449
-
528
+
450
529
  // 使用状态组合算法
530
+ this.debug(`[开关控制] 路数=${channels}, 目标路=${targetChannel}, 目标状态=${targetState ? '开' : '关'}, 当前状态=0x${currentState ? currentState.toString(16).toUpperCase() : 'null'}`);
451
531
  const stateValue = this.protocolHandler.buildSwitchState(channels, targetChannel, targetState, currentState);
452
532
 
453
533
  let controlParam;
454
534
  let stateValueHex;
455
535
  if (Buffer.isBuffer(stateValue)) {
456
- // 6路开关,2字节状态
536
+ // 6-8路开关,2字节状态
457
537
  controlParam = stateValue;
458
538
  stateValueHex = stateValue.toString('hex').toUpperCase();
539
+ this.debug(`[开关控制] 生成2字节状态值: 0x${stateValueHex} (${Array.from(stateValue).map(b => '0x' + b.toString(16).toUpperCase()).join(', ')})`);
459
540
  } else {
460
541
  // 1-4路开关,1字节状态
461
542
  controlParam = Buffer.from([stateValue]);
462
543
  stateValueHex = stateValue.toString(16).toUpperCase();
544
+ this.debug(`[开关控制] 生成1字节状态值: 0x${stateValueHex}`);
463
545
  }
464
546
 
465
- this.debug(`发送开关控制: 地址0x${networkAddr.toString(16).toUpperCase()}, 第${targetChannel}路${targetState ? '开' : '关'}, 状态值0x${stateValueHex}`);
466
- const frame = this.protocolHandler.buildDeviceControlFrame(networkAddr, attrType, controlParam);
547
+ this.log(`[开关控制] ${device.name} 第${targetChannel}路${targetState ? '开' : '关'}`);
548
+ const frame = this.protocolHandler.buildDeviceControlFrame(networkAddr, actualMsgType, controlParam);
467
549
  return await this.client.sendFrame(frame, 1);
468
550
 
469
551
  } catch (error) {