node-red-contrib-symi-mesh 1.9.5 → 1.9.7
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 +68 -204
- package/lib/device-manager.js +14 -4
- package/lib/protocol.js +5 -6
- package/lib/sync-utils.js +22 -0
- package/nodes/symi-gateway.js +27 -13
- package/nodes/symi-ha-sync.js +57 -62
- package/nodes/symi-knx-bridge.html +34 -1
- package/nodes/symi-knx-bridge.js +1063 -353
- package/nodes/symi-knx-ha-bridge.js +25 -2
- package/nodes/symi-mqtt.js +287 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,15 +17,21 @@
|
|
|
17
17
|
- **单次发现**:`53 12 00 41` 设备列表查询仅在部署/重启时执行一次,避免重复查询导致丢包
|
|
18
18
|
- **MQTT Discovery**:自动发布HA Discovery配置,设备即插即用
|
|
19
19
|
- **双向状态同步**:支持0x80状态事件,实时反馈设备状态变化
|
|
20
|
-
-
|
|
20
|
+
- **依赖主动上报与主动校准**:移除常规轮询,正常运行完全依赖设备主动上报;仅在 KNX/Mesh 同步校准时使用 `0x32` 指令进行主动查询,确保状态对齐的准确性
|
|
21
21
|
- **三合一设备检测**:通过设备主动上报的 `0x94` 消息或 `0x68/0x6B` 属性自动识别三合一面板
|
|
22
22
|
- **多设备类型**:支持开关、灯光、窗帘、温控器、传感器等12+种设备
|
|
23
23
|
- **三合一面板**:完整支持空调+新风+地暖三合一控制面板,自动识别
|
|
24
24
|
- **RS485/Modbus集成**:支持第三方485设备双向同步,内置协议模板,支持中弘VRF、SYMI面板协议
|
|
25
25
|
- **RS485同步增强**:`symi-rs485-sync` 节点已全面重构,采用统一防环路机制,支持多台空调内机批量状态同步和三合一子实体增强映射
|
|
26
|
-
-
|
|
26
|
+
- **自动状态同步**:支持状态自动校准与防死循环,开启后 Mesh 或 KNX 设备动作将触发延迟状态读取,强制对齐两端状态
|
|
27
|
+
- **查询优化**:严格区分 KNX 读取请求(GroupValue_Read),查询时直接回复缓存状态,不再触发设备控制,彻底解决“查询即关闭”问题
|
|
28
|
+
- **Mesh 主动查询**:同步时自动下发 `0x32` 指令查询 Mesh 实时状态,确保校准依据准确
|
|
29
|
+
- **介入日志**:控制台实时输出 `[Mesh->KNX介入]` 日志,清晰展示防死循环与校准过程
|
|
30
|
+
- **批量处理优化**:
|
|
27
31
|
- **KNX-HA集成**:支持KNX与Home Assistant实体直接双向同步
|
|
28
|
-
- **窗帘同步优化**:专门针对无限位Mesh
|
|
32
|
+
- **窗帘同步优化**:专门针对无限位Mesh窗帘模组优化,支持控制锁定(默认3s,可配置)和即时状态同步,彻底解决状态死循环和丢包问题
|
|
33
|
+
- **可配置锁定时间**:针对慢速窗帘电机,支持在网关节点的“显示全局同步设置”中配置**调光/窗帘锁时间**(500ms-50000ms),默认3000ms;所有桥接/同步节点共享该参数
|
|
34
|
+
- **反馈确认熔断**:智能反馈确认机制,自动检测设备离线状态,避免对不存在/离线设备持续重试
|
|
29
35
|
- **云端同步**:从酒店云云平台自动获取设备名称和场景信息
|
|
30
36
|
- **稳定可靠**:完善的错误处理和自动重连机制
|
|
31
37
|
- **设备去重保证**:所有节点设备列表均采用唯一性保证,确保设备不重复
|
|
@@ -106,10 +112,10 @@ node-red-restart
|
|
|
106
112
|
- 串口路径: /dev/ttyUSB0
|
|
107
113
|
- 波特率: 115200
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
115
|
+
**全局同步设置**(常规查询已移除):
|
|
116
|
+
- **常规查询**: 已移除 `53 32` 定期轮询,系统完全依赖设备主动上报(0x80状态事件)
|
|
117
|
+
- 这样可以避免频繁查询导致的无线丢包问题,提高系统稳定性
|
|
118
|
+
- **按需主动查询 (v1.9.7)**:仅在执行校准或特定同步逻辑时主动下发查询指令,确保数据对齐
|
|
113
119
|
|
|
114
120
|
### 3. 添加MQTT桥接节点
|
|
115
121
|
|
|
@@ -138,15 +144,16 @@ node-red-restart
|
|
|
138
144
|
|
|
139
145
|
---
|
|
140
146
|
|
|
141
|
-
|
|
147
|
+
##### 日志与排障(生产默认静默)
|
|
142
148
|
|
|
143
149
|
本插件面向酒店/大规模部署,**默认不向 Node-RED 侧边栏刷屏**(包括 `node.error/node.warn/node.log`),并对可选的诊断输出做了**限流**,避免网络抖动/断线重连时产生日志风暴。
|
|
144
150
|
|
|
145
|
-
|
|
151
|
+
#### 默认行为(推荐生产)
|
|
146
152
|
|
|
147
153
|
- **不设置任何环境变量**:日志保持静默,仅通过节点状态(Status)显示关键状态(如“未配置网关/同步失败”等)。
|
|
154
|
+
- **KNX输入日志**:KNX输入日志已调整为 `debug` 级别,生产环境下默认不显示,仅在开启调试模式时可见。
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
#### 需要排障时开启诊断日志
|
|
150
157
|
|
|
151
158
|
通过环境变量开启(重启 Node-RED 生效):
|
|
152
159
|
|
|
@@ -333,7 +340,7 @@ node-red
|
|
|
333
340
|
- 支持场景通道选择(按键X场景、Mesh场景)
|
|
334
341
|
- 支持实体ID搜索和下拉选择
|
|
335
342
|
|
|
336
|
-
|
|
343
|
+
** 双向同步连接方式(重要)**
|
|
337
344
|
|
|
338
345
|
要实现完整的双向同步,必须连接HA事件节点到`symi-ha-sync`节点的输入端:
|
|
339
346
|
|
|
@@ -472,7 +479,6 @@ node-red
|
|
|
472
479
|
- **KNX→Mesh批量处理**:当KNX同时触发同一Mesh设备的多个通道时,自动合并为一行码发送,提升效率
|
|
473
480
|
- **Mesh→KNX批量处理**:当Mesh同时改变同一设备的多个开关通道时,批量发送到不同的KNX地址,按通道顺序排序确保一致性
|
|
474
481
|
- 批量处理时间窗口:100ms内收到的同一设备的开关命令会自动合并
|
|
475
|
-
- **自动状态校准**:新增功能,可选全局自动读取状态,解决手动操作后的同步延迟
|
|
476
482
|
- **防死循环**:统一使用800ms防死循环时间窗口,增强型状态回传过滤逻辑
|
|
477
483
|
|
|
478
484
|
**配置步骤**:
|
|
@@ -505,12 +511,15 @@ npm install node-red-contrib-knx-ultimate
|
|
|
505
511
|
这是为了解决"状态不同步导致的二次控制失败"而设计的核心功能。
|
|
506
512
|
|
|
507
513
|
**工作原理**:
|
|
508
|
-
1. **触发**:当你在米家/Mesh
|
|
509
|
-
2.
|
|
510
|
-
3.
|
|
511
|
-
-
|
|
512
|
-
-
|
|
513
|
-
4.
|
|
514
|
+
1. **触发**:当你在米家/Mesh 端发起控制,或 KNX 总线上出现动作(GroupValue_Write)时,系统启动可配置的延迟倒计时(默认 3 秒)。
|
|
515
|
+
2. **规定时间内完成查询**:延迟结束后,**先**向所有涉及的 Mesh 设备下发 `0x32` 状态查询,约 **1 秒后**再向已映射的 KNX 组地址发送 GroupValue_Read;收到 GroupValue_Response 后与 Mesh 实时状态比对。
|
|
516
|
+
3. **状态对齐**:
|
|
517
|
+
- **智能比对**:比较 KNX 状态与 Mesh 状态(Mesh 状态来自 0x32 查询后的缓存)。
|
|
518
|
+
- **按需同步**:若不一致,仅向 Mesh 下发校准命令(标记为校准,不触发反向 KNX 控制);若一致,仅更新内部缓存,不下发任何控制。
|
|
519
|
+
4. **查询不改变 KNX 状态**:KNX 侧仅发送 GroupValue_Read、接收 GroupValue_Response,绝不向总线发送 Write,因此状态查询不会改变 KNX 开关状态。
|
|
520
|
+
5. **无真实设备/无反馈时的防护**:若某 KNX 组地址无真实设备或无法反馈,读回可能为默认/错误值,若直接按读回值同步到 Mesh 会导致误关灯。本节点已做防护:**若近期(5 秒内)我们向该地址写过与读回值相反的值**(例如刚写过 ON 但读回 OFF),则仅更新缓存、不向 Mesh 下发校准,并打日志「近期已向KNX写入与读回值相反,可能无真实设备/反馈,仅更新缓存不同步到Mesh」。使用真实 KNX 设备时,读回与写入一致则正常同步。
|
|
521
|
+
6. **介入日志**:控制台输出 `[DelayedSync]` / `[Mesh->KNX介入]` 等日志,便于核对校准过程。
|
|
522
|
+
7. **效果**:在规定延迟内完成 Mesh 与 KNX 的状态查询与比对,确保两端对齐,避免二次控制失败。
|
|
514
523
|
|
|
515
524
|
**配置方法**:
|
|
516
525
|
- **开启自动状态校准**:勾选此项开启全局校准。
|
|
@@ -536,7 +545,9 @@ npm install node-red-contrib-knx-ultimate
|
|
|
536
545
|
1. 查询Mesh设备实际状态
|
|
537
546
|
2. 如果状态不一致,重发命令确保同步
|
|
538
547
|
3. 如果状态一致,确认成功
|
|
539
|
-
4. 最多重试
|
|
548
|
+
4. 最多重试5次(MAX_RETRY_COUNT),确保“最后一次KNX命令”最终在Mesh侧达成
|
|
549
|
+
- **快速连按最终收敛(Last Write Wins)**:在 3 秒 KNX 主控窗口内,若Mesh尾帧/抖动导致最终状态跑偏,会自动纠错补发(限频,最多5次),确保最终一致。
|
|
550
|
+
- **多路开关解析一致性**:`switchState` 采用每路 2-bit 编码解码(`decodeSwitchState2Bit`),避免状态矛盾触发反向控制或误确认。
|
|
540
551
|
- **Mesh控制KNX**:
|
|
541
552
|
- 发送命令后,记录待确认的命令(3秒超时)
|
|
542
553
|
- 收到KNX状态反馈(控制地址或状态地址)且与期望值一致时,确认成功并记录日志:`[反馈确认] ✓ Mesh控制KNX成功: ...`
|
|
@@ -669,188 +680,41 @@ RS485通信桥接,支持Modbus协议透传与自定义指令映射。
|
|
|
669
680
|
|
|
670
681
|
## 更新日志
|
|
671
682
|
|
|
672
|
-
### v1.9.
|
|
673
|
-
|
|
674
|
-
####
|
|
675
|
-
-
|
|
676
|
-
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
-
|
|
695
|
-
-
|
|
696
|
-
-
|
|
697
|
-
-
|
|
698
|
-
-
|
|
699
|
-
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
-
|
|
705
|
-
-
|
|
706
|
-
-
|
|
707
|
-
|
|
708
|
-
-
|
|
709
|
-
-
|
|
710
|
-
|
|
711
|
-
### v1.9.2 (2026-02-02)
|
|
712
|
-
|
|
713
|
-
#### 回显检测优化
|
|
714
|
-
- **正常情况下不会回显**:KNX反馈应该使用状态地址(knxAddrStatus),而我们发送的是控制地址(knxAddrCmd),地址不同,不会造成回显
|
|
715
|
-
- **优化回显检测逻辑**:如果收到的是状态地址且与控制地址不同,不当作回显(正常情况下状态地址不会回显)
|
|
716
|
-
- **可配置回显检测时间窗口**:支持在节点配置中设置回显检测时间窗口(默认500ms),适用于反馈也使用控制地址的特殊情况
|
|
717
|
-
- **确保大量设备控制时也能正确工作**:优化后的逻辑确保在50个KNX开关组地址场景下也能完美解析处理同步Mesh对应的开关状态,不会造成回显反控的情况
|
|
718
|
-
|
|
719
|
-
#### 修复
|
|
720
|
-
- **【关键修复】KNX->Mesh控制必须确保同步成功**:
|
|
721
|
-
- **问题**:KNX控制Mesh后,如果Mesh没有及时反馈状态,系统就停止处理,导致同步失败
|
|
722
|
-
- **修复**:
|
|
723
|
-
- 增强重试机制:从1次重试增加到5次(MAX_RETRY_COUNT),确保必须同步成功
|
|
724
|
-
- 递归重试逻辑:超时后查询Mesh状态,如果状态不一致则重发命令,继续等待反馈,直到成功或达到最大重试次数
|
|
725
|
-
- 改进反馈确认:支持多种状态格式(`switch_${channel}`、`switch`、`switchState`位掩码),确保能正确识别Mesh状态反馈
|
|
726
|
-
- 重试时复用待确认记录:重试时不会创建新的待确认记录,而是更新现有的记录,避免重复确认
|
|
727
|
-
- 确保双向同步可靠性:**谁主动发起的控制,另一方必须达到最终的同步状态效果**
|
|
728
|
-
- **【关键修复】场景批量命令导致反向同步问题**:
|
|
729
|
-
- **问题**:场景触发时,多个KNX设备在短时间内(如500ms内)陆续动作,只有被直接控制的设备会更新单设备时间戳,场景中的其他设备如果没有被直接控制,或者`isUserControl`判断错误,就会漏掉检查,导致Mesh状态返回时触发反向同步到KNX
|
|
730
|
-
- **修复**:
|
|
731
|
-
- 新增全局KNX活动时间戳`lastKnxActivityTime`,每次收到KNX命令时更新(第1815行)
|
|
732
|
-
- 在Mesh→KNX同步检查时,优先检查全局时间戳(第462-468行通用设备,第549-555行开关设备,第618-623行调光灯设备)
|
|
733
|
-
- 无论哪个设备被KNX控制,只要在800ms活动窗口内,就阻止所有Mesh→KNX同步
|
|
734
|
-
- 与单设备时间戳形成双层保护机制,确保场景批量命令时不会触发反向同步
|
|
735
|
-
- **【关键优化】批量处理逻辑完善**:
|
|
736
|
-
- **Mesh→KNX方向**:
|
|
737
|
-
- 当Mesh设备的所有通道同时变化时(如4键或6键面板同时按下多个按键),`handleSwitchState`方法会解析一行代码的所有按键状态(`lib/device-manager.js`第464-617行)
|
|
738
|
-
- 解析后的状态会触发`device-state-changed`事件,包含所有变化的按键状态(`changedState`对象)
|
|
739
|
-
- KNX Bridge会遍历所有匹配的映射,收集所有开关命令到`batchCommands`数组,使用相同的时间戳(`nodes/symi-knx-bridge.js`第460-591行)
|
|
740
|
-
- 批量加入队列后统一处理,确保所有映射的KNX地址都能正确同步(第845-863行)
|
|
741
|
-
- **不会一个一个按键去列队,而是一行代码给该开关的所有按键去同步真实状态**
|
|
742
|
-
- **KNX→Mesh方向**:
|
|
743
|
-
- 当KNX同时触发同一Mesh设备的多个通道时,会收集所有命令到`cmds`数组(批量处理逻辑)
|
|
744
|
-
- 使用`syncKnxToMeshBatch`方法,收集所有通道的目标状态到`channelStates`对象(第1422-1473行)
|
|
745
|
-
- 使用`buildSwitchState`按通道顺序应用状态变化,构建最终状态(第1496-1511行)
|
|
746
|
-
- **一行代码发送整个面板状态**,不会逐个按键发送(第1528-1562行)
|
|
747
|
-
- KNX会正确解析一行代码一个开关的所有按键的状态,触发KNX的开关组地址正确完成同步
|
|
748
|
-
- **支持4键和6键面板(1-2-3-4-6键)**,100%正确匹配,单个按键动作或多个按键同时动作都能正确处理
|
|
749
|
-
|
|
750
|
-
### v1.9.1 (2026-01-31)
|
|
751
|
-
|
|
752
|
-
#### 修复
|
|
753
|
-
- **【关键修复】AutoSync读取地址错误导致状态丢失**:
|
|
754
|
-
- **错误**:Mesh控制KNX后,AutoSync读取了状态地址(knxAddrStatus)而不是控制地址(knxAddrCmd),导致读取的状态与Mesh发送的地址不一致,出现"Mesh控制KNX后状态丢失"的问题
|
|
755
|
-
- **修复**:AutoSync现在优先读取控制地址(knxAddrCmd),如果配置了独立的状态地址且与控制地址不同,才读取状态地址。确保AutoSync读取的地址与Mesh发送的地址一致
|
|
756
|
-
- **knxControlTimestamps检查过于严格**:
|
|
757
|
-
- **错误**:`knxControlTimestamps`检查在所有情况下都会阻止Mesh→KNX同步,包括状态反馈场景,导致状态反馈被错误阻止
|
|
758
|
-
- **修复**:现在只在`isUserControl=true`时检查,避免状态反馈被错误阻止
|
|
759
|
-
- **KNX控制保护期错误阻止KNX→Mesh同步**:
|
|
760
|
-
- **错误**:`knxControlTimestamps`被错误地用于阻止KNX→Mesh同步,导致KNX控制一次后无法再次控制Mesh设备
|
|
761
|
-
- **修复**:删除了错误的检查逻辑,`knxControlTimestamps`只用于阻止Mesh→KNX同步,KNX可以随时控制Mesh设备
|
|
762
|
-
- **AutoSync防抖机制跳过状态匹配**:
|
|
763
|
-
- **错误**:AutoSync防抖机制在时间窗口内会跳过状态匹配,导致某些Mesh控制后没有触发状态匹配
|
|
764
|
-
- **修复**:改为重置定时器而不是跳过,确保每次Mesh控制后都会触发状态匹配
|
|
765
|
-
- **AutoSync状态校准使用缓存导致误判**:
|
|
766
|
-
- **错误**:状态校准逻辑直接使用Mesh缓存状态,可能导致因缓存不准确而误判状态不一致
|
|
767
|
-
- **修复**:当检测到KNX状态与Mesh缓存状态不一致时,先查询Mesh设备的实际状态(发送0x32查询指令),等待500ms让Mesh设备响应后,再次检查状态,确保使用真实状态。只有在查询后确认状态仍不一致时,才发送校准命令到Mesh设备
|
|
768
|
-
- **KNX控制后状态反馈被误判为用户控制**:
|
|
769
|
-
- **错误**:KNX控制Mesh设备后,Mesh返回的状态反馈被误判为用户控制,触发Mesh→KNX反向同步,导致死循环
|
|
770
|
-
- **修复**:当收到0xB0控制响应或0xB2查询响应后,500ms内的状态反馈帧(包括开关和窗帘)会被正确识别为控制响应,标记为`isUserControl=false`,不会触发反向同步
|
|
771
|
-
- **批量发送时间戳记录时机错误**:
|
|
772
|
-
- **错误**:批量发送时时间戳记录时机不正确,导致回显检测不准确,自己发送的命令被误判为回显
|
|
773
|
-
- **修复**:批量发送时时间戳在实际发送时记录,确保时间戳准确,提升回显检测准确性
|
|
774
|
-
- **首次部署查询逻辑优化**:
|
|
775
|
-
- **错误**:三合一设备检测使用固定2秒等待时间,查询完成后使用固定3秒等待时间,导致查询时间不准确
|
|
776
|
-
- **修复**:
|
|
777
|
-
- 三合一设备检测:发送查询请求后立即继续查询完整状态,不再等待固定时间(响应通过事件异步处理)
|
|
778
|
-
- 查询完成等待:根据实际查询的设备数量动态计算等待时间(查询间隔50ms × 设备数量 + 响应时间200ms + 缓冲时间200ms)
|
|
779
|
-
- 确保查询的目的是:根据匹配的同步数据去查询真实映射设备的状态,然后同步给KNX/HA/MQTT等系统
|
|
780
|
-
- 查询完成后正确传递设备列表给同步节点,确保状态校准基于实际查询到的设备
|
|
781
|
-
- **部署后逻辑**:
|
|
782
|
-
1. 先获取mesh网关设备列表
|
|
783
|
-
2. 然后根据映射节点的同步设备去同步状态,把真实设备的状态发送给mesh设备去同步状态
|
|
784
|
-
3. 然后就稳定的根据双方的主动发起的控制去同步另外一方的状态,并且不会造成死循环的情况
|
|
785
|
-
|
|
786
|
-
#### 优化
|
|
787
|
-
- **AutoSync触发范围扩展**:为所有设备类型(开关、场景、调光灯、窗帘、空调、新风、地暖)的Mesh控制都添加了AutoSync触发,确保每次Mesh同步动作后都能匹配一次状态
|
|
788
|
-
- **批量处理优化(全节点)**:
|
|
789
|
-
- **KNX Bridge双向批量处理**:
|
|
790
|
-
- **KNX→Mesh批量处理**:当KNX同时触发同一Mesh设备的多个通道时,自动合并为一行码发送
|
|
791
|
-
- **Mesh→KNX批量处理**:当Mesh同时改变同一设备的多个开关通道时,批量发送到不同的KNX地址,按通道顺序排序确保一致性
|
|
792
|
-
- 批量处理时间窗口:100ms内收到的同一设备的开关命令会自动合并
|
|
793
|
-
- 支持状态组合算法:正确计算多路开关的最终状态值,确保所有通道状态一次性同步
|
|
794
|
-
- 协议格式优化:开关控制消息类型统一为0x02 (TYPE_ON_OFF),1-4路使用1字节参数,6-8路使用2字节参数(小端序)
|
|
795
|
-
- **HA Sync批量处理**:当HA同时控制同一Mesh设备的多个通道时,自动合并为一行码发送
|
|
796
|
-
- **RS485 Bridge批量处理**:当RS485同时控制同一Mesh设备的多个通道时,自动合并为一行码发送
|
|
797
|
-
- 优化网络传输效率:减少Mesh网关的通信压力,提升响应速度,确保高效稳定的双向同步
|
|
798
|
-
|
|
799
|
-
### v1.9.0 (2026-01-29)
|
|
800
|
-
|
|
801
|
-
#### 修复
|
|
802
|
-
- **KNX-HA Bridge映射显示修复**:
|
|
803
|
-
- **错误**:映射列表左右两侧(KNX实体和HA实体)选择器不显示,映射数据绑定问题导致数据无法正确保存
|
|
804
|
-
- **修复**:修复了映射数据绑定问题,确保映射数据正确保存到容器;修复了映射加载逻辑,清空现有映射后再加载;优化了初始化顺序:先加载KNX实体,再加载映射,最后加载HA实体;修复了KNX和HA选项更新时机,在所有相关操作后都会同步更新两个选项列表
|
|
805
|
-
- **KNX-HA Bridge代码质量优化**:
|
|
806
|
-
- **错误**:防死循环跳过日志过多,节点关闭时队列处理可能继续运行导致内存泄漏,命令队列去重逻辑不够完善
|
|
807
|
-
- **修复**:优化日志级别为debug级别;添加 `isClosed` 标志确保节点关闭时队列处理正确停止;完善防死循环保护,验证所有同步路径都有防死循环检查;优化命令队列去重逻辑,检查值是否相同而不仅仅是时间窗口;添加状态缓存检查,同步前检查值是否真的改变
|
|
808
|
-
- **日志刷屏问题修复**:
|
|
809
|
-
- **错误**:KNX-HA Bridge的 `/ha-entities` 接口在编辑器重复请求时反复打日志;MQTT重连时重复发布Discovery配置导致日志刷屏
|
|
810
|
-
- **修复**:将日志级别降为 `debug`,并添加30秒缓存;MQTT连接时不再清空已发布设备记录,避免重复生成实体日志
|
|
811
|
-
- **首次部署状态查询**(v1.9.1):
|
|
812
|
-
- **首次启动时**:执行一次状态查询,用于:
|
|
813
|
-
- **主动查询三合一设备**:通过查询 `0x68` 新风开关和 `0x6B` 地暖开关来检测三合一设备
|
|
814
|
-
- 查询结果会自动调用 `markAsThreeInOne` 持久化保存到文件
|
|
815
|
-
- 确保网关断网断电后不会变回温控器(从文件恢复三合一状态)
|
|
816
|
-
- 去重逻辑:已确认的三合一设备不会重新检测
|
|
817
|
-
- 获取每个同步映射的初始状态(用于KNX-HA Bridge等同步节点进行状态校准)
|
|
818
|
-
- **运行过程中**:不再主动使用 `53 32` 查询命令,完全依赖设备主动上报
|
|
819
|
-
- **原因**:运行过程中的查询命令会导致丢包,影响网关正常设备上报和下发
|
|
820
|
-
- **改进**:系统在首次启动时查询一次后,完全依赖设备主动上报(0x80状态事件),提高稳定性
|
|
821
|
-
- **三合一检测**:
|
|
822
|
-
- 首次启动时:主动查询 `0x68/0x6B` 检测,结果持久化保存
|
|
823
|
-
- 运行过程中:依赖设备主动上报的 `0x94` 消息或 `0x68/0x6B` 属性(也会持久化保存)
|
|
824
|
-
- **持久化保存**:三合一设备状态保存在 `~/.node-red/symi-mesh-data/three-in-one-devices-{gatewayId}.json`
|
|
825
|
-
- **修复**:现在默认值会正确显示为"关闭 (默认)"
|
|
826
|
-
- **设备控制节点场景ID保存和加载问题**:
|
|
827
|
-
- **错误**:选择虚拟场景时场景ID没有正确保存,场景ID在编辑面板中不显示,场景ID输入框显示逻辑不正确
|
|
828
|
-
- **修复**:场景ID会正确保存到配置中;场景ID会正确加载和显示已保存的值;场景ID输入框会根据通道类型(mesh_scene或scene1-6)正确显示/隐藏
|
|
829
|
-
|
|
830
|
-
#### 优化
|
|
831
|
-
- **首次部署状态查询**(v1.9.1):
|
|
832
|
-
- 首次启动时主动查询三合一设备(0x68/0x6B),结果持久化保存
|
|
833
|
-
- 运行过程中不再使用 `53 32` 查询命令,完全依赖设备主动上报,避免查询导致丢包问题
|
|
834
|
-
- KNX Bridge的AutoSync状态校准使用独立的查询机制,不会与首次部署查询冲突
|
|
835
|
-
- **KNX Bridge双向同步优化**:正确处理状态反馈地址(statusAddr),状态反馈不会记录时间戳,不会触发反向控制保护;AutoSync校准命令发送后立即更新控制时间戳,防止反向同步;优化队列处理,确保在大量KNX数据时稳定运行
|
|
836
|
-
|
|
837
|
-
## 更新日志(历史版本)
|
|
838
|
-
|
|
839
|
-
### v1.9.0 (2026-01-29)
|
|
840
|
-
|
|
841
|
-
#### 新增功能
|
|
842
|
-
- **按键场景触发支持**:在所有同步节点(KNX、RS485、HA、MQTT)的通道选择中,新增"按键X场景"选项(X为1-6),支持场景ID范围2-95
|
|
843
|
-
- **虚拟场景实体**:新增永久生效的虚拟场景设备,固定MAC地址`00:00:00:00:00:00`,支持全协议联动
|
|
844
|
-
- **设备发现优化**:`53 12 00 41` 设备列表查询仅在部署/重启时执行一次,避免重复查询导致丢包
|
|
845
|
-
- **KNX状态反馈地址优化**:区分控制地址和状态反馈地址的处理逻辑,避免状态反馈地址触发反向控制
|
|
846
|
-
- **设备发现增强**:自动处理TCP分包粘包问题,完整性验证,10秒超时保护,去重机制,进度跟踪
|
|
847
|
-
|
|
848
|
-
#### 修复
|
|
849
|
-
- **配置持久化问题**:修复了"选择场景按键后部署不生效"的问题,恢复Node-RED原生的"确认保存"机制
|
|
850
|
-
- **RS485同步节点虚拟设备注册问题**:修复了虚拟设备注册问题
|
|
851
|
-
- **连接错误处理**:网关连接失败时采用静默重连机制,网络错误不输出错误日志,避免反复提醒
|
|
852
|
-
|
|
853
|
-
#### 优化
|
|
854
|
-
- **窗帘控制优化**:针对RS485窗帘增加1秒的控制锁(防回弹锁定),防止因控制过快导致的丢包或状态死循环
|
|
855
|
-
- **UI交互改进**:所有同步节点UI适配场景选择逻辑,支持根据通道类型动态显示场景ID输入框
|
|
856
|
-
- **稳定性增强**:完善各同步节点的全局状态查询参与机制,设备去重机制,映射关系持久化,确保配置不丢失
|
|
683
|
+
### v1.9.7 (2026-02-28)
|
|
684
|
+
|
|
685
|
+
#### 自动状态校准 (AutoSync) 与状态查询逻辑
|
|
686
|
+
- **KNX Bridge「自动同步状态」**:开启后,Mesh 或 KNX 动作触发可配置延迟(默认 3s);在规定时间内完成:先对涉及设备发 `0x32` 查 Mesh 状态,约 1s 后发 KNX GroupValue_Read,收到 GroupValue_Response 后与 Mesh 比对,**不一致则仅同步到 Mesh(校准),一致则仅更新缓存不下发**;校准命令带 `isCalibration`,不触发反向 KNX 控制。
|
|
687
|
+
- **KNX 状态查询不改变总线状态**:仅发送 GroupValue_Read、接收 Response,绝不向 KNX 总线写 Write,查询不会改变 KNX 开关状态。
|
|
688
|
+
- **GroupValue_Read 处理**:严格区分 Read/Response/Write;对 GroupValue_Read 只回复缓存状态,不触发 Mesh 控制,修复 Read 分支中 mapping/addrFunc 未定义导致的引用错误。
|
|
689
|
+
- **DelayedSync 读响应不误触 HA**:部分 KNX 库将 GroupValue_Response 标为 Write 输出,导致 KNX-HA Bridge 误执行关灯。通过 global 约定 DelayedSync 读阶段与地址列表,KNX-HA Bridge 在此窗口内忽略这些地址的报文,避免状态查询触发 HA 控制。
|
|
690
|
+
- **Mesh 设备查询与 MAC 格式**:DelayedSync 与 DeviceManager `getDeviceByMac` 兼容带冒号/无冒号 MAC,确保能正确触发 0x32 查询并获取 Mesh 状态参与比对(修复「已触发 0 个 Mesh 设备的状态查询」)。
|
|
691
|
+
- **DelayedSync 误关灯防护**:当 KNX 组地址无真实设备或无反馈时,读回值可能错误(如刚写入 ON 却读回 OFF),若直接同步到 Mesh 会误关灯。已增加防护:5 秒内若我们向该地址写过与读回值相反的值,则仅更新缓存、不同步到 Mesh,并用日志提示,使用真实设备时行为正常。
|
|
692
|
+
|
|
693
|
+
#### 界面与体验
|
|
694
|
+
- **配置项布局优化**:修复「自动同步状态」在某些分辨率下的换行问题。
|
|
695
|
+
- **日志分级**:生产环境默认静默,关键介入逻辑使用 `node.log` 确保可追溯。
|
|
696
|
+
|
|
697
|
+
### v1.9.0 - v1.9.6 历史迭代汇总
|
|
698
|
+
|
|
699
|
+
#### 核心修复与优化
|
|
700
|
+
- **防反向控制与回显保护**:
|
|
701
|
+
- 引入 **3秒全局 KNX 主控窗口**,确保快速连按时“最后一次操作”生效,阻止延迟反馈导致的状态回弹。
|
|
702
|
+
- 完善 **Last Write Wins (最终写入获胜)** 逻辑,多路开关采用 2-bit 编码解析,确保状态收敛。
|
|
703
|
+
- 支持可配置的 **回显检测时间窗口** (默认 500ms),严格区分控制地址与状态地址。
|
|
704
|
+
- **批量处理与性能**:
|
|
705
|
+
- 实现 **双向批量处理**:同一设备的多个通道动作合并为单条协议指令(一行代码),大幅降低总线负载。
|
|
706
|
+
- 优化 **部署查询逻辑**:启动时 AutoSync 与 StateQuery 互斥,避免重复读取;动态计算设备列表查询等待时间。
|
|
707
|
+
- **网关限流机制**:统一全局同步参数(队列长度 100,间隔 50ms),保障 50+ 实体大规模场景稳定性。
|
|
708
|
+
- **同步与兼容性**:
|
|
709
|
+
- **窗帘/调光同步增强**:区分主控来源(`isFromStateQuery`),部署或后台同步时不驱动真实电机;支持调光/窗帘锁定时间配置。
|
|
710
|
+
- **反馈确认闭环**:对用户控制引入 5 次重试 + 状态查询闭环,确保控制指令 100% 送达。
|
|
711
|
+
- **多协议联动**:修复 KNX-HA Bridge、MQTT Sync 等映射保存与显示问题,支持空调/新风/地暖三合一面板自动识别。
|
|
712
|
+
- **智能初始化与发现**:
|
|
713
|
+
- **首次启动状态快照**:仅在部署后执行一次全局状态查询,用于三合一设备检测与持久化缓存(`~/.node-red/symi-mesh-data/`),运行中完全依赖 0x80 事件上报,彻底解决查询导致的丢包问题。
|
|
714
|
+
- **增强设备发现**:自动处理 TCP 分包粘包,提供 10s 超时保护与完整性报告,去重机制按 index 确保唯一。
|
|
715
|
+
- **虚拟化与场景支持**:
|
|
716
|
+
- 支持 **虚拟场景实体** (MAC: 00:00:00:00:00:00),支持 KNX、RS485、HA、MQTT 全协议联动触发。
|
|
717
|
+
- 新增 **按键场景触发 (0x34)**,支持场景 ID 2-95,满足本地场景上报需求。
|
|
718
|
+
- **工程化改进**:
|
|
719
|
+
- 生产环境默认 **静默日志**,关键逻辑使用 `debug` 级并带 30s 缓存,避免日志刷屏。
|
|
720
|
+
- 修复了大量编辑器红三角校验、变量初始化顺序、内存泄漏及配置持久化保存隐患。
|
package/lib/device-manager.js
CHANGED
|
@@ -493,9 +493,8 @@ class DeviceInfo {
|
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
//
|
|
497
|
-
// 1-4
|
|
498
|
-
// 5-6路开关:2字节,小端序
|
|
496
|
+
// 严格按协议 V1.0 3.5.1.2 TYPE_ON_OFF:每2位表示1路,b01=关 b10=开,低2位为第1路
|
|
497
|
+
// 1-4路:1字节;6-8路:2字节小端序。解析后 switch_N 为 true=开 false=关
|
|
499
498
|
|
|
500
499
|
// 保存旧状态用于检测变化
|
|
501
500
|
const oldStates = {};
|
|
@@ -940,7 +939,18 @@ class DeviceManager extends EventEmitter {
|
|
|
940
939
|
}
|
|
941
940
|
|
|
942
941
|
getDeviceByMac(mac) {
|
|
943
|
-
|
|
942
|
+
let device = this.devices.get(mac);
|
|
943
|
+
if (!device && mac && typeof mac === "string") {
|
|
944
|
+
const normalized = mac.toLowerCase().replace(/:/g, "");
|
|
945
|
+
if (normalized.length === 12 && normalized !== mac) {
|
|
946
|
+
device = this.devices.get(normalized);
|
|
947
|
+
}
|
|
948
|
+
if (!device && normalized.length === 12 && !mac.includes(":")) {
|
|
949
|
+
const withColons = normalized.match(/.{2}/g).join(":");
|
|
950
|
+
device = this.devices.get(withColons);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return device || null;
|
|
944
954
|
}
|
|
945
955
|
|
|
946
956
|
getDeviceByAddress(addr) {
|
package/lib/protocol.js
CHANGED
|
@@ -56,7 +56,7 @@ class ProtocolHandler {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* 构建开关状态值(严格按协议 V1.0 3.5.1.2 TYPE_ON_OFF:b01=关 b10=开,低2位为第1路)
|
|
60
60
|
* @param {number} channels - 开关路数 (1-8)
|
|
61
61
|
* @param {number} targetChannel - 目标路数 (1-8)
|
|
62
62
|
* @param {boolean} targetState - 目标状态 (true=开, false=关)
|
|
@@ -122,25 +122,24 @@ class ProtocolHandler {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
*
|
|
125
|
+
* 解析开关状态值(严格按协议 V1.0 3.5.1.2 TYPE_ON_OFF:b01=关 b10=开,每路2bit低2位为第1路)
|
|
126
126
|
* @param {number|Buffer} stateValue - 状态值
|
|
127
127
|
* @param {number} channels - 开关路数
|
|
128
|
-
* @returns {Array<boolean>} - 每路的开关状态数组
|
|
128
|
+
* @returns {Array<boolean>} - 每路的开关状态数组 true=开 false=关
|
|
129
129
|
*/
|
|
130
130
|
parseSwitchState(stateValue, channels) {
|
|
131
131
|
let value;
|
|
132
132
|
if (Buffer.isBuffer(stateValue)) {
|
|
133
|
-
// 6
|
|
133
|
+
// 6-8路开关,小端序:低字节在前
|
|
134
134
|
value = stateValue[0] | (stateValue[1] << 8);
|
|
135
135
|
} else {
|
|
136
136
|
value = stateValue;
|
|
137
137
|
}
|
|
138
|
-
|
|
139
138
|
const states = [];
|
|
140
139
|
for (let i = 0; i < channels; i++) {
|
|
141
140
|
const bitPos = i * 2;
|
|
142
141
|
const bits = (value >> bitPos) & 0x03;
|
|
143
|
-
states.push(bits === 0x02); //
|
|
142
|
+
states.push(bits === 0x02); // 协议:b01=关 b10=开,不得反写
|
|
144
143
|
}
|
|
145
144
|
return states;
|
|
146
145
|
}
|
package/lib/sync-utils.js
CHANGED
|
@@ -382,6 +382,27 @@ function getSubEntityByHyqwType(hyqwType) {
|
|
|
382
382
|
return null;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
/**
|
|
386
|
+
* 从 switchState(2-bit/通道)解析某一路的开关状态
|
|
387
|
+
*
|
|
388
|
+
* 说明:协议中多路开关的 switchState 按“每路 2-bit”编码:
|
|
389
|
+
* - 0x01 => OFF
|
|
390
|
+
* - 0x02 => ON
|
|
391
|
+
* 其他值(0x00/0x03)视为“未知/不确定”,返回 undefined
|
|
392
|
+
*
|
|
393
|
+
* @param {number} switchState - 原始 switchState(通常为 1~2 字节组合后的整数)
|
|
394
|
+
* @param {number} channel - 通道号,从 1 开始
|
|
395
|
+
* @returns {boolean|undefined} - true/false 或 undefined(无法解析)
|
|
396
|
+
*/
|
|
397
|
+
function decodeSwitchState2Bit(switchState, channel) {
|
|
398
|
+
if (typeof switchState !== "number" || typeof channel !== "number" || channel < 1) return undefined;
|
|
399
|
+
const bitPos = (channel - 1) * 2;
|
|
400
|
+
const twoBits = (switchState >> bitPos) & 0x03;
|
|
401
|
+
if (twoBits === 0x01) return false;
|
|
402
|
+
if (twoBits === 0x02) return true;
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
385
406
|
module.exports = {
|
|
386
407
|
SyncUtils,
|
|
387
408
|
StateCache,
|
|
@@ -390,6 +411,7 @@ module.exports = {
|
|
|
390
411
|
getThreeInOneSubEntity,
|
|
391
412
|
getThreeInOneSubTypes,
|
|
392
413
|
getSubEntityByHyqwType,
|
|
414
|
+
decodeSwitchState2Bit,
|
|
393
415
|
// 导出常量供外部使用
|
|
394
416
|
DEFAULT_TIMEOUT,
|
|
395
417
|
COVER_TIMEOUT,
|
package/nodes/symi-gateway.js
CHANGED
|
@@ -496,31 +496,30 @@ module.exports = function(RED) {
|
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
if (device
|
|
499
|
+
// 始终向同步节点发送 device-state-changed,便于首次查询后做状态对齐
|
|
500
|
+
// 查询期间或查询响应的事件带 isFromStateQuery,同步节点可只同步位置不执行动作(窗帘/调光)
|
|
501
|
+
if (device) {
|
|
502
502
|
// 【基于协议类型的精准判断】根据协议文档:
|
|
503
503
|
// - 0x80 05 (NODE_ACK): 协议明确表示"开关动作源为 Mesh网络",直接标记为isUserControl=false
|
|
504
504
|
// - 0x80 06 (NODE_STATUS): 需要检查payload中是否包含msg_type=0x0D(本地按键触发)
|
|
505
505
|
// - 0xB2查询响应: 标记查询响应后的状态事件为isUserControl=false
|
|
506
|
-
|
|
507
506
|
const isCurtainEvent = (event.attrType === 0x05 || event.attrType === 0x06);
|
|
508
507
|
let isUserControl = false;
|
|
509
|
-
|
|
510
|
-
//
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
this.isQueryResponse = false; // 清除标志
|
|
508
|
+
|
|
509
|
+
// 先取查询响应标志,再清除,用于本事件的 isFromStateQuery
|
|
510
|
+
const wasQueryResponse = this.isQueryResponse;
|
|
511
|
+
if (wasQueryResponse) {
|
|
512
|
+
this.isQueryResponse = false;
|
|
515
513
|
this.debug(`[状态反馈识别] 地址=0x${event.networkAddress.toString(16).toUpperCase()}, attrType=0x${event.attrType.toString(16).toUpperCase()}, 识别为查询响应(来源:0xB2), isUserControl=false`);
|
|
516
514
|
}
|
|
515
|
+
const isFromStateQuery = this.isQueryingStates || wasQueryResponse;
|
|
517
516
|
|
|
518
517
|
// 根据subOpcode判断
|
|
519
518
|
if (event.subOpcode === 0x05) {
|
|
520
519
|
// NODE_ACK: 协议明确表示"开关动作源为 Mesh网络"
|
|
521
520
|
// 直接标记为isUserControl=false,无需时间关联
|
|
522
521
|
isUserControl = false;
|
|
523
|
-
if (!
|
|
522
|
+
if (!wasQueryResponse) {
|
|
524
523
|
this.debug(`[状态反馈识别] 地址=0x${event.networkAddress.toString(16).toUpperCase()}, subOpcode=0x05(NODE_ACK), 协议明确表示Mesh网络控制, isUserControl=false`);
|
|
525
524
|
}
|
|
526
525
|
} else if (event.subOpcode === 0x06) {
|
|
@@ -576,7 +575,7 @@ module.exports = function(RED) {
|
|
|
576
575
|
}
|
|
577
576
|
}
|
|
578
577
|
|
|
579
|
-
if (
|
|
578
|
+
if (wasQueryResponse) {
|
|
580
579
|
// 查询响应,不是用户操作
|
|
581
580
|
isUserControl = false;
|
|
582
581
|
} else if (hasLocalKeyTrigger) {
|
|
@@ -613,7 +612,8 @@ module.exports = function(RED) {
|
|
|
613
612
|
state: device.state,
|
|
614
613
|
subOpcode: event.subOpcode,
|
|
615
614
|
isUserControl: isUserControl, // 基于协议类型的精准判断
|
|
616
|
-
isSceneExecution: this.sceneExecutionInProgress
|
|
615
|
+
isSceneExecution: this.sceneExecutionInProgress,
|
|
616
|
+
isFromStateQuery: isFromStateQuery // 首次查询或查询响应,窗帘/调光只同步位置不执行动作
|
|
617
617
|
});
|
|
618
618
|
}
|
|
619
619
|
|
|
@@ -1080,6 +1080,20 @@ module.exports = function(RED) {
|
|
|
1080
1080
|
}
|
|
1081
1081
|
};
|
|
1082
1082
|
|
|
1083
|
+
SymiGatewayNode.prototype.queryDeviceStatus = async function(networkAddr, attrType = 0x00) {
|
|
1084
|
+
if (!this.connected || !this.client) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
const frame = this.protocolHandler.buildDeviceStatusQueryFrame(networkAddr, attrType);
|
|
1089
|
+
await this.client.sendFrame(frame, 2);
|
|
1090
|
+
return true;
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
this.error(`[queryDeviceStatus] 查询设备 0x${networkAddr.toString(16).toUpperCase()} 属性 0x${attrType.toString(16).toUpperCase()} 失败: ${error.message}`);
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1083
1097
|
SymiGatewayNode.prototype.sendControl = async function(networkAddr, attrType, param, opcode) {
|
|
1084
1098
|
if (!this.connected) {
|
|
1085
1099
|
throw new Error("Not connected");
|