node-red-contrib-symi-mesh 1.6.7 → 1.6.9
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 +235 -19
- package/lib/device-manager.js +11 -3
- package/nodes/symi-485-bridge.js +26 -27
- package/nodes/symi-gateway.js +7 -0
- package/nodes/symi-knx-bridge.js +63 -18
- package/nodes/symi-knx-ha-bridge.html +370 -0
- package/nodes/symi-knx-ha-bridge.js +524 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- **三合一面板**:完整支持空调+新风+地暖三合一控制面板,自动识别
|
|
17
17
|
- **RS485/Modbus集成**:支持第三方485设备双向同步,内置协议模板
|
|
18
18
|
- **KNX集成**:支持与KNX系统双向同步
|
|
19
|
+
- **KNX-HA集成**:支持KNX与Home Assistant实体直接双向同步
|
|
19
20
|
- **云端同步**:从酒店云云平台自动获取设备名称和场景信息
|
|
20
21
|
- **稳定可靠**:完善的错误处理和自动重连机制
|
|
21
22
|
|
|
@@ -179,6 +180,208 @@ node-red-restart
|
|
|
179
180
|
|
|
180
181
|
每个功能独立同步,互不干扰。
|
|
181
182
|
|
|
183
|
+
## KNX-HA双向同步
|
|
184
|
+
|
|
185
|
+
本节点支持KNX与Home Assistant实体直接双向同步,实现KNX系统与HA的实时互联互通。
|
|
186
|
+
|
|
187
|
+
### 功能特点
|
|
188
|
+
|
|
189
|
+
- **直接实体同步**:通过HA events-state节点实时接收HA状态变化事件
|
|
190
|
+
- **KNX实体导入**:支持Tab分隔格式导入KNX组地址配置,与KNX桥接节点100%一致
|
|
191
|
+
- **HA实体加载**:从HA服务器节点自动加载所有支持的实体
|
|
192
|
+
- **快速输入**:支持直接输入实体ID或名称搜索,带autocomplete提示,支持806+实体
|
|
193
|
+
- **多设备类型**:开关、灯光、窗帘、空调、风扇
|
|
194
|
+
- **双向同步**:自动处理KNX↔HA状态同步
|
|
195
|
+
- **智能防抖**:调光300ms、窗帘500ms防抖,避免过程中频繁同步
|
|
196
|
+
- **防死循环**:内置800ms防抖机制
|
|
197
|
+
- **持久化保存**:映射配置自动保存,重启后保持
|
|
198
|
+
- **宽屏界面**:对话框最小900px宽度,适配大屏幕
|
|
199
|
+
|
|
200
|
+
### 配置步骤
|
|
201
|
+
|
|
202
|
+
#### 1. 安装依赖节点
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
cd ~/.node-red
|
|
206
|
+
npm install node-red-contrib-knx-ultimate
|
|
207
|
+
npm install node-red-contrib-home-assistant-websocket
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### 2. 配置HA服务器
|
|
211
|
+
|
|
212
|
+
1. 添加Home Assistant服务器配置节点(如果还没有)
|
|
213
|
+
2. 输入HA的URL和访问令牌
|
|
214
|
+
3. 测试连接确保成功
|
|
215
|
+
|
|
216
|
+
#### 3. 添加KNX-HA桥接节点
|
|
217
|
+
|
|
218
|
+
从"Symi Mesh"分类中拖入"KNX-HA桥接"节点
|
|
219
|
+
|
|
220
|
+
#### 4. 选择HA服务器
|
|
221
|
+
|
|
222
|
+
在节点配置中选择已配置的Home Assistant服务器节点
|
|
223
|
+
|
|
224
|
+
#### 5. 导入KNX实体
|
|
225
|
+
|
|
226
|
+
1. 点击"下载模板"获取导入格式示例
|
|
227
|
+
2. 按照Tab分隔格式准备KNX组地址数据
|
|
228
|
+
3. 点击"导入"按钮,粘贴数据
|
|
229
|
+
4. 确认导入
|
|
230
|
+
|
|
231
|
+
**导入格式示例**:
|
|
232
|
+
```
|
|
233
|
+
玄关射灯 switch 1/1/28 1/2/28
|
|
234
|
+
客厅吊灯 light 1/1/10 1/2/10 1/3/10 1/4/10
|
|
235
|
+
客厅布帘 cover 2/1/5 2/2/5 2/3/5
|
|
236
|
+
主卧空调 climate 3/1/1 3/2/1 3/3/1 3/4/1 3/5/1
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### 6. 加载HA实体
|
|
240
|
+
|
|
241
|
+
1. 选择HA服务器后,系统会自动加载实体(约2秒延迟)
|
|
242
|
+
2. 如果未成功加载,点击"刷新"按钮重新加载
|
|
243
|
+
3. 系统使用HA REST API直接获取实体列表
|
|
244
|
+
|
|
245
|
+
#### 7. 配置映射
|
|
246
|
+
|
|
247
|
+
1. 点击"添加"按钮
|
|
248
|
+
2. 选择KNX实体
|
|
249
|
+
3. 输入或选择HA实体(支持直接输入实体ID或输入名称快速搜索)
|
|
250
|
+
4. 可以配置多个映射关系
|
|
251
|
+
|
|
252
|
+
#### 8. 连接KNX节点
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
[knxUltimate-in] → [KNX-HA桥接] → [knxUltimate-out]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**knxUltimate-in配置**:
|
|
259
|
+
- Listen to all GA:勾选
|
|
260
|
+
- Telegram type:写入
|
|
261
|
+
- Notify on write:勾选
|
|
262
|
+
- Notify on response:勾选
|
|
263
|
+
|
|
264
|
+
**knxUltimate-out配置**:
|
|
265
|
+
- Listen to all GA:勾选(Universal Mode)
|
|
266
|
+
- Output type:Write
|
|
267
|
+
|
|
268
|
+
#### 9. 连接HA节点(实现HA→KNX同步)
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
[HA server-state-changed] → [KNX-HA桥接]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**HA server-state-changed配置**:
|
|
275
|
+
- 节点类型:events: state changed
|
|
276
|
+
- 实体ID:选择映射中的HA实体(或留空监听所有)
|
|
277
|
+
- 输出属性:data(包含entity_id、new_state、old_state)
|
|
278
|
+
|
|
279
|
+
**重要**:server-state-changed节点必须直接连接到KNX-HA桥接节点的输入,不要通过knxUltimate-in
|
|
280
|
+
|
|
281
|
+
#### 10. 部署
|
|
282
|
+
|
|
283
|
+
点击"部署"按钮,开始双向同步
|
|
284
|
+
|
|
285
|
+
### 支持的设备类型
|
|
286
|
+
|
|
287
|
+
| KNX设备类型 | HA实体类型 | 同步内容 | KNX地址字段 |
|
|
288
|
+
|------------|-----------|----------|-------------|
|
|
289
|
+
| 开关 | switch | 开/关状态 | 命令, 状态 |
|
|
290
|
+
| 灯光 | light | 开关、亮度、色温 | 开关, 状态, 亮度, 色温 |
|
|
291
|
+
| 窗帘 | cover | 开关、位置、停止 | 上下, 位置, 停止 |
|
|
292
|
+
| 空调 | climate | 开关、温度、模式 | 开关, 温度, 模式, 风速, 当前温度 |
|
|
293
|
+
| 风扇 | fan | 开关、风速 | 开关, 风速 |
|
|
294
|
+
|
|
295
|
+
### 连接方式
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
[knxUltimate-in] → [KNX-HA桥接] → [knxUltimate-out]
|
|
299
|
+
[server-state-changed] → [KNX-HA桥接] ↗
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**说明**:
|
|
303
|
+
- KNX→HA:knxUltimate-in → KNX-HA桥接,自动调用HA服务
|
|
304
|
+
- HA→KNX:server-state-changed → KNX-HA桥接,桥接节点发送KNX命令到knxUltimate-out
|
|
305
|
+
|
|
306
|
+
### 工作原理
|
|
307
|
+
|
|
308
|
+
#### KNX → HA 同步
|
|
309
|
+
|
|
310
|
+
1. KNX设备状态变化(如开关按下)
|
|
311
|
+
2. knxUltimate-in节点接收GroupValue_Write
|
|
312
|
+
3. KNX-HA桥接节点检测到变化
|
|
313
|
+
4. 调用HA REST API服务(如switch.turn_on)
|
|
314
|
+
5. HA实体状态更新
|
|
315
|
+
|
|
316
|
+
#### HA → KNX 同步
|
|
317
|
+
|
|
318
|
+
1. HA实体状态变化(如在HA界面操作)
|
|
319
|
+
2. server-state-changed节点实时推送state_changed事件
|
|
320
|
+
3. KNX-HA桥接节点接收HA事件
|
|
321
|
+
4. 桥接节点发送KNX GroupValue_Write命令到knxUltimate-out
|
|
322
|
+
5. KNX设备执行动作
|
|
323
|
+
|
|
324
|
+
**防抖机制**:
|
|
325
|
+
- 调光灯亮度:300ms防抖,只同步最终值
|
|
326
|
+
- 窗帘位置:500ms防抖,避免移动过程中频繁同步
|
|
327
|
+
- 开关控制:立即同步,无延迟
|
|
328
|
+
|
|
329
|
+
### 注意事项
|
|
330
|
+
|
|
331
|
+
1. **5秒初始化延迟**:部署后前5秒不同步,等待系统初始化
|
|
332
|
+
2. **事件驱动**:HA→KNX通过events-state节点实时推送,无轮询开销
|
|
333
|
+
3. **防死循环**:KNX/HA控制后800ms内,反向状态变化不会同步
|
|
334
|
+
4. **智能防抖**:调光300ms、窗帘500ms防抖,只同步最终值
|
|
335
|
+
5. **持久化配置**:所有映射配置自动保存,重启后保持
|
|
336
|
+
6. **内存优化**:命令队列限制100条,自动清理过期缓存
|
|
337
|
+
7. **宽屏界面**:对话框最小1000px,映射列表高度400px
|
|
338
|
+
8. **快速输入**:支持直接输入实体ID或输入名称快速定位
|
|
339
|
+
9. **断线恢复**:支持断电断网后自动重连
|
|
340
|
+
|
|
341
|
+
### 故障排查
|
|
342
|
+
|
|
343
|
+
**KNX→HA不工作**:
|
|
344
|
+
1. 检查日志是否有`[KNX输入]`输出
|
|
345
|
+
2. 确认knxUltimate-in节点已启用"Listen to all GA"
|
|
346
|
+
3. 确认KNX组地址与映射中的地址一致
|
|
347
|
+
4. 检查HA URL和Token是否正确
|
|
348
|
+
5. 手动操作KNX设备,观察日志
|
|
349
|
+
6. 查看调试输出端口的消息
|
|
350
|
+
|
|
351
|
+
**HA→KNX不工作**:
|
|
352
|
+
1. 检查日志是否有`[HA->KNX]`输出
|
|
353
|
+
2. 确认HA实体ID与映射中的实体ID一致
|
|
354
|
+
3. 确认HA服务器节点已连接并启用WebSocket
|
|
355
|
+
4. 在HA中操作实体,观察日志
|
|
356
|
+
5. 检查knxUltimate-out节点配置
|
|
357
|
+
6. 确认knxUltimate-out已启用"Listen to all GA"(Universal Mode)
|
|
358
|
+
|
|
359
|
+
**加载HA实体失败**:
|
|
360
|
+
1. 检查HA服务器节点是否正确配置
|
|
361
|
+
2. 确认HA服务器节点已连接
|
|
362
|
+
3. 点击"刷新"按钮重新加载
|
|
363
|
+
4. 检查是否安装了node-red-contrib-home-assistant-websocket
|
|
364
|
+
5. 查看Node-RED日志中的详细错误信息
|
|
365
|
+
|
|
366
|
+
### 使用场景
|
|
367
|
+
|
|
368
|
+
**适合使用KNX-HA桥接的场景**:
|
|
369
|
+
- HA中已有KNX集成,希望与其他系统整合
|
|
370
|
+
- 需要KNX设备与HA中的Zigbee、WiFi等设备联动
|
|
371
|
+
- 希望在HA中统一管理所有设备
|
|
372
|
+
- 需要利用HA的自动化和场景功能
|
|
373
|
+
- 已安装node-red-contrib-home-assistant-websocket,共享HA配置
|
|
374
|
+
|
|
375
|
+
**与KNX直接集成的区别**:
|
|
376
|
+
- KNX直接集成:HA作为KNX系统的一部分
|
|
377
|
+
- KNX-HA桥接:KNX与HA作为两个独立系统,通过桥接互通
|
|
378
|
+
|
|
379
|
+
**优势**:
|
|
380
|
+
- 无需修改现有KNX配置
|
|
381
|
+
- 可以选择性同步部分设备
|
|
382
|
+
- 支持与HA中任意实体同步
|
|
383
|
+
- 配置灵活,易于调整
|
|
384
|
+
|
|
182
385
|
## 协议说明
|
|
183
386
|
|
|
184
387
|
### 核心协议格式
|
|
@@ -853,6 +1056,7 @@ A: 每个房间部署一个云端同步节点,配置对应的酒店ID和房间
|
|
|
853
1056
|
| **Symi Cloud Sync** | 云端同步设备名称和场景 | [云端数据同步](#云端数据同步) |
|
|
854
1057
|
| **Symi RS485 Bridge** | RS485设备双向同步 | [RS485/Modbus双向同步](#rs485modbus双向同步) |
|
|
855
1058
|
| **Symi KNX Bridge** | KNX系统双向同步 | [KNX双向同步](#knx双向同步) |
|
|
1059
|
+
| **Symi KNX-HA Bridge** | KNX与HA实体双向同步 | [KNX-HA双向同步](#knx-ha双向同步) |
|
|
856
1060
|
| **RS485 Debug** | RS485总线数据抓包调试 | [RS485调试节点](#rs485调试节点) |
|
|
857
1061
|
|
|
858
1062
|
## 开发者信息
|
|
@@ -1017,6 +1221,35 @@ node-red-contrib-symi-mesh/
|
|
|
1017
1221
|
|
|
1018
1222
|
## 更新日志
|
|
1019
1223
|
|
|
1224
|
+
### v1.6.9 (2025-12-20)
|
|
1225
|
+
- **KNX-HA双向同步**:新增`symi-knx-ha-bridge`节点
|
|
1226
|
+
- 直接连接KNX与HA实体,实现双向同步
|
|
1227
|
+
- 支持Tab分隔格式导入KNX组地址配置(与KNX桥接节点100%一致)
|
|
1228
|
+
- 使用共享HA服务器节点,通过REST API自动加载HA实体列表
|
|
1229
|
+
- 支持开关、灯光、窗帘、空调、风扇等设备类型
|
|
1230
|
+
- 完整双向同步:KNX↔HA实时状态同步
|
|
1231
|
+
- 事件驱动架构,通过HA events-state节点实时接收事件
|
|
1232
|
+
- 智能防抖:调光300ms、窗帘500ms,只同步最终值
|
|
1233
|
+
- 内置防死循环机制(800ms防抖)
|
|
1234
|
+
- 快速输入框:支持输入实体ID或名称搜索,自动提示806+实体
|
|
1235
|
+
- 手动刷新按钮,随时重新加载HA实体
|
|
1236
|
+
- 宽屏界面:对话框最小1000px宽度
|
|
1237
|
+
- 持久化配置保存,内存优化
|
|
1238
|
+
- 连接knxUltimate节点,无缝集成
|
|
1239
|
+
- 适用于已有KNX系统与HA整合的场景
|
|
1240
|
+
|
|
1241
|
+
### v1.6.8 (2025-12-15)
|
|
1242
|
+
- **KNX双向同步修复**:彻底修复米家/面板操作KNX不同步的问题
|
|
1243
|
+
- 修复0x45消息类型处理:米家/面板操作发送msgType=0x45,不再限制channels>=6
|
|
1244
|
+
- 支持1-4路开关2字节参数解析(米家/面板场景触发格式)
|
|
1245
|
+
- 场景执行通知(0x11)后自动查询设备状态
|
|
1246
|
+
- **syncLock阻塞修复**:移除handleMeshStateChange中syncLock检查
|
|
1247
|
+
- 避免队列处理期间丢失状态变化事件
|
|
1248
|
+
- 改用per-device时间戳防回环机制
|
|
1249
|
+
- **processQueue健壮性**:添加try/finally确保processing标志正确重置
|
|
1250
|
+
- **防死循环参数调整**:LOOP_PREVENTION_MS调整为800ms
|
|
1251
|
+
- **同时修复RS485桥接**:应用相同syncLock修复
|
|
1252
|
+
|
|
1020
1253
|
### v1.6.7 (2025-12-10)
|
|
1021
1254
|
- **KNX双向同步完善**:`symi-knx-bridge`节点全面升级
|
|
1022
1255
|
- **knxUltimate消息格式**:符合官方规范,使用`destination`+`payload`+`dpt`+`event`
|
|
@@ -1064,23 +1297,6 @@ node-red-contrib-symi-mesh/
|
|
|
1064
1297
|
- **内存安全**:命令队列限制100条,节点关闭时清理所有缓存
|
|
1065
1298
|
- **文档更新**:添加三合一面板RS485配置说明和协议对照表
|
|
1066
1299
|
|
|
1067
|
-
### v1.6.5 (2025-12-06)
|
|
1068
|
-
- **杜亚窗帘协议**:原生支持杜亚窗帘协议(A6B6),2字节地址,自动CRC16计算
|
|
1069
|
-
- 帧格式:55 [地址高] [地址低] 03 [动作/位置] [CRC16低] [CRC16高]
|
|
1070
|
-
- 支持打开(01)、关闭(02)、暂停(03)、百分比(04+位置)
|
|
1071
|
-
- **窗帘控制智能判断**:根据当前位置判断方向
|
|
1072
|
-
- 位置>=50% + curtainStatus变化 → 发关闭码
|
|
1073
|
-
- 位置<50% + curtainStatus变化 → 发打开码
|
|
1074
|
-
- 暂停(curtainStatus=3)最高优先级
|
|
1075
|
-
- **窗帘百分比模式修复**:修复百分比控制后开/关命令失效的问题
|
|
1076
|
-
- 百分比控制时进入百分比模式(inPosMode)
|
|
1077
|
-
- 窗帘到位(curtainStatus=0)后自动退出百分比模式
|
|
1078
|
-
- 退出后开/关命令可正常发送
|
|
1079
|
-
- **发码防抖**:500ms内不重复发相同码,避免Mesh状态混乱
|
|
1080
|
-
- **设备类型过滤**:映射只响应对应类型的状态变化
|
|
1081
|
-
- **RS485调试增强**:新增协议测试发送功能
|
|
1082
|
-
- **初始化延迟**:20秒,避免部署时误发命令
|
|
1083
|
-
|
|
1084
1300
|
## 许可证
|
|
1085
1301
|
|
|
1086
1302
|
MIT License
|
|
@@ -1092,8 +1308,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1092
1308
|
## 关于
|
|
1093
1309
|
|
|
1094
1310
|
**作者**: SYMI 亖米
|
|
1095
|
-
**版本**: 1.6.
|
|
1311
|
+
**版本**: 1.6.9
|
|
1096
1312
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1097
|
-
**最后更新**: 2025-12-
|
|
1313
|
+
**最后更新**: 2025-12-20
|
|
1098
1314
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
1099
1315
|
**npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
|
package/lib/device-manager.js
CHANGED
|
@@ -85,7 +85,9 @@ class DeviceInfo {
|
|
|
85
85
|
case 0x45:
|
|
86
86
|
// 6-8路开关状态上报(2字节,小端序)
|
|
87
87
|
// 用于场景执行后的状态同步
|
|
88
|
-
|
|
88
|
+
// 注意:米家/面板操作会发送0x45类型,不管实际路数
|
|
89
|
+
// 窗帘(type=5)不使用0x45
|
|
90
|
+
if (this.deviceType !== 5) {
|
|
89
91
|
this.handleSwitchState(parameters);
|
|
90
92
|
}
|
|
91
93
|
break;
|
|
@@ -385,8 +387,14 @@ class DeviceInfo {
|
|
|
385
387
|
const value = parameters[0];
|
|
386
388
|
this.state.switch = value === 0x02;
|
|
387
389
|
} else if (this.channels <= 4) {
|
|
388
|
-
// 1-4
|
|
389
|
-
|
|
390
|
+
// 1-4路开关:通常1字节,但米家/面板操作可能发送2字节(0x45类型)
|
|
391
|
+
let value;
|
|
392
|
+
if (parameters.length >= 2) {
|
|
393
|
+
// 2字节:小端序(米家/面板场景触发)
|
|
394
|
+
value = parameters[0] | (parameters[1] << 8);
|
|
395
|
+
} else {
|
|
396
|
+
value = parameters[0];
|
|
397
|
+
}
|
|
390
398
|
// 保存原始状态值供控制时使用
|
|
391
399
|
this.state.switchState = value;
|
|
392
400
|
for (let i = 0; i < this.channels; i++) {
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -530,7 +530,7 @@ module.exports = function(RED) {
|
|
|
530
530
|
|
|
531
531
|
// Mesh设备状态变化处理(事件驱动)
|
|
532
532
|
const handleMeshStateChange = (eventData) => {
|
|
533
|
-
|
|
533
|
+
// 只检查initializing,不检查syncLock以避免丢失事件
|
|
534
534
|
if (node.initializing) return;
|
|
535
535
|
|
|
536
536
|
const mac = eventData.device.macAddress;
|
|
@@ -972,7 +972,7 @@ module.exports = function(RED) {
|
|
|
972
972
|
|
|
973
973
|
// RS485 device state change handler (event-driven)
|
|
974
974
|
const handleModbusStateChange = (data) => {
|
|
975
|
-
|
|
975
|
+
// 不检查syncLock以避免丢失事件,使用时间戳防回环
|
|
976
976
|
|
|
977
977
|
const mapping = node.findRS485Mapping(data.device.modbusAddress);
|
|
978
978
|
if (!mapping) return;
|
|
@@ -1025,36 +1025,36 @@ module.exports = function(RED) {
|
|
|
1025
1025
|
if (node.processing || node.commandQueue.length === 0) return;
|
|
1026
1026
|
|
|
1027
1027
|
node.processing = true;
|
|
1028
|
-
node.syncLock = true;
|
|
1029
1028
|
let multiChange = node.commandQueue.length > 1;
|
|
1030
1029
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1030
|
+
try {
|
|
1031
|
+
while (node.commandQueue.length > 0) {
|
|
1032
|
+
const cmd = node.commandQueue.shift();
|
|
1033
|
+
try {
|
|
1034
|
+
if (cmd.direction === 'mesh-to-modbus') {
|
|
1035
|
+
await node.syncMeshToModbus(cmd);
|
|
1036
|
+
} else if (cmd.direction === 'modbus-to-mesh') {
|
|
1037
|
+
await node.syncModbusToMesh(cmd);
|
|
1038
|
+
}
|
|
1039
|
+
// 命令之间延迟50ms
|
|
1040
|
+
await node.sleep(50);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
node.error(`同步失败: ${err.message}`);
|
|
1038
1043
|
}
|
|
1039
|
-
// 命令之间延迟50ms
|
|
1040
|
-
await node.sleep(50);
|
|
1041
|
-
} catch (err) {
|
|
1042
|
-
node.error(`同步失败: ${err.message}`);
|
|
1043
1044
|
}
|
|
1044
|
-
}
|
|
1045
1045
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1046
|
+
// 如果发生多次变化,安排验证
|
|
1047
|
+
if (multiChange && !node.pendingVerify) {
|
|
1048
|
+
node.pendingVerify = true;
|
|
1049
|
+
setTimeout(() => {
|
|
1050
|
+
node.verifySync();
|
|
1051
|
+
node.pendingVerify = false;
|
|
1052
|
+
}, 200);
|
|
1053
|
+
}
|
|
1054
|
+
} finally {
|
|
1055
|
+
node.processing = false;
|
|
1056
|
+
node.lastSyncTime = Date.now();
|
|
1053
1057
|
}
|
|
1054
|
-
|
|
1055
|
-
node.syncLock = false;
|
|
1056
|
-
node.processing = false;
|
|
1057
|
-
node.lastSyncTime = Date.now();
|
|
1058
1058
|
};
|
|
1059
1059
|
|
|
1060
1060
|
// 多实体变化后验证同步状态
|
|
@@ -2098,7 +2098,6 @@ module.exports = function(RED) {
|
|
|
2098
2098
|
node.curtainCache = {};
|
|
2099
2099
|
node.lastSentTime = {};
|
|
2100
2100
|
node.processing = false;
|
|
2101
|
-
node.syncLock = false;
|
|
2102
2101
|
|
|
2103
2102
|
node.log('[RS485 Bridge] 节点已清理');
|
|
2104
2103
|
done();
|
package/nodes/symi-gateway.js
CHANGED
|
@@ -217,6 +217,13 @@ module.exports = function(RED) {
|
|
|
217
217
|
Buffer.from([frame.checksum])
|
|
218
218
|
]).toString('hex').toUpperCase();
|
|
219
219
|
this.log(`[场景执行] 收到场景执行通知事件: 场景ID=${sceneId}, 设备地址=0x${event.networkAddress.toString(16).toUpperCase()}, 原始帧=${frameHex}`);
|
|
220
|
+
|
|
221
|
+
// 发出场景执行事件,让桥接节点知道需要查询设备状态
|
|
222
|
+
this.emit('scene-executed', {
|
|
223
|
+
sceneId: sceneId,
|
|
224
|
+
triggerAddress: event.networkAddress,
|
|
225
|
+
timestamp: Date.now()
|
|
226
|
+
});
|
|
220
227
|
continue;
|
|
221
228
|
}
|
|
222
229
|
|
package/nodes/symi-knx-bridge.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 支持开关、窗帘等设备的双向状态同步
|
|
4
4
|
* 事件驱动架构,命令队列顺序处理,防死循环机制
|
|
5
5
|
*
|
|
6
|
-
* 版本: 1.6.
|
|
6
|
+
* 版本: 1.6.8
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
module.exports = function(RED) {
|
|
@@ -151,7 +151,7 @@ module.exports = function(RED) {
|
|
|
151
151
|
node.lastKnxToMesh = {}; // 记录KNX->Mesh发送时间,防止回环
|
|
152
152
|
|
|
153
153
|
// 防死循环参数
|
|
154
|
-
const LOOP_PREVENTION_MS =
|
|
154
|
+
const LOOP_PREVENTION_MS = 800; // 800ms内不处理反向同步,防止回环
|
|
155
155
|
const DEBOUNCE_MS = 100; // 100ms防抖
|
|
156
156
|
const MAX_QUEUE_SIZE = 100; // 最大队列大小
|
|
157
157
|
|
|
@@ -249,7 +249,9 @@ module.exports = function(RED) {
|
|
|
249
249
|
|
|
250
250
|
// ========== Mesh设备状态变化处理 ==========
|
|
251
251
|
const handleMeshStateChange = (eventData) => {
|
|
252
|
-
|
|
252
|
+
// 只检查initializing,不检查syncLock
|
|
253
|
+
// syncLock会导致队列处理期间丢失事件,改用per-device时间戳防回环
|
|
254
|
+
if (node.initializing) return;
|
|
253
255
|
|
|
254
256
|
const mac = eventData.device.macAddress;
|
|
255
257
|
const state = eventData.state || {};
|
|
@@ -489,25 +491,25 @@ module.exports = function(RED) {
|
|
|
489
491
|
if (node.processing || node.commandQueue.length === 0) return;
|
|
490
492
|
|
|
491
493
|
node.processing = true;
|
|
492
|
-
node.syncLock = true;
|
|
493
494
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
495
|
+
try {
|
|
496
|
+
while (node.commandQueue.length > 0) {
|
|
497
|
+
const cmd = node.commandQueue.shift();
|
|
498
|
+
try {
|
|
499
|
+
if (cmd.direction === 'mesh-to-knx') {
|
|
500
|
+
await node.syncMeshToKnx(cmd);
|
|
501
|
+
} else if (cmd.direction === 'knx-to-mesh') {
|
|
502
|
+
await node.syncKnxToMesh(cmd);
|
|
503
|
+
}
|
|
504
|
+
await node.sleep(50); // 命令间隔50ms
|
|
505
|
+
} catch (err) {
|
|
506
|
+
node.error(`同步失败: ${err.message}`);
|
|
501
507
|
}
|
|
502
|
-
await node.sleep(50); // 命令间隔50ms
|
|
503
|
-
} catch (err) {
|
|
504
|
-
node.error(`同步失败: ${err.message}`);
|
|
505
508
|
}
|
|
509
|
+
} finally {
|
|
510
|
+
node.processing = false;
|
|
511
|
+
node.lastSyncTime = Date.now();
|
|
506
512
|
}
|
|
507
|
-
|
|
508
|
-
node.syncLock = false;
|
|
509
|
-
node.processing = false;
|
|
510
|
-
node.lastSyncTime = Date.now();
|
|
511
513
|
};
|
|
512
514
|
|
|
513
515
|
// ========== Mesh -> KNX 同步 ==========
|
|
@@ -1018,6 +1020,48 @@ module.exports = function(RED) {
|
|
|
1018
1020
|
// 监听Mesh设备状态变化
|
|
1019
1021
|
node.gateway.on('device-state-changed', handleMeshStateChange);
|
|
1020
1022
|
|
|
1023
|
+
// ========== 场景执行事件处理 ==========
|
|
1024
|
+
// 当收到场景执行通知时,查询所有已映射设备的状态
|
|
1025
|
+
const handleSceneExecuted = (eventData) => {
|
|
1026
|
+
if (node.initializing) return;
|
|
1027
|
+
|
|
1028
|
+
node.log(`[场景执行] 检测到场景${eventData.sceneId}执行,延迟查询设备状态`);
|
|
1029
|
+
|
|
1030
|
+
// 延迟300ms后查询设备状态,给设备执行时间
|
|
1031
|
+
setTimeout(async () => {
|
|
1032
|
+
// 收集所有已映射的唯一设备地址
|
|
1033
|
+
const mappedAddresses = new Set();
|
|
1034
|
+
for (const mapping of node.mappings) {
|
|
1035
|
+
const mac = node.normalizeMac(mapping.meshMac);
|
|
1036
|
+
const device = node.gateway.deviceManager.getDeviceByMac(mac);
|
|
1037
|
+
if (device && device.networkAddress) {
|
|
1038
|
+
mappedAddresses.add(device.networkAddress);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (mappedAddresses.size === 0) {
|
|
1043
|
+
node.debug(`[场景执行] 没有已映射的设备需要查询`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
node.log(`[场景执行] 查询${mappedAddresses.size}个设备的状态`);
|
|
1048
|
+
|
|
1049
|
+
// 逐个查询设备状态
|
|
1050
|
+
for (const addr of mappedAddresses) {
|
|
1051
|
+
try {
|
|
1052
|
+
// 查询开关状态 (0x02)
|
|
1053
|
+
const queryFrame = node.gateway.protocolHandler.buildDeviceStatusQueryFrame(addr, 0x02);
|
|
1054
|
+
await node.gateway.client.sendFrame(queryFrame, 2);
|
|
1055
|
+
await node.sleep(100);
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
node.debug(`[场景执行] 查询设备0x${addr.toString(16)}状态失败: ${err.message}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}, 300);
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
node.gateway.on('scene-executed', handleSceneExecuted);
|
|
1064
|
+
|
|
1021
1065
|
// ========== 清理 ==========
|
|
1022
1066
|
node.on('close', function(done) {
|
|
1023
1067
|
// 清除初始化定时器
|
|
@@ -1028,6 +1072,7 @@ module.exports = function(RED) {
|
|
|
1028
1072
|
// 移除事件监听
|
|
1029
1073
|
if (node.gateway) {
|
|
1030
1074
|
node.gateway.removeListener('device-state-changed', handleMeshStateChange);
|
|
1075
|
+
node.gateway.removeListener('scene-executed', handleSceneExecuted);
|
|
1031
1076
|
}
|
|
1032
1077
|
|
|
1033
1078
|
// 清空队列和缓存
|