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 CHANGED
@@ -17,15 +17,21 @@
17
17
  - **单次发现**:`53 12 00 41` 设备列表查询仅在部署/重启时执行一次,避免重复查询导致丢包
18
18
  - **MQTT Discovery**:自动发布HA Discovery配置,设备即插即用
19
19
  - **双向状态同步**:支持0x80状态事件,实时反馈设备状态变化
20
- - **完全依赖主动上报**:已移除 `53 32` 状态查询命令,完全依赖设备主动上报,避免查询导致丢包
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
- - **KNX集成**:支持与KNX系统双向同步,新增状态自动校准功能
26
+ - **自动状态同步**:支持状态自动校准与防死循环,开启后 Mesh 或 KNX 设备动作将触发延迟状态读取,强制对齐两端状态
27
+ - **查询优化**:严格区分 KNX 读取请求(GroupValue_Read),查询时直接回复缓存状态,不再触发设备控制,彻底解决“查询即关闭”问题
28
+ - **Mesh 主动查询**:同步时自动下发 `0x32` 指令查询 Mesh 实时状态,确保校准依据准确
29
+ - **介入日志**:控制台实时输出 `[Mesh->KNX介入]` 日志,清晰展示防死循环与校准过程
30
+ - **批量处理优化**:
27
31
  - **KNX-HA集成**:支持KNX与Home Assistant实体直接双向同步
