node-red-contrib-symi-mesh 1.8.12 → 1.8.13
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 +25 -140
- package/lib/tcp-client.js +4 -3
- package/nodes/symi-gateway.js +1 -0
- package/nodes/symi-knx-bridge.js +94 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -704,25 +704,19 @@ node-red-contrib-symi-mesh/
|
|
|
704
704
|
|
|
705
705
|
## 更新日志
|
|
706
706
|
|
|
707
|
-
### v1.8.
|
|
708
|
-
- **KNX
|
|
709
|
-
-
|
|
710
|
-
-
|
|
711
|
-
-
|
|
712
|
-
- **字段扩展**:在经典界面中无缝集成了场景配置所需的“场景号”和“动作”字段。
|
|
713
|
-
- **配置窗口优化**:保留了配置窗口的尺寸自动扩展功能,提供更宽敞的编辑视野。
|
|
714
|
-
|
|
715
|
-
### v1.8.11 (2026-01-16)
|
|
707
|
+
### v1.8.13 (2026-01-18)
|
|
708
|
+
- **KNX 并发控制逻辑修复(关键更新)**:
|
|
709
|
+
- **禁止状态地址反向触发**:彻底修复了 KNX 总线在场景执行或多设备并发响应时,Mesh 设备出现“相反操作”或错误控制的问题。现在系统严格区分控制地址(Cmd)和状态地址(Status),严禁状态反馈报文触发对 Mesh 设备的控制指令。
|
|
710
|
+
- **Buffer 类型数据解析增强**:修复了当 KNX 节点传递 Buffer 类型数据(如 `<Buffer 01>`)时,系统错误解析为 `false` (关) 导致的操作反转问题。现在能正确识别 Buffer 格式的开关量。
|
|
711
|
+
- **并发稳定性**:优化了输入处理逻辑,在高并发场景下(如 KNX 场景同时触发几十个设备)能稳定运行,消除了因状态反馈震荡导致的逻辑错误。
|
|
716
712
|
|
|
717
|
-
|
|
718
|
-
- **僵尸节点彻底清除**:修复了在删除或修改 KNX 场景映射并重新部署后,旧的配置逻辑仍在后台运行的问题。
|
|
719
|
-
- **原因分析**:旧节点实例销毁时,未能完全解绑网关事件监听器,导致“僵尸节点”继续响应事件。
|
|
720
|
-
- **解决方案**:引入 `node.isClosed` 标志位,强制拦截销毁后的所有逻辑执行;同时修复了网关连接状态监听器的内存泄漏问题。
|
|
721
|
-
- **内存泄漏修复**:将所有匿名事件监听器改为具名函数,确保在节点关闭时能被正确移除,防止多次部署后的内存累积。
|
|
722
|
-
- **空指针异常防护**:在 `symi-gateway` 中增加了多处连接状态检查,防止网关断开后后台任务访问已销毁的客户端对象,彻底消除 `Cannot read properties of null (reading 'sendFrame')` 报错刷屏。
|
|
723
|
-
- **UI 显示优化**:修复了 KNX 开关类型在配置列表中错误显示“扩展”列数据的问题,现在开关类型的扩展列将正确显示为“-”。
|
|
713
|
+
### v1.8.10+ (2026-01-18)
|
|
724
714
|
|
|
725
|
-
|
|
715
|
+
**KNX 并发控制逻辑修复(关键更新)**:
|
|
716
|
+
- **禁止状态地址反向触发**:彻底修复了 KNX 总线在场景执行或多设备并发响应时,Mesh 设备出现“相反操作”或错误控制的问题。现在系统严格区分控制地址(Cmd)和状态地址(Status),严禁状态反馈报文触发对 Mesh 设备的控制指令。
|
|
717
|
+
- **Buffer 类型数据解析增强**:修复了当 KNX 节点传递 Buffer 类型数据(如 `<Buffer 01>`)时,系统错误解析为 `false` (关) 导致的操作反转问题。现在能正确识别 Buffer 格式的开关量。
|
|
718
|
+
- **并发稳定性**:优化了输入处理逻辑,在高并发场景下(如 KNX 场景同时触发几十个设备)能稳定运行,消除了因状态反馈震荡导致的逻辑错误。
|
|
719
|
+
- **日志优化**:优化了 TCP 客户端和网关初始化逻辑,对连接超时和离线错误进行节流处理,避免在断网情况下日志刷屏。
|
|
726
720
|
|
|
727
721
|
**新增功能**:
|
|
728
722
|
- **KNX 场景联动支持**:新增“场景”设备类型,支持 Mesh 开关按键与 KNX 场景的双向联动。
|
|
@@ -738,127 +732,18 @@ node-red-contrib-symi-mesh/
|
|
|
738
732
|
- **逻辑说明**:
|
|
739
733
|
- **KNX -> Mesh**:收到 KNX 场景号 -> Mesh 开关执行指定状态(如设为 0,则执行关)。
|
|
740
734
|
- **Mesh -> KNX**:Mesh 开关变为指定状态(如变为关) -> 发送 KNX 场景号。
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
-
|
|
749
|
-
-
|
|
750
|
-
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
- **修复场景**:KNX 控制打开(ON)后,立即手动关闭 Mesh 设备(OFF),旧版本会因死循环锁未过期而丢弃 OFF 状态上报。
|
|
754
|
-
- **新逻辑**:KNX 发送 ON 指令只锁定 Mesh->KNX 的 ON 上报,不影响 Mesh->KNX 的 OFF 上报,确保快速连续反向操作能 100% 同步。
|
|
755
|
-
- **状态同步零延迟**:移除不必要的通用防抖检查,确保 Mesh 端的状态变化能毫秒级同步到 KNX 总线。
|
|
756
|
-
|
|
757
|
-
### v1.8.8 (2026-01-14)
|
|
758
|
-
|
|
759
|
-
**KNX 双向同步深度修复与协议鲁棒性增强**:
|
|
760
|
-
- **分包与粘包彻底解决**:重构 `ProtocolHandler` 缓存机制,支持 **8+1 分包、多包连续粘包**等极端情况下的完整解析。通过 Buffer 累加与帧头部实时扫描技术,确保无论串口数据如何切分,都能还原为完整的协议帧。
|
|
761
|
-
- **KNX 双向同步死循环修复**:
|
|
762
|
-
- **MAC 地址标准化**:统一所有节点(配置、事件、缓存)的 MAC 地址为小写且无冒号格式,彻底解决由于大小写不一致导致的“找不到映射”或“防死循环误杀”问题。
|
|
763
|
-
- **设备级回显消除**:引入 `allKnxAddrs` 关联检查,KNX 发出指令后,自动屏蔽该设备下所有关联地址(命令/状态/位置)的短时回显,防止自发自收导致的同步环路。
|
|
764
|
-
- **4 键开关通道精准匹配**:修复多路开关状态上报时,`switch_1` 到 `switch_4` 的动态订阅逻辑,确保 Mesh 端的每一路状态都能精准同步到对应的 KNX 组地址。
|
|
765
|
-
- **生产环境日志优化**:所有原始报文降级为 `debug` 级别,仅保留关键的同步逻辑日志为 `log` 级别,确保在长时间运行下不占用额外硬盘 IO,防止 Node-RED 变慢。
|
|
766
|
-
- **三合一面板持久化增强**:完善 `symi-mesh-data` 目录下的 JSON 持久化逻辑,确保三合一设备类型在 Node-RED 重启后能立即恢复,无需重新探测。
|
|
767
|
-
|
|
768
|
-
### v1.8.7 (2026-01-08)
|
|
769
|
-
|
|
770
|
-
**生产环境日志优化与稳定性增强**:
|
|
771
|
-
- **错误日志节流 (Throttling)**:在网关连接和 RS485 配置中引入 60 秒节流机制,同类网络错误(如 `ECONNREFUSED`)每分钟仅记录一次,彻底解决离线时的日志刷屏问题。
|
|
772
|
-
- **日志级别降级**:将所有节点的 `node.error` 和 `node.warn` 统一降级为 `node.log` (Info 级别),保持 Node-RED 控制台整洁,仅在调试模式下显示详细信息。
|
|
773
|
-
- **TCP 客户端优化**:在 `tcp-client` 库级别拦截常见的网络波动报错,提升系统在高频重连场景下的静默稳定性。
|
|
774
|
-
- **全量节点适配**:完成 MQTT、HA同步、云端同步、RS485、KNX 等所有功能节点的日志规范化清理。
|
|
775
|
-
|
|
776
|
-
### v1.8.6 (2026-01-07)
|
|
777
|
-
|
|
778
|
-
**核心修复与同步增强**:
|
|
779
|
-
- **网关初始化修复**:修复 `symi-gateway` 节点在初始化查询设备状态时的语法错误 (SyntaxError),确保节点能正常加载。
|
|
780
|
-
- **三合一属性路由优化**:优化 `symi-ha-sync` 路由逻辑,支持将 0x02 属性(开关)正确重定向至三合一子实体处理器,解决三合一面板状态同步失效问题。
|
|
781
|
-
- **HA Climate 协议匹配**:修复 HA `climate` 实体协议不匹配问题,将空调/地暖开关操作映射为 `hvac_mode` (heat/cool/off),解决同步时的 500 错误。
|
|
782
|
-
- **风速同步双向修复**:
|
|
783
|
-
- **中英文映射支持**:在 `HA_TO_FAN_MODE` 中增加中文风速(高风/中风/低风/自动)映射,解决 HA 界面操作时的“未知风速值”警告。
|
|
784
|
-
- **Symi -> HA 值转换**:同步至 HA 时自动将英文风速转换为中文,匹配中文版 HA 实体要求,彻底解决同步时的 500 错误。
|
|
785
|
-
- **详细日志追踪**:增加 `[Symi->HA] 发送 HA 请求` 详细日志,包含完整的服务调用路径和 Payload。
|
|
786
|
-
|
|
787
|
-
**三合一设备持久化优化**:
|
|
788
|
-
- **文件持久化存储**:三合一设备类型信息保存到 `~/.node-red/symi-mesh-data/` 目录,实现永久记忆。
|
|
789
|
-
- **检测逻辑优化**:延长检测等待时间至 10 秒(分 20 次检查),解决因网络延迟导致的识别失败。
|
|
790
|
-
- **时序修复**:提前保存 `needsThreeInOneCheck` 状态,确保在响应到达时能正确触发持久化流程。
|
|
791
|
-
|
|
792
|
-
**Mesh -> HA 同步增强**:
|
|
793
|
-
- **状态缓存细粒度化**:采用 `StateCache` 实现 `{mac}_{subEntity}_{property}` 级别的精确对比,只同步真正变化的属性。
|
|
794
|
-
- **调试日志增强**:三合一相关事件统一使用 `log` 级别输出,便于快速排查同步链路问题。
|
|
795
|
-
|
|
796
|
-
**设备管理器功能增强**:
|
|
797
|
-
- 新增 `markAsThreeInOne` / `markAsThermostat` 等持久化管理方法,提升系统重启后的设备恢复速度。
|
|
798
|
-
|
|
799
|
-
### v1.8.5 (2026-01-06)
|
|
800
|
-
|
|
801
|
-
**通用同步工具类重构**:
|
|
802
|
-
- 新增 `lib/sync-utils.js` 通用同步工具模块
|
|
803
|
-
- 统一 SyncUtils 类:提供防环路逻辑,支持不同设备类型的超时配置
|
|
804
|
-
- 统一 StateCache 类:状态缓存对比,避免重复同步
|
|
805
|
-
- 统一 SyncQueue 类:同步命令队列管理,支持高负载场景
|
|
806
|
-
|
|
807
|
-
**所有同步节点统一使用 SyncUtils**:
|
|
808
|
-
- HA同步节点 (`symi-ha-sync.js`) 使用 SyncUtils 替代原有防环路实现
|
|
809
|
-
- MQTT同步节点 (`symi-mqtt-sync.js`) 使用 SyncUtils 替代原有防环路实现
|
|
810
|
-
- KNX桥接节点 (`symi-knx-bridge.js`) 重构使用 SyncUtils,移除独立的 LOOP_PREVENTION_MS
|
|
811
|
-
- RS485桥接节点 (`symi-485-bridge.js`) 重构使用 SyncUtils,统一防环路逻辑
|
|
812
|
-
- 统一超时配置:普通设备2秒、窗帘30秒、亮度800ms
|
|
813
|
-
|
|
814
|
-
**MQTT品牌同步三合一子实体支持**:
|
|
815
|
-
- 新增 `meshSubEntity` 映射字段,支持三合一面板子实体映射
|
|
816
|
-
- 空调子实体 → HYQW type 12 (开关/温度/模式/风速)
|
|
817
|
-
- 新风子实体 → HYQW type 36 (开关/风速)
|
|
818
|
-
- 地暖子实体 → HYQW type 16 (开关/温度)
|
|
819
|
-
- 双向同步:HYQW设备状态变化自动同步到Mesh三合一面板
|
|
820
|
-
|
|
821
|
-
**代码质量优化**:
|
|
822
|
-
- 消除重复的防环路代码,所有同步节点共用 SyncUtils
|
|
823
|
-
- 节点关闭时正确调用 SyncUtils.destroy() 清理资源
|
|
824
|
-
- 减少内存泄漏风险,统一时间戳清理机制
|
|
825
|
-
|
|
826
|
-
**三合一状态缓存细粒度优化**:
|
|
827
|
-
- HA同步节点使用 StateCache 实现细粒度状态对比
|
|
828
|
-
- 缓存key格式优化为 `{mac}_{subEntity}_{property}`
|
|
829
|
-
- 只同步真正变化的属性,减少冗余HA API调用
|
|
830
|
-
- 支持空调、新风、地暖各属性独立变化检测
|
|
831
|
-
|
|
832
|
-
### v1.8.4 (2026-01-06)
|
|
833
|
-
|
|
834
|
-
**HA同步节点窗帘双向同步重大修复**:
|
|
835
|
-
- 实现"谁发起控制就只听谁的命令"逻辑,彻底解决窗帘双向同步死循环问题
|
|
836
|
-
- Mesh控制时:只同步位置到HA,不发送动作命令
|
|
837
|
-
- HA控制时:发送动作/位置到Mesh,运动过程中忽略Mesh的所有反馈
|
|
838
|
-
- 停止后延迟5秒释放控制权,确保延迟反馈也被正确过滤
|
|
839
|
-
- 只处理HA的`opening`/`closing`状态,忽略`open`/`closed`状态反馈
|
|
840
|
-
|
|
841
|
-
**其他优化**:
|
|
842
|
-
- 修复HA state_changed事件解析,支持更多消息格式变体
|
|
843
|
-
- 优化空调同步逻辑:只在开关状态真正变化时同步
|
|
844
|
-
- 优化调光同步逻辑:HA发起调光时忽略Mesh步进反馈
|
|
845
|
-
- 增加状态变化检测:无变化时跳过处理
|
|
846
|
-
- 过滤sensor类型实体,避免不必要的处理
|
|
847
|
-
|
|
848
|
-
**MQTT品牌同步协议修复**:
|
|
849
|
-
- 窗帘设备协议修复:修复窗帘fn=1功能码的正确解析
|
|
850
|
-
- 设备类型独立处理:重构syncToMesh和syncToMqtt函数
|
|
851
|
-
- 完整功能码支持:灯具、空调、窗帘、地暖、新风
|
|
852
|
-
|
|
853
|
-
**三合一面板深度集成**:
|
|
854
|
-
- 子设备选择:HA同步节点新增三合一子设备选择功能
|
|
855
|
-
- 完整属性同步:支持空调、新风、地暖的全功能双向同步
|
|
856
|
-
|
|
857
|
-
**稳定性修复**:
|
|
858
|
-
- 修复Mesh控制窗帘时HA反向发送命令导致窗帘停止的问题
|
|
859
|
-
- 修复HA控制窗帘时Mesh反馈导致循环控制的问题
|
|
860
|
-
- 修复窗帘打开命令被防死循环机制阻止的问题
|
|
861
|
-
- 修复空调off->off重复日志的问题
|
|
735
|
+
|
|
736
|
+
**历史版本功能合并 (v1.8.4 - v1.8.10)**:
|
|
737
|
+
- **三合一面板深度集成**:
|
|
738
|
+
- 完整支持空调、新风、地暖的全功能双向同步。
|
|
739
|
+
- 持久化存储设备类型,重启后自动恢复,无需重复探测。
|
|
740
|
+
- 优化了状态缓存机制,只同步真正变化的属性,减少资源消耗。
|
|
741
|
+
- **稳定性增强**:
|
|
742
|
+
- **错误日志节流**:在网关连接和 RS485 配置中引入节流机制,彻底解决离线时的日志刷屏问题。
|
|
743
|
+
- **通用同步工具类**:重构了所有同步节点(HA、MQTT、KNX、RS485),统一使用 `SyncUtils` 类进行防环路和状态缓存管理。
|
|
744
|
+
- **窗帘同步修复**:实现了"谁发起控制就只听谁的命令"逻辑,彻底解决窗帘双向同步死循环问题。
|
|
745
|
+
- **协议鲁棒性**:增强了分包与粘包处理,修复了 Buffer 类型数据解析问题。
|
|
746
|
+
|
|
862
747
|
|
|
863
748
|
## 常见问题排查
|
|
864
749
|
|
|
@@ -897,8 +782,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
897
782
|
---
|
|
898
783
|
|
|
899
784
|
**作者**: SYMI 亖米
|
|
900
|
-
**版本**: 1.8.
|
|
785
|
+
**版本**: 1.8.13
|
|
901
786
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
902
|
-
**最后更新**: 2026-01-
|
|
787
|
+
**最后更新**: 2026-01-18
|
|
903
788
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
904
789
|
**npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
|
package/lib/tcp-client.js
CHANGED
|
@@ -51,7 +51,8 @@ class TCPClient extends EventEmitter {
|
|
|
51
51
|
if (this.client) {
|
|
52
52
|
this.client.destroy();
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
// 超时通常也是网络问题,使用warn而不是error,并由上层逻辑决定是否显示
|
|
55
|
+
// this.logger.error('Connection timeout'); // 移除错误日志,避免刷屏
|
|
55
56
|
this.handleDisconnect();
|
|
56
57
|
reject(new Error('Connection timeout'));
|
|
57
58
|
}
|
|
@@ -109,9 +110,9 @@ class TCPClient extends EventEmitter {
|
|
|
109
110
|
this.logger.warn(`TCP连接失败: 无法连接到 ${this.host}:${this.port}`);
|
|
110
111
|
}
|
|
111
112
|
// 只在首次连接或重要错误时记录,避免大量重复日志
|
|
112
|
-
// ECONNRESET通常是网络波动,不记录错误
|
|
113
|
+
// ECONNRESET/EHOSTDOWN 通常是网络波动,不记录错误
|
|
113
114
|
// EHOSTUNREACH/ETIMEDOUT 也是常见网络错误,不记录
|
|
114
|
-
else if (!this.connected && !['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ETIMEDOUT'].includes(error.code)) {
|
|
115
|
+
else if (!this.connected && !['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'EHOSTDOWN', 'ETIMEDOUT'].includes(error.code)) {
|
|
115
116
|
this.logger.log('TCP client error: ' + error.message);
|
|
116
117
|
}
|
|
117
118
|
|
package/nodes/symi-gateway.js
CHANGED
package/nodes/symi-knx-bridge.js
CHANGED
|
@@ -1086,11 +1086,20 @@ module.exports = function(RED) {
|
|
|
1086
1086
|
// 确定地址功能(优先使用地址匹配,比DPT更可靠)
|
|
1087
1087
|
const addrFunc = node.getKnxAddrFunction(mapping, groupAddr);
|
|
1088
1088
|
|
|
1089
|
+
// 通用值解析:支持 Boolean, Number, String, Buffer
|
|
1090
|
+
const parseBoolean = (val) => {
|
|
1091
|
+
if (Buffer.isBuffer(val)) {
|
|
1092
|
+
return val.length > 0 && val[0] !== 0;
|
|
1093
|
+
}
|
|
1094
|
+
return (val === 1 || val === true || val === 'on' || val === 'ON' || val === '1');
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1089
1097
|
// 根据设备类型和地址功能处理
|
|
1098
|
+
// 重要修复:严禁 Status/反馈地址触发控制逻辑,防止反向操作和死循环
|
|
1090
1099
|
if (mapping.deviceType === 'switch') {
|
|
1091
|
-
// 开关命令(只处理cmd地址)
|
|
1092
|
-
if (addrFunc === 'cmd'
|
|
1093
|
-
const switchValue = (value
|
|
1100
|
+
// 开关命令(只处理cmd地址,忽略status地址)
|
|
1101
|
+
if (addrFunc === 'cmd') {
|
|
1102
|
+
const switchValue = parseBoolean(value);
|
|
1094
1103
|
const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_switch_${switchValue}`;
|
|
1095
1104
|
|
|
1096
1105
|
// 防死循环检查
|
|
@@ -1100,7 +1109,7 @@ module.exports = function(RED) {
|
|
|
1100
1109
|
return;
|
|
1101
1110
|
}
|
|
1102
1111
|
|
|
1103
|
-
node.log(`[KNX
|
|
1112
|
+
node.log(`[KNX->Mesh] 开关控制: ${mapping.name} CH${mapping.meshChannel} = ${switchValue ? 'ON' : 'OFF'}`);
|
|
1104
1113
|
node.queueCommand({
|
|
1105
1114
|
direction: 'knx-to-mesh',
|
|
1106
1115
|
mapping: mapping,
|
|
@@ -1111,20 +1120,20 @@ module.exports = function(RED) {
|
|
|
1111
1120
|
});
|
|
1112
1121
|
}
|
|
1113
1122
|
}
|
|
1114
|
-
// 场景触发逻辑
|
|
1123
|
+
// 场景触发逻辑
|
|
1115
1124
|
else if (mapping.deviceType === 'scene') {
|
|
1116
1125
|
if (addrFunc === 'cmd') { // 场景通常只有一个组地址,这里我们用cmdAddr匹配
|
|
1117
1126
|
// 场景 DPT 17.001 实际上是 0-63 的整数
|
|
1118
1127
|
// 但有时也会用 DPT 5.001 (0-255)
|
|
1119
|
-
|
|
1128
|
+
let receivedScene = 0;
|
|
1129
|
+
if (Buffer.isBuffer(value)) receivedScene = value[0];
|
|
1130
|
+
else receivedScene = parseInt(value) || 0;
|
|
1131
|
+
|
|
1120
1132
|
const targetScene = parseInt(mapping.sceneNumber) || 1; // 配置的场景号 (1-64)
|
|
1121
1133
|
|
|
1122
1134
|
// KNX场景值通常是 场景号-1 (例如场景1发送0)
|
|
1123
1135
|
// 但也有设备发送直接的场景号,这里我们兼容两种情况:
|
|
1124
1136
|
// 如果接收值 == 配置值,或者 接收值 == 配置值-1,都认为匹配
|
|
1125
|
-
// 更严谨的做法是:DPT 17.001 规定传输值 = 场景号 - 1
|
|
1126
|
-
// 所以如果配置的是1 (UI显示1),实际收到应该是0
|
|
1127
|
-
// 我们这里做宽容匹配:
|
|
1128
1137
|
|
|
1129
1138
|
let matched = false;
|
|
1130
1139
|
// 情况A: 标准 DPT 17.001, 收到值 = 场景号-1
|
|
@@ -1155,11 +1164,11 @@ module.exports = function(RED) {
|
|
|
1155
1164
|
}
|
|
1156
1165
|
}
|
|
1157
1166
|
}
|
|
1158
|
-
// 窗帘设备
|
|
1167
|
+
// 窗帘设备
|
|
1159
1168
|
else if (mapping.deviceType === 'cover') {
|
|
1160
1169
|
const deviceKey = (mapping.meshMac || '').toLowerCase().replace(/:/g, '');
|
|
1161
1170
|
const now = Date.now();
|
|
1162
|
-
const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_cover`; // 窗帘使用通用key
|
|
1171
|
+
const loopKey = `${mapping.meshMac}_${mapping.meshChannel}_cover`; // 窗帘使用通用key
|
|
1163
1172
|
|
|
1164
1173
|
// KNX主动发起控制,直接抢占锁定(用户操作优先)
|
|
1165
1174
|
node.controlLock[deviceKey] = { controller: 'knx', lockUntil: now + COVER_CONTROL_LOCK_MS };
|
|
@@ -1176,13 +1185,18 @@ module.exports = function(RED) {
|
|
|
1176
1185
|
// 停止时解除锁定
|
|
1177
1186
|
delete node.controlLock[deviceKey];
|
|
1178
1187
|
}
|
|
1179
|
-
else if (addrFunc === 'position'
|
|
1180
|
-
|
|
1188
|
+
else if (addrFunc === 'position') { // 仅 position 地址触发,忽略 status
|
|
1189
|
+
let pos = 0;
|
|
1190
|
+
if (Buffer.isBuffer(value)) pos = value[0];
|
|
1191
|
+
else pos = parseInt(value) || 0;
|
|
1192
|
+
|
|
1193
|
+
if (mapping.invertPosition) pos = 100 - pos;
|
|
1194
|
+
|
|
1181
1195
|
node.log(`[KNX->Mesh] 窗帘位置: ${pos}%`);
|
|
1182
1196
|
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'cover_position', value: pos, key: loopKey, sourceAddr: groupAddr });
|
|
1183
1197
|
}
|
|
1184
1198
|
}
|
|
1185
|
-
// 调光灯设备
|
|
1199
|
+
// 调光灯设备
|
|
1186
1200
|
else if (mapping.deviceType.startsWith('light_')) {
|
|
1187
1201
|
const deviceKey = `${(mapping.meshMac || '').toLowerCase().replace(/:/g, '')}_light`;
|
|
1188
1202
|
const now = Date.now();
|
|
@@ -1190,59 +1204,107 @@ module.exports = function(RED) {
|
|
|
1190
1204
|
// KNX主动发起控制,直接抢占锁定(用户操作优先)
|
|
1191
1205
|
node.controlLock[deviceKey] = { controller: 'knx', lockUntil: now + COVER_CONTROL_LOCK_MS };
|
|
1192
1206
|
|
|
1193
|
-
if (addrFunc === 'cmd'
|
|
1207
|
+
if (addrFunc === 'cmd') { // 仅 cmd 地址触发,忽略 status
|
|
1194
1208
|
// 开关
|
|
1195
|
-
const sw = (value
|
|
1209
|
+
const sw = parseBoolean(value);
|
|
1210
|
+
const loopKey = `${mapping.meshMac}_light_switch`;
|
|
1211
|
+
|
|
1212
|
+
if (node.shouldPreventSync('knx-to-mesh', loopKey)) {
|
|
1213
|
+
node.debug(`[KNX->Mesh] 调光灯跳过: ${loopKey}`);
|
|
1214
|
+
done && done();
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1196
1218
|
node.log(`[KNX->Mesh] 调光灯开关: ${sw ? 'ON' : 'OFF'}`);
|
|
1197
1219
|
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'light_switch', value: sw, key: loopKey, sourceAddr: groupAddr });
|
|
1198
1220
|
}
|
|
1199
1221
|
else if (addrFunc === 'brightness') {
|
|
1200
1222
|
// 亮度 (KNX 0-255 -> Mesh 0-100)
|
|
1201
|
-
|
|
1223
|
+
let val = 0;
|
|
1224
|
+
if (Buffer.isBuffer(value)) val = value[0];
|
|
1225
|
+
else val = parseInt(value) || 0;
|
|
1226
|
+
|
|
1227
|
+
const brightness = Math.round(val * 100 / 255);
|
|
1228
|
+
const loopKey = `${mapping.meshMac}_light_brightness`;
|
|
1229
|
+
|
|
1230
|
+
if (node.shouldPreventSync('knx-to-mesh', loopKey)) {
|
|
1231
|
+
done && done();
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1202
1235
|
node.log(`[KNX->Mesh] 调光灯亮度: ${brightness}%`);
|
|
1203
1236
|
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'light_brightness', value: brightness, key: loopKey, sourceAddr: groupAddr });
|
|
1204
1237
|
}
|
|
1205
1238
|
else if (addrFunc === 'colorTemp') {
|
|
1206
1239
|
// 色温
|
|
1207
|
-
|
|
1208
|
-
|
|
1240
|
+
let ct = 0;
|
|
1241
|
+
if (Buffer.isBuffer(value)) ct = value[0];
|
|
1242
|
+
else ct = parseInt(value) || 50;
|
|
1243
|
+
|
|
1244
|
+
const loopKey = `${mapping.meshMac}_light_ct`;
|
|
1245
|
+
if (node.shouldPreventSync('knx-to-mesh', loopKey)) return;
|
|
1246
|
+
|
|
1247
|
+
node.log(`[KNX->Mesh] 调光灯色温: ${ct}`);
|
|
1248
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'light_color_temp', value: ct, key: loopKey, sourceAddr: groupAddr });
|
|
1209
1249
|
}
|
|
1210
1250
|
}
|
|
1211
1251
|
// 空调设备
|
|
1212
1252
|
else if (mapping.deviceType === 'climate') {
|
|
1213
1253
|
if (addrFunc === 'cmd') {
|
|
1214
|
-
|
|
1254
|
+
const sw = parseBoolean(value);
|
|
1255
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'climate_switch', value: sw, key: `${mapping.meshMac}_climate_sw`, sourceAddr: groupAddr });
|
|
1215
1256
|
}
|
|
1216
|
-
else if (addrFunc === 'temp'
|
|
1217
|
-
|
|
1257
|
+
else if (addrFunc === 'temp') { // 忽略 status
|
|
1258
|
+
let temp = 24;
|
|
1259
|
+
// DPT 9.001 浮点数解析通常由 knx-ultimate 完成,这里假设是数字
|
|
1260
|
+
if (typeof value === 'number') temp = value;
|
|
1261
|
+
else temp = parseFloat(value) || 24;
|
|
1262
|
+
|
|
1263
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'climate_temp', value: temp, key: `${mapping.meshMac}_climate_temp`, sourceAddr: groupAddr });
|
|
1218
1264
|
}
|
|
1219
1265
|
else if (addrFunc === 'mode') {
|
|
1220
|
-
|
|
1266
|
+
let mode = 1;
|
|
1267
|
+
if (Buffer.isBuffer(value)) mode = value[0];
|
|
1268
|
+
else mode = parseInt(value) || 1;
|
|
1269
|
+
|
|
1270
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'climate_mode', value: mode, key: `${mapping.meshMac}_climate_mode`, sourceAddr: groupAddr });
|
|
1221
1271
|
}
|
|
1222
1272
|
else if (addrFunc === 'fanSpeed') {
|
|
1223
|
-
|
|
1224
|
-
|
|
1273
|
+
let fan = 1;
|
|
1274
|
+
if (Buffer.isBuffer(value)) fan = value[0];
|
|
1275
|
+
else fan = parseInt(value) || 1;
|
|
1276
|
+
|
|
1277
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'climate_fan', value: fan, key: `${mapping.meshMac}_climate_fan`, sourceAddr: groupAddr });
|
|
1225
1278
|
}
|
|
1226
1279
|
}
|
|
1227
1280
|
// 新风设备
|
|
1228
1281
|
else if (mapping.deviceType === 'fresh_air') {
|
|
1229
1282
|
if (addrFunc === 'cmd') {
|
|
1230
|
-
|
|
1283
|
+
const sw = parseBoolean(value);
|
|
1284
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'fresh_air_switch', value: sw, key: `${mapping.meshMac}_fa_sw`, sourceAddr: groupAddr });
|
|
1231
1285
|
}
|
|
1232
|
-
else if (addrFunc === 'fanSpeed'
|
|
1286
|
+
else if (addrFunc === 'fanSpeed') { // 忽略 status
|
|
1233
1287
|
// 百分比转风速: >66=高(1), >33=中(2), <=33=低(3)
|
|
1234
|
-
|
|
1288
|
+
let pct = 0;
|
|
1289
|
+
if (Buffer.isBuffer(value)) pct = value[0];
|
|
1290
|
+
else pct = parseInt(value) || 0;
|
|
1291
|
+
|
|
1235
1292
|
const speed = pct > 66 ? 1 : pct > 33 ? 2 : 3;
|
|
1236
|
-
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'fresh_air_speed', value: speed, key:
|
|
1293
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'fresh_air_speed', value: speed, key: `${mapping.meshMac}_fa_speed`, sourceAddr: groupAddr });
|
|
1237
1294
|
}
|
|
1238
1295
|
}
|
|
1239
1296
|
// 地暖设备
|
|
1240
1297
|
else if (mapping.deviceType === 'floor_heating') {
|
|
1241
1298
|
if (addrFunc === 'cmd') {
|
|
1242
|
-
|
|
1299
|
+
const sw = parseBoolean(value);
|
|
1300
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'floor_heating_switch', value: sw, key: `${mapping.meshMac}_fh_sw`, sourceAddr: groupAddr });
|
|
1243
1301
|
}
|
|
1244
|
-
else if (addrFunc === 'temp'
|
|
1245
|
-
|
|
1302
|
+
else if (addrFunc === 'temp') { // 忽略 status
|
|
1303
|
+
let temp = 24;
|
|
1304
|
+
if (typeof value === 'number') temp = value;
|
|
1305
|
+
else temp = parseFloat(value) || 24;
|
|
1306
|
+
|
|
1307
|
+
node.queueCommand({ direction: 'knx-to-mesh', mapping, type: 'floor_heating_temp', value: temp, key: `${mapping.meshMac}_fh_temp`, sourceAddr: groupAddr });
|
|
1246
1308
|
}
|
|
1247
1309
|
}
|
|
1248
1310
|
|