node-red-contrib-symi-mesh 1.7.0 → 1.7.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.
package/README.md CHANGED
@@ -382,6 +382,7 @@ npm install node-red-contrib-home-assistant-websocket
382
382
  - 支持与HA中任意实体同步
383
383
  - 配置灵活,易于调整
384
384
 
385
+
385
386
  ## 协议说明
386
387
 
387
388
  ### 核心协议格式
@@ -1254,6 +1255,30 @@ node-red-contrib-symi-mesh/
1254
1255
 
1255
1256
  ## 更新日志
1256
1257
 
1258
+ ### v1.7.1 (2025-12-21)
1259
+ - **自定义协议全面修复**:完善自定义开关/窗帘/空调双向同步
1260
+ - **空调风速控制**:修复风速变化事件触发机制
1261
+ - 添加fanMode字段支持(mesh空调实际使用的字段)
1262
+ - 修复DeviceInfo事件触发,正确发送device-state-changed事件
1263
+ - 支持风速值1-4(1=高, 2=中, 3=低, 4=自动)
1264
+ - 自动识别温控器0x02消息中的风速控制
1265
+ - **空调开关控制**:修复开关状态变化事件触发
1266
+ - 温控器0x02开关消息正确触发device-state-changed事件
1267
+ - 自定义空调开关码(acSendOn/acSendOff)正确发送到RS485总线
1268
+ - **RS485收码匹配**:修复hexStr格式处理
1269
+ - hexStr完全去掉空格,确保标准格式(如030610330001BD27)
1270
+ - 用户录入支持带空格格式(如01 06 10 34 00 01 0D 04)
1271
+ - 自动匹配并触发mesh实体动作
1272
+ - **事件系统优化**:
1273
+ - DeviceInfo添加manager引用,正确触发事件
1274
+ - 修复事件名称不匹配问题(stateChange -> device-state-changed)
1275
+ - 确保所有自定义码双向同步正常
1276
+ - **队列处理**:
1277
+ - 命令队列顺序处理,防止并发冲突
1278
+ - 500ms防死循环机制
1279
+ - 队列限制100条,防止内存溢出
1280
+ - **调试日志**:添加详细调试日志,方便排查问题
1281
+
1257
1282
  ### v1.7.0 (2025-12-21)
1258
1283
  - **自定义协议增强**:RS485桥接节点自定义协议功能全面升级
1259
1284
  - 自定义开关:添加发开、发关、收开、收关4组码
@@ -1314,34 +1339,6 @@ node-red-contrib-symi-mesh/
1314
1339
  - 多设备类型:开关、调光灯、窗帘、空调、新风、地暖
1315
1340
  - 智能通道选择:根据Mesh设备实际路数显示可选通道
1316
1341
 
1317
- ### v1.6.6 (2025-12-09)
1318
- - **三合一面板完整双向同步**:支持空调+新风+地暖独立RS485映射
1319
- - 三合一0x94协议完整解析:空调(开关/模式/风速/温度)、地暖(开关/温度)、新风(开关/风速)
1320
- - Mesh→RS485:三合一状态变化自动发送对应RS485帧
1321
- - RS485→Mesh:RS485帧自动同步到三合一面板对应功能
1322
- - 支持climateSwitch/climateMode/fanMode/floorHeatingSwitch/freshAirSwitch等字段
1323
- - **开关双向同步修复**:修复米家App全开/全关只触发部分按键的问题
1324
- - 命令队列防抖逻辑修复:只有完全相同映射才合并,不同映射独立处理
1325
- - 支持同一Mesh设备多通道分别绑定不同RS485设备
1326
- - **RS485多映射匹配**:修复同一从机地址多个映射时只匹配第一个的问题
1327
- - findAllRS485Mappings返回所有匹配的映射
1328
- - rs485Channel精确匹配:根据寄存器地址确定通道
1329
- - **网络异常处理增强**:彻底修复Node.js 18+ AggregateError导致崩溃的问题
1330
- - 全局uncaughtException/unhandledRejection处理器
1331
- - TCP连接过程同步错误捕获
1332
- - 只捕获网络相关错误,不影响其他异常
1333
- - **三合一面板自动识别**:设备列表API返回isThreeInOne标志
1334
- - **窗帘双协议兼容**:同时支持米家和小程序两种控制协议
1335
- - 米家协议: `subOpcode=0x06, status: 0=打开中, 1=关闭中, 2=停止`
1336
- - 小程序协议: `subOpcode=0x05, status: 1=打开, 2=关闭, 3=停止`
1337
- - **话语前湾协议完善**:
1338
- - 空调:客厅/主卧/次卧1/次卧2,寄存器0x0FA0-0x0FAF
1339
- - 地暖:客餐厅(60)/主卧(61)/次卧1(62)/次卧2(63),寄存器0x0039/0x0043
1340
- - 新风:从机60,开关0x0039(开=1),风速0x004B(高=2,低=0)
1341
- - **地址输入修复**:允许从机地址为0(话语前湾空调地址是0)
1342
- - **内存安全**:命令队列限制100条,节点关闭时清理所有缓存
1343
- - **文档更新**:添加三合一面板RS485配置说明和协议对照表
1344
-
1345
1342
  ## 许可证
