node-red-contrib-symi-mesh 1.7.7 → 1.7.8

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
@@ -368,7 +368,7 @@ npm install node-red-contrib-home-assistant-websocket
368
368
  **适合使用KNX-HA桥接的场景**:
369
369
  - HA中已有KNX集成,希望与其他系统整合
370
370
  - 需要KNX设备与HA中的Zigbee、WiFi等设备联动
371
- - 希望在HA中统一管理所有设备
371
+ - 可以在HA中统一管理所有设备
372
372
  - 需要利用HA的自动化和场景功能
373
373
  - 已安装node-red-contrib-home-assistant-websocket,共享HA配置
374
374
 
@@ -383,6 +383,46 @@ npm install node-red-contrib-home-assistant-websocket
383
383
  - 配置灵活,易于调整
384
384
 
385
385
 
386
+ ### HA双向同步
387
+
388
+ Symi HA同步节点 (`symi-ha-sync`) 用于实现Symi蓝牙Mesh设备与Home Assistant实体的直接双向同步。
389
+
390
+ #### 功能特性
391
+
392
+ 1. **配置复用**:
393
+ - 复用`symi-mqtt`配置节点获取Symi设备信息
394
+ - 复用`server`配置节点连接Home Assistant
395
+ 2. **双向同步**:
396
+ - **Symi -> HA**:Mesh设备状态变化 -> 更新HA实体状态
397
+ - **HA -> Symi**:HA实体状态变化 -> 控制Mesh设备
398
+ 3. **防死循环**:
399
+ - 内置800ms冷却机制
400
+ - 区分Symi触发和HA触发,避免信号震荡
401
+ 4. **便捷配置**:
402
+ - 自动加载所有Symi设备和HA实体
403
+ - 支持按键通道选择(1-4键)
404
+ - 支持实体ID搜索和下拉选择
405
+
406
+ #### 配置步骤
407
+
408
+ 1. **添加节点**:从左侧拖入`Symi HA Sync`节点
409
+ 2. **选择MQTT配置**:选择已有的`symi-mqtt`配置节点(共享Symi网关连接)
410
+ 3. **选择HA服务器**:选择已有的`server`配置节点(共享HA连接)
411
+ 4. **添加映射**:
412
+ - 点击"添加"按钮
413
+ - **Symi设备**:下拉选择Mesh设备(显示名称和MAC)
414
+ - **按键**:选择控制通道(按键1-4)
415
+ - **HA实体**:输入或选择要同步的HA实体ID(如`switch.living_room_light`)
416
+ 5. **部署**:点击部署,立即生效
417
+
418
+ #### 注意事项
419
+
420
+ - **MQTT配置**:必须选择`symi-mqtt`配置节点,用于获取设备列表和接收Mesh事件
421
+ - **HA连接**:必须确保HA服务器节点连接正常
422
+ - **实体类型**:建议同步相同类型的实体(如开关对开关,调光灯对灯光)
423
+ - **多通道设备**:对于多键开关,请分别为每个按键添加一条映射
424
+
425
+
386
426
  ## 协议说明
387
427
 
388
428
  ### 核心协议格式
