node-red-contrib-symi-mesh 1.7.5 → 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 +101 -75
- package/nodes/symi-485-bridge.html +71 -14
- package/nodes/symi-485-bridge.js +13 -13
- package/nodes/symi-ha-sync.html +451 -0
- package/nodes/symi-ha-sync.js +787 -0
- package/nodes/symi-mqtt-brand.js +243 -59
- package/nodes/symi-mqtt-sync.html +151 -42
- package/nodes/symi-mqtt-sync.js +45 -18
- package/package.json +2 -1
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
|
-
-
|
|
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,67 +1503,64 @@ node-red-contrib-symi-mesh/
|
|
|
1462
1503
|
|
|
1463
1504
|
## 更新日志
|
|
1464
1505
|
|
|
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
|
+
- 防死循环机制增强:双向时间戳检查
|
|
1553
|
+
|
|
1554
|
+
### v1.7.7 (2026-01-05)
|
|
1555
|
+
- **RS485桥接节点逻辑优化**:
|
|
1556
|
+
- 修复自定义码模式下"反馈"按钮点击部署后依旧开启的问题
|
|
1557
|
+
- 完善 `oneditsave` 逻辑,确保反馈选项状态被持久化保存
|
|
1558
|
+
- **防循环优化**:优化 `loopKey` 生成逻辑,加入 `meshChannel` 字段,支持多通道设备独立防循环,解决多路开关干扰问题
|
|
1559
|
+
- **冷却时间调整**:将防死循环冷却时间从 500ms 延长至 800ms,提高复杂网络环境下的同步稳定性
|
|
1560
|
+
- **反馈逻辑细化**:严格执行"反馈"勾选逻辑,未勾选时禁止回环发送自定义码到总线
|
|
1561
|
+
- **调试增强**:增加自定义码匹配成功的详细日志(包含设备 MAC 和具体动作)
|
|
1562
|
+
|
|
1465
1563
|
### 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协议要求
|
|
1524
|
-
|
|
1525
|
-
### v1.7.2 (2025-12-22)
|
|
1526
1564
|
- **RS485双向同步节点**:新增`symi-rs485-sync`独立节点实现两种不同RS485协议之间的双向同步
|
|
1527
1565
|
- 支持中弘VRF网关协议(功能码0x31-0x34控制,0x50查询)
|
|
1528
1566
|
- 支持SYMI空调面板Modbus协议
|
|
@@ -1587,18 +1625,6 @@ node-red-contrib-symi-mesh/
|
|
|
1587
1625
|
- 队列限制100条,防止内存溢出
|
|
1588
1626
|
- **调试日志**:添加详细调试日志,方便排查问题
|
|
1589
1627
|
|
|
1590
|
-
### v1.7.0 (2025-12-21)
|
|
1591
|
-
- **自定义协议增强**:RS485桥接节点自定义协议功能全面升级
|
|
1592
|
-
- 自定义开关:添加发开、发关、收开、收关4组码
|
|
1593
|
-
- 翻转模式:收开码=收关码时,收到后自动翻转开关状态
|
|
1594
|
-
- 自定义窗帘:添加发开、发关、发停、收开、收关、收停6组码
|
|
1595
|
-
- 自定义空调:新增完整收发码支持(开关、风速、模式、温度)
|
|
1596
|
-
- 支持最多24字节(72个十六进制字符)数据录入
|
|
1597
|
-
- 自定义模式自动隐藏地址字段
|
|
1598
|
-
- 配置持久化保存,重启后保持
|
|
1599
|
-
- 防死循环机制优化
|
|
1600
|
-
- 内存优化,防止日志溢出
|
|
1601
|
-
|
|
1602
1628
|
## 许可证
|
|
1603
1629
|
|
|
1604
1630
|
MIT License
|
|
@@ -1610,8 +1636,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1610
1636
|
## 关于
|
|
1611
1637
|
|
|
1612
1638
|
**作者**: SYMI 亖米
|
|
1613
|
-
**版本**: 1.7.
|
|
1639
|
+
**版本**: 1.7.8
|
|
1614
1640
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1615
|
-
**最后更新**:
|
|
1641
|
+
**最后更新**: 2026-01-05
|
|
1616
1642
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
1617
1643
|
**npm包**: https://www.npmjs.com/package/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
|
-
|
|
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) :
|
|
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,11 +708,14 @@
|
|
|
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() || '',
|
|
664
716
|
address: address,
|
|
665
|
-
rs485Channel: parseInt($(this).find('.rs485-channel').val()) || 1
|
|
717
|
+
rs485Channel: parseInt($(this).find('.rs485-channel').val()) || 1,
|
|
718
|
+
feedback: $(this).find('.feedback-checkbox').is(':checked')
|
|
666
719
|
};
|
|
667
720
|
// 保存杜亚窗帘2字节地址
|
|
668
721
|
if (m.brand === 'duya') {
|
|
@@ -765,6 +818,8 @@
|
|
|
765
818
|
mappings.push(m);
|
|
766
819
|
});
|
|
767
820
|
this.mappings = JSON.stringify(mappings);
|
|
821
|
+
// 保存缓存的设备列表
|
|
822
|
+
this.cachedMeshDevices = JSON.stringify(cachedMeshDevices);
|
|
768
823
|
}
|
|
769
824
|
});
|
|
770
825
|
</script>
|
|
@@ -832,6 +887,8 @@
|
|
|
832
887
|
<div class="form-tips" style="margin-top: 10px;">
|
|
833
888
|
<p><b>提示:</b>开关设备需要分别选择Mesh和RS485的按键,可自由配置对应关系(如Mesh第3路 ↔ RS485第1路)</p>
|
|
834
889
|
</div>
|
|
890
|
+
|
|
891
|
+
<input type="hidden" id="node-input-cachedMeshDevices">
|
|
835
892
|
</script>
|
|
836
893
|
|
|
837
894
|
<script type="text/html" data-help-name="symi-rs485-bridge">
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -964,7 +964,7 @@ module.exports = function(RED) {
|
|
|
964
964
|
node.commandQueue = [];
|
|
965
965
|
node.processing = false;
|
|
966
966
|
node.syncLock = false;
|
|
967
|
-
node.lastSyncTime =
|
|
967
|
+
node.lastSyncTime = {};
|
|
968
968
|
node.pendingVerify = false;
|
|
969
969
|
|
|
970
970
|
// RS485连接信息
|
|
@@ -1597,7 +1597,7 @@ module.exports = function(RED) {
|
|
|
1597
1597
|
}
|
|
1598
1598
|
} finally {
|
|
1599
1599
|
node.processing = false;
|
|
1600
|
-
node.
|
|
1600
|
+
node.lastQueueProcessTime = Date.now();
|
|
1601
1601
|
}
|
|
1602
1602
|
};
|
|
1603
1603
|
|
|
@@ -1709,12 +1709,12 @@ module.exports = function(RED) {
|
|
|
1709
1709
|
const codes = mapping.customCodes;
|
|
1710
1710
|
|
|
1711
1711
|
// 检查反馈选项:如果feedback=false,检查是否是RS485触发的状态变化
|
|
1712
|
-
// 如果是RS485触发的(
|
|
1713
|
-
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1712
|
+
// 如果是RS485触发的(800ms内有同步记录),则跳过发送反馈码,防止死循环
|
|
1713
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
|
|
1714
1714
|
if (mapping.feedback === false) {
|
|
1715
|
-
const lastSync = node.lastSyncTime ? node.lastSyncTime[loopKey] : 0;
|
|
1716
|
-
if (lastSync && Date.now() - lastSync <
|
|
1717
|
-
node.debug(`[Mesh->自定义]
|
|
1715
|
+
const lastSync = (node.lastSyncTime && typeof node.lastSyncTime === 'object') ? node.lastSyncTime[loopKey] : 0;
|
|
1716
|
+
if (lastSync && Date.now() - lastSync < 800) {
|
|
1717
|
+
node.debug(`[Mesh->自定义] 反馈已禁用,跳过回环发送`);
|
|
1718
1718
|
return;
|
|
1719
1719
|
}
|
|
1720
1720
|
}
|
|
@@ -1753,7 +1753,7 @@ module.exports = function(RED) {
|
|
|
1753
1753
|
const hexCode = switchValue ? codes.sendOn : codes.sendOff;
|
|
1754
1754
|
if (hexCode) {
|
|
1755
1755
|
// 记录发送时间用于防死循环
|
|
1756
|
-
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1756
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
|
|
1757
1757
|
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
1758
1758
|
node.lastSyncTime[loopKey] = Date.now();
|
|
1759
1759
|
|
|
@@ -1811,7 +1811,7 @@ module.exports = function(RED) {
|
|
|
1811
1811
|
node.lastSentTime[cacheKey] = now;
|
|
1812
1812
|
|
|
1813
1813
|
// 记录发送时间用于防死循环
|
|
1814
|
-
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1814
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
|
|
1815
1815
|
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
1816
1816
|
node.lastSyncTime[loopKey] = now;
|
|
1817
1817
|
|
|
@@ -1825,7 +1825,7 @@ module.exports = function(RED) {
|
|
|
1825
1825
|
node.debug(`[自定义空调] 处理状态: ${JSON.stringify(state)}, codes存在: ${!!codes}`);
|
|
1826
1826
|
|
|
1827
1827
|
// 记录发送时间用于防死循环
|
|
1828
|
-
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
1828
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
|
|
1829
1829
|
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
1830
1830
|
|
|
1831
1831
|
// 去重处理:记录已处理的命令类型
|
|
@@ -2813,14 +2813,14 @@ module.exports = function(RED) {
|
|
|
2813
2813
|
if (matchedAction) {
|
|
2814
2814
|
// 防死循环:检查是否刚刚从Mesh发送过来
|
|
2815
2815
|
// 使用映射特定的时间戳,避免其他设备的同步影响
|
|
2816
|
-
const loopKey = `${mapping.meshMac}_${mapping.device}`;
|
|
2816
|
+
const loopKey = `${mapping.meshMac}_${mapping.device}_${mapping.meshChannel || 1}`;
|
|
2817
2817
|
if (!node.lastSyncTime) node.lastSyncTime = {};
|
|
2818
|
-
if (node.lastSyncTime[loopKey] && Date.now() - node.lastSyncTime[loopKey] <
|
|
2818
|
+
if (node.lastSyncTime[loopKey] && Date.now() - node.lastSyncTime[loopKey] < 800) {
|
|
2819
2819
|
node.debug(`[防循环] 忽略刚刚同步的帧: ${hexFormatted}`);
|
|
2820
2820
|
return;
|
|
2821
2821
|
}
|
|
2822
2822
|
|
|
2823
|
-
node.log(`[
|
|
2823
|
+
node.log(`[自定义码匹配成功] 设备:${mapping.device}, MAC:${mapping.meshMac}, 动作:${JSON.stringify(matchedAction)}`);
|
|
2824
2824
|
|
|
2825
2825
|
// 输出调试信息到节点输出端口
|
|
2826
2826
|
node.send({
|