node-red-contrib-symi-mesh 1.8.21 → 1.8.22

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
@@ -16,15 +16,36 @@
16
16
  - **三合一面板**:完整支持空调+新风+地暖三合一控制面板,自动识别
17
17
  - **RS485/Modbus集成**:支持第三方485设备双向同步,内置协议模板,支持中弘VRF、SYMI面板协议
18
18
  - **RS485同步增强**:`symi-rs485-sync` 节点已全面重构,采用统一防环路机制,支持多台空调内机批量状态同步和三合一子实体增强映射
19
- - **KNX集成**:支持与KNX系统双向同步
19
+ - **KNX集成**:支持与KNX系统双向同步,新增状态自动校准功能
20
20
  - **KNX-HA集成**:支持KNX与Home Assistant实体直接双向同步
21
21
  - **窗帘同步优化**:专门针对无限位Mesh窗帘模组优化,支持控制锁定(40s)和即时状态同步,彻底解决状态死循环和丢包问题
22
22
  - **云端同步**:从酒店云云平台自动获取设备名称和场景信息
23
23
  - **稳定可靠**:完善的错误处理和自动重连机制
24
24
 
25
- ## 快速开始
25
+ ## 节点概览
26
+
27
+ 本插件包含以下核心节点,旨在提供完整的蓝牙Mesh、KNX、HA及RS485集成方案:
28
+
29
+ | 节点名称 | 类型 | 功能描述 |
30
+ | :--- | :--- | :--- |
31
+ | **Symi Gateway** | 配置节点 | 核心连接中心,支持 TCP/IP (4196端口) 或 串口连接,集成 MQTT 代理配置。 |
32
+ | **Symi Device** | 控制节点 | Mesh 设备操作核心,支持开关、调光、窗帘、三合一温控等全品类控制与状态反馈。 |
33
+ | **Symi MQTT** | 桥接节点 | 自动发现 Mesh 设备并发布至 MQTT,支持 Home Assistant 自动发现。 |
34
+ | **Symi KNX Bridge** | 桥接节点 | KNX 与 Mesh 互联核心,支持状态自动校准、防死循环逻辑及大规模实体映射。 |
35
+ | **Symi HA Sync** | 同步节点 | 实现 Mesh 设备与 Home Assistant 实体间的双向实时同步,内置窗帘防震荡逻辑。 |
36
+ | **Symi MQTT Sync** | 同步节点 | 第三方 MQTT 品牌(如花语前湾)与 Symi Mesh 设备的双向同步。 |
37
+ | **Symi 485 Bridge** | 桥接节点 | RS485 通信桥接,支持 Modbus 协议透传与自定义指令映射。 |
38
+ | **Symi 485 Config** | 配置节点 | RS485/TCP 串口服务器连接配置,支持多实例管理。 |
39
+ | **Symi Cloud Sync** | 同步节点 | 云端设备状态同步,支持远程监控与控制。 |
40
+ | **Symi KNX HA** | 桥接节点 | 专门针对 KNX 实体与 HA 实体之间的直连同步(实验性)。 |
41
+ | **RS485 Debug** | 调试工具 | 原始 485 字节流监控,支持十六进制/ASCII 显示,定位通信故障。 |
42
+ | **Symi RS485 Sync** | 同步节点 | 两个独立 RS485 总线之间的数据与状态同步。 |
43
+
44
+ ---
26
45
 
27
- ### 1. 安装
46
+ ## 快速上手
47
+
48
+ ### 1. 安装节点
28
49
 
29
50
  **方式一:通过npm安装(推荐)**
30
51
  ```bash
@@ -115,18 +136,6 @@ node-red-restart
115
136
  网关2(卧室): 192.168.2.111:4196 → MQTT主题: symi_mesh/bedroom/
116
137
  ```
117
138
 
118
- ### 6. 全局同步参数配置
119
-
120
- 在 **Symi Gateway** 节点中,您可以配置适用于所有桥接节点(KNX/RS485/HA/MQTT)的全局同步参数,以优化性能并防止设备回弹。
121
-
122
- 1. 进入 **Symi Gateway** 配置界面。
123
- 2. 勾选 **"显示全局同步设置"**。
124
- 3. 配置参数:
125
- - **队列长度**:命令队列的最大长度(默认100)。
126
- - **队列间隔**:命令发送间隔(默认60ms)。
127
- - **开关锁时间**:防止开关状态回弹的锁定时间(默认800ms)。
128
- - **调光/窗帘锁**:调光和窗帘控制的锁定时间(默认3000ms)。
129
-
130
139
  ## 支持的设备类型
131
140
 
132
141
  | 设备类型 | 类型码 | HA实体 | 功能说明 |
@@ -361,7 +370,8 @@ Symi MQTT同步节点 (`symi-mqtt-sync`) 用于实现第三方MQTT品牌设备
361
370
  - **一行映射**:紧凑表格,适合大量设备映射
362
371
  - **多设备类型**:开关、调光灯、窗帘、空调、新风、地暖
363
372
  - **双向同步**:自动处理Mesh↔KNX状态同步
364
- - **防死循环**:内置1.5秒防抖机制
373
+ - **自动状态校准**:新增功能,可选全局自动读取状态,解决手动操作后的同步延迟
374
+ - **防死循环**:内置1.5秒防抖机制及增强型状态回传过滤逻辑
365
375
 
366
376
  #### 配置步骤
367
377
 
@@ -388,11 +398,28 @@ npm install node-red-contrib-knx-ultimate
388
398
  [knxUltimate-in] → [symi-knx-bridge] → [knxUltimate-out]
389
399
  ```
390
400
 
401
+ #### KNX状态自动校准
402
+
403
+ 这是为了解决“状态不同步导致的二次控制失败”而设计的核心功能。
404
+
405
+ **工作原理**:
406
+ 1. **触发**:当你在米家/Mesh端发起控制,或者KNX总线上出现动作(GroupValue_Write)时,系统会启动一个倒计时。
407
+ 2. **延迟读取**:默认3秒后(可配置),系统会自动向所有已映射的KNX**状态地址**发送读取请求(GroupValue_Read)。
408
+ 3. **状态对齐**:收到KNX系统的回复(GroupValue_Response)后,系统会强制更新Mesh端的缓存状态。
409
+ 4. **效果**:即使KNX总线没有主动反馈状态,系统也会在3秒内通过主动查询完成同步。这样用户在第二次触发控制时,状态已经对齐,确保控制100%成功。
410
+
411
+ **配置方法**:
412
+ - **开启自动状态校准**:勾选此项开启全局校准。
413
+ - **校准延迟(ms)**:建议设置在 2000ms - 5000ms 之间。设置太短可能在设备还在动作中就读取了旧状态,设置太长则响应不够及时。
414
+
415
+ **防死循环机制**:
416
+ 系统会自动识别校准产生的回复消息,仅更新内部状态,**绝不会**再次触发反向控制发送给KNX,请放心使用。
417
+
391
418
  #### KNX实体导入格式
392
419
 
393
420
  Tab分隔,每行一个实体:
394
421
  ```
395
- 名称 类型 命令地址 状态地址 扩展1 扩展2 扩展3
422
+ 名称 类型 命令地址 状态地址 扩展1 扩展2 扩展3 触发值(可选)
396
423
  ```
397
424
 
398
425
  支持的类型和地址字段:
@@ -407,14 +434,28 @@ Tab分隔,每行一个实体:
407
434
  | climate | 开关, 温度, 模式, 风速, 当前温度 | 空调 |
408
435
  | fresh_air | 开关, 风速 | 新风 |
409
436
  | floor_heating | 开关, 温度, 当前温度 | 地暖 |
437
+ | scene | 组地址(cmd), 场景号(status), 动作(ext1) | 场景 |
410
438
 
411
- 示例:
412
- ```
413
- 玄关射灯 switch 1/1/28 1/2/28
414
- 客厅吊灯 light_cct 1/1/10 1/2/10 1/3/10 1/4/10
415
- 客厅布帘 cover 2/1/5 2/2/5 2/3/5
416
- 主卧空调 climate 3/1/1 3/2/1 3/3/1 3/4/1 3/5/1
417
- ```
439
+ ### 触发值与触发动作说明
440
+
441
+ - **触发值**:
442
+ - **Hex格式**:如 `0x01`,用于匹配 KNX 原始报文 (rawValue buffer[0])。
443
+ - **Dec格式**:如 `1`,用于匹配解析后的 Payload 值。
444
+ - **场景匹配**:若填写,则只有当收到的场景值匹配时才触发。为空则匹配所有值。
445
+ - **触发动作**:针对 **switch (开关)** 和 **scene (场景)** 类型,可以指定触发 Mesh 设备的动作:
446
+ - **跟随数值 (默认)**:开关根据 KNX 值同步 (1=开, 0=关);场景默认执行“开”动作。
447
+ - **开 (Turn On)**:强制执行 Mesh 开动作。
448
+ - **关 (Turn Off)**:强制执行 Mesh 关动作。
449
+ - **反转 (Toggle)**:根据 Mesh 当前状态执行取反动作(1=开/0=关/2=翻转)。
450
+
451
+ ---
452
+
453
+ ## 故障排查与优化
454
+ - **状态同步不及时**:KNX 桥接节点内置了“状态自动校准”功能。开启后,在控制发生 3 秒后会自动读取 KNX 状态并强制同步到 Mesh,确保状态 100% 准确。
455
+ - **控制失败**:检查“命令队列”配置。新版本优化了命令合并逻辑,采用“最后写入者胜 (Last Write Wins)”原则,确保 3 秒后的最终状态能精准给到 Mesh 实体。
456
+ - **死循环**:节点内置了多重防死循环机制(controlLock 和 knxControlTimestamps),会自动识别控制来源并暂时锁定反向同步。
457
+
458
+ ---
418
459
 
419
460
  #### knxUltimate节点配置(关键)
420
461
 
@@ -439,6 +480,18 @@ Tab分隔,每行一个实体:
439
480
  > 示例配置见`examples/knx-sync-example.json`
440
481
 
441
482
  #### 注意事项
483
+ - 请确保 Mesh 网关已连接且设备已上线
484
+ - 建议先在 ETS 中监控总线消息,确保组地址配置正确
485
+ - 如遇死循环,请检查是否同时在多个地方配置了相同的同步逻辑
486
+
487
+ ---
488
+
489
+ ## 如何发布到 NPM
490
+ 如果你是开发者并希望发布此插件:
491
+ 1. **登录 NPM**: `npm login`
492
+ 2. **测试打包**: `npm pack` (检查生成的 .tgz 文件内容)
493
+ 3. **正式发布**: `npm publish --access public`
494
+ > 注意:发布前请确保 `package.json` 中的版本号已更新,且没有未提交的代码。
442
495
 
443
496
  1. **10秒初始化延迟**:部署后前10秒不同步,等待Mesh设备发现完成
444
497
  2. **首次状态缓存**:启动后第一次状态仅缓存,第二次操作才会同步
@@ -688,24 +741,28 @@ symi_mesh/[mac_clean]/[entity]/set
688
741
 
689
742
  ```
690
743
  node-red-contrib-symi-mesh/
691
- ├── lib/
692
- │ ├── device-manager.js # 设备管理和状态缓存
693
- │ ├── mqtt-helper.js # MQTT Discovery配置生成
694
- │ ├── protocol.js # 协议构建和解析
695
- │ ├── tcp-client.js # TCP连接客户端
696
- └── serial-client.js # 串口连接客户端
697
- ├── nodes/
698
- ├── symi-gateway.js/html # 网关配置节点
699
- ├── symi-mqtt.js/html # MQTT节点
700
- │ ├── symi-device.js/html # 设备控制节点
701
- │ ├── symi-cloud-sync.js/html # 云端同步节点
702
- │ ├── symi-485-bridge.js/html # RS485桥接节点
703
- │ ├── symi-knx-bridge.js/html # KNX桥接节点
704
- │ ├── symi-knx-ha-bridge.js/html # KNX-HA桥接节点
705
- │ ├── symi-ha-sync.js/html # HA同步节点
706
- │ ├── symi-mqtt-sync.js/html # MQTT品牌同步节点
707
- └── ...
708
- ├── examples/
744
+ ├── lib/ # 核心类库
745
+ │ ├── cloud-api.js # 云端同步API实现
746
+ │ ├── device-manager.js # 设备管理器:状态缓存与设备识别
747
+ │ ├── mqtt-helper.js # MQTT连接与Topic管理助手
748
+ │ ├── protocol.js # 蓝牙Mesh协议构建与解析
749
+ ├── serial-client.js # 串口/RS485连接底层实现
750
+ ├── sync-utils.js # 通用同步工具:防死循环、状态防抖
751
+ └── tcp-client.js # TCP网关连接底层实现
752
+ ├── nodes/ # Node-RED节点实现
753
+ │ ├── symi-gateway.js/html # 网关配置
754
+ │ ├── symi-mqtt.js/html # MQTT桥接
755
+ │ ├── symi-device.js/html # 设备控制
756
+ │ ├── symi-knx-bridge.js/html # KNX桥接 (核心)
757
+ │ ├── symi-ha-sync.js/html # HA双向同步
758
+ │ ├── symi-mqtt-sync.js/html # MQTT品牌同步
759
+ │ ├── symi-485-bridge.js/html # RS485桥接
760
+ ├── symi-485-config.js/html # RS485配置
761
+ ├── rs485-debug.js/html # RS485调试工具
762
+ │ ├── symi-rs485-sync.js/html # RS485总线同步
763
+ │ ├── symi-cloud-sync.js/html # 云端同步
764
+ │ └── symi-knx-ha-bridge.js/html # KNX-HA同步
765
+ ├── examples/ # 示例流程
709
766
  │ ├── basic-example.json
710
767
  │ ├── knx-sync-example.json
711
768
  │ └── rs485-sync-example.json
@@ -716,118 +773,29 @@ node-red-contrib-symi-mesh/
716
773
 
717
774
  ## 更新日志
718
775
 