1346
1343
 
1347
1344
  MIT License
@@ -1353,7 +1350,7 @@ Copyright (c) 2025 SYMI 亖米
1353
1350
  ## 关于
1354
1351
 
1355
1352
  **作者**: SYMI 亖米
1356
- **版本**: 1.7.0
1353
+ **版本**: 1.7.1
1357
1354
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1358
1355
  **最后更新**: 2025-12-21
1359
1356
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
@@ -13,7 +13,7 @@ const DEVICE_TYPE_NAMES = {
13
13
  };
14
14
 
15
15
  class DeviceInfo {
16
- constructor(data) {
16
+ constructor(data, manager) {
17
17
  this.macAddress = data.macAddress;
18
18
  this.networkAddress = data.networkAddress;
19
19
  this.deviceType = data.deviceType;
@@ -25,6 +25,7 @@ class DeviceInfo {
25
25
  this.state = {};
26
26
  this.lastSeen = Date.now();
27
27
  this.isThreeInOne = false;
28
+ this.manager = manager; // 保存DeviceManager引用
28
29
  }
29
30
 
30
31
  generateName() {
@@ -199,7 +200,16 @@ class DeviceInfo {
199
200
  case 0x1C:
200
201
  // 风速设置(温控器和三合一空调都使用)
201
202
  if (parameters.length > 0) {
203
+ const oldFanMode = this.state.fanMode;
202
204
  this.state.fanMode = parameters[0];
205
+ // 触发状态变化事件
206
+ if (oldFanMode !== this.state.fanMode) {
207
+ this.emit('stateChange', {
208
+ device: this,
209
+ state: { fanMode: this.state.fanMode },
210
+ attrType: attrType
211
+ });
212
+ }
203
213
  }
204
214
  break;
205
215
  case 0x1D:
@@ -316,8 +326,10 @@ class DeviceInfo {
316
326
  }
317
327
 
318
328
  // byte2: bit[0-2]=空调风速 (协议值: 0=自动, 1=低, 2=中, 4=高)
329
+ // 统一转换为标准Mesh值: 1=高, 2=中, 3=低, 4=自动
319
330
  const fanLevel = byte2 & 0x07;
320
- this.state.fanMode = fanLevel;
331
+ const fanMap = { 0: 4, 1: 3, 2: 2, 4: 1 };
332
+ this.state.fanMode = fanMap[fanLevel] || 3; // 默认低风
321
333
 
322
334
  // byte3: 空调设定温度 (16-30°C)
323
335
  if (byte3 >= 16 && byte3 <= 30) {
@@ -353,8 +365,10 @@ class DeviceInfo {
353
365
  this.state.freshAirMode = freshMode;
354
366
 
355
367
  // byte7: bit[0-2]=新风风速 (协议值: 0=自动, 1=低, 2=中, 4=高)
368
+ // 统一转换为标准Mesh值: 1=高, 2=中, 3=低, 4=自动
356
369
  const freshFan = byte7 & 0x07;
357
- this.state.freshAirSpeed = freshFan;
370
+ const freshFanMap = { 0: 4, 1: 3, 2: 2, 4: 1 };
371
+ this.state.freshAirSpeed = freshFanMap[freshFan] || 4; // 默认自动
358
372
 
359
373
  }
360
374
 
@@ -385,7 +399,44 @@ class DeviceInfo {
385
399
  if (this.channels === 1) {
386
400
  // 单路开关
387
401
  const value = parameters[0];
388
- this.state.switch = value === 0x02;
402
+ console.log(`[DeviceManager] handleSwitchState: deviceType=${this.deviceType}, channels=${this.channels}, value=${value}, name=${this.name}`);
403
+ // 温控器设备:0x02消息可能是开关或风速
404
+ if (this.deviceType === 10) {
405
+ console.log(`[DeviceManager] 温控器设备,检查风速: value=${value}`);
406
+ // 如果值是1-4,是风速控制
407
+ if (value >= 1 && value <= 4) {
408
+ const oldFanMode = this.state.fanMode;
409
+ this.state.fanMode = value;
410
+ console.log(`[DeviceManager] 风速变化: ${oldFanMode} -> ${this.state.fanMode}`);
411
+ // 总是触发事件,即使值没变
412
+ console.log(`[DeviceManager] 触发fanMode事件`);
413
+ // 注意:这里不能直接emit,因为这是DeviceInfo实例,不是DeviceManager
414
+ // 需要通过DeviceManager来触发事件
415
+ if (this.manager) {
416
+ this.manager.emit('device-state-changed', {
417
+ device: this,
418
+ state: { fanMode: this.state.fanMode },
419
+ attrType: 0x1C
420
+ });
421
+ }
422
+ } else {
423
+ // 否则是开关控制
424
+ console.log(`[DeviceManager] 开关控制: value=${value}`);
425
+ const oldSwitch = this.state.switch;
426
+ this.state.switch = value === 0x02;
427
+ // 触发开关状态变化事件
428
+ if (oldSwitch !== this.state.switch && this.manager) {
429
+ console.log(`[DeviceManager] 触发switch事件: ${oldSwitch} -> ${this.state.switch}`);
430
+ this.manager.emit('device-state-changed', {
431
+ device: this,
432
+ state: { switch: this.state.switch },
433
+ attrType: 0x02
434
+ });
435
+ }
436
+ }
437
+ } else {
438
+ this.state.switch = value === 0x02;
439
+ }
389
440
  } else if (this.channels <= 4) {
390
441
  // 1-4路开关:通常1字节,但米家/面板操作可能发送2字节(0x45类型)
391
442
  let value;
@@ -601,7 +652,7 @@ class DeviceManager extends EventEmitter {
601
652
  let nameChanged = false;
602
653
 
603
654
  if (!device) {
604
- device = new DeviceInfo(deviceData);
655
+ device = new DeviceInfo(deviceData, this);
605
656
  this.devices.set(mac, device);
606
657
  isNew = true;
607
658
  } else {
@@ -524,6 +524,8 @@ module.exports = function(RED) {
524
524
 
525
525
  // 状态缓存 - 用于检测真正变化的开关
526
526
  node.stateCache = {};
527
+ // 空调模式缓存 - 用于温度控制时确定当前模式
528
+ node.climateCache = {};
527
529
  // 首次启动标记 - 跳过初始状态同步
528
530
  node.initializing = true;
529
531
  // 启动后延迟20秒再开始同步(Mesh网关需要15秒以上完成设备发现)
@@ -804,12 +806,22 @@ module.exports = function(RED) {
804
806
  );
805
807
  const hasACChange = isAC && (
806
808
  changed.targetTemp !== undefined ||
809
+ changed.acTargetTemp !== undefined ||
810
+ changed.temperature !== undefined ||
807
811
  changed.acMode !== undefined ||
812
+ changed.climateMode !== undefined ||
813
+ changed.mode !== undefined ||
808
814
  changed.acFanSpeed !== undefined ||
809
- // 三合一面板使用climate前缀
815
+ changed.fanSpeed !== undefined ||
816
+ changed.climateFanSpeed !== undefined ||
817
+ changed.fanMode !== undefined ||
818
+ changed.fan_speed !== undefined ||
819
+ changed.fanLevel !== undefined ||
820
+ changed.fan_level !== undefined ||
821
+ changed.speed !== undefined ||
822
+ changed.acSwitch !== undefined ||
810
823
  changed.climateSwitch !== undefined ||
811
- changed.climateMode !== undefined ||
812
- changed.fanMode !== undefined
824
+ changed.switch !== undefined
813
825
  );
814
826
  // 新风设备检测
815
827
  const isFreshAir = device.includes('fresh_air');
@@ -1233,25 +1245,50 @@ module.exports = function(RED) {
1233
1245
  }
1234
1246
  // 空调类型
1235
1247
  else if (mapping.device === 'custom_climate') {
1248
+ node.log(`[自定义空调] 处理状态: ${JSON.stringify(state)}, codes存在: ${!!codes}`);
1236
1249
  for (const [key, value] of Object.entries(state)) {
1237
1250
  let hexCode = null;
1238
- if (key === 'acSwitch' || key === 'climateSwitch') {
1239
- hexCode = value ? codes.acSendOn : codes.acSendOff;
1251
+
1252
+ // 1. 开关控制 (处理 acSwitch, climateSwitch, switch)
1253
+ if (key === 'acSwitch' || key === 'climateSwitch' || key === 'switch') {
1254
+ const isOn = value === true || value === 1 || value === '1' || value === 'on' || value === 'ON';
1255
+ hexCode = isOn ? (codes.acSendOn || codes.sendOn) : (codes.acSendOff || codes.sendOff);
1256
+ node.log(`[自定义空调] 开关控制: ${key}=${value}, isOn=${isOn}, hexCode=${hexCode}`);
1240
1257
  }
1241
- else if (key === 'acFanSpeed' || key === 'fanSpeed') {
1242
- if (value === 1) hexCode = codes.fanSendHigh;
1243
- else if (value === 2) hexCode = codes.fanSendMid;
1244
- else if (value === 3) hexCode = codes.fanSendLow;
1258
+ // 2. 风速控制
1259
+ else if (['acFanSpeed', 'fanSpeed', 'climateFanSpeed', 'fanMode', 'fan_speed', 'fanLevel', 'fan_level', 'speed'].includes(key)) {
1260
+ const val = parseInt(value);
1261
+ node.log(`[自定义空调] 风速控制: ${key}=${value}, val=${val}`);
1262
+ // 标准Mesh协议: 1=高, 2=中, 3=低, 4=自动
1263
+ if (val === 1) {
1264
+ hexCode = codes.fanSendHigh;
1265
+ node.log(`[自定义空调] 高风, hexCode=${hexCode}`);
1266
+ }
1267
+ else if (val === 2) {
1268
+ hexCode = codes.fanSendMid;
1269
+ node.log(`[自定义空调] 中风, hexCode=${hexCode}`);
1270
+ }
1271
+ else if (val === 3 || val === 0 || val === 4) {
1272
+ hexCode = codes.fanSendLow;
1273
+ node.log(`[自定义空调] 低风, hexCode=${hexCode}`);
1274
+ }
1245
1275
  }
1246
- else if (key === 'acMode' || key === 'climateMode') {
1276
+ // 3. 模式控制
1277
+ else if (key === 'acMode' || key === 'climateMode' || key === 'mode') {
1247
1278
  if (value === 1) hexCode = codes.modeSendCool;
1248
1279
  else if (value === 2) hexCode = codes.modeSendHeat;
1249
1280
  else if (value === 4) hexCode = codes.modeSendDry;
1250
1281
  else if (value === 3) hexCode = codes.modeSendFan;
1282
+
1283
+ // 更新缓存中的模式
1284
+ if (!node.climateCache[mapping.meshMac]) node.climateCache[mapping.meshMac] = {};
1285
+ node.climateCache[mapping.meshMac].mode = value;
1251
1286
  }
1252
- else if (key === 'targetTemp' || key === 'acTargetTemp') {
1287
+ // 4. 温度控制
1288
+ else if (key === 'targetTemp' || key === 'acTargetTemp' || key === 'temperature') {
1253
1289
  const temp = Math.round(value);
1254
- const mode = state.acMode || state.climateMode || 1; // 1=Cool, 2=Heat, 3=Fan, 4=Dry
1290
+ // 优先从当前变化中获取模式,否则从缓存中获取,默认制冷(1)
1291
+ const mode = state.acMode || state.climateMode || (node.climateCache[mapping.meshMac] ? node.climateCache[mapping.meshMac].mode : 1);
1255
1292
 
1256
1293
  // 1. 优先尝试特定温度码,如 tempSendCool16
1257
1294
  const modeSuffix = (mode === 2) ? 'Heat' : 'Cool';
@@ -1529,19 +1566,44 @@ module.exports = function(RED) {
1529
1566
  for (const [key, value] of Object.entries(state)) {
1530
1567
  try {
1531
1568
  // 开关类型
1532
- if (key === 'switch') {
1569
+ if (key === 'switch' || key.startsWith('switch_')) {
1570
+ const ch = key.startsWith('switch_') ? parseInt(key.replace('switch_', '')) : channel;
1533
1571
  const onOff = value ? 0x02 : 0x01;
1534
- const param = Buffer.from([channel - 1, onOff]);
1572
+ const param = Buffer.from([ch - 1, onOff]);
1573
+ await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
1574
+ node.log(`[自定义->Mesh] 开关${ch}: ${value ? '开' : '关'}`);
1575
+ }
1576
+ // 空调开关
1577
+ else if (key === 'acSwitch' || key === 'climateSwitch') {
1578
+ const param = Buffer.from([value ? 0x02 : 0x01]);
1535
1579
  await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
1536
- node.log(`[自定义->Mesh] 开关${channel}: ${value ? '开' : '关'}`);
1580
+ node.log(`[自定义->Mesh] 空调开关: ${value ? '开' : '关'}`);
1581
+ }
1582
+ // 空调模式
1583
+ else if (key === 'acMode' || key === 'climateMode' || key === 'mode') {
1584
+ const param = Buffer.from([value]);
1585
+ await node.gateway.sendControl(meshDevice.networkAddress, 0x1D, param);
1586
+ node.log(`[自定义->Mesh] 空调模式: ${value}`);
1587
+ }
1588
+ // 空调风速
1589
+ else if (key === 'acFanSpeed' || key === 'fanMode' || key === 'fanSpeed') {
1590
+ const param = Buffer.from([value]);
1591
+ await node.gateway.sendControl(meshDevice.networkAddress, 0x1C, param);
1592
+ node.log(`[自定义->Mesh] 空调风速: ${value}`);
1593
+ }
1594
+ // 目标温度
1595
+ else if (key === 'acTargetTemp' || key === 'targetTemp') {
1596
+ const param = Buffer.from([Math.round(value)]);
1597
+ await node.gateway.sendControl(meshDevice.networkAddress, 0x1B, param);
1598
+ node.log(`[自定义->Mesh] 目标温度: ${value}°C`);
1537
1599
  }
1538
1600
  // 窗帘类型
1539
- else if (key === 'action' || key === 'position') {
1601
+ else if (key === 'action' || key === 'position' || key === 'curtainAction' || key === 'curtainPosition') {
1540
1602
  // 窗帘动作: 1=打开, 2=关闭, 3=停止
1541
1603
  let action = 0x03; // 停止
1542
- if (value === 'open') action = 0x01; // 打开
1543
- else if (value === 'close') action = 0x02; // 关闭
1544
- else if (value === 'stop') action = 0x03; // 停止
1604
+ if (value === 'open' || value === 1) action = 0x01; // 打开
1605
+ else if (value === 'close' || value === 2) action = 0x02; // 关闭
1606
+ else if (value === 'stop' || value === 3) action = 0x03; // 停止
1545
1607
 
1546
1608
  const param = Buffer.from([action]);
1547
1609
  // 0x05是窗帘动作控制属性
@@ -1751,7 +1813,7 @@ module.exports = function(RED) {
1751
1813
  node.parseRS485Frame = function(frame) {
1752
1814
  if (frame.length < 4) return;
1753
1815
 
1754
- const hexStr = frame.toString('hex').toUpperCase();
1816
+ const hexStr = frame.toString('hex').toUpperCase().replace(/\s/g, ''); // 确保完全去掉空格
1755
1817
  const hexFormatted = hexStr.match(/.{1,2}/g)?.join(' ') || '';
1756
1818
 
1757
1819
  node.log(`[RS485收到] ${hexFormatted} (${frame.length}字节)`);
@@ -1829,24 +1891,30 @@ module.exports = function(RED) {
1829
1891
  const codes = mapping.customCodes;
1830
1892
  let matchedAction = null;
1831
1893
 
1832
- node.debug(`[自定义码检测] 检查映射: device=${mapping.device}, meshMac=${mapping.meshMac}, codes=${JSON.stringify(codes)}`);
1894
+ node.log(`[自定义码检测] 检查映射: device=${mapping.device}, meshMac=${mapping.meshMac}`);
1833
1895
 
1834
1896
  // 开关类型:匹配recvOn/recvOff
1835
1897
  if (mapping.device === 'custom_switch') {
1836
1898
  const recvOn = (codes.recvOn || '').replace(/\s/g, '').toUpperCase();
1837
1899
  const recvOff = (codes.recvOff || '').replace(/\s/g, '').toUpperCase();
1900
+ node.log(`[自定义开关] recvOn=${recvOn}, recvOff=${recvOff}, hexStr=${hexStr}`);
1901
+ node.log(`[自定义开关] 包含recvOn? ${hexStr.includes(recvOn)}, 包含recvOff? ${hexStr.includes(recvOff)}`);
1838
1902
 
1839
1903
  // 翻转模式:收开码=收关码
1840
1904
  if (recvOn && recvOff && recvOn === recvOff && hexStr.includes(recvOn)) {
1841
1905
  const device = node.gateway.deviceManager.getDeviceByMac(mapping.meshMac);
1842
- const currentState = device?.state?.switch || false;
1906
+ const channel = mapping.meshChannel || 1;
1907
+ const stateKey = mapping.meshChannel > 1 ? `switch_${channel}` : 'switch';
1908
+ const currentState = device?.state?.[stateKey] || false;
1843
1909
  matchedAction = { switch: !currentState };
1844
- node.log(`[自定义开关] 翻转: ${currentState} -> ${!currentState}`);
1910
+ node.log(`[自定义开关] 翻转: ${stateKey} ${currentState} -> ${!currentState}`);
1845
1911
  } else {
1846
1912
  if (recvOn && hexStr.includes(recvOn)) {
1847
1913
  matchedAction = { switch: true };
1914
+ node.log(`[自定义开关] 匹配到收开码`);
1848
1915
  } else if (recvOff && hexStr.includes(recvOff)) {
1849
1916
  matchedAction = { switch: false };
1917
+ node.log(`[自定义开关] 匹配到收关码`);
1850
1918
  }
1851
1919
  }
1852
1920
  }
@@ -1882,7 +1950,25 @@ module.exports = function(RED) {
1882
1950
  matchedAction = { acFanSpeed: 3 };
1883
1951
  }
1884
1952
  else if (codes.modeRecvCool && hexStr.includes(codes.modeRecvCool.replace(/\s/g, '').toUpperCase())) {
1885
- for (const m of modes) {
1953
+ matchedAction = { acMode: 1 };
1954
+ } else if (codes.modeRecvHeat && hexStr.includes(codes.modeRecvHeat.replace(/\s/g, '').toUpperCase())) {
1955
+ matchedAction = { acMode: 2 };
1956
+ } else if (codes.modeRecvDry && hexStr.includes(codes.modeRecvDry.replace(/\s/g, '').toUpperCase())) {
1957
+ matchedAction = { acMode: 4 };
1958
+ } else if (codes.modeRecvFan && hexStr.includes(codes.modeRecvFan.replace(/\s/g, '').toUpperCase())) {
1959
+ matchedAction = { acMode: 3 };
1960
+ }
1961
+
1962
+ // 温度匹配 (独立于模式匹配,因为一帧可能同时包含模式和温度,或者只有温度)
1963
+ if (!matchedAction) {
1964
+ let foundTemp = false;
1965
+ const climateModes = [
1966
+ { suffix: 'Cool', val: 1 },
1967
+ { suffix: 'Heat', val: 2 }
1968
+ ];
1969
+
1970
+ // 1. 优先匹配 16-30度 独立码
1971
+ for (const m of climateModes) {
1886
1972
  for (let t = 16; t <= 30; t++) {
1887
1973
  const key = `tempRecv${m.suffix}${t}`;
1888
1974
  const code = (codes[key] || '').replace(/\s/g, '').toUpperCase();
@@ -1912,7 +1998,6 @@ module.exports = function(RED) {
1912
1998
  const suffix = parts[1];
1913
1999
  if (hexStr.includes(prefix)) {
1914
2000
  // 尝试从 hexStr 中提取温度
1915
- // 简单起见,如果包含前缀且后面有2位十六进制+后缀
1916
2001
  const startIdx = hexStr.indexOf(prefix) + prefix.length;
1917
2002
  const tempHex = hexStr.substring(startIdx, startIdx + 2);
1918
2003
  const remaining = hexStr.substring(startIdx + 2);
@@ -1934,6 +2019,11 @@ module.exports = function(RED) {
1934
2019
  }
1935
2020
 
1936
2021
  if (matchedAction) {
2022
+ // 更新缓存中的模式
2023
+ if (matchedAction.acMode !== undefined) {
2024
+ if (!node.climateCache[mapping.meshMac]) node.climateCache[mapping.meshMac] = {};
2025
+ node.climateCache[mapping.meshMac].mode = matchedAction.acMode;
2026
+ }
1937
2027
  node.log(`[自定义空调] 匹配: ${JSON.stringify(matchedAction)}`);
1938
2028
  }
1939
2029
  }
@@ -1342,11 +1342,27 @@ module.exports = function(RED) {
1342
1342
 
1343
1343
  } else if (topic.includes('/fan_mode/set')) {
1344
1344
  // 温控器/三合一空调风速 (0x1C协议: 1=高, 2=中, 3=低, 4=自动)
1345
+ node.log(`[MQTT] 收到风速控制: topic=${topic}, payload=${payload}`);
1345
1346
  const fans = { 'high': 1, 'medium': 2, 'low': 3, 'auto': 4 };
1346
1347
  const fan = fans[payload];
1348
+ node.log(`[MQTT] 风速映射: ${payload} -> ${fan}`);
1347
1349
  if (fan !== undefined) {
1348
1350
  commands.push({ attrType: 0x1C, param: Buffer.from([fan]) });
1349
- this.debug(`[MQTT解析] 空调风速命令: 0x1C=0x${fan.toString(16).toUpperCase()} (${payload})`);
1351
+ node.log(`[MQTT解析] 空调风速命令: 0x1C=0x${fan.toString(16).toUpperCase()} (${payload})`);
1352
+
1353
+ // 立即触发状态变化事件,通知RS485桥接节点
1354
+ if (device && node.gateway && node.gateway.deviceManager) {
1355
+ node.log(`[MQTT] 触发风速状态变化事件: fanMode=${fan}`);
1356
+ device.state.fanMode = fan;
1357
+ node.gateway.deviceManager.emit('stateChange', {
1358
+ device: device,
1359
+ state: { fanMode: fan },
1360
+ attrType: 0x1C,
1361
+ isUserControl: true
1362
+ });
1363
+ } else {
1364
+ node.warn(`[MQTT] 无法触发事件: device=${!!device}, gateway=${!!node.gateway}, deviceManager=${!!node.gateway?.deviceManager}`);
1365
+ }
1350
1366
  }
1351
1367
 
1352
1368
  } else if (topic.endsWith('/cover/set')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {