node-red-contrib-symi-mesh 1.9.12 → 1.9.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
@@ -3,8 +3,8 @@
3
3
  一个为Node-RED设计的Symi蓝牙Mesh网关集成包,提供完整的设备控制和Home Assistant MQTT Discovery自动发现功能。
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/node-red-contrib-symi-mesh.svg)](https://www.npmjs.com/package/node-red-contrib-symi-mesh)
6
- [![Node-RED](https://img.shields.io/badge/Node--RED-%3E%3D4.0.0-red)](https://nodered.org)
7
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22.0.0-green)](https://nodejs.org)
6
+ [![Node-RED](https://img.shields.io/badge/Node--RED-%3E%3D3.0.0-red)](https://nodered.org)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.0.0-green)](https://nodejs.org)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
 
10
10
  ## 功能特性
@@ -28,7 +28,6 @@
28
28
  - **查询优化**:严格区分 KNX 读取请求(GroupValue_Read),查询时直接回复缓存状态,不再触发设备控制,彻底解决“查询即关闭”问题
29
29
  - **Mesh 主动查询**:同步时自动下发 `0x32` 指令查询 Mesh 实时状态,确保校准依据准确
30
30
  - **介入日志**:控制台实时输出 `[Mesh->KNX介入]` 日志,清晰展示防死循环与校准过程
31
- - **批量处理优化**:
32
31
  - **KNX-HA集成**:支持KNX与Home Assistant实体直接双向同步
33
32
  - **窗帘同步优化**:专门针对无限位Mesh窗帘模组优化,支持控制锁定(默认3s,可配置)和即时状态同步,彻底解决状态死循环和丢包问题
34
33
  - **可配置锁定时间**:针对慢速窗帘电机,支持在网关节点的“显示全局同步设置”中配置**调光/窗帘锁时间**(500ms-50000ms),默认3000ms;所有桥接/同步节点共享该参数
@@ -38,7 +37,7 @@
38
37
  - **设备去重保证**:所有节点设备列表均采用唯一性保证,确保设备不重复
39
38
  - **配置持久化**:所有映射关系自动保存,重启后自动恢复,确保配置不丢失
40
39
  - **静默重连**:网络错误静默处理,自动重连,避免日志刷屏
41
- - **生产级静默日志**:默认不刷 Node-RED 侧边栏;需要排障时可通过环境变量开启 debug/trace,并带限流保护
40
+ - **生产级日志策略**:默认仅输出低频关键业务日志(用户操作、控制回执、同步介入),高频诊断需环境变量开启,重复警告自动限流
42
41
 
43
42
  ## 节点概览
44
43
 
@@ -48,10 +47,11 @@
48
47
  | :--- | :--- | :--- |
49
48
  | **Symi Gateway** | 配置节点 | 核心连接中心,支持 TCP/IP (4196端口) 或 串口连接,集成 MQTT 代理配置。 |
50
49
  | **Symi Device** | 控制节点 | Mesh 设备操作核心,支持开关、调光、窗帘、三合一温控等全品类控制与状态反馈。 |
51
- | **Symi MQTT** | 桥接节点 | 自动发现 Mesh 设备并发布至 MQTT,支持 Home Assistant 自动发现。 |
50
+ | **Symi MQTT** | 配置节点 | 共享的 MQTT+网关配置(自动发现 Mesh 设备并发布至 MQTT,支持 Home Assistant 自动发现),供 HA Sync / MQTT Sync 等节点复用。 |
52
51
  | **Symi KNX Bridge** | 桥接节点 | KNX 与 Mesh 互联核心,支持状态自动校准、防死循环逻辑及大规模实体映射。 |
53
52
  | **Symi HA Sync** | 同步节点 | 实现 Mesh 设备与 Home Assistant 实体间的双向实时同步,内置窗帘防震荡逻辑。 |
54
53
  | **Symi MQTT Sync** | 同步节点 | 第三方 MQTT 品牌(如花语前湾)与 Symi Mesh 设备的双向同步。 |
54
+ | **Symi MQTT Brand** | 配置节点 | 第三方品牌 MQTT 服务器连接配置(如 HYQW 花语前湾协议),供 MQTT Sync 节点选择。 |
55
55
  | **Symi 485 Bridge** | 桥接节点 | RS485 通信桥接,支持 Modbus 协议透传与自定义指令映射。 |
56
56
  | **Symi 485 Config** | 配置节点 | RS485/TCP 串口服务器连接配置,支持多实例管理。 |
57
57
  | **Symi Cloud Sync** | 同步节点 | 云端设备状态同步,支持远程监控与控制。 |
@@ -118,9 +118,9 @@ node-red-restart
118
118
  - 这样可以避免频繁查询导致的无线丢包问题,提高系统稳定性
119
119
  - **按需主动查询 (v1.9.7)**:仅在执行校准或特定同步逻辑时主动下发查询指令,确保数据对齐
120
120
 
121
- ### 3. 添加MQTT桥接节点
121
+ ### 3. 添加MQTT配置节点
122
122
 
123
- 添加"Symi MQTT"节点到流程中:
123
+ 添加"Symi MQTT"配置节点(配置节点,从"配置节点"列表或依赖节点的配置下拉中创建):
124
124
 
125
125
  **节点配置**:
126
126
  - **网关**: 选择已配置的网关节点(必填)
@@ -130,7 +130,7 @@ node-red-restart
130
130
  - **HA前缀**: homeassistant(默认,可修改)
131
131
  - **名称**: 可选,用于标识节点
132
132
 
133
- > **注意**: MQTT配置在MQTT节点中完成,每个MQTT节点可以配置不同的MQTT服务器。
133
+ > **注意**: MQTT配置在MQTT节点中完成,每个MQTT节点可以配置不同的MQTT服务器;该节点同时是 HA Sync / MQTT Sync 等节点的设备信息来源。
134
134
 
135
135
  ### 4. 部署并验证
136
136
 
@@ -143,16 +143,18 @@ node-red-restart
143
143
  3. 在Home Assistant中查看自动发现的设备
144
144
  4. 测试设备控制和状态反馈
145
145
 
146
+ > **KNX 用户看这里**:如果你要用 KNX 桥接(Mesh 设备与 KNX 总线双向同步),请先执行 `cd ~/.node-red && npm install node-red-contrib-knx-ultimate` 安装 KNX 节点,然后直接阅读下文 **「Symi KNX Bridge 桥接节点」** 章节的完整配置步骤(含实体导入格式与示例流程 08 的导入方法)。
147
+
146
148
  ---
147
149
 
148
- ##### 日志与排障(生产默认静默)
150
+ ##### 日志与排障
149
151
 
150
- 本插件面向酒店/大规模部署,**默认不向 Node-RED 侧边栏刷屏**(包括 `node.error/node.warn/node.log`),并对可选的诊断输出做了**限流**,避免网络抖动/断线重连时产生日志风暴。
152
+ 本插件面向酒店/大规模部署,日志策略为**默认只输出低频关键业务日志**(用户物理操作、控制回执、同步介入、场景事件),高频诊断信息(状态帧解析、映射匹配等)仅在调试模式可见;可能出现重复的警告(如网络噪音导致的解析失败)做了**限流**(默认同 key 60 秒一条),避免日志风暴。
151
153
 
152
154
  #### 默认行为(推荐生产)
153
155
 
154
- - **不设置任何环境变量**:日志保持静默,仅通过节点状态(Status)显示关键状态(如“未配置网关/同步失败”等)。
155
- - **KNX输入日志**:KNX输入日志已调整为 `debug` 级别,生产环境下默认不显示,仅在开启调试模式时可见。
156
+ - **不设置任何环境变量**:仅输出低频关键业务日志,节点状态(Status)显示连接与映射情况。
157
+ - 网络类未捕获异常(ECONNRESET 等)已被全局兜底,不会刷屏或导致 Node-RED 崩溃。
156
158
 
157
159
  #### 需要排障时开启诊断日志
158
160
 
@@ -171,7 +173,7 @@ export SYMI_LOG_INTERVAL_MS=60000
171
173
  node-red
172
174
  ```
173
175
 
174
- > 排障结束后建议清除环境变量并重启 Node-RED,恢复生产静默。
176
+ > 排障结束后建议清除环境变量并重启 Node-RED,恢复默认日志量。
175
177
 
176
178
  ### 5. 示例流程(examples)
177
179
 
@@ -184,8 +186,8 @@ node-red
184
186
  - **05 - RS485 桥接**:`examples/05-symi-rs485-bridge.json`(`symi-485-bridge` + `symi-485-config`)
185
187
  - **06 - RS485 A↔B 同步**:`examples/06-symi-rs485-sync.json`(`symi-rs485-sync` + `symi-485-config`)
186
188
  - **07 - RS485 抓包调试**:`examples/07-rs485-debug.json`(`rs485-debug` + `symi-485-config`)
187
- - **08 - KNX 桥接**:`examples/08-symi-knx-bridge.json`(`symi-knx-bridge`;如需对接 KNX 建议安装 `node-red-contrib-knx-ultimate`)
188
- - **09 - KNX ↔ HA 桥接**:`examples/09-symi-knx-ha-bridge.json`(`symi-knx-ha-bridge`;需安装 HA/KNX 相关节点)
189
+ - **08 - KNX 桥接**:`examples/08-symi-knx-bridge.json`(`symi-knx-bridge`;**导入前必须先安装** `node-red-contrib-knx-ultimate`,流程已内置 knxUltimate 配置与接线,导入后只需修改网关串口/TCP 与 KNX 组地址即可运行,否则 knxUltimate 节点会显示为"未知节点")
190
+ - **09 - KNX ↔ HA 桥接**:`examples/09-symi-knx-ha-bridge.json`(`symi-knx-ha-bridge`;**导入前必须先安装** `node-red-contrib-knx-ultimate` 与 Home Assistant 相关节点)
189
191
  - **10 - 云端同步**:`examples/10-symi-cloud-sync.json`(`symi-cloud-sync`)
190
192
 
191
193
  ### 6. 多网关配置(可选)
@@ -230,7 +232,7 @@ node-red
230
232
  | 插卡取电 | 0x09 | switch + binary_sensor | 插卡检测+开关控制 |
231
233
  | 温控器/三合一 | 0x0A | climate | 温度/模式/风速控制,当前温度采集;空调+新风+地暖三合一控制面板 |
232
234
  | 温湿度传感器 | 0x0B | sensor | 温湿度监测 |
233
- | 情景开关 | 0x0C (12) | scene | 开关+场景功能(1-4路,固定场景个数) |
235
+ | 情景开关 | 0x0C (12) | switch | 开关+场景功能(1-4路,固定场景个数;HA 实体为 switch,场景触发走按键场景通道) |
234
236
  | 五色调光灯 | 0x18 | light | RGB+亮度+色温调节 |
235
237
  | 四输入八输出 | 0x27 (39) | switch | 8路独立控制,支持场景绑定 |
236
238
 
@@ -546,8 +548,8 @@ npm install node-red-contrib-knx-ultimate
546
548
  1. 查询Mesh设备实际状态
547
549
  2. 如果状态不一致,重发命令确保同步
548
550
  3. 如果状态一致,确认成功
549
- 4. 最多重试5次(MAX_RETRY_COUNT),确保“最后一次KNX命令”最终在Mesh侧达成
550
- - **快速连按最终收敛(Last Write Wins)**:在 3 秒 KNX 主控窗口内,若Mesh尾帧/抖动导致最终状态跑偏,会自动纠错补发(限频,最多5次),确保最终一致。
551
+ 4. 最多重试1次(避免场景批量控制时重发过多报文挤爆总线;配合乐观回写与熔断窗口保障最终一致)
552
+ - **快速连按最终收敛(Last Write Wins)**:在 3 秒 KNX 主控窗口内,若Mesh尾帧/抖动导致最终状态跑偏,会自动纠错补发(限频,最多1次),确保最终一致。
551
553
  - **多路开关解析一致性**:`switchState` 采用每路 2-bit 编码解码(`decodeSwitchState2Bit`),避免状态矛盾触发反向控制或误确认。
552
554
  - **Mesh控制KNX**:
553
555
  - 发送命令后,记录待确认的命令(3秒超时)
@@ -581,6 +583,25 @@ Tab分隔,每行一个实体:
581
583
  | floor_heating | 开关, 温度, 当前温度 | 地暖 |
582
584
  | scene | 组地址(cmd), 场景号(triggerValue), Mesh动作(triggerAction) | 场景 |
583
585
 
586
+ **三合一设备(空调+新风+地暖)必须拆成 3 条映射**:
587
+
588
+ 一条 Mesh 三合一设备同时承载空调、新风、地暖三个子系统,在 KNX 桥接节点里必须为它建立 **3 条映射**,每条映射同一个 Mesh 设备、选择不同的 KNX 子实体:
589
+
590
+ | 映射 | 实体类型 | 实体名称 | 命令地址 | 状态/扩展地址 |
591
+ |------|---------|---------|---------|---------------|
592
+ | 空调 | `climate` | e.g. 温控器 | cmdAddr 开关 | statusAddr 温度、ext1 模式、ext2 风速、ext3 当前温度 |
593
+ | 新风 | `fresh_air` | e.g. 新风 | cmdAddr 开关 | statusAddr 风速 |
594
+ | 地暖 | `floor_heating` | e.g. 地暖 | cmdAddr 开关 | statusAddr 温度、ext1 当前温度 |
595
+
596
+ 映射一旦建立,Mesh 设备上报时会自动按设备类型分发给 3 条映射分别同步到对应 KNX 地址,互不干扰。设备发现日志会显示 `找到3个映射`,代表三条子系统均被关联。
597
+
598
+ **三合一 KNX 实体导入示例**(Tab 分隔):
599
+ ```
600
+ 温控器 climate 0/0/11 0/0/12 0/0/13 0/0/14 0/1/14
601
+ 新风 fresh_air 0/0/15 0/0/16
602
+ 地暖 floor_heating 0/0/17 0/0/18 0/1/18
603
+ ```
604
+
584
605
  **触发值与触发动作说明**:
585
606
 
586
607
  - **开关类型(switch)**:
@@ -590,9 +611,10 @@ Tab分隔,每行一个实体:
590
611
  - **场景类型(scene)**:
591
612
  - `triggerValue` 字段表示 **场景号**(如 `1`、`2`),用于和 KNX 侧的场景值对应;
592
613
  - `triggerAction` 表示 **Mesh 侧要执行的动作**,仅在场景类型下出现下拉框,可选:
593
- - `on`:触发场景对应的“开/激活”动作
594
- - `off`:触发场景对应的“关/取消”动作
595
- - `toggle`:在 Mesh 当前状态基础上反转
614
+ - `1`:触发场景对应的“开/激活”动作(对应导入模板动作 1=开)
615
+ - `0`:触发场景对应的“关/取消”动作(对应导入模板动作 0=关)
616
+ - `2`:在 Mesh 当前状态基础上反转(对应导入模板动作 2=翻转)
617
+ - 留空:跟随 KNX 数值(默认)
596
618
 
597
619
  ### Symi RS485 Bridge 桥接节点
598
620
 
@@ -626,7 +648,7 @@ RS485通信桥接,支持Modbus协议透传与自定义指令映射。
626
648
 
627
649
  2. **添加RS485同步节点**:
628
650
  - 从左侧拖入`Symi RS485 Sync`节点
629
- - 选择网关节点(用于获取Mesh设备)
651
+ - 选择网关节点(可选,用于 Mesh 设备参与 A↔B 同步;纯两条 RS485 总线间同步可留空,示例 06 即为无网关的纯总线同步)
630
652
  - 选择RS485配置A和配置B
631
653
 
632
654
  3. **配置映射**:
@@ -681,6 +703,72 @@ RS485通信桥接,支持Modbus协议透传与自定义指令映射。
681
703
 
682
704
  ## 更新日志
683
705
 
706
+ ### v1.9.22 (2026-08-17)
707
+
708
+ #### 三合一设备 KNX 映射修正(配置层)
709
+ - **修复**:三合一设备(空调+新风+地暖)此前只建了空调(climate)一条映射。现正确拆分为 3 条映射(`climate`/`fresh_air`/`floor_heating`),并存指向同一 Mesh 设备,三者自动按上报字段分流同步到各自 KNX 地址。
710
+ - **修复**:4 键零火面板的按键 3/4 原设计为场景键(全开/全关),此前被误复用为普通开关映射导致冲突。已删除冲突的普通开关映射,恢复 2 按键 + 2 场景的正确结构。
711
+ - **验证**(本地融合网关实测):三合一 Mesh 主动上报时日志显示 `找到3个映射`,空调温度/模式→0/0/12、0/0/13,新风开关/风速→0/0/15、0/0/16,地暖开关/温度→0/0/17、0/0/18,全部正确;AutoSync 成功读回各状态地址。9 条映射全部生效。
712
+
713
+ #### 基于实测回报时长的窗口收窄(消除"点击没反应")
714
+ - **问题**:连续操作几次后某次点按无反应,需等 3 秒"锁"结束后才恢复。根因是防回环窗口按最坏情况设 3 秒,但设备执行回报实测仅 100-115ms(KNX→Mesh)、1-10ms(Mesh→KNX),窗口过度导致用户连续操作被误锁。
715
+ - **修改**(数据驱动收窄,`symi-knx-bridge.js`):
716
+ - 单设备 KNX→Mesh 防反写窗口:3s → **800ms**(覆盖回报+尾帧,满足实体 <1 秒响应)
717
+ - Mesh→KNX 命令回环窗口:2s → **600ms**(覆盖 1-10ms 回报)
718
+ - 用户操作宽限期(值反转判定):800ms → **300ms**
719
+ - **效果**:实体控制锁 <1 秒,场景后 0.8 秒内即可正常操作实体,无感知延迟;防回环仍由回报时间窗口 + 值语义 + SyncUtils 防死循环三重保障,不会死循环。
720
+
721
+ ### v1.9.21 (2026-08-17)
722
+
723
+ #### 场景后实体响应优化(核心体验修复)
724
+ - **修复**:场景触发后一段时间内单独控制实体不生效的问题。三处协同优化:
725
+ - **移除全局 KNX 活动窗口拦截**:场景暴风期间每个总线 Write 都会持续刷新该窗口,导致场景结束后数秒内所有 Mesh→KNX 用户操作被误拦。防回环改由"命令回环窗口 + 单设备控制窗口"(均带值语义)精确承担。
726
+ - **命令回环窗口 6s→2s 并引入值语义**:窗口内值相同视为命令回执(拦),值反转且超过 800ms 宽限期视为用户快速反操作(放行)。场景把灯全开后,用户 0.8 秒起即可反向单独关灯,立即生效。
727
+ - **场景遮蔽(SceneVeil)默认 3000→2000ms**:Mesh 设备执行回报通常 1 秒内到齐,2 秒足够总线平息,契合"场景 1-2 秒可接受"的标准体验(节点属性 500-30000ms 可调)。
728
+ - **新增**:命令队列实时优先——场景批量命令积压(串口 50ms/帧逐一排空)时,用户单独操作的实体命令自动插队到队首,下一个调度周期立即执行,保证实体控制 <1 秒响应;同一设备的命令仍按 Last-Write-Wins 原地合并,不重复发送。
729
+
730
+ #### 处理模型说明(与 KNX 网关标准对齐)
731
+ - 总线涌入大量组地址时:仅匹配已映射的有效地址,未映射地址零开销忽略;有效命令进入串行队列按 Mesh 硬件节拍(50ms/帧)逐一处理。
732
+ - 同一开关设备的多个通道自动合并为一行协议发送(100ms 批量窗口);其他实体一行协议对应一个实体状态。
733
+
734
+ ### v1.9.20 (2026-08-17)
735
+
736
+ #### 同步链路关键修复
737
+ - **修复**:KNX 执行器从状态地址(statusAddr)回报状态时,被误判为"KNX 总线控制活动"刷新了全局 3 秒活动窗口,导致 Mesh→KNX 写入后用户 3 秒内的 Mesh 物理操作被静默拦截(表现为"开灯后再按关灯无反应")。现在只有控制地址(cmdAddr)的 GroupValue_Write 才刷新全局窗口。
738
+ - **修复**:三处防回环拦截点(全局 KNX 活动窗口、单设备控制窗口)原先静默跳过且无任何日志,现恢复 debug 级别诊断输出,排障时可开启 SYMI_LOG_LEVEL=debug 查看,生产环境默认静默。
739
+
740
+ #### 代码审查修复(基于 npm v1.9.12 全量比对审查)
741
+ - **修复**:KNX 桥接节点移除运行时统计面板(RuntimeStats),避免其每 5 秒覆盖"未配置网关/请添加映射/网关断开"等关键状态提示;网关节点保留该统计(显示连接状态、收发计数、运行时长)。
742
+ - **修复**:状态事件解析失败警告增加限流(同 key 默认 60 秒一条),避免 Mesh 网络噪音刷屏警告。
743
+ - **修复**:KNX 桥接"场景映射匹配"诊断日志降为 debug 级别,避免高频刷屏。
744
+ - **合规**:移除 package.json 中虚报覆盖率的占位测试脚本,改为诚实的测试占位。
745
+ - **清理**:删除临时调试脚本、npm 冗余版本副本、重复/一次性文档,项目根目录仅保留发布必要文件。
746
+
747
+ ### v1.9.13 - v1.9.19 (2026-08) KNX/Mesh 同步链路可靠性迭代
748
+
749
+ > **注**:v1.9.13 ~ v1.9.21 未在 npm 独立发布,以下改动随 **v1.9.22** 一并发布。
750
+
751
+ #### 事件链路与同步可靠性(核心)
752
+ - **单一事件出口(v1.9.15)**:移除网关节点对设备状态事件的双重转发,统一由状态事件队列单一出口分发(带精准 isUserControl/isFromStateQuery 标记),彻底解决下游同步节点双重收到事件导致的 Mesh→KNX 状态增量丢失。
753
+ - **命令回环窗口(v1.9.17)**:部分设备固件不在状态帧携带控制源标记,仅凭 isUserControl 会永久阻断 Mesh→KNX 同步。改用"命令回环窗口"识别:窗口内视为命令反馈回环跳过反写,窗口外视为本地按键/App 操作放行同步。
754
+ - **用户操作豁免(v1.9.17)**:补齐两处 isUserControl=true 豁免实现,用户物理操作不受 KNX 控制窗口阻挡,与注释声明保持一致。
755
+ - **状态反馈地址简化(v1.9.17)**:KNX 执行器状态回执仅更新缓存,不再触发立即校准,避免回执先于 Mesh 设备执行到位导致的同一命令重复下发。
756
+
757
+ #### 场景同步规范化(v1.9.18)
758
+ - 场景控制 DPT 由 17.001 修正为 **18.001(Scene Control)**,与 KNX 标准及融合网关场景绑定完全对齐。
759
+ - 修复多个场景共享同一 KNX 组地址时的映射匹配:遍历该组地址下全部场景映射,按触发值精确命中。
760
+ - 场景遮蔽窗口(SceneVeil)默认 2000ms,开放节点属性配置(500-30000ms),阻止 KNX 场景触发后的总线回环。
761
+
762
+ #### 节拍与总线保护(v1.9.19)
763
+ - **KNX→Mesh 方向去除叠加节拍**:串口客户端已内置 50ms/帧串行队列,桥接队列不再额外 sleep,快速操作/场景批量数据不积压。
764
+ - **Mesh→KNX 方向保留可配置限速**:防止场景瞬时挤爆 KNX 总线。
765
+ - **重试次数 5→1**:反馈确认超时最多补发 1 次,避免场景批量控制时重发报文挤爆总线。
766
+
767
+ #### 可观测性(v1.9.15 - v1.9.16)
768
+ - 网关节点新增运行时统计(连接状态、收发计数、错误计数、重连次数、运行时长)。
769
+ - 用户物理操作输出 `[状态上报] 用户操作` 可见日志,控制回执/状态上报/场景事件帧输出 `[设备帧]` 日志,便于现场确认同步链路。
770
+ - 示例流程 08/09 内置 knxUltimate 配置与接线,导入后仅需改两处地址即可完成 Mesh↔KNX 双向同步。
771
+
684
772
  ### v1.9.12 (2026-04-10)
685
773
 
686
774
  #### 安装兼容性修复(必须升级)
@@ -702,7 +790,7 @@ RS485通信桥接,支持Modbus协议透传与自定义指令映射。
702
790
  ### v1.9.0 - v1.9.10 历史迭代汇总
703
791
 
704
792
  - **v1.9.10 (2026-03-30)**:发布包安装可靠性(必须)——修复 `package.json / package-lock.json` 中误将自身 `node-red-contrib-symi-mesh-1.9.9.tgz` 写成 `file:` 本地依赖的问题;删除该错误引用后,`npm install node-red-contrib-symi-mesh@1.9.10` 会走正常 registry 安装流程,避免客户侧 `tarball data ... corrupted` / `ENOENT ... .tgz` 安装失败。
705
- - **v1.9.9 (2026-03-28,2026-03-30 行为补充)**:KNX 桥同步可靠性(推荐所有 KNX 项目升级)——Mesh 写 KNX 后的对称主控、KNX 面板优先、状态反馈去重、场景遮蔽(SceneVeil)、默认关闭 LWW;同时补上 DelayedSync 与 Mesh 查询回包(pendingMeshQueries / 650ms 早清 / 900ms 抑制)、校准下发仅依赖 `gateway.sendControl`、MQTT connack timeout 防崩、以及“自动同步状态”勾选与运行一致(`autoSyncEnabled` 值的健壮解析)。另:现场日志说明汇总见 `docs/日志.md`。
793
+ - **v1.9.9 (2026-03-28,2026-03-30 行为补充)**:KNX 桥同步可靠性(推荐所有 KNX 项目升级)——Mesh 写 KNX 后的对称主控、KNX 面板优先、状态反馈去重、场景遮蔽(SceneVeil)、默认关闭 LWW;同时补上 DelayedSync 与 Mesh 查询回包(pendingMeshQueries / 650ms 早清 / 900ms 抑制)、校准下发仅依赖 `gateway.sendControl`、MQTT connack timeout 防崩、以及“自动同步状态”勾选与运行一致(`autoSyncEnabled` 值的健壮解析)。
706
794
  - **v1.9.8 (2026-03-23)**:稳定性、协议兼容性与合规性增强——引擎升级(Node.js >= 22.x、NPM >= 10.x、Node-RED >= 4.x)、高并发与抗丢包测试策略、DelayedSync 增强与防死循环闭环、双向防死循环击穿修复(本地物理按键 `isUserControl=true` 时强制跳过锁定窗口)、以及安全与代码规范(修复依赖 CVE、eslint 通过)。
707
795
  - **v1.9.7 (2026-02-28)**:自动状态校准(AutoSync)与状态查询逻辑——KNX Bridge 自动同步状态、查询不改变总线状态、GroupValue_Read/Response/Write 严格区分、DelayedSync 读响应不误触 HA、Mesh 设备查询与 MAC 兼容、误关灯防护,以及界面与体验(布局优化、日志分级)。
708
796
  - **v1.9.0 - v1.9.6 历史迭代汇总(核心修复与优化)**:防反向控制与回显保护(3 秒主控窗口、回显检测窗口等)、双向批量处理与网关限流、窗帘/调光同步增强与主控来源区分、反馈确认闭环、对多协议联动的映射保存/显示修复、首次启动状态快照与设备发现增强、虚拟化与场景支持(虚拟场景实体、按键场景触发 0x34),以及工程化改进(静默日志与配置/内存泄漏隐患修复等)。
@@ -2,20 +2,71 @@
2
2
  {
3
3
  "id": "tab_knx_bridge",
4
4
  "type": "tab",
5
- "label": "08 - Symi KNX Bridge (无外部依赖)",
5
+ "label": "08 - Symi KNX Bridge",
6
6
  "disabled": false,
7
- "info": "示例目标:导入后即可看到 symi-knx-bridge 节点结构,且不依赖 knxUltimate 节点(避免缺失类型导致导入报错)。\n\n使用时建议安装:node-red-contrib-knx-ultimate,然后按 README 的连线方式接入 knxUltimate-in/out。\n\n导入后必改:\n1) cfg_symi_gateway_knx.host 改为你的网关IP\n2) 打开 KNX桥接 节点,导入 KNX 实体并添加映射\n"
7
+ "info": "示例目标:导入后即可完成 Mesh KNX 的双向同步,仅需修改两处网关地址。\n\n【导入前必读】本示例内置了 knxUltimate 节点(node-red-contrib-knx-ultimate)。\n请先安装该节点:Node-RED 菜单 节点管理 → 搜索 knx-ultimate 安装,否则导入会提示缺失节点类型。\n\n导入后必改(仅两处):\n1) cfg_symi_gateway_knx.host 改为你的 Mesh 网关 IP\n2) 打开 knxUltimate 配置节点,把 host 改为你的 KNX IP 网关地址(默认 1.2.3.4,端口 3671)\n\n然后打开 KNX桥接 节点:\n- 点击\"下载模板\"获取实体格式,或\"添加\"一条条录入 KNX 实体\n- 点击\"添加映射\",把 Mesh 设备与 KNX 实体一一对应\n"
8
8
  },
9
9
  {
10
10
  "id": "c_knx_bridge_intro",
11
11
  "type": "comment",
12
12
  "z": "tab_knx_bridge",
13
- "name": "本示例不包含 knxUltimate 节点,保证导入不报缺失类型",
14
- "info": "推荐接线(安装 knxUltimate 后):\n[knxUltimate-in] → [KNX桥接] → [knxUltimate-out]\n\nKNX桥接输出1:发往 KNX\nKNX桥接输出2:调试信息(可接 debug",
13
+ "name": "双向同步:knxUltimate ↔ KNX桥接 ↔ Mesh网关",
14
+ "info": "接线已内置:\n[knxUltimate(Universal)] → [KNX桥接] → [knxUltimate(Universal)]\n\nKNX桥接输出1:KNX 写入/读取报文(回传给 knxUltimate)\nKNX桥接输出2:调试信息(可接 debug)\n\n防死循环:KNX桥接内置读请求拦截、状态地址精确同步、场景遮蔽窗口,Mesh↔KNX 双向同步不会产生回环。",
15
15
  "x": 360,
16
16
  "y": 80,
17
17
  "wires": []
18
18
  },
19
+ {
20
+ "id": "cfg_knx_ultimate",
21
+ "type": "knxUltimate-config",
22
+ "z": "",
23
+ "name": "KNX IP 网关(改这里)",
24
+ "host": "1.2.3.4",
25
+ "port": "3671",
26
+ "physAddr": "15.15.201",
27
+ "suppressACKRequest": false,
28
+ "csv": "",
29
+ "KNXEthInterface": "Auto",
30
+ "KNXEthInterfaceManuallyInput": "",
31
+ "autoReconnect": "yes"
32
+ },
33
+ {
34
+ "id": "knx_ultimate_io",
35
+ "type": "knxUltimate",
36
+ "z": "tab_knx_bridge",
37
+ "server": "cfg_knx_ultimate",
38
+ "topic": "0/0/0",
39
+ "setTopicType": "str",
40
+ "outputtopic": "",
41
+ "dpt": "",
42
+ "initialread": 0,
43
+ "notifyreadrequest": true,
44
+ "notifyresponse": true,
45
+ "notifywrite": true,
46
+ "notifyreadrequestalsorespondtobus": false,
47
+ "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "0",
48
+ "name": "knxUltimate 双向",
49
+ "outputtype": "write",
50
+ "outputRBE": "true",
51
+ "inputRBE": "false",
52
+ "formatmultiplyvalue": 1,
53
+ "formatnegativevalue": "leave",
54
+ "formatdecimalsvalue": 999,
55
+ "passthrough": "no",
56
+ "sendMsgToKNXCode": "",
57
+ "receiveMsgFromKNXCode": "",
58
+ "listenallga": true,
59
+ "gaSecure": false,
60
+ "buttonEnabled": true,
61
+ "buttonMode": "toggle",
62
+ "buttonStaticValue": "",
63
+ "buttonToggleInitial": "false",
64
+ "periodicSend": false,
65
+ "periodicSendInterval": 60,
66
+ "x": 120,
67
+ "y": 170,
68
+ "wires": [["n_knx_bridge"]]
69
+ },
19
70
  {
20
71
  "id": "n_knx_bridge",
21
72
  "type": "symi-knx-bridge",
@@ -24,28 +75,14 @@
24
75
  "gateway": "cfg_symi_gateway_knx",
25
76
  "mappings": "[]",
26
77
  "knxEntities": "[]",
27
- "autoSync": true,
28
- "syncDelay": 3000,
29
- "x": 250,
78
+ "echoWindowEnabled": false,
79
+ "echoWindow": 500,
80
+ "autoSyncEnabled": false,
81
+ "autoSyncDelay": 3,
82
+ "sceneVeilMs": 3000,
83
+ "x": 300,
30
84
  "y": 170,
31
- "wires": [["dbg_knx_out"], ["dbg_knx_info"]]
32
- },
33
- {
34
- "id": "dbg_knx_out",
35
- "type": "debug",
36
- "z": "tab_knx_bridge",
37
- "name": "输出1(KNX写入报文)",
38
- "active": true,
39
- "tosidebar": true,
40
- "console": false,
41
- "tostatus": false,
42
- "complete": "payload",
43
- "targetType": "msg",
44
- "statusVal": "",
45
- "statusType": "auto",
46
- "x": 510,
47
- "y": 150,
48
- "wires": []
85
+ "wires": [["knx_ultimate_io"], ["dbg_knx_info"]]
49
86
  },
50
87
  {
51
88
  "id": "dbg_knx_info",
@@ -67,7 +104,7 @@
67
104
  {
68
105
  "id": "cfg_symi_gateway_knx",
69
106
  "type": "symi-gateway",
70
- "name": "Symi网关(改IP)",
107
+ "name": "Symi Mesh网关(改IP)",
71
108
  "connectionType": "tcp",
72
109
  "host": "192.168.2.110",
73
110
  "port": 4196,
@@ -82,5 +119,4 @@
82
119
  "lockTime": 800,
83
120
  "dimmerLockTime": 3000
84
121
  }
85
- ]
86
-
122
+ ]
@@ -2,20 +2,71 @@
2
2
  {
3
3
  "id": "tab_knx_ha_bridge",
4
4
  "type": "tab",
5
- "label": "09 - Symi KNX-HA Bridge (无外部依赖)",
5
+ "label": "09 - Symi KNX-HA Bridge",
6
6
  "disabled": false,
7
- "info": "示例目标:导入 symi-knx-ha-bridge 节点结构,且不包含外部 HA/KNX 节点,避免导入缺失类型。\n\n使用时你需要:\n- 安装 node-red-contrib-home-assistant-websocket,配置 HA Server,然后在节点里选择 haServer\n- (可选)安装 node-red-contrib-knx-ultimate 作为 KNX in/out\n\n推荐接线:\n[knxUltimate-in] [KNX-HA桥接] [knxUltimate-out]\n[server-state-changed events: all(state_changed)] [KNX-HA桥接]"
7
+ "info": "示例目标:导入后即可完成 KNX Home Assistant 的双向同步。\n\n【导入前必读】本示例内置了 knxUltimate 节点(node-red-contrib-knx-ultimate)。\n请先安装该节点:Node-RED 菜单 节点管理 → 搜索 knx-ultimate 安装,否则导入会提示缺失节点类型。\n同时需要安装 node-red-contrib-home-assistant-websocket 并配置 HA Server。\n\n导入后必改:\n1) 打开 knxUltimate 配置节点,把 host 改为你的 KNX IP 网关地址(默认 1.2.3.4,端口 3671)\n2) 打开 KNX-HA桥接 节点,选择 haServer\n3) 添加 KNX 实体并建立映射到 HA entity_id\n4) 部署后观察输出1/2(可接 debug)"
8
8
  },
9
9
  {
10
10
  "id": "c_knx_ha_bridge_intro",
11
11
  "type": "comment",
12
12
  "z": "tab_knx_ha_bridge",
13
- "name": "本示例不包含 HA/KNX 外部节点,保证导入不报缺失类型",
14
- "info": "导入后:\n1) 打开 KNX-HA桥接 节点,选择 haServer\n2) 添加 KNX 实体并建立映射到 HA entity_id\n3) 部署后观察输出1/2(可接 debug)",
13
+ "name": "双向同步:knxUltimate ↔ KNX-HA桥接 ↔ HA Server",
14
+ "info": "接线已内置:\n[knxUltimate(Universal)] [KNX-HA桥接] [knxUltimate(Universal)]\n\nKNX-HA桥接输出1:KNX 写入报文(回传给 knxUltimate)\nKNX-HA桥接输出2:调试信息(可接 debug)\n\n防死循环:KNX-HA桥接只处理 GroupValue_Write,忽略 Read/Response,并内置锁与去重,双向同步不会回环。",
15
15
  "x": 380,
16
16
  "y": 80,
17
17
  "wires": []
18
18
  },
19
+ {
20
+ "id": "cfg_knx_ultimate_ha",
21
+ "type": "knxUltimate-config",
22
+ "z": "",
23
+ "name": "KNX IP 网关(改这里)",
24
+ "host": "1.2.3.4",
25
+ "port": "3671",
26
+ "physAddr": "15.15.201",
27
+ "suppressACKRequest": false,
28
+ "csv": "",
29
+ "KNXEthInterface": "Auto",
30
+ "KNXEthInterfaceManuallyInput": "",
31
+ "autoReconnect": "yes"
32
+ },
33
+ {
34
+ "id": "knx_ultimate_ha_io",
35
+ "type": "knxUltimate",
36
+ "z": "tab_knx_ha_bridge",
37
+ "server": "cfg_knx_ultimate_ha",
38
+ "topic": "0/0/0",
39
+ "setTopicType": "str",
40
+ "outputtopic": "",
41
+ "dpt": "",
42
+ "initialread": 0,
43
+ "notifyreadrequest": true,
44
+ "notifyresponse": true,
45
+ "notifywrite": true,
46
+ "notifyreadrequestalsorespondtobus": false,
47
+ "notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized": "0",
48
+ "name": "knxUltimate 双向",
49
+ "outputtype": "write",
50
+ "outputRBE": "true",
51
+ "inputRBE": "false",
52
+ "formatmultiplyvalue": 1,
53
+ "formatnegativevalue": "leave",
54
+ "formatdecimalsvalue": 999,
55
+ "passthrough": "no",
56
+ "sendMsgToKNXCode": "",
57
+ "receiveMsgFromKNXCode": "",
58
+ "listenallga": true,
59
+ "gaSecure": false,
60
+ "buttonEnabled": true,
61
+ "buttonMode": "toggle",
62
+ "buttonStaticValue": "",
63
+ "buttonToggleInitial": "false",
64
+ "periodicSend": false,
65
+ "periodicSendInterval": 60,
66
+ "x": 120,
67
+ "y": 170,
68
+ "wires": [["n_knx_ha_bridge"]]
69
+ },
19
70
  {
20
71
  "id": "n_knx_ha_bridge",
21
72
  "type": "symi-knx-ha-bridge",
@@ -24,26 +75,9 @@
24
75
  "haServer": "",
25
76
  "mappings": "[]",
26
77
  "knxEntities": "[]",
27
- "x": 260,
78
+ "x": 300,
28
79
  "y": 170,
29
- "wires": [["dbg_knx_ha_out"], ["dbg_knx_ha_info"]]
30
- },
31
- {
32
- "id": "dbg_knx_ha_out",
33
- "type": "debug",
34
- "z": "tab_knx_ha_bridge",
35
- "name": "输出1(KNX写入报文)",
36
- "active": true,
37
- "tosidebar": true,
38
- "console": false,
39
- "tostatus": false,
40
- "complete": "payload",
41
- "targetType": "msg",
42
- "statusVal": "",
43
- "statusType": "auto",
44
- "x": 520,
45
- "y": 150,
46
- "wires": []
80
+ "wires": [["knx_ultimate_ha_io"], ["dbg_knx_ha_info"]]
47
81
  },
48
82
  {
49
83
  "id": "dbg_knx_ha_info",
@@ -58,9 +92,8 @@
58
92
  "targetType": "msg",
59
93
  "statusVal": "",
60
94
  "statusType": "auto",
61
- "x": 510,
95
+ "x": 500,
62
96
  "y": 210,
63
97
  "wires": []
64
98
  }
65
- ]
66
-
99
+ ]
@@ -728,8 +728,12 @@ class DeviceManager extends EventEmitter {
728
728
  if (this.threeInOneStoragePath && fs.existsSync(this.threeInOneStoragePath)) {
729
729
  const data = fs.readFileSync(this.threeInOneStoragePath, "utf8");
730
730
  const parsed = JSON.parse(data);
731
- this.logger.log(`[DeviceManager] 从文件加载了三合一设备: ${JSON.stringify(parsed)}`);
732
- return parsed;
731
+ // 结构校验:必须是对象(且非数组),损坏时回退空对象并告警
732
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
733
+ this.logger.log(`[DeviceManager] 从文件加载了三合一设备: ${JSON.stringify(parsed)}`);
734
+ return parsed;
735
+ }
736
+ this.logger.error("[DeviceManager] 三合一设备文件结构异常(非对象),已忽略并回退空对象");
733
737
  } else {
734
738
  this.logger.log("[DeviceManager] 三合一设备文件不存在,返回空对象");
735
739
  }
@@ -739,20 +743,26 @@ class DeviceManager extends EventEmitter {
739
743
  return {};
740
744
  }
741
745
 
742
- // 保存三合一设备列表到文件
746
+ // 保存三合一设备列表到文件(原子写:临时文件 + rename,避免断电/崩溃留下截断 JSON)
743
747
  saveThreeInOneDevices() {
744
748
  try {
745
749
  if (this.threeInOneStoragePath) {
746
750
  const content = JSON.stringify(this.threeInOneDevices, null, 2);
747
751
  this.logger.log(`[DeviceManager] 正在保存三合一设备到: ${this.threeInOneStoragePath}`);
748
752
  this.logger.log(`[DeviceManager] 保存内容: ${content}`);
749
- fs.writeFileSync(this.threeInOneStoragePath, content);
753
+ const tmpPath = `${this.threeInOneStoragePath}.tmp`;
754
+ fs.writeFileSync(tmpPath, content);
755
+ fs.renameSync(tmpPath, this.threeInOneStoragePath);
750
756
  this.logger.log(`[DeviceManager] 已保存 ${Object.keys(this.threeInOneDevices).length} 个三合一设备记录到文件`);
751
757
  } else {
752
758
  this.logger.error("[DeviceManager] 存储路径为空,无法保存");
753
759
  }
754
760
  } catch (e) {
755
761
  this.logger.error(`[DeviceManager] 保存三合一设备文件失败: ${e.message}`);
762
+ // 清理残留临时文件
763
+ try {
764
+ if (this.threeInOneStoragePath) fs.unlinkSync(`${this.threeInOneStoragePath}.tmp`);
765
+ } catch (e2) { /* 忽略 */ }
756
766
  }
757
767
  }
758
768
 
package/lib/protocol.js CHANGED
@@ -96,8 +96,8 @@ class ProtocolHandler {
96
96
 
97
97
  stateValue = (stateValue & mask) | newBits;
98
98
  return stateValue;
99
- } else if (channels === 6 || channels === 8) {
100
- // 6-8路开关,2字节状态,小端序
99
+ } else if (channels >= 5) {
100
+ // 5-8路开关,2字节状态,小端序
101
101
  // 协议说明:低2位开始表示第一路开关(与1-4路一致)
102
102
  // 第1路=bits0-1, 第2路=bits2-3, ..., 第8路=bits14-15
103
103
  const defaultState = 0x5555; // 全关状态