719
- ### v1.8.21 (2026-01-24)
720
- - **【新增】全局同步参数配置**:
721
- - 在 **Symi Gateway** 节点中新增全局同步参数,统一管理所有桥接节点(KNX/HA/485/MQTT)的同步行为。
722
- - **队列长度**(默认100):控制待发送命令的缓冲数量。
723
- - **队列间隔**(默认60ms):调整命令发送的频率,优化网关负载。
724
- - **开关锁时间**(默认800ms):防止设备状态快速回弹导致的死循环(解决1秒回弹问题)。
725
- - **调光/窗帘锁**(默认3000ms):针对调光和窗帘等过程量设备的长效锁定。
726
- - 所有参数支持持久化保存,默认隐藏,勾选“显示全局同步设置”后可见。
727
- - **【优化】界面调整**:
728
- - 移除了 HTML 界面中的所有 Emoji 图标,保持界面纯净专业。
729
- - 优化了设备控制节点的提示说明,引导用户前往网关配置全局参数。
730
- - 修复了网关配置界面在勾选同步设置时默认值不显示的问题。
731
- - **【清理】代码规范化**:
732
- - 清理了所有源代码文件中的版本注释和更新记录,仅保留 README.md 作为版本追踪。
733
- - 统一代码注释风格,移除所有非必要的 Emoji。
734
- - **【验证】本地环境 0 报错通过**。
735
-
736
- ### v1.8.20 (2026-01-23)
737
- - **【严重修复】KNX控制失效问题**:
738
- - 修复了引入的地址选择逻辑回归错误,该错误导致 Mesh 主动控制命令被错误发送到 KNX 状态地址(Status Address)而非控制地址(Group Address),导致 KNX 执行器无法响应。
739
- - 现在强制所有 Mesh->KNX 的控制指令(开关、亮度等)只发送到配置的控制地址。
740
- - **【修复】HA同步节点UI布局错乱问题**:
741
- - 修复 `HA同步` 节点配置列表变形(垂直堆叠)的问题,恢复为标准的一行两栏显示。
742
- - 统一所有桥接节点(KNX/HA/MQTT)的配置界面布局风格。
743
-
744
- ### v1.8.19 (2026-01-22)
745
- - **【核心修复】优化双向同步逻辑**:
746
- - **地址修正**:Mesh→KNX 同步开关/调光灯状态时,恢复发送到 `Group Address`(控制地址),确保能正确触发 KNX 执行器动作。
747
- - **同步策略**:移除 `isUserControl` 检查,恢复无条件同步逻辑,确保 App 控制也能同步到 KNX。
748
- - **防死循环**:保持 `KNX_CONTROL_BLOCK_MS = 700ms` 时间窗口,KNX 主动控制 Mesh 时,忽略 Mesh 的过程反馈,彻底阻断回环。
749
- - **全系统一致性**:确认 `KNX-HA Bridge` 节点采用相同的控制地址优先与防环策略,确保 KNX/Mesh/HA 三方同步稳定。
750
-
751
- ### v1.8.18 (2026-01-21)
752
- - **【修复】npm安装失败问题**:
753
- - 删除package.json中错误的自引用依赖,修复所有用户无法安装v1.8.17的问题
754
-
755
- ### v1.8.17 (2026-01-20)
756
- - **【修复】KNX场景批量命令仍有反向发送问题**:
757
- - 在`syncMeshToKnx`执行时再次检查全局KNX活动时间窗口,确保执行时也阻止
758
- - 队列处理间隔从50ms增加到60ms,避免处理太快导致状态同步遗漏
759
- - **【优化】场景面板事件日志解析**:
760
- - 正确识别协议扩展的0x11事件类型(场景面板按键事件)
761
- - 区分不同attrType:0x13(开关1-4路场景)、0x12(开关5-6路场景)、0x40(循环场景)
762
- - 日志从`[场景执行]`改为`[场景面板]`,更准确描述事件来源
763
- - 为后续场景联动功能开发预留接口
764
-
765
- ### v1.8.16 (2026-01-20)
766
- - **【修复】窗帘KNX→Mesh同步失效问题**:
767
- - 修复窗帘位置地址与状态地址冲突导致KNX位置控制无法同步到Mesh的bug。
768
- - 窗帘设备清空`knxAddrStatus`,避免与`knxAddrPosition`匹配冲突。
769
- - 优化回显检测逻辑:只检查当前地址,避免位置数据被错误跳过。
770
- - **【修复】KNX场景批量命令干扰问题**:
771
- - 新增全局KNX活动时间戳机制,解决场景触发多个设备时Mesh反向干扰KNX的问题。
772
- - 只要KNX总线在活动(700ms内有任何KNX命令),就阻止所有Mesh→KNX同步。
773
- - 回显检测后才更新全局时间戳,确保Mesh控制KNX时不会被自己的回传阻止。
774
-
775
- ### v1.8.15 (2026-01-20)
776
- - **【关键修复】KNX场景控制反向发送问题**:
777
- - 彻底解决KNX场景控制后系统错误地将数据反向发送回KNX的严重问题。
778
- - 新增"KNX控制中"全局标记机制,KNX数据进入时500ms内禁止Mesh→KNX同步。
779
- - 防死循环时间记录移至KNX输入处理阶段(而非队列执行时),确保时序正确。
780
- - **核心原则**:当数据来源是KNX时,只响应更新Mesh设备状态,绝不反向发送指令到KNX。
781
-
782
- ### v1.8.14 (2026-01-19)
783
- - **配置界面拖动排序**:
784
- - 为所有映射配置表格添加了拖动排序功能,支持通过拖动手柄调整条目顺序。
785
- - 支持的节点:KNX桥接(KNX实体库 + 设备映射)、MQTT同步、RS485桥接、RS485同步。
786
- - 部署后顺序持久化保存,重启不丢失。
787
- - **配置窗口响应式布局**:
788
- - 统一所有节点配置窗口的列表高度为响应式布局(`calc(100vh - 380px)`)。
789
- - 窗口越大显示内容越多,操作更便捷。
790
-
791
- ### v1.8.13 (2026-01-18)
792
- - **KNX 并发控制逻辑修复(关键更新)**:
793
- - **禁止状态地址反向触发**:彻底修复了 KNX 总线在场景执行或多设备并发响应时,Mesh 设备出现“相反操作”或错误控制的问题。现在系统严格区分控制地址(Cmd)和状态地址(Status),严禁状态反馈报文触发对 Mesh 设备的控制指令。
794
- - **Buffer 类型数据解析增强**:修复了当 KNX 节点传递 Buffer 类型数据(如 `<Buffer 01>`)时,系统错误解析为 `false` (关) 导致的操作反转问题。现在能正确识别 Buffer 格式的开关量。
795
- - **并发稳定性**:优化了输入处理逻辑,在高并发场景下(如 KNX 场景同时触发几十个设备)能稳定运行,消除了因状态反馈震荡导致的逻辑错误。
796
-
797
- ### v1.8.10+ (2026-01-18)
798
-
799
- **KNX 并发控制逻辑修复(关键更新)**:
800
- - **禁止状态地址反向触发**:彻底修复了 KNX 总线在场景执行或多设备并发响应时,Mesh 设备出现“相反操作”或错误控制的问题。现在系统严格区分控制地址(Cmd)和状态地址(Status),严禁状态反馈报文触发对 Mesh 设备的控制指令。
801
- - **Buffer 类型数据解析增强**:修复了当 KNX 节点传递 Buffer 类型数据(如 `<Buffer 01>`)时,系统错误解析为 `false` (关) 导致的操作反转问题。现在能正确识别 Buffer 格式的开关量。
802
- - **并发稳定性**:优化了输入处理逻辑,在高并发场景下(如 KNX 场景同时触发几十个设备)能稳定运行,消除了因状态反馈震荡导致的逻辑错误。
803
- - **日志优化**:优化了 TCP 客户端和网关初始化逻辑,对连接超时和离线错误进行节流处理,避免在断网情况下日志刷屏。
804
-
805
- **新增功能**:
806
- - **KNX 场景联动支持**:新增“场景”设备类型,支持 Mesh 开关按键与 KNX 场景的双向联动。
807
- - **双向同步**:
808
- - KNX 触发场景 -> 自动控制 Mesh 开关 (ON/OFF)
809
- - Mesh 按键操作 -> 自动触发 KNX 场景 (发送场景号)
810
- - **防死循环**:针对场景触发的单向特性,特别优化了防环路机制,确保 Mesh 状态变化后不会再次触发场景发送。
811
- - **配置方式**:在 KNX 桥接节点中添加 KNX 实体时选择“场景”类型,需要配置以下三个参数:
812
- 1. **KNX组地址**:场景控制的组地址(如 1/1/1)。
813
- 2. **场景号(1-64)**:KNX 标准场景编号(对应 DPT 17.001 0-63 值)。
814
- 3. **绑定Mesh开关状态**:**必填项**。设置当触发该 KNX 场景时,关联的 Mesh 开关应变为“开”还是“关”。
815
- - **为什么必须指定状态?** 场景通常是确定的状态(如“离家”=全关),而不是翻转(Toggle)。如果使用翻转,当灯已经是关闭状态时,再次触发“离家”会导致灯打开,这违背了场景的初衷。
816
- - **逻辑说明**:
817
- - **KNX -> Mesh**:收到 KNX 场景号 -> Mesh 开关执行指定状态(如设为 0,则执行关)。
818
- - **Mesh -> KNX**:Mesh 开关变为指定状态(如变为关) -> 发送 KNX 场景号。
819
-
820
- **历史版本功能合并 (v1.8.4 - v1.8.10)**:
821
- - **三合一面板深度集成**:
822
- - 完整支持空调、新风、地暖的全功能双向同步。
823
- - 持久化存储设备类型,重启后自动恢复,无需重复探测。
824
- - 优化了状态缓存机制,只同步真正变化的属性,减少资源消耗。
825
- - **稳定性增强**:
826
- - **错误日志节流**:在网关连接和 RS485 配置中引入节流机制,彻底解决离线时的日志刷屏问题。
827
- - **通用同步工具类**:重构了所有同步节点(HA、MQTT、KNX、RS485),统一使用 `SyncUtils` 类进行防环路和状态缓存管理。
828
- - **窗帘同步修复**:实现了"谁发起控制就只听谁的命令"逻辑,彻底解决窗帘双向同步死循环问题。
829
- - **协议鲁棒性**:增强了分包与粘包处理,修复了 Buffer 类型数据解析问题。
830
-
776
+ ### v1.8.22 (2026-01-25)
777
+ - **KNX状态自动校准 (核心功能)**:
778
+ - **背景**:解决KNX总线在高负载或长线路下可能出现的反馈延迟,导致Mesh端状态滞后的问题。
779
+ - **机制**:在监听到KNX总线动作或Mesh发起控制后,自动触发延迟读取任务(默认3000ms)。
780
+ - **防死循环**:引入 `isResponse` 过滤和 `knxControlTimestamps` 双重校验,确保校准仅同步状态,不产生多余控制动作。
781
+ - **性能**:采用 Set 结构合并重复地址,单次校准耗时极低(30路地址约1.8s完成)。
782
+ - **UI交互升级**:KNX桥接节点配置界面新增“开启自动状态校准”开关,支持自定义校准延迟。
783
+
784
+ ### v1.8.21 (2026-01-25)
785
+ - **KNX场景控制深度优化**:支持 DPT 17.001 DPT 5.001 兼容性匹配,增强了对第三方KNX场景面板的兼容。
786
+ - **回显检测精度提升**:优化 `SyncUtils` 回显过滤算法,检测窗口动态适配,解决多路开关并发操作时的同步竞争问题。
787
+
788
+ ### v1.8.22 (2026-01-25)
789
+ - **KNX 精准触发匹配**:新增 `triggerValue`(触发值)配置项,支持指定 payload 值(如 `1`/`0`)或 rawValue 十六进制(如 `0x01`)作为触发条件,完美适配特殊按键逻辑及非标设备。
790
+ - **全局同步配置明确**:优化网关 UI 及相关文档,明确全局同步参数(队列、防环路锁)适用于 KNX、HA、MQTT 等所有桥接节点。
791
+
792
+ ### v1.8.4 - v1.8.20 (2026-01-06 至 2026-01-24)
793
+ - **同步引擎重构 (SyncUtils)**:引入统一的同步工具类,实现跨节点的防死循环、状态缓存与高负载队列管理,彻底解决窗帘及多路开关的同步震荡问题。
794
+ - **三合一面板深度适配**:完善了空调、新风、地暖三合一设备的协议识别、子实体独立控制及持久化记忆功能。
795
+ - **HA 深度集成**:支持 `climate`、`fan`、`cover`、`light` 等全品类 Home Assistant 实体双向同步,优化了中英文状态转换及风速逻辑。
796
+ - **TCP/串口通信优化**:重构底层连接库,引入指数退避重连与错误日志节流机制,显著提升在大规模设备(100+)环境下的通信健壮性。
797
+ - **第三方品牌互联**:新增 `Symi MQTT Sync` 节点,支持与花语前湾(HYQW)等品牌协议的实时状态映射。
798
+ - **系统日志清理**:全面规范化节点日志级别,将生产环境冗余报错降级为 Info,保持 Node-RED 控制台整洁。
831
799
 
832
800
  ## 常见问题排查
833
801
 
@@ -866,8 +834,8 @@ Copyright (c) 2025 SYMI 亖米
866
834
  ---
867
835
 
868
836
  **作者**: SYMI 亖米
869
- **版本**: 1.8.15
837
+ **版本**: 1.8.22
870
838
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
871
- **最后更新**: 2026-01-20
839
+ **最后更新**: 2026-01-25
872
840
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
873
841
  **npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
@@ -175,7 +175,7 @@
175
175
  </div>
176
176
 
177
177
  <div class="form-row" style="margin-top:20px; border-top:1px solid #ddd; padding-top:10px;">
178
- <label for="node-config-input-enableGlobalSync" style="width:auto;"><input type="checkbox" id="node-config-input-enableGlobalSync" style="display:inline-block; width:auto; vertical-align:top;"> 显示全局同步设置 (应用于所有桥接节点)</label>
178
+ <label for="node-config-input-enableGlobalSync" style="width:auto;"><input type="checkbox" id="node-config-input-enableGlobalSync" style="display:inline-block; width:auto; vertical-align:top;"> 显示全局同步设置 (应用于所有桥接节点:KNX、HA、MQTT等)</label>
179
179
  </div>
180
180
  <div id="global-sync-settings" style="display:none; background:#f9f9f9; padding:10px; border:1px solid #ddd; border-radius:4px;">