28
- - **窗帘同步优化**:专门针对无限位Mesh窗帘模组优化,支持控制锁定(40s)和即时状态同步,彻底解决状态死循环和丢包问题
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
- - **状态查询延时**: 已移除,不再使用 `53 32` 命令查询设备状态
111
- - 系统完全依赖设备主动上报(0x80状态事件)来获取设备状态
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端发起控制,或者KNX总线上出现动作(GroupValue_Write)时,系统会启动一个倒计时。
509
- 2. **延迟读取**:默认3000ms(3秒)后,系统会自动向所有已映射的KNX**状态地址**发送读取请求(GroupValue_Read)。延迟时间可在节点配置中自定义(建议2000-5000ms)。
510
- 3. **状态对齐**:收到KNX系统的回复(GroupValue_Response)后,系统会比较KNX状态和Mesh状态:
511
- - 如果状态一致:记录日志,不执行操作
512
- - 如果状态不一致:立即发送校准命令到Mesh,强制对齐状态
513
- 4. **效果**:即使KNX总线没有主动反馈状态,系统也会在配置的延迟时间后通过主动查询完成同步。这样用户在第二次触发控制时,状态已经对齐,确保控制100%成功。
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. 最多重试1次,避免死循环
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.5 (2026-02-05)
673
-
674
- #### 启动时重复读取优化
675
- - **修复启动时重复读取问题**:启动时 AutoSync StateQuery 都会触发,导致重复读取(48+48=96个请求),增加 KNX 总线负载
676
- - **防重复机制**:
677
- - 记录 AutoSync 执行时间戳 `lastAutoSyncTime`
678
- - StateQuery 检查:如果 AutoSync 在最近 5 秒内执行过,则跳过 StateQuery 读取,避免重复
679
- - 效果:启动时只执行一次读取(AutoSync),不再重复,减少 KNX 总线负载和重复日志
680
- - **影响说明**:
681
- - **不修复的影响**:每次启动会发送双倍读取请求(如48个设备会发送96个请求),虽然不会导致功能错误,但会增加 KNX 总线负载,在大型系统中可能影响性能,且会产生重复日志
682
- - **修复后**:启动时只执行一次读取,减少总线负载,日志更清晰,适合生产环境长期运行
683
-
684
- #### 工程化与生产日志策略(客户无感)
685
- - **ESLint/打包验证闭环**:补齐 `.eslintrc.js` 与 `npm run lint` / `npm pack` 流程,确保发布前可重复校验
686
- - **日志恢复可观测**:保留节点内原有的 `node.log/node.warn/node.error` 行为,用于长期运行时的关键业务日志;仅在少数高频诊断点使用节流 Logger,避免刷屏
687
- - **主控识别与步进防抖**:针对窗帘/调光等带步进设备,完善 HA/KNX 与 Mesh 之间的“主控识别 + 回显抑制”,确保:
688
- - HA/KNX 发起控制时,Mesh 只跟随状态,不反向修改 HA/KNX
689
- - Mesh 发起控制时,HA/KNX 只做一次性状态对齐,不把步进反馈当成新指令
690
-
691
- ### v1.9.4 (2026-02-03)
692
-
693
- #### 全局同步默认值与 Symi KNX Bridge 稳定性
694
- - **统一全局同步默认值**:网关配置中的“显示全局同步设置”默认采用队列长度 **100**、队列间隔 **50ms**、开关锁时间 **800ms**、调光/窗帘锁时间 **3000ms**,范围分别为 100-300 / 50-200ms / 500-3000ms / 500-50000ms,所有桥接节点(KNX、HA、MQTT 等)共享这一组限流与防抖参数,确保在 40~50 个组地址/实体的大规模场景下也能稳定工作。
695
- - **修复 KNX Bridge 映射保存丢失问题**:`symi-knx-bridge` 编辑器在 `oneditsave` 中错误引用了仅存在于 `oneditprepare` 作用域内的 `devices` 变量,导致部署时抛出前端异常、映射列表无法持久化;现在改为直接从 DOM 下拉选项的 `data-*` 属性和文本恢复设备名称、类型和通道数,并在缺失时回退到已有映射数据,确保每次编辑后映射都能正确保存和恢复。
696
- - **增强所有节点映射列表的持久化一致性**:对包含映射表的节点(MQTT Sync、HA Sync、RS485 Bridge、RS485 Sync 等)的 `oneditsave` 逻辑进行审查,统一采用“从 DOM/隐藏字段收集数据 → 写入配置字段(JSON 字符串)”的模式,避免依赖临时内存变量,保证在网关/外部服务离线时依然可以从已保存配置中完整恢复映射列表。
697
- - **AutoSync 校准命令不再触发反馈重试闭环**:区分 KNX 用户控制和 AutoSync 状态校准;对带 `isCalibration=true` 标记的 KNX→Mesh 校准命令,仅发送一次并记录到本地缓存,不创建待确认记录、不触发 5 次重试逻辑,从而避免在 Mesh 设备掉线时周期性输出多轮 `[反馈确认] ✗ KNX控制Mesh失败(已重试5次)` 告警。
698
- - **保持用户控制的强一致性反馈确认**:普通 KNX→Mesh 控制依然使用 5 次递归重试 + 状态查询 + `switchState` 位掩码解析的闭环机制,依旧保证“用户在 KNX 侧最后一次操作的目标状态”必须在 Mesh 侧达成,同时将中间的超时/查询失败日志保持在 `debug` 级别,生产环境只在最终失败时输出一条 `warn` 级告警。
699
- - **方向性与回显保护说明**:明确 KNX→Mesh 控制周期内 Mesh 只作为执行端与状态上报端——由 KNX 写入产生的状态变化会被标记为“非用户控制”,并在 800ms 全局 KNX 活动窗口内阻止任何 Mesh→KNX 反向写入;同时回显检测仅作用于控制地址,在 500ms 窗口内对“值相同的回包”直接丢弃,状态地址与指示灯反馈永远不会被当作新的控制命令,彻底避免 KNX/Mesh 之间因为回显导致的死循环。
700
-
701
- ### v1.9.3 (2026-02-02)
702
-
703
- #### Symi KNX Bridge 稳定性修复
704
- - **修复编辑器红三角问题**:`symi-knx-bridge` 节点的 `echoWindow` 配置在旧 `flows.json` 中缺失时会导致校验失败,节点虽然工作正常但编辑器一直显示红色错误标记;现在允许 `echoWindow` 为空/未定义时通过校验,并在内部使用默认值 500ms。
705
- - **修复 KNX 输入解析异常**:修复 `parseBoolean` 在 KNX 输入处理流程中被提前使用导致的 `ReferenceError: Cannot access 'parseBoolean' before initialization`,该问题会在 Mesh→KNX 同步反馈时刷大量红色错误日志并影响反馈确认重试逻辑。
706
- - **反馈确认逻辑稳固**:
707
- - 在不改变 1.9.2 行为的前提下,确保 KNX 侧状态反馈在所有路径中都能被正确解析、确认和清理待确认记录,避免错误重试和“看起来已经同步但日志持续重试”的情况。
708
- - **快速多次控制仅认最后一次状态(Last Write Wins)**:同一设备同一通道/同一 KNX 组地址的待确认记录在新增时会覆盖旧记录,确保在类似你日志中 0/0/1 快速连按、出现多次“反馈确认/重试”时,只追踪“最后一次希望达到的状态”,中间过程状态(含查询重试)不会被再次强制执行或反向触发。
709
- - **降噪优化,避免生产环境刷屏**:反馈确认过程中关于“超时未收到反馈(重试x/x)”“查询后状态不一致/无法获取状态”“Mesh设备不存在/查询失败,重发命令”等内部诊断信息统一下调为 `debug` 级日志,正常 Info/Warn 级别只会在最终失败(已重试 5 次仍不同步)时输出一条告警,确保长期运行不刷屏。
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
+ - 修复了大量编辑器红三角校验、变量初始化顺序、内存泄漏及配置持久化保存隐患。
@@ -493,9 +493,8 @@ class DeviceInfo {
493
493
  }
494
494
  }
495
495
 
496
- // 根据协议:TYPE_ON_OFF,每2位表示1路开关,b01=关,b10=开
497
- // 1-4路开关:1字节
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
- return this.devices.get(mac);
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); // 0x02=开, 0x01=关
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,
@@ -496,31 +496,30 @@ module.exports = function(RED) {
496
496
  }
497
497
  }
498
498
 
499
- // 只在非查询状态期间才发送state-changed事件到MQTT
500
- // 查询状态期间的事件只用于更新设备状态,不触发MQTT发布
501
- if (device && !this.isQueryingStates) {
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
- // 检查是否是查询响应(0xB2后)
511
- const isQueryResponse = this.isQueryResponse;
512
- if (isQueryResponse) {
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 (!isQueryResponse) {
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 (isQueryResponse) {
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");