@@ -1291,6 +1331,7 @@ A: 每个房间部署一个云端同步节点,配置对应的酒店ID和房间
1291
1331
  | **Symi RS485 Bridge** | RS485设备双向同步 | [RS485/Modbus双向同步](#rs485modbus双向同步) |
1292
1332
  | **Symi KNX Bridge** | KNX系统双向同步 | [KNX双向同步](#knx双向同步) |
1293
1333
  | **Symi KNX-HA Bridge** | KNX与HA实体双向同步 | [KNX-HA双向同步](#knx-ha双向同步) |
1334
+ | **Symi HA Sync** | HA实体双向同步 | [HA双向同步](#ha双向同步) |
1294
1335
  | **Symi MQTT Sync** | 第三方MQTT品牌设备同步 | v1.7.3新增 |
1295
1336
  | **Symi MQTT Brand** | 品牌MQTT配置节点 | v1.7.3新增 |
1296
1337
  | **RS485 Debug** | RS485总线数据抓包调试 | [RS485调试节点](#rs485调试节点) |
@@ -1462,65 +1503,53 @@ node-red-contrib-symi-mesh/
1462
1503
 
1463
1504
  ## 更新日志
1464
1505
 
1465
- ### v1.7.5 (2025-12-24)
1466
- - **API路径兼容性修复**:修复HassOS环境下节点配置界面无法加载设备列表的问题
1467
- - **根本原因**:HassOS Node-RED使用nginx反向代理,绝对路径`/api`会被拦截,需使用相对路径`api`
1468
- - **修复范围**:所有节点HTML中的`$.getJSON()`调用统一使用相对路径
1469
- - **开发规范**:后续开发节点时,所有前端API调用必须使用相对路径,不带前导斜杠
1470
- - **KNX Bridge双向同步修复**:修复Mesh控制KNX开关不工作的问题
1471
- - **根本原因**:knxUltimate节点当`setTopicType: "str"`时使用`msg.topic`作为目标地址,而非`msg.destination`
1472
- - **修复方案**:发送消息同时包含`topic`和`destination`字段,确保兼容所有knxUltimate配置
1473
- - **消息格式**:`{ topic, destination, payload, dpt, event }`
1474
- - **防死循环**:不包含`knx`对象,避免触发knxUltimate的循环引用保护机制
1475
- - **用户体验优化**:
1476
- - 设备列表加载时显示"加载中..."提示
1477
- - API调用失败时显示"加载失败,请重试"提示
1478
-
1479
- ### v1.7.4 (2025-12-24)
1480
- - **串口配置完善**:完整的串口参数配置,解决HassOS环境串口锁定问题
1481
- - **完整参数**:波特率、数据位(7/8)、停止位(1/2)、校验位(None/Even/Odd)
1482
- - **串口解锁**:添加`lock: false`参数,避免HassOS环境下"Cannot lock port"错误
1483
- - **配置持久化**:所有串口参数自动保存,重启后保持
1484
- - **统一界面**:symi-gateway和symi-485-config使用相同的串口配置界面
1485
- - **串口兼容性修复**:兼容serialport v9和v10+,确保HassOS环境正常使用串口
1486
- - `serial-client.js`:动态检测serialport版本
1487
- - `symi-485-config.js`:兼容v9/v10+ API
1488
- - `symi-485-bridge.js`:兼容v9/v10+ API
1489
- - **KNX Bridge窗帘/调光同步优化**:重新设计步进设备同步逻辑,解决双向控制时乱动问题
1490
- - **控制锁定机制**:谁先发起控制谁锁定,3秒内忽略另一方的反馈
1491
- - **窗帘设备**:Mesh控制→锁定→忽略KNX反馈;KNX控制→锁定→忽略Mesh反馈
1492
- - **调光设备**:同样应用控制锁定机制,避免亮度调节过程中的反馈干扰
1493
- - **停止解锁**:窗帘stopped动作会解除锁定,允许下一次控制
1494
- - **位置/亮度同步**:只同步最终值,不同步过程中的步进状态
1495
-
1496
- ### v1.7.3 (2025-12-24)
1497
- - **MQTT同步节点**:新增`symi-mqtt-sync`节点实现第三方MQTT品牌设备与Mesh设备双向同步
1498
- - **双MQTT配置节点架构**:
1499
- - Mesh MQTT:选择`symi-mqtt`配置节点,获取Mesh设备列表
1500
- - 品牌MQTT:选择`symi-mqtt-brand`配置节点(支持下拉选择+编辑+添加)
1501
- - **品牌MQTT配置节点**:新增`symi-mqtt-brand`配置节点
1502
- - 支持配置MQTT服务器地址、用户名、密码
1503
- - 支持HYQW协议(项目代码、设备SN)
1504
- - 自动发现品牌MQTT设备实体
1505
- - 可扩展支持更多品牌协议
1506
- - **实体映射**:
1507
- - 左边选择Mesh设备+通道
1508
- - 右边选择品牌设备+通道(灯具支持多路)
1509
- - 相同类型实体一对一映射
1510
- - **设备类型**:灯具(8)、空调(12)、窗帘(14)、地暖(16)、新风(36)
1511
- - **双向同步**:MQTT↔Mesh实时状态同步,2秒防抖防死循环
1512
- - **输入输出端口**:支持连接debug节点调试同步数据
1513
- - **错误日志限流**:网络故障时每60秒最多记录一次错误
1514
- - **断线自动重连**:5秒重连间隔
1515
- - **串口支持优化**:
1516
- - 所有串口节点支持手动输入+搜索选择
1517
- - 兼容serialport v9/v10+,兼容HassOS环境
1518
- - 串口错误日志限流,避免长时间故障时日志爆炸
1519
- - **稳定性优化**:
1520
- - 错误日志频率限制,长时间网络故障不影响系统性能
1521
- - 静默处理非关键错误,生产级稳定性
1522
- - 内存安全,无调试日志,断电断网恢复后正常工作
1523
- - 缓存队列处理,符合MQTT协议要求
1506
+ ### v1.7.8 (2026-01-05)
1507
+ - **配置持久化增强**:所有同步节点的设备列表和映射配置持久保存
1508
+ - **MQTT同步节点**:Mesh设备和品牌设备列表持久化,断线后仍可显示已配置的映射
1509
+ - **HA同步节点**:Symi设备和HA实体列表持久化,断线后仍可显示已配置的映射
1510
+ - **RS485桥接节点**:Mesh设备列表持久化,断线后仍可显示已配置的映射
1511
+ - **离线设备显示**:[离线]标记缓存中但当前不在线的设备,[未找到]标记不在缓存中的设备
1512
+ - **刷新按钮**:各节点添加独立刷新按钮,可手动刷新设备列表
1513
+ - **MQTT品牌同步增强**:完整对接HYQW(花语前湾)MQTT协议
1514
+ - **完整设备类型支持**:灯具(8)、空调(12)、窗帘(14)、地暖(16)、新风(36)
1515
+ - **功能码映射**:完整的fn/fv到Mesh属性双向转换
1516
+ - 灯具:开关(fn=1)、亮度(fn=2, 0-100)
1517
+ - 空调:开关(fn=1)、温度(fn=2, 18-29°C)、模式(fn=3)、风速(fn=4)
1518
+ - 窗帘:动作(fn=1, 开/关/停)、位置(fn=2, 0-100%)
1519
+ - 地暖:开关(fn=1)、温度(fn=2, 5-35°C)
1520
+ - 新风:开关(fn=1)、风速(fn=3)
1521
+ - **可扩展架构**:BRAND_PROTOCOLS对象支持添加新品牌协议
1522
+ - **双向状态同步**:HYQW↔Mesh实时同步,2秒防抖防死循环
1523
+ - **自动设备发现**:自动发现品牌MQTT设备,限制200个
1524
+ - **错误日志限流**:每60秒最多记录一次错误,避免日志爆炸
1525
+ - **自动重连**:5秒重连间隔,断线自动恢复
1526
+ - **资源清理**:完善的定时器和事件监听器清理机制
1527
+ - **HA同步节点完整重构**:实现所有实体类型的完美双向同步
1528
+ - **完整实体类型支持**:
1529
+ - switch/input_boolean:开关状态 (on/off)
1530
+ - light:开关 + 亮度 (0-255 ↔ 0-100)
1531
+ - cover:开/关/停 + 位置 (0-100%)
1532
+ - climate:开关 + 温度 + 模式(cool/heat/fan_only/dry) + 风速(high/medium/low/auto)
1533
+ - fan:开关 + 风速
1534
+ - **Mesh属性完整映射**:
1535
+ - 0x02 开关状态(单路/多路)
1536
+ - 0x03 亮度 (0-100)
1537
+ - 0x05 窗帘运行状态
1538
+ - 0x06 窗帘位置 (0-100)
1539
+ - 0x1B 目标温度 (16-30°C)
1540
+ - 0x1C 风速 (1=高/2=中/3=低/4=自动)
1541
+ - 0x1D 空调模式 (1=制冷/2=制热/3=送风/4=除湿)
1542
+ - **智能按键选择**:只有多路开关才显示按键选择,温控器/窗帘/调光灯显示"-"
1543
+ - **设备类型标签**:自动识别并显示设备类型([温控器]、[窗帘]、[调光灯]、[N路开关])
1544
+ - **智能防抖机制**:窗帘和调光灯使用500ms防抖,只同步最终位置/亮度
1545
+ - **防死循环增强**:2秒冷却时间 + 双向时间戳检查
1546
+ - **配置持久化**:设备列表和映射配置持久保存
1547
+ - **内存泄漏防护**:定时清理过期时间戳和防抖定时器
1548
+ - **代码质量优化**:
1549
+ - 同步时间戳Map自动清理60秒以上的条目
1550
+ - 设备发现数量限制,防止内存溢出
1551
+ - 完善的节点关闭清理逻辑
1552
+ - 防死循环机制增强:双向时间戳检查
1524
1553
 
1525
1554
  ### v1.7.7 (2026-01-05)
1526
1555
  - **RS485桥接节点逻辑优化**:
@@ -1596,18 +1625,6 @@ node-red-contrib-symi-mesh/
1596
1625
  - 队列限制100条,防止内存溢出
1597
1626
  - **调试日志**:添加详细调试日志,方便排查问题
1598
1627
 
1599
- ### v1.7.0 (2025-12-21)
1600
- - **自定义协议增强**:RS485桥接节点自定义协议功能全面升级
1601
- - 自定义开关:添加发开、发关、收开、收关4组码
1602
- - 翻转模式:收开码=收关码时,收到后自动翻转开关状态
1603
- - 自定义窗帘:添加发开、发关、发停、收开、收关、收停6组码
1604
- - 自定义空调:新增完整收发码支持(开关、风速、模式、温度)
1605
- - 支持最多24字节(72个十六进制字符)数据录入
1606
- - 自定义模式自动隐藏地址字段
1607
- - 配置持久化保存,重启后保持
1608
- - 防死循环机制优化
1609
- - 内存优化,防止日志溢出
1610
-
1611
1628
  ## 许可证
1612
1629
 
1613
1630
  MIT License
@@ -1619,7 +1636,7 @@ Copyright (c) 2025 SYMI 亖米
1619
1636
  ## 关于
1620
1637
 
1621
1638
  **作者**: SYMI 亖米
1622
- **版本**: 1.7.6
1639
+ **版本**: 1.7.8
1623
1640
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1624
1641
  **最后更新**: 2026-01-05
1625
1642
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
@@ -6,7 +6,9 @@
6
6
  name: { value: '' },
7
7
  gateway: { value: '', type: 'symi-gateway', required: true },
8
8
  rs485Config: { value: '', type: 'symi-485-config', required: true },
9
- mappings: { value: '[]' }
9
+ mappings: { value: '[]' },
10
+ // 持久化缓存:保存设备列表,断线后仍可显示
11
+ cachedMeshDevices: { value: '[]' }
10
12
  },
11
13
  inputs: 1,
12
14
  outputs: 1,
@@ -26,6 +28,7 @@
26
28
  var meshDevices = [];
27
29
  var protocolData = {};
28
30
  var mappings = [];
31
+ var cachedMeshDevices = [];
29
32
 
30
33
  // 设置编辑面板更宽
31
34
  var panel = $('#dialog-form').parent();
@@ -34,23 +37,58 @@
34
37
  }
35
38
 
36
39
  try { mappings = JSON.parse(node.mappings || '[]'); } catch(e) { mappings = []; }
40
+ try { cachedMeshDevices = JSON.parse(node.cachedMeshDevices || '[]'); } catch(e) { cachedMeshDevices = []; }
41
+
42
+ // 合并设备列表:在线设备 + 缓存设备(去重)
43
+ function mergeDevices(onlineDevices, cachedDevices) {
44
+ var merged = [];
45
+ var keys = new Set();
46
+
47
+ // 先添加在线设备
48
+ (onlineDevices || []).forEach(function(d) {
49
+ var key = (d.mac || '').toLowerCase().replace(/:/g, '');
50
+ if (key && !keys.has(key)) {
51
+ keys.add(key);
52
+ d._online = true;
53
+ merged.push(d);
54
+ }
55
+ });
56
+
57
+ // 再添加缓存中不在线的设备
58
+ (cachedDevices || []).forEach(function(d) {
59
+ var key = (d.mac || '').toLowerCase().replace(/:/g, '');
60
+ if (key && !keys.has(key)) {
61
+ keys.add(key);
62
+ d._online = false;
63
+ merged.push(d);
64
+ }
65
+ });
66
+
67
+ return merged;
68
+ }
37
69
 
38
70
  // 加载Mesh设备 - 等待gateway选择框初始化完成
39
71
  function loadMeshDevices(callback) {
40
72
  var gatewayId = $('#node-input-gateway').val();
41
73
  if (!gatewayId) {
42
- meshDevices = [];
74
+ meshDevices = mergeDevices([], cachedMeshDevices);
43
75
  if (callback) callback();
44
76
  return;
45
77
  }
46
78
  $.getJSON('symi-rs485-bridge/mesh-devices/' + gatewayId)
47
79
  .done(function(devices) {
48
- meshDevices = devices || [];
80
+ meshDevices = mergeDevices(devices || [], cachedMeshDevices);
81
+ // 更新缓存
82
+ if (devices && devices.length > 0) {
83
+ cachedMeshDevices = devices.map(function(d) {
84
+ return { mac: d.mac, name: d.name, channels: d.channels };
85
+ });
86
+ }
49
87
  console.log('[RS485 Bridge] 加载Mesh设备:', meshDevices.length);
50
88
  if (callback) callback();
51
89
  })
52
90
  .fail(function() {
53
- meshDevices = [];
91
+ meshDevices = mergeDevices([], cachedMeshDevices);
54
92
  if (callback) callback();
55
93
  });
56
94
  }
@@ -81,25 +119,34 @@
81
119
  return device.channels || 1;
82
120
  }
83
121
 
84
- // 构建Mesh设备选项(不包含按键,按键单独选择)
85
- function getMeshOptions(selectedMac) {
122
+ // 构建Mesh设备选项(不包含按键,按键单独选择)- 支持离线显示
123
+ function getMeshOptions(selectedMac, savedName) {
86
124
  var html = '<option value="">-- 选择 --</option>';
87
125
  var selMacNorm = (selectedMac || '').toLowerCase().replace(/:/g, '');
126
+ var found = false;
88
127
  meshDevices.forEach(function(d) {
89
128
  var devMacNorm = (d.mac || '').toLowerCase().replace(/:/g, '');
90
129
  var selected = (devMacNorm === selMacNorm && selMacNorm !== '') ? ' selected' : '';
91
- html += '<option value="' + d.mac + '" data-channels="' + (d.channels || 1) + '"' + selected + '>' + d.name + '</option>';
130
+ if (selected) found = true;
131
+ var statusIcon = d._online === false ? ' [离线]' : '';
132
+ var style = d._online === false ? ' style="color:#999;"' : '';
133
+ html += '<option value="' + d.mac + '" data-channels="' + (d.channels || 1) + '" data-name="' + (d.name || '') + '"' + selected + style + '>' + (d.name || d.mac) + statusIcon + '</option>';
92
134
  });
135
+ // 如果已选择的设备不在列表中,添加它
136
+ if (selMacNorm && !found) {
137
+ var displayName = savedName || selectedMac;
138
+ html += '<option value="' + selectedMac + '" selected style="color:#c00;">' + displayName + ' [未找到]</option>';
139
+ }
93
140
  return html;
94
141
  }
95
142
 
96
- // 构建Mesh按键选项(仅当开关设备时显示)
97
- function getMeshChannelOptions(mac, selectedChannel) {
143
+ // 构建Mesh按键选项(仅当开关设备时显示)- 支持保存的通道数
144
+ function getMeshChannelOptions(mac, selectedChannel, savedChannels) {
98
145
  var macNorm = (mac || '').toLowerCase().replace(/:/g, '');
99
146
  var device = meshDevices.find(function(d) {
100
147
  return (d.mac || '').toLowerCase().replace(/:/g, '') === macNorm;
101
148
  });
102
- var channels = device ? (device.channels || 1) : 0;
149
+ var channels = device ? (device.channels || 1) : (savedChannels || 1);
103
150
  if (channels <= 1) return '';
104
151
  var html = '<select class="mesh-channel">';
105
152
  for (var i = 1; i <= channels; i++) {
@@ -207,8 +254,8 @@
207
254
  row.html(
208
255
  '<div class="mapping-main">' +
209
256
  '<div class="mesh-col">' +
210
- ' <select class="mesh-select">' + getMeshOptions(m.meshMac) + '</select>' +
211
- ' <span class="mesh-ch-wrap">' + getMeshChannelOptions(m.meshMac, m.meshChannel || 1) + '</span>' +
257
+ ' <select class="mesh-select">' + getMeshOptions(m.meshMac, m.meshName) + '</select>' +
258
+ ' <span class="mesh-ch-wrap">' + getMeshChannelOptions(m.meshMac, m.meshChannel || 1, m.meshChannels) + '</span>' +
212
259
  '</div>' +
213
260
  '<div class="arrow-col"><i class="fa fa-arrows-h"></i></div>' +
214
261
  '<div class="brand-col">' +
@@ -279,10 +326,13 @@
279
326
  var row = $(this).closest('.mapping-row');
280
327
  var idx = row.data('idx');
281
328
  var mac = $(this).val();
329
+ var opt = $(this).find('option:selected');
282
330
  mappings[idx].meshMac = mac || '';
331
+ mappings[idx].meshName = opt.data('name') || opt.text().replace(' [离线]', '').replace(' [未找到]', '');
332
+ mappings[idx].meshChannels = parseInt(opt.data('channels')) || 1;
283
333
  mappings[idx].meshChannel = 1;
284
334
  // 更新Mesh按键选择器
285
- row.find('.mesh-ch-wrap').html(getMeshChannelOptions(mac, 1));
335
+ row.find('.mesh-ch-wrap').html(getMeshChannelOptions(mac, 1, mappings[idx].meshChannels));
286
336
  bindEvents(); // 重新绑定新元素事件
287
337
  });
288
338
 
@@ -658,6 +708,8 @@
658
708
 
659
709
  var m = {
660
710
  meshMac: $(this).find('.mesh-select').val() || '',
711
+ meshName: $(this).find('.mesh-select option:selected').data('name') || $(this).find('.mesh-select option:selected').text().replace(' [离线]', '').replace(' [未找到]', ''),
712
+ meshChannels: parseInt($(this).find('.mesh-select option:selected').data('channels')) || 1,
661
713
  meshChannel: parseInt($(this).find('.mesh-channel').val()) || 1,
662
714
  brand: $(this).find('.brand-select').val() || '',
663
715
  device: $(this).find('.device-select').val() || '',
@@ -766,6 +818,8 @@
766
818
  mappings.push(m);
767
819
  });
768
820
  this.mappings = JSON.stringify(mappings);
821
+ // 保存缓存的设备列表
822
+ this.cachedMeshDevices = JSON.stringify(cachedMeshDevices);
769
823
  }
770
824
  });
771
825
  </script>
@@ -833,6 +887,8 @@
833
887
  <div class="form-tips" style="margin-top: 10px;">
834
888
  <p><b>提示:</b>开关设备需要分别选择Mesh和RS485的按键,可自由配置对应关系(如Mesh第3路 ↔ RS485第1路)</p>
835
889
  </div>
890
+
891
+ <input type="hidden" id="node-input-cachedMeshDevices">
836
892
  </script>
837
893
 
838
894
  <script type="text/html" data-help-name="symi-rs485-bridge">