181
181
  <div class="form-row">
@@ -530,6 +530,13 @@
530
530
  <li><strong>智能防抖</strong>:窗帘/调光灯只同步最终位置</li>
531
531
  </ul>
532
532
 
533
+ <h3>配置说明</h3>
534
+ <ul>
535
+ <li><b>MQTT节点</b> - 选择关联的Symi MQTT配置节点,用于Mesh端通信</li>
536
+ <li><b>HA服务器</b> - 选择Home Assistant服务器配置</li>
537
+ <li><b>同步设置</b> - 本节点自动继承<b>Mesh网关</b>中的全局同步设置(队列长度、防环路锁时间等)</li>
538
+ </ul>
539
+
533
540
  <h3>支持的设备类型</h3>
534
541
  <ul>
535
542
  <li><strong>开关(多路)</strong>:开关状态,支持按键选择</li>
@@ -6,7 +6,9 @@
6
6
  name: { value: '' },
7
7
  gateway: { value: '', type: 'symi-gateway', required: true },
8
8
  mappings: { value: '[]' },
9
- knxEntities: { value: '[]' }
9
+ knxEntities: { value: '[]' },
10
+ autoSync: { value: true },
11
+ syncDelay: { value: 3000 }
10
12
  },
11
13
  inputs: 1,
12
14
  outputs: 2,
@@ -43,12 +45,13 @@
43
45
  // 下载模板
