node-red-contrib-symi-mesh 1.2.4 → 1.3.1

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.
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Symi Cloud Sync Node
3
+ * 云端数据同步节点 - 获取设备名称和场景信息
4
+ */
5
+
6
+ const CloudAPI = require('../lib/cloud-api');
7
+ const { generateSceneButtonConfig } = require('../lib/mqtt-helper');
8
+
9
+ module.exports = function(RED) {
10
+ function SymiCloudSyncNode(config) {
11
+ RED.nodes.createNode(this, config);
12
+ const node = this;
13
+
14
+ node.gateway = RED.nodes.getNode(config.gateway);
15
+ node.mqttConfig = RED.nodes.getNode(config.mqttConfig);
16
+ node.appId = config.appId;
17
+ node.appSecret = config.appSecret;
18
+ node.hotelId = config.hotelId;
19
+ node.roomNo = config.roomNo;
20
+ node.roomUuid = config.roomUuid;
21
+ node.autoSync = config.autoSync !== false;
22
+ node.selectedDevices = config.selectedDevices || [];
23
+ node.selectedScenes = config.selectedScenes || [];
24
+
25
+ if (!node.gateway) {
26
+ node.error('未配置网关');
27
+ node.status({ fill: 'red', shape: 'ring', text: '未配置网关' });
28
+ return;
29
+ }
30
+
31
+ if (!node.appId || !node.appSecret) {
32
+ node.warn('未配置云端认证信息');
33
+ node.status({ fill: 'yellow', shape: 'ring', text: '未配置认证' });
34
+ return;
35
+ }
36
+
37
+ node.cloudAPI = new CloudAPI(node.appId, node.appSecret, node);
38
+ node.syncInProgress = false;
39
+
40
+ node.status({ fill: 'yellow', shape: 'ring', text: '等待同步' });
41
+
42
+ const loadCachedData = () => {
43
+ const cached = node.context().get('cloudData');
44
+ if (cached) {
45
+ node.log(`加载缓存的云端数据: ${cached.devices?.length || 0}个设备, ${cached.scenes?.length || 0}个场景`);
46
+ return cached;
47
+ }
48
+ return null;
49
+ };
50
+
51
+ const saveCachedData = (data) => {
52
+ node.context().set('cloudData', data);
53
+ node.log('云端数据已缓存');
54
+ };
55
+
56
+ const syncFromCloud = async () => {
57
+ if (node.syncInProgress) {
58
+ node.warn('同步正在进行中,跳过');
59
+ return;
60
+ }
61
+
62
+ if (!node.hotelId || !node.roomNo) {
63
+ node.warn('未配置酒店ID或房间号,跳过同步');
64
+ node.status({ fill: 'yellow', shape: 'ring', text: '未配置房间' });
65
+ return;
66
+ }
67
+
68
+ node.syncInProgress = true;
69
+ node.status({ fill: 'blue', shape: 'dot', text: '同步中...' });
70
+
71
+ try {
72
+ node.log(`开始从云端获取数据: 酒店ID=${node.hotelId}, 房间号=${node.roomNo}`);
73
+
74
+ const roomInfo = await node.cloudAPI.getRoomInfo({
75
+ hotel_id: parseInt(node.hotelId),
76
+ room_no: node.roomNo
77
+ });
78
+
79
+ if (!roomInfo || !roomInfo.device_list) {
80
+ throw new Error('云端返回数据格式错误');
81
+ }
82
+
83
+ const cloudData = {
84
+ room_no: roomInfo.room_no,
85
+ devices: roomInfo.device_list || [],
86
+ scenes: roomInfo.scene_list || [],
87
+ lastSync: new Date().toISOString()
88
+ };
89
+
90
+ node.log(`云端数据获取成功: ${cloudData.devices.length}个设备, ${cloudData.scenes.length}个场景`);
91
+
92
+ saveCachedData(cloudData);
93
+
94
+ applyCloudData(cloudData);
95
+
96
+ node.status({ fill: 'green', shape: 'dot', text: `已同步 ${new Date().toLocaleTimeString()}` });
97
+
98
+ } catch (error) {
99
+ node.error(`云端同步失败: ${error.message}`);
100
+ node.status({ fill: 'red', shape: 'ring', text: '同步失败' });
101
+
102
+ const cached = loadCachedData();
103
+ if (cached) {
104
+ node.log('使用缓存的云端数据');
105
+ applyCloudData(cached);
106
+ node.status({ fill: 'yellow', shape: 'dot', text: '使用缓存' });
107
+ }
108
+ } finally {
109
+ node.syncInProgress = false;
110
+ }
111
+ };
112
+
113
+ const applyCloudData = (cloudData) => {
114
+ if (!cloudData || !cloudData.devices) {
115
+ node.warn('无可用的云端数据');
116
+ return;
117
+ }
118
+
119
+ const devices = node.gateway.deviceManager.getAllDevices();
120
+ let matchedCount = 0;
121
+ let updatedCount = 0;
122
+
123
+ const devicesToSync = node.selectedDevices.length > 0
124
+ ? cloudData.devices.filter(d => node.selectedDevices.includes(d.mac))
125
+ : cloudData.devices;
126
+
127
+ devicesToSync.forEach(cloudDevice => {
128
+ // 云端MAC地址是反序存储的,需要反转后匹配
129
+ const cloudMacRaw = cloudDevice.mac.toLowerCase().replace(/[:-]/g, '');
130
+ const cloudMacReversed = cloudMacRaw.match(/.{2}/g)?.reverse().join('') || cloudMacRaw;
131
+
132
+ const localDevice = devices.find(d => {
133
+ const localMac = d.macAddress.toLowerCase().replace(/[:-]/g, '');
134
+ return localMac === cloudMacReversed;
135
+ });
136
+
137
+ if (localDevice) {
138
+ matchedCount++;
139
+ let deviceUpdated = false;
140
+
141
+ // 优先使用nick_name,如果是"未命名"则使用device_name,如果device_name也是"未命名"则保持原有名称
142
+ let newName = cloudDevice.nick_name;
143
+ if (!newName || newName === '未命名' || newName.trim() === '') {
144
+ newName = cloudDevice.device_name;
145
+ }
146
+ if (!newName || newName === '未命名' || newName.trim() === '') {
147
+ newName = localDevice.name; // 保持原有名称(包含设备类型)
148
+ }
149
+
150
+ if (localDevice.name !== newName) {
151
+ const oldName = localDevice.name;
152
+ localDevice.name = newName;
153
+ deviceUpdated = true;
154
+ node.log(`设备名称已更新: ${oldName} -> ${newName} (MAC: ${cloudDevice.mac})`);
155
+ }
156
+
157
+ // 同步按键名称
158
+ if (cloudDevice.sub_device && Array.isArray(cloudDevice.sub_device) && cloudDevice.sub_device.length > 0) {
159
+ const newSubNames = cloudDevice.sub_device.map(sub => sub.sub_name);
160
+ const oldSubNames = localDevice.subDeviceNames || [];
161
+
162
+ node.log(`[按键名称] 设备: ${localDevice.name}, 云端按键数: ${cloudDevice.sub_device.length}, 按键名称: [${newSubNames.join(', ')}]`);
163
+
164
+ // 检查是否有变化
165
+ const hasChanged = newSubNames.length !== oldSubNames.length ||
166
+ newSubNames.some((name, idx) => name !== oldSubNames[idx]);
167
+
168
+ if (hasChanged) {
169
+ localDevice.subDeviceNames = newSubNames;
170
+ deviceUpdated = true;
171
+ node.log(`[按键名称] 设备按键名称已更新: ${localDevice.name} -> [${newSubNames.join(', ')}]`);
172
+ } else {
173
+ node.log(`[按键名称] 设备按键名称无变化: ${localDevice.name}`);
174
+ }
175
+ } else {
176
+ node.log(`[按键名称] 设备无sub_device数据: ${localDevice.name}`);
177
+ }
178
+
179
+ if (deviceUpdated) {
180
+ updatedCount++;
181
+ }
182
+ } else {
183
+ node.log(`云端设备未匹配: ${cloudDevice.device_name} (云端MAC: ${cloudMacRaw} → 反转后: ${cloudMacReversed})`);
184
+ }
185
+ });
186
+
187
+ node.log(`设备匹配完成: ${matchedCount}/${devicesToSync.length} 匹配, ${updatedCount} 个名称已更新`);
188
+
189
+ if (cloudData.scenes && cloudData.scenes.length > 0) {
190
+ const scenesToSync = node.selectedScenes.length > 0
191
+ ? cloudData.scenes.filter(s => node.selectedScenes.includes(s.scene_id))
192
+ : cloudData.scenes;
193
+
194
+ node.context().set('scenes', scenesToSync);
195
+ node.log(`场景列表已保存: ${scenesToSync.map(s => s.scene_name).join(', ')}`);
196
+
197
+ if (node.mqttConfig && scenesToSync.length > 0) {
198
+ publishSceneButtons(scenesToSync);
199
+ }
200
+ }
201
+
202
+ if (updatedCount > 0 && node.mqttConfig) {
203
+ setTimeout(() => {
204
+ node.log('重新发布MQTT Discovery配置(名称已更新)');
205
+ node.mqttConfig.publishAllDiscovery(devices);
206
+ }, 1000);
207
+ }
208
+ };
209
+
210
+ const publishSceneButtons = (scenes) => {
211
+ if (!node.mqttConfig || !node.mqttConfig.mqttClient) {
212
+ node.warn('MQTT客户端未连接,无法发布场景按钮');
213
+ return;
214
+ }
215
+
216
+ const mqttClient = node.mqttConfig.mqttClient;
217
+ const mqttPrefix = node.mqttConfig.mqttPrefix || 'homeassistant';
218
+
219
+ const roomNo = node.roomNo || 'unknown';
220
+ node.log(`开始发布 ${scenes.length} 个场景按钮到Home Assistant`);
221
+
222
+ scenes.forEach((scene, index) => {
223
+ const config = generateSceneButtonConfig(scene, roomNo, mqttPrefix);
224
+
225
+ setTimeout(() => {
226
+ mqttClient.publish(config.topic, config.payload, { retain: true }, (err) => {
227
+ if (err) {
228
+ node.error(`发布场景按钮失败: ${scene.scene_name}, ${err.message}`);
229
+ } else {
230
+ node.debug(`场景按钮已发布: ${scene.scene_name}`);
231
+ }
232
+ });
233
+ }, index * 100);
234
+ });
235
+
236
+ // 订阅场景触发主题
237
+ node.mqttConfig.subscribeSceneTriggers(roomNo);
238
+ };
239
+
240
+ node.gateway.on('device-list-complete', () => {
241
+ if (node.autoSync) {
242
+ setTimeout(() => {
243
+ node.log('设备列表完成,开始自动同步云端数据');
244
+ syncFromCloud();
245
+ }, 2000);
246
+ } else {
247
+ const cached = loadCachedData();
248
+ if (cached) {
249
+ node.log('自动同步已禁用,使用缓存数据');
250
+ applyCloudData(cached);
251
+ }
252
+ }
253
+ });
254
+
255
+ // 监听场景触发事件
256
+ if (node.mqttConfig) {
257
+ node.mqttConfig.on('scene-trigger', (topic, message) => {
258
+ const roomNo = node.roomNo || 'unknown';
259
+ if (topic.startsWith(`symi_mesh/room_${roomNo}/scene/`) && topic.endsWith('/trigger')) {
260
+ const sceneIdMatch = topic.match(/scene\/(\d+)\/trigger/);
261
+ if (sceneIdMatch) {
262
+ const sceneId = parseInt(sceneIdMatch[1]);
263
+ const scenes = node.context().get('scenes') || [];
264
+ const scene = scenes.find(s => s.scene_id === sceneId);
265
+
266
+ if (scene) {
267
+ node.log(`[场景控制] 收到场景触发请求: ${scene.scene_name} (ID: ${sceneId})`);
268
+
269
+ if (node.gateway && node.gateway.connected) {
270
+ // 构建场景控制帧
271
+ const frame = node.gateway.protocolHandler.buildSceneControlFrame(sceneId);
272
+ node.log(`[场景控制] 场景控制帧: ${frame.toString('hex').toUpperCase()}`);
273
+
274
+ node.gateway.sendScene(sceneId).then(() => {
275
+ node.log(`[场景控制] ✓ 场景控制命令已发送: ${scene.scene_name} (ID: ${sceneId})`);
276
+ }).catch(err => {
277
+ node.error(`[场景控制] ✗ 场景控制命令发送失败: ${err.message}`);
278
+ });
279
+ } else {
280
+ node.error('[场景控制] 网关未连接,无法执行场景');
281
+ }
282
+ } else {
283
+ node.warn(`[场景控制] 场景ID ${sceneId} 未找到`);
284
+ }
285
+ }
286
+ }
287
+ });
288
+ }
289
+
290
+ node.on('input', async function(msg) {
291
+ if (msg.payload === 'sync' || msg.payload === true) {
292
+ node.log('收到手动同步请求');
293
+ await syncFromCloud();
294
+ } else if (msg.payload && msg.payload.scene_id !== undefined) {
295
+ const scenes = node.context().get('scenes') || [];
296
+ const scene = scenes.find(s => s.scene_id === msg.payload.scene_id);
297
+ if (scene) {
298
+ node.log(`执行场景: ${scene.scene_name} (ID: ${scene.scene_id})`);
299
+ msg.payload = { scene_id: scene.scene_id };
300
+ node.send(msg);
301
+ } else {
302
+ node.warn(`场景ID ${msg.payload.scene_id} 不存在`);
303
+ }
304
+ }
305
+ });
306
+
307
+ node.on('close', (done) => {
308
+ node.log('云端同步节点关闭');
309
+ done();
310
+ });
311
+ }
312
+
313
+ RED.nodes.registerType('symi-cloud-sync', SymiCloudSyncNode);
314
+
315
+ RED.httpAdmin.post('/symi-cloud-sync/test-connection', async (req, res) => {
316
+ try {
317
+ const { appId, appSecret } = req.body;
318
+ if (!appId || !appSecret) {
319
+ return res.json({ success: false, error: '缺少认证信息' });
320
+ }
321
+
322
+ const api = new CloudAPI(appId, appSecret);
323
+ await api.getHotelList(1, 1);
324
+
325
+ res.json({ success: true });
326
+ } catch (error) {
327
+ res.json({ success: false, error: error.message });
328
+ }
329
+ });
330
+
331
+ RED.httpAdmin.post('/symi-cloud-sync/get-hotels', async (req, res) => {
332
+ try {
333
+ const { appId, appSecret } = req.body;
334
+ if (!appId || !appSecret) {
335
+ return res.json({ success: false, error: '缺少认证信息' });
336
+ }
337
+
338
+ const api = new CloudAPI(appId, appSecret);
339
+ const result = await api.getHotelList(1, 100);
340
+
341
+ res.json({
342
+ success: true,
343
+ hotels: result.data || []
344
+ });
345
+ } catch (error) {
346
+ res.json({ success: false, error: error.message });
347
+ }
348
+ });
349
+
350
+ RED.httpAdmin.post('/symi-cloud-sync/get-rooms', async (req, res) => {
351
+ try {
352
+ const { appId, appSecret, hotelId } = req.body;
353
+ if (!appId || !appSecret || !hotelId) {
354
+ return res.json({ success: false, error: '缺少必要参数' });
355
+ }
356
+
357
+ const api = new CloudAPI(appId, appSecret);
358
+ const result = await api.getRoomList(hotelId, 1, 100);
359
+
360
+ res.json({
361
+ success: true,
362
+ rooms: result.data || []
363
+ });
364
+ } catch (error) {
365
+ res.json({ success: false, error: error.message });
366
+ }
367
+ });
368
+
369
+ RED.httpAdmin.post('/symi-cloud-sync/get-room-info', async (req, res) => {
370
+ try {
371
+ const { appId, appSecret, roomUuid } = req.body;
372
+ if (!appId || !appSecret || !roomUuid) {
373
+ return res.json({ success: false, error: '缺少必要参数' });
374
+ }
375
+
376
+ const api = new CloudAPI(appId, appSecret);
377
+ const result = await api.getRoomInfo({ room_uuid: roomUuid });
378
+
379
+ res.json({
380
+ success: true,
381
+ roomInfo: result
382
+ });
383
+ } catch (error) {
384
+ res.json({ success: false, error: error.message });
385
+ }
386
+ });
387
+ };
388
+
@@ -5,6 +5,7 @@
5
5
  defaults: {
6
6
  name: { value: '' },
7
7
  gateway: { value: '', type: 'symi-gateway', required: true },
8
+ mqttConfig: { value: '', type: 'symi-mqtt' },
8
9
  deviceMac: { value: '', required: true },
9
10
  channel: { value: 1 },
10
11
  deviceData: { value: '' },
@@ -161,7 +162,12 @@
161
162
  <label for="node-input-gateway"><i class="fa fa-server"></i> 网关</label>
162
163
  <input type="text" id="node-input-gateway">
163
164
  </div>
164
-
165
+
166
+ <div class="form-row">
167
+ <label for="node-input-mqttConfig"><i class="fa fa-exchange"></i> MQTT配置</label>
168
+ <input type="text" id="node-input-mqttConfig">
169
+ </div>
170
+
165
171
  <div class="form-row">
166
172
  <label for="node-input-deviceMac"><i class="fa fa-microchip"></i> 设备</label>
167
173
  <select id="node-input-deviceMac" style="width:70%">
@@ -10,8 +10,8 @@
10
10
  baudRate: { value: 115200 }
11
11
  },
12
12
  label: function() {
13
- return this.name || (this.connectionType === 'tcp'
14
- ? `Symi Gateway (${this.host}:${this.port})`
13
+ return this.name || (this.connectionType === 'tcp'
14
+ ? `Symi Gateway (${this.host}:${this.port})`
15
15
  : `Symi Gateway (${this.serialPort})`);
16
16
  },
17
17
  oneditprepare: function() {
@@ -33,7 +33,7 @@
33
33
  <label for="node-config-input-name"><i class="fa fa-tag"></i> 名称</label>
34
34
  <input type="text" id="node-config-input-name" placeholder="网关名称">
35
35
  </div>
36
-
36
+
37
37
  <div class="form-row">
38
38
  <label for="node-config-input-connectionType"><i class="fa fa-plug"></i> 连接方式</label>
39
39
  <select id="node-config-input-connectionType">
@@ -10,21 +10,27 @@ const { ProtocolHandler, parseStatusEvent, OP_RESP_DEVICE_LIST } = require('../l
10
10
  module.exports = function(RED) {
11
11
  function SymiGatewayNode(config) {
12
12
  RED.nodes.createNode(this, config);
13
-
13
+
14
14
  this.name = config.name;
15
15
  this.connectionType = config.connectionType || 'tcp';
16
16
  this.host = config.host;
17
17
  this.port = parseInt(config.port) || 4196;
18
18
  this.serialPort = config.serialPort;
19
19
  this.baudRate = parseInt(config.baudRate) || 115200;
20
-
20
+
21
21
  this.client = null;
22
22
  this.deviceManager = new DeviceManager(this.context(), this);
23
23
  this.protocolHandler = new ProtocolHandler();
24
24
  this.connected = false;
25
25
  this.deviceListComplete = false;
26
26
  this.isQueryingStates = false; // 标记是否正在查询状态
27
-
27
+
28
+ // 状态事件处理队列 - 确保场景执行后的大量状态更新能被正确处理
29
+ this.stateEventQueue = [];
30
+ this.isProcessingStateEvent = false;
31
+ this.sceneExecutionInProgress = false; // 场景执行中标志
32
+ this.sceneExecutionTimer = null; // 场景执行超时定时器
33
+
28
34
  this.log(`Initializing Symi Gateway: ${this.connectionType === 'tcp' ? `${this.host}:${this.port}` : this.serialPort}`);
29
35
 
30
36
  // 监听三合一设备检测事件
@@ -113,6 +119,20 @@ module.exports = function(RED) {
113
119
  // 初始连接失败,但自动重连会继续尝试
114
120
  this.error(`Initial connection failed: ${error.message}, will retry automatically`);
115
121
  }
122
+
123
+ // 节点关闭时清理资源
124
+ this.on('close', (done) => {
125
+ // 清理场景执行定时器
126
+ if (this.sceneExecutionTimer) {
127
+ clearTimeout(this.sceneExecutionTimer);
128
+ this.sceneExecutionTimer = null;
129
+ }
130
+
131
+ // 清空队列
132
+ this.stateEventQueue = [];
133
+
134
+ done();
135
+ });
116
136
  };
117
137
 
118
138
  SymiGatewayNode.prototype.disconnect = async function() {
@@ -131,16 +151,75 @@ module.exports = function(RED) {
131
151
  if (frame.opcode === OP_RESP_DEVICE_LIST && frame.status === 0x00) {
132
152
  this.parseDeviceListFrame(frame);
133
153
  } else if (frame.isDeviceStatusEvent()) {
154
+ // 将状态事件加入队列处理
155
+ this.stateEventQueue.push(frame);
156
+ this.processStateEventQueue();
157
+ } else if (frame.opcode === 0xB0) {
158
+ // 控制命令响应
159
+ this.debug(`[控制响应] 0xB0: ${frameHex} ${frame.status === 0 ? '(成功)' : '(失败)'}`)
160
+ } else if (frame.opcode === 0xB4) {
161
+ // 场景控制响应
162
+ if (frame.status === 0) {
163
+ this.log(`[场景控制] 场景控制命令已被网关接受`);
164
+ // 标记场景执行开始,2秒内的状态事件都认为是场景执行导致的
165
+ this.sceneExecutionInProgress = true;
166
+ if (this.sceneExecutionTimer) {
167
+ clearTimeout(this.sceneExecutionTimer);
168
+ }
169
+ this.sceneExecutionTimer = setTimeout(() => {
170
+ this.sceneExecutionInProgress = false;
171
+ this.log(`[场景控制] 场景执行完成`);
172
+ }, 2000);
173
+ } else {
174
+ this.error(`[场景控制] 场景控制命令失败: status=${frame.status}`);
175
+ }
176
+ } else {
177
+ // 其他frame类型
178
+ this.debug(`[其他Frame] ${frameHex}`);
179
+ }
180
+ };
181
+
182
+ // 处理状态事件队列 - 使用异步队列确保所有事件都被处理
183
+ SymiGatewayNode.prototype.processStateEventQueue = async function() {
184
+ if (this.isProcessingStateEvent || this.stateEventQueue.length === 0) {
185
+ return;
186
+ }
187
+
188
+ this.isProcessingStateEvent = true;
189
+
190
+ while (this.stateEventQueue.length > 0) {
191
+ const frame = this.stateEventQueue.shift();
192
+
134
193
  try {
135
194
  const event = parseStatusEvent(frame);
136
- this.debug(`[状态事件] 地址=0x${event.networkAddress.toString(16).toUpperCase()}, 消息类型=0x${event.attrType.toString(16).toUpperCase()}, 参数=[${Array.from(event.parameters).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
137
-
195
+
196
+ // 检查是否是场景执行通知事件
197
+ if (event.subOpcode === 0x11) {
198
+ const sceneId = event.parameters.length > 0 ? event.parameters[0] : 'unknown';
199
+ const frameHex = Buffer.concat([
200
+ Buffer.from([frame.header, frame.opcode, frame.status, frame.length]),
201
+ frame.payload,
202
+ Buffer.from([frame.checksum])
203
+ ]).toString('hex').toUpperCase();
204
+ this.log(`[场景执行] 收到场景执行通知事件: 场景ID=${sceneId}, 设备地址=0x${event.networkAddress.toString(16).toUpperCase()}, 原始帧=${frameHex}`);
205
+ continue;
206
+ }
207
+
208
+ // 记录状态事件
209
+ const logPrefix = this.sceneExecutionInProgress ? '[场景执行]' : '[状态事件]';
210
+ if (this.sceneExecutionInProgress) {
211
+ // 场景执行期间使用log级别,便于查看
212
+ this.log(`${logPrefix} 地址=0x${event.networkAddress.toString(16).toUpperCase()}, 消息类型=0x${event.attrType.toString(16).toUpperCase()}, 参数=[${Array.from(event.parameters).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
213
+ } else {
214
+ this.debug(`${logPrefix} 地址=0x${event.networkAddress.toString(16).toUpperCase()}, 消息类型=0x${event.attrType.toString(16).toUpperCase()}, 参数=[${Array.from(event.parameters).map(p => '0x' + p.toString(16).toUpperCase()).join(', ')}]`);
215
+ }
216
+
138
217
  const device = this.deviceManager.updateDeviceState(
139
218
  event.networkAddress,
140
219
  event.attrType,
141
220
  event.parameters
142
221
  );
143
-
222
+
144
223
  // 只在非查询状态期间才发送state-changed事件到MQTT
145
224
  // 查询状态期间的事件只用于更新设备状态,不触发MQTT发布
146
225
  if (device && !this.isQueryingStates) {
@@ -148,24 +227,26 @@ module.exports = function(RED) {
148
227
  device: device,
149
228
  attrType: event.attrType,
150
229
  parameters: event.parameters,
151
- state: device.state
230
+ state: device.state,
231
+ isSceneExecution: this.sceneExecutionInProgress // 标记是否是场景执行导致的状态变化
152
232
  });
153
233
  }
234
+
235
+ // 添加小延迟,避免CPU占用过高
236
+ if (this.stateEventQueue.length > 0) {
237
+ await new Promise(resolve => setImmediate(resolve));
238
+ }
154
239
  } catch (error) {
155
240
  this.error(`Error parsing status event: ${error.message}`);
156
241
  }
157
- } else if (frame.opcode === 0xB0) {
158
- // 控制命令响应
159
- this.debug(`[控制响应] 0xB0: ${frameHex} ${frame.status === 0 ? '(成功)' : '(失败)'}`)
160
- } else {
161
- // 其他frame类型
162
- this.debug(`[其他Frame] ${frameHex}`);
163
242
  }
243
+
244
+ this.isProcessingStateEvent = false;
164
245
  };
165
246
 
166
247
  SymiGatewayNode.prototype.parseDeviceListFrame = function(frame) {
167
248
  if (frame.payload.length < 16) return;
168
-
249
+
169
250
  const maxDevices = frame.payload[0];
170
251
  const index = frame.payload[1];
171
252
  const macBytes = frame.payload.slice(2, 8);
@@ -209,14 +290,15 @@ module.exports = function(RED) {
209
290
  SymiGatewayNode.prototype.queryAllDeviceStates = async function() {
210
291
  const devices = this.deviceManager.getAllDevices();
211
292
  this.log(`开始查询${devices.length}个设备的初始状态...`);
212
-
293
+
213
294
  for (const device of devices) {
214
295
  try {
215
296
  let queryAttrs = [];
216
-
297
+
217
298
  // 根据设备类型查询不同属性
218
299
  if ([1, 2, 3].includes(device.deviceType)) {
219
300
  queryAttrs = [0x02]; // 开关状态
301
+ this.log(`查询开关设备: ${device.name} (地址=0x${device.networkAddress.toString(16).toUpperCase()}, 路数=${device.channels})`);
220
302
  } else if (device.deviceType === 9) {
221
303
  queryAttrs = [0x02, 0x0E]; // 插卡取电:开关状态、插卡状态
222
304
  } else if (device.deviceType === 4 || device.deviceType === 0x18) {
@@ -367,17 +449,20 @@ module.exports = function(RED) {
367
449
 
368
450
  // 使用状态组合算法
369
451
  const stateValue = this.protocolHandler.buildSwitchState(channels, targetChannel, targetState, currentState);
370
-
452
+
371
453
  let controlParam;
454
+ let stateValueHex;
372
455
  if (Buffer.isBuffer(stateValue)) {
373
456
  // 6路开关,2字节状态
374
457
  controlParam = stateValue;
458
+ stateValueHex = stateValue.toString('hex').toUpperCase();
375
459
  } else {
376
460
  // 1-4路开关,1字节状态
377
461
  controlParam = Buffer.from([stateValue]);
462
+ stateValueHex = stateValue.toString(16).toUpperCase();
378
463
  }
379
-
380
- this.debug(`发送开关控制: 地址0x${networkAddr.toString(16).toUpperCase()}, 第${targetChannel}路${targetState ? '开' : '关'}, 状态值0x${stateValue.toString(16).toUpperCase()}`);
464
+
465
+ this.debug(`发送开关控制: 地址0x${networkAddr.toString(16).toUpperCase()}, 第${targetChannel}路${targetState ? '开' : '关'}, 状态值0x${stateValueHex}`);
381
466
  const frame = this.protocolHandler.buildDeviceControlFrame(networkAddr, attrType, controlParam);
382
467
  return await this.client.sendFrame(frame, 1);
383
468