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 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.7
1311
+ **版本**: 1.6.9
1096
1312
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1097
- **最后更新**: 2025-12-10
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
@@ -85,7 +85,9 @@ class DeviceInfo {
85
85
  case 0x45:
86
86
  // 6-8路开关状态上报(2字节,小端序)
87
87
  // 用于场景执行后的状态同步
88
- if (this.channels >= 6) {
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路开关:1字节,每2位表示1路
389
- const value = parameters[0];
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++) {
@@ -530,7 +530,7 @@ module.exports = function(RED) {
530
530
 
531
531
  // Mesh设备状态变化处理(事件驱动)
532
532
  const handleMeshStateChange = (eventData) => {
533
- if (node.syncLock) return;
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
- if (node.syncLock) return;
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
- 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);
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
- if (multiChange && !node.pendingVerify) {
1048
- node.pendingVerify = true;
1049
- setTimeout(() => {
1050
- node.verifySync();
1051
- node.pendingVerify = false;
1052
- }, 200);
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();
@@ -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
 
@@ -3,7 +3,7 @@
3
3
  * 支持开关、窗帘等设备的双向状态同步
4
4
  * 事件驱动架构,命令队列顺序处理,防死循环机制
5
5
  *
6
- * 版本: 1.6.7
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 = 1000; // 500ms内不处理反向同步
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
- if (node.syncLock || node.initializing) return;
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
- while (node.commandQueue.length > 0) {
495
- const cmd = node.commandQueue.shift();
496
- try {
497
- if (cmd.direction === 'mesh-to-knx') {
498
- await node.syncMeshToKnx(cmd);
499
- } else if (cmd.direction === 'knx-to-mesh') {
500
- await node.syncKnxToMesh(cmd);
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
  // 清空队列和缓存