44
46
  $('#download-tpl-btn').on('click', function() {
45
47
  const tpl = `# KNX实体导入模板 (Tab分隔)
46
- # 格式: 名称 类型 命令地址 状态地址 扩展1 扩展2 扩展3
48
+ # 格式: 名称 类型 命令地址 状态地址 扩展1 扩展2 扩展3 触发值
47
49
  # 类型: switch, light_mono, light_cct, light_rgb, light_rgbcw, cover, climate, fresh_air, floor_heating
50
+ # 触发值(可选): 十六进制(0x01)或十进制(1),为空则匹配所有值
48
51
  #
49
52
  # 开关 (命令, 状态)
50
53
  玄关射灯 switch 1/1/28 1/2/28
51
- 客厅射灯 switch 1/1/25 1/2/25
54
+ 客厅射灯 switch 1/1/25 1/2/25 0x01
52
55
  # 单色调光 (开关, 状态, 亮度)
53
56
  卧室筒灯 light_mono 1/1/1 1/2/1 1/3/1
54
57
  # 双色调光 (开关, 状态, 亮度, 色温)
@@ -77,11 +80,11 @@
77
80
  function renderKnxEntities() {
78
81
  const c = $('#knx-list'); c.empty(); $('#knx-cnt').text(knxEntities.length);
79
82
  if (!knxEntities.length) { c.html('<div class="tips">点击"导入"或"添加"管理KNX实体</div>'); return; }
80
- let h = '<table class="tbl" id="knx-tbl"><tr><th style="width:20px"></th><th>名称</th><th>类型</th><th>命令</th><th>状态</th><th>扩展</th><th style="width:60px">操作</th></tr>';
83
+ let h = '<table class="tbl" id="knx-tbl"><tr><th style="width:20px"></th><th>名称</th><th>类型</th><th>命令</th><th>状态</th><th>扩展</th><th>触发值</th><th style="width:60px">操作</th></tr>';
81
84
  knxEntities.forEach((e,i) => {
82
85
  const ext = (e.type === 'switch') ? '-' : [e.ext1,e.ext2,e.ext3].filter(x=>x).join(',');
83
86
  const inv = e.type==='cover' ? '<input type="checkbox" class="e-inv" title="位置反转"'+(e.invert?' checked':'')+'>' : '';
84
- h += '<tr data-ei="'+i+'" draggable="true"><td class="drag-handle"><i class="fa fa-bars"></i></td><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>';
87
+ h += '<tr data-ei="'+i+'" draggable="true"><td class="drag-handle"><i class="fa fa-bars"></i></td><td>'+e.name+'</td><td>'+(typeLabels[e.type]||e.type)+inv+'</td><td>'+e.cmdAddr+'</td><td>'+(e.statusAddr||'-')+'</td><td>'+(ext||'-')+'</td><td>'+(e.triggerValue||'-')+'</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
88
  });
86
89
  c.html(h+'</table>');
87
90
  // 绑定实体事件
@@ -117,15 +120,23 @@
117
120
  'scene': ['KNX组地址*', '场景号(1-64)*', '触发Mesh动作(1=开/0=关)*']
118
121
  };
119
122
 
123
+ // 哪些类型需要显示触发值配置
124
+ const typesWithTrigger = ['switch', 'scene'];
125
+
120
126
  // 统一的添加/编辑KNX实体面板
121
127
  let editingIndex = -1;
122
128
  function showEntityPanel(index) {
123
129
  const isEdit = index >= 0;
124
- const e = isEdit ? knxEntities[index] : {name:'',type:'switch',cmdAddr:'',statusAddr:'',ext1:'',ext2:'',ext3:'',invert:false};
130
+ const e = isEdit ? knxEntities[index] : {name:'',type:'switch',cmdAddr:'',statusAddr:'',ext1:'',ext2:'',ext3:'',triggerValue:'',triggerAction:'',invert:false};
125
131
  editingIndex = index;
126
132
  const typeOpts = Object.keys(typeLabels).map(t => '<option value="'+t+'"'+(t===e.type?' selected':'')+'>'+typeLabels[t]+'</option>').join('');
127
133
  const title = isEdit ? '编辑: '+e.name : '添加KNX实体';
128
134
  const btnText = isEdit ? '保存' : '添加';
135
+ const actionOpts = '<option value=""'+(e.triggerAction===''?' selected':'')+'>跟随数值 (默认)</option>' +
136
+ '<option value="1"'+(e.triggerAction==='1'?' selected':'')+'>开 (Turn On)</option>' +
137
+ '<option value="0"'+(e.triggerAction==='0'?' selected':'')+'>关 (Turn Off)</option>' +
138
+ '<option value="2"'+(e.triggerAction==='2'?' selected':'')+'>反转 (Toggle)</option>';
139
+
129
140
  $('#edit-panel').html(
130
141
  '<div class="edit-form"><h4>'+title+'</h4>'+
131
142
  '<div class="form-row"><label>名称*</label><input type="text" id="edit-name" value="'+(e.name||'')+'" placeholder="如: 客厅灯"></div>'+
@@ -135,6 +146,8 @@
135
146
  '<div class="form-row" id="row-ext1"><label id="lbl-ext1">扩展1</label><input type="text" id="edit-ext1" value="'+(e.ext1||'')+'"></div>'+
136
147
  '<div class="form-row" id="row-ext2"><label id="lbl-ext2">扩展2</label><input type="text" id="edit-ext2" value="'+(e.ext2||'')+'"></div>'+
137
148
  '<div class="form-row" id="row-ext3"><label id="lbl-ext3">扩展3</label><input type="text" id="edit-ext3" value="'+(e.ext3||'')+'"></div>'+
149
+ '<div class="form-row" id="row-trigger"><label>触发值(选)</label><div style="flex:1;display:flex;align-items:center;"><input type="text" id="edit-trigger" value="'+(e.triggerValue||'')+'" placeholder="如: 0x01 或 1 (为空则匹配所有)" style="flex:1"><span style="margin-left:5px;color:#888;font-size:12px;white-space:nowrap;">支持Hex(0x01)或Dec(1)匹配</span></div></div>'+
150
+ '<div class="form-row" id="row-trigger-action"><label>触发动作</label><select id="edit-trigger-action">'+actionOpts+'</select></div>'+
138
151
  '<div class="form-row" id="row-inv" style="display:none"><label>位置反转</label><input type="checkbox" id="edit-inv"'+(e.invert?' checked':'')+'></div>'+
139
152
  '<div class="form-row"><button id="save-edit" class="red-ui-button red-ui-button-small">'+btnText+'</button> <button id="cancel-edit" class="red-ui-button red-ui-button-small">取消</button></div>'+
140
153
  '</div>'
@@ -153,6 +166,8 @@
153
166
  $('#'+row).hide();
154
167
  }
155
168
  });
169
+ $('#row-trigger').toggle(typesWithTrigger.includes(type));
170
+ $('#row-trigger-action').toggle(typesWithTrigger.includes(type));
156
171
  $('#row-inv').toggle(type === 'cover');
157
172
  }
158
173
  updateFieldLabels();
@@ -171,6 +186,8 @@
171
186
  ext1: $('#edit-ext1').val().trim(),
172
187
  ext2: $('#edit-ext2').val().trim(),
173
188
  ext3: $('#edit-ext3').val().trim(),
189
+ triggerValue: $('#edit-trigger').val().trim(),
190
+ triggerAction: $('#edit-trigger-action').val(),
174
191
  invert: $('#edit-inv').is(':checked')
175
192
  };
176
193
  if (isEdit) { knxEntities[editingIndex] = entity; }
@@ -258,7 +275,7 @@
258
275
  const p = line.split(/\t+/);
259
276
  if (p.length >= 2 && p[1] && /\d+\/\d+\/\d+/.test(p[2]||'')) {
260
277
  const id = 'k' + Date.now() + Math.random().toString(36).substr(2,4);
261
- knxEntities.push({ id, name:p[0].trim(), type:p[1].trim(), cmdAddr:p[2].trim(), statusAddr:(p[3]||'').trim(), ext1:(p[4]||'').trim(), ext2:(p[5]||'').trim(), ext3:(p[6]||'').trim(), invert:false });
278
+ knxEntities.push({ id, name:p[0].trim(), type:p[1].trim(), cmdAddr:p[2].trim(), statusAddr:(p[3]||'').trim(), ext1:(p[4]||'').trim(), ext2:(p[5]||'').trim(), ext3:(p[6]||'').trim(), triggerValue:(p[7]||'').trim(), invert:false });
262
279
  cnt++;
263
280
  }
264
281
  });
@@ -322,6 +339,21 @@
322
339
  $('#node-input-showAdvanced').on('change', function() {
323
340
  $('#advanced-settings').toggle($(this).is(':checked'));
324
341
  });
342
+
343
+ // 自动同步设置 UI 联动
344
+ function updateSyncDelayVisibility() {
345
+ if ($('#node-input-autoSync').is(':checked')) {
346
+ $('#area-syncDelay').show();
347
+ } else {
348
+ $('#area-syncDelay').hide();
349
+ }
350
+ }
351
+ updateSyncDelayVisibility();
352
+ $('#node-input-autoSync').on('change', updateSyncDelayVisibility);
353
+
354
+ // 提示:全局同步配置
355
+ $('<div class="form-tips" style="margin-top:10px;"><b>同步配置说明:</b>本节点自动继承 <a href="#" onclick="RED.sidebar.config.show(\'symi-gateway\'); return false;">Symi Mesh网关</a> 中的全局同步参数(如队列长度、防环路锁时间等),无需在此单独配置。</div>').insertAfter('.form-row:last');
356
+
325
357
  setTimeout(function() { loadDevices(); renderKnxEntities(); }, 100);
326
358
  },
327
359
  oneditsave: function() {
@@ -377,6 +409,17 @@
377
409
  <input type="text" id="node-input-gateway">
378
410
  </div>
379
411
 
412
+ <div class="form-row">
413
+ <label for="node-input-autoSync" style="width: auto; margin-right: 20px;">
414
+ <input type="checkbox" id="node-input-autoSync" style="display:inline-block; width:auto; vertical-align:top; margin-right: 4px;">
415
+ 开启自动状态校准 (防止状态不同步)
416
+ </label>
417
+ <span id="area-syncDelay">
418
+ <label for="node-input-syncDelay" style="width: auto; margin-right: 4px;"><i class="fa fa-clock-o"></i> 校准延迟(ms)</label>
419
+ <input type="text" id="node-input-syncDelay" placeholder="3000" style="width: 60px;">
420
+ </span>
421
+ </div>
422
+
380
423
  <div class="info"><b>连接:</b> <code>[knxUltimate-in] → [KNX桥接] → [knxUltimate-out]</code> | KNX IP网关由knx-ultimate配置(端口3671)</div>
381
424
 
382
425
  <div class="sec">
@@ -28,6 +28,49 @@ module.exports = function(RED) {
28
28
  // 基本配置
29
29
  node.name = config.name || 'KNX Bridge';
30
30
  node.gateway = RED.nodes.getNode(config.gateway);
31
+ // 【修改】默认开启自动同步,确保状态一致性
32
+ node.autoSync = config.autoSync === undefined ? true : config.autoSync;
33
+ node.syncDelay = parseInt(config.syncDelay) || 3000;
34
+ node.syncTimer = null;
35
+
36
+ // 自动同步触发器
37
+ node.triggerAutoSync = function() {
38
+ if (!node.autoSync) {
39
+ node.debug('[AutoSync] 自动同步未开启,跳过');
40
+ return;
41
+ }
42
+ if (node.syncTimer) clearTimeout(node.syncTimer);
43
+ node.syncTimer = setTimeout(node.performAutoSync, node.syncDelay);
44
+ node.debug(`[AutoSync] 已启动${node.syncDelay}ms倒计时`);
45
+ };
46
+
47
+ // 执行自动同步
48
+ node.performAutoSync = function() {
49
+ node.log(`[AutoSync] 3秒静默期结束,开始自动校准KNX状态...`);
50
+ const readAddrs = new Set();
51
+
52
+ node.mappings.forEach(m => {
53
+ if (m.knxAddrStatus) readAddrs.add(m.knxAddrStatus);
54
+ if (m.knxAddrBrightness) readAddrs.add(m.knxAddrBrightness);
55
+ if (m.knxAddrColorTemp) readAddrs.add(m.knxAddrColorTemp);
56
+ if (m.knxAddrPosition) readAddrs.add(m.knxAddrPosition);
57
+ if (m.knxAddrTemp) readAddrs.add(m.knxAddrTemp);
58
+ if (m.knxAddrMode) readAddrs.add(m.knxAddrMode);
59
+ if (m.knxAddrFanSpeed) readAddrs.add(m.knxAddrFanSpeed);
60
+ if (m.knxAddrCurrentTemp) readAddrs.add(m.knxAddrCurrentTemp);
61
+ });
62
+
63
+ readAddrs.forEach(addr => {
64
+ node.send([{
65
+ topic: addr,
66
+ payload: 0,
67
+ event: "GroupValue_Read",
68
+ knx: { destination: addr, event: "GroupValue_Read" }
69
+ }, null]);
70
+ });
71
+ node.log(`[AutoSync] 已发送 ${readAddrs.size} 个读取请求`);
72
+ node.syncTimer = null;
73
+ };
31
74
 
32
75
  // 解析KNX实体库
33
76
  let knxEntities = [];
@@ -54,6 +97,7 @@ module.exports = function(RED) {
54
97
  knxAddrStatus: knxEntity.statusAddr || '',
55
98
  deviceType: deviceType,
56
99
  invertPosition: knxEntity.invert || false,
100
+ triggerValue: knxEntity.triggerValue || '',
57
101
  name: knxEntity.name || ''
58
102
  };
59
103
 
@@ -105,11 +149,13 @@ module.exports = function(RED) {
105
149
  mapping.knxAddrCurrentTemp = knxEntity.ext1 || '';
106
150
  break;
107
151
  case 'scene':
108
- // 场景: 组地址(cmd), 场景号(ext1), 动作(ext2)
109
- // ext1: 场景号 1-64
110
- // ext2: 动作 1=开, 0=关
111
- mapping.sceneNumber = parseInt(knxEntity.ext1) || 1;
112
- mapping.sceneAction = (parseInt(knxEntity.ext2) === 1);
152
+ // 场景: 组地址(cmd), 场景号(status), 动作(ext1)
153
+ // status: 场景号 1-64 (HTML模板中映射到statusAddr)
154
+ // ext1: 动作 1=开, 0=关 (HTML模板中映射到ext1)
155
+ mapping.sceneNumber = parseInt(knxEntity.statusAddr) || 1;
156
+ mapping.sceneAction = (parseInt(knxEntity.ext1) === 1);
157
+ // 清空 statusAddr 避免被当作 KNX 地址读取
158
+ mapping.knxAddrStatus = '';
113
159
  break;
114
160
  }
115
161
 
@@ -644,18 +690,20 @@ module.exports = function(RED) {
644
690
  node.commandQueue.shift();
645
691
  }
646
692
 
647
- // 检查队列中是否有相同映射的命令(防抖)
693
+ // 检查队列中是否有相同映射的命令
694
+ // 【优化】移除DEBOUNCE_MS限制,只要是相同key的命令就覆盖
695
+ // 这样确保执行的是最新的命令状态(Last Write Wins)
648
696
  const existing = node.commandQueue.find(c =>
649
697
  c.direction === cmd.direction &&
650
698
  c.mapping.meshMac === cmd.mapping.meshMac &&
651
699
  c.mapping.meshChannel === cmd.mapping.meshChannel &&
652
- c.type === cmd.type &&
653
- Date.now() - (c.timestamp || 0) < DEBOUNCE_MS
700
+ c.type === cmd.type
654
701
  );
655
702
 
656
703
  if (existing) {
657
704
  existing.value = cmd.value;
658
- node.debug(`[队列] 合并命令: ${cmd.key}`);
705
+ existing.timestamp = Date.now(); // 更新时间戳
706
+ node.debug(`[队列] 合并/更新命令: ${cmd.key} -> ${cmd.value}`);
659
707
  return;
660
708
  }
661
709
 
@@ -985,9 +1033,28 @@ module.exports = function(RED) {
985
1033
  if (type === 'switch') {
986
1034
  const channel = mapping.meshChannel || 1;
987
1035
  const totalChannels = meshDevice.channels || 1;
988
- const param = Buffer.from([totalChannels, channel, value ? 1 : 0]);
1036
+
1037
+ // 【新增】支持 Toggle 动作
1038
+ let finalValue = value;
1039
+ if (value === 'toggle') {
1040
+ const mac = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
1041
+ // 尝试从缓存获取当前状态
1042
+ const cached = node.stateCache[mac] || {};
1043
+ const switchKey = `switch_${channel}`;
1044
+ const currentVal = cached[switchKey];
1045
+
1046
+ // 如果缓存中有状态,取反;否则默认开
1047
+ if (currentVal !== undefined) {
1048
+ finalValue = !(currentVal === true || currentVal === 1 || currentVal === 'on' || currentVal === 'ON');
1049
+ } else {
1050
+ finalValue = true; // 默认开
1051
+ }
1052
+ node.log(`[KNX->Mesh] Toggle动作: 当前=${currentVal}, 目标=${finalValue ? 'ON' : 'OFF'}`);
1053
+ }
1054
+
1055
+ const param = Buffer.from([totalChannels, channel, finalValue ? 1 : 0]);
989
1056
  await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
990
- node.log(`[KNX->Mesh] 发送开关: ${meshDevice.name} CH${channel} = ${value ? 'ON' : 'OFF'}`);
1057
+ node.log(`[KNX->Mesh] 发送开关: ${meshDevice.name} CH${channel} = ${finalValue ? 'ON' : 'OFF'}`);
991
1058
  }
992
1059
  else if (type === 'cover_action') {
993
1060
  const action = value === 'open' ? 1 : value === 'close' ? 2 : 3;
@@ -1108,6 +1175,8 @@ module.exports = function(RED) {
1108
1175
  const groupAddr = msg.knx?.destination || msg.topic || '';
1109
1176
  const value = msg.payload;
1110
1177
  const dpt = node.normalizeDpt(msg.knx?.dpt || msg.dpt || '');
1178
+ const event = msg.knx?.event || msg.event || 'GroupValue_Write';
1179
+ const isResponse = event === 'GroupValue_Response';
1111
1180
 
1112
1181
  if (!groupAddr) {
1113
1182
  done && done();
@@ -1122,6 +1191,11 @@ module.exports = function(RED) {
1122
1191
  return;
1123
1192
  }
1124
1193
 
1194
+ // 只有Write事件触发自动同步倒计时
1195
+ if (event === 'GroupValue_Write') {
1196
+ node.triggerAutoSync();
1197
+ }
1198
+
1125
1199
  // 检查是否是我们自己发出去的命令(防止自己发的命令被监听后又处理)
1126
1200
  // 【修复】只检查当前地址是否刚发送过相同的值,不检查所有关联地址
1127
1201
  // 否则窗帘位置状态会被错误地当作回显跳过
@@ -1145,6 +1219,45 @@ module.exports = function(RED) {
1145
1219
  // 确定地址功能(优先使用地址匹配,比DPT更可靠)
1146
1220
  const addrFunc = node.getKnxAddrFunction(mapping, groupAddr);
1147
1221
 
1222
+ // 【新增】触发值匹配逻辑
1223
+ // 如果实体配置了 triggerValue,则必须匹配该值才触发
1224
+ // 支持Hex格式(0x01)匹配 rawValue buffer[0]
1225
+ // 支持Dec格式(1)匹配 payload
1226
+ if (mapping.triggerValue) {
1227
+ let isMatch = false;
1228
+ const tv = mapping.triggerValue.trim();
1229
+
1230
+ if (tv.startsWith('0x') || tv.startsWith('0X')) {
1231
+ // Hex匹配 rawValue
1232
+ if (msg.rawValue && Buffer.isBuffer(msg.rawValue) && msg.rawValue.length > 0) {
1233
+ const targetVal = parseInt(tv, 16);
1234
+ // 对于 DPT1/Boolean,rawValue 可能是位操作后的结果,但 knx-ultimate 通常返回 buffer
1235
+ // DPT 1.001: buffer[0] & 1 ?
1236
+ // 用户日志显示 rawValue: buffer[1] 0: 0x1。说明是一个字节。
1237
+ // 这里我们比较第一个字节
1238
+ if (msg.rawValue[0] === targetVal) {
1239
+ isMatch = true;
1240
+ }
1241
+ }
1242
+ } else {
1243
+ // Dec匹配 payload
1244
+ // 尝试匹配数字或布尔值
1245
+ if (msg.payload == tv) { // 弱类型比较 "1"==1, "true"=="true"
1246
+ isMatch = true;
1247
+ } else if (tv === 'true' && msg.payload === true) {
1248
+ isMatch = true;
1249
+ } else if (tv === 'false' && msg.payload === false) {
1250
+ isMatch = true;
1251
+ }
1252
+ }
1253
+
1254
+ if (!isMatch) {
1255
+ node.debug(`[KNX输入] 跳过(触发值不匹配): ${groupAddr}, exp=${tv}, actPayload=${msg.payload}, actRaw=${msg.rawValue ? msg.rawValue.toString('hex') : 'null'}`);
1256
+ done && done();
1257
+ return;
1258
+ }
1259
+ }
1260
+
1148
1261
  // 通用值解析:支持 Boolean, Number, String, Buffer
1149
1262
  const parseBoolean = (val) => {
1150
1263
  if (Buffer.isBuffer(val)) {
@@ -1159,10 +1272,18 @@ module.exports = function(RED) {
1159
1272
 
1160
1273
  // 根据设备类型和地址功能处理
1161
1274
  // 重要修复:严禁 Status/反馈地址触发控制逻辑,防止反向操作和死循环
1275
+ // 【自动同步修正】如果是自动同步触发的Response消息,允许Status地址更新Mesh状态
1162
1276
  if (mapping.deviceType === 'switch') {
1163
- // 开关命令(只处理cmd地址,忽略status地址)
1164
- if (addrFunc === 'cmd') {
1165
- const switchValue = parseBoolean(value);
1277
+ // 开关命令(只处理cmd地址,忽略status地址,除非是Response)
1278
+ if (addrFunc === 'cmd' || (isResponse && addrFunc === 'status')) {
1279
+ let switchValue = parseBoolean(value);
1280
+
1281
+ // 【新增】应用 Trigger Action (1=On, 0=Off, 2=Toggle)
1282
+ // 如果设置了triggerAction,则忽略接收到的值,强制执行指定动作
1283
+ if (mapping.triggerAction === '1') switchValue = true;
1284
+ else if (mapping.triggerAction === '0') switchValue = false;
1285
+ else if (mapping.triggerAction === '2') switchValue = 'toggle';
1286
+
1166
1287
  const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_switch_${switchValue}`;
1167
1288
 
1168
1289
  // 防死循环检查
@@ -1211,7 +1332,18 @@ module.exports = function(RED) {
1211
1332
  else if (receivedScene === targetScene) matched = true;
1212
1333
 
1213
1334
  if (matched) {
1214
- const action = (mapping.sceneAction === 'on' || mapping.sceneAction === true || mapping.sceneAction === 1);
1335
+ // 【新增】支持 Trigger Action (1=On, 0=Off, 2=Toggle)
1336
+ // 默认动作是 ON (兼容旧逻辑 sceneAction)
1337
+ let action = true;
1338
+
1339
+ if (mapping.triggerAction === '0') action = false;
1340
+ else if (mapping.triggerAction === '2') action = 'toggle';
1341
+ else if (mapping.triggerAction === '1') action = true;
1342
+ else {
1343
+ // 兼容旧配置
1344
+ action = (mapping.sceneAction === 'on' || mapping.sceneAction === true || mapping.sceneAction === 1);
1345
+ }
1346
+
1215
1347
  const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_scene_${receivedScene}`;
1216
1348
 
1217
1349
  // 防死循环
@@ -1288,7 +1420,7 @@ module.exports = function(RED) {
1288
1420
  // 格式必须与handleMeshStateChange中的deviceKey一致: ${macNormalized}_${meshChannel}
1289
1421
  node.knxControlTimestamps[`${lightMacNormalized}_${mapping.meshChannel}`] = now;
1290
1422
 
1291
- if (addrFunc === 'cmd') { // 仅 cmd 地址触发,忽略 status
1423
+ if (addrFunc === 'cmd' || (isResponse && addrFunc === 'status')) { // 仅 cmd 地址触发,忽略 status (除非是Response)
1292
1424
  // 开关
1293
1425
  const sw = parseBoolean(value);
1294
1426
  const loopKey = `${mapping.meshMac}_light_switch`;
@@ -1340,7 +1472,7 @@ module.exports = function(RED) {
1340
1472
  // 【关键】记录KNX控制时间戳
1341
1473
  node.knxControlTimestamps[deviceKey] = Date.now();
1342
1474
 
1343
- if (addrFunc === 'cmd') {
1475
+ if (addrFunc === 'cmd' || addrFunc === 'status') {
1344
1476
  const sw = parseBoolean(value);
1345
1477
  const loopKey = `${mapping.meshMac}_climate_sw`;
1346
1478
  node.recordSyncTime('knx-to-mesh', loopKey);
@@ -515,14 +515,11 @@
515
515
  </ul>
516
516
 
517
517
  <h3>配置说明</h3>
518
- <dl class="message-properties">
519
- <dt>Mesh MQTT</dt>
520
- <dd>选择symi-mqtt配置节点,用于获取Mesh设备列表和状态同步</dd>
521
- <dt>品牌MQTT</dt>
522
- <dd>选择品牌MQTT配置节点(如HYQW),用于获取品牌设备列表</dd>
523
- <dt>实体映射</dt>
524
- <dd>配置Mesh设备与品牌设备的对应关系(配置会持久保存)</dd>
525
- </dl>
518
+ <ul>
519
+ <li><b>Mesh MQTT</b> - 选择关联的Symi MQTT配置节点,用于Mesh端通信</li>
520
+ <li><b>品牌MQTT</b> - 选择第三方品牌的MQTT配置节点(如HYQW)</li>
521
+ <li><b>同步设置</b> - 本节点自动继承<b>Mesh网关</b>中的全局同步设置(队列长度、防环路锁时间等)</li>
522
+ </ul>
526
523
 
527
524
  <h3>离线设备显示</h3>
528
525
  <p>当MQTT断开时,已配置的设备仍会显示在列表中:</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.8.21",
3
+ "version": "1.8.22",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {
@@ -46,6 +46,7 @@
46
46
  "dependencies": {
47
47
  "axios": "^1.7.9",
48
48
  "mqtt": "^5.10.0",
49
+ "node-red-contrib-symi-mesh": "file:node-red-contrib-symi-mesh-1.8.22.tgz",
49
50
  "serialport": "^12.0.0"
50
51
  },
51
52
  "engines": {