node-red-contrib-symi-mesh 1.8.1 → 1.8.2
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 +97 -4
- package/nodes/symi-ha-sync.js +103 -35
- package/nodes/symi-mqtt-sync.js +155 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -396,7 +396,7 @@ Symi HA同步节点 (`symi-ha-sync`) 用于实现Symi蓝牙Mesh设备与Home Ass
|
|
|
396
396
|
- **Symi -> HA**:Mesh设备状态变化 -> 更新HA实体状态
|
|
397
397
|
- **HA -> Symi**:HA实体状态变化 -> 控制Mesh设备
|
|
398
398
|
3. **防死循环**:
|
|
399
|
-
- 内置
|
|
399
|
+
- 内置2秒冷却机制(窗帘30秒)
|
|
400
400
|
- 区分Symi触发和HA触发,避免信号震荡
|
|
401
401
|
4. **便捷配置**:
|
|
402
402
|
- 自动加载所有Symi设备和HA实体
|
|
@@ -422,6 +422,87 @@ Symi HA同步节点 (`symi-ha-sync`) 用于实现Symi蓝牙Mesh设备与Home Ass
|
|
|
422
422
|
- **实体类型**:建议同步相同类型的实体(如开关对开关,调光灯对灯光)
|
|
423
423
|
- **多通道设备**:对于多键开关,请分别为每个按键添加一条映射
|
|
424
424
|
|
|
425
|
+
#### 三合一面板配置
|
|
426
|
+
|
|
427
|
+
三合一面板(空调+新风+地暖)需要分别配置每个子设备的映射:
|
|
428
|
+
|
|
429
|
+
1. **选择三合一设备**:在Symi设备下拉框中选择三合一面板
|
|
430
|
+
2. **选择子设备**:在按键选择器中选择要同步的功能:
|
|
431
|
+
- **空调**:同步开关、温度、模式、风速
|
|
432
|
+
- **新风**:同步开关、风速
|
|
433
|
+
- **地暖**:同步开关、温度
|
|
434
|
+
3. **选择HA实体**:
|
|
435
|
+
- 空调 → climate实体
|
|
436
|
+
- 新风 → fan实体
|
|
437
|
+
- 地暖 → climate实体
|
|
438
|
+
|
|
439
|
+
**示例配置**:
|
|
440
|
+
```
|
|
441
|
+
三合一面板_xxx [空调] ↔ climate.living_room_ac
|
|
442
|
+
三合一面板_xxx [新风] ↔ fan.living_room_fresh_air
|
|
443
|
+
三合一面板_xxx [地暖] ↔ climate.living_room_floor_heating
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### 窗帘同步说明
|
|
447
|
+
|
|
448
|
+
窗帘设备采用特殊的同步机制,避免运动过程中的步进反馈干扰:
|
|
449
|
+
|
|
450
|
+
- **30秒防死循环窗口**:窗帘运动时间较长,使用30秒防死循环(普通设备2秒)
|
|
451
|
+
- **1.5秒防抖**:等待窗帘位置稳定后再同步,避免步进码干扰
|
|
452
|
+
- **运动状态跟踪**:记录运动发起方(Symi/HA),忽略对方的中间状态反馈
|
|
453
|
+
- **opening/closing过滤**:HA窗帘处于运动状态时,不同步位置变化到Mesh
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
### MQTT品牌同步
|
|
457
|
+
|
|
458
|
+
Symi MQTT同步节点 (`symi-mqtt-sync`) 用于实现第三方MQTT品牌设备与Symi Mesh设备的双向状态同步。
|
|
459
|
+
|
|
460
|
+
#### 功能特性
|
|
461
|
+
|
|
462
|
+
1. **双MQTT配置**:
|
|
463
|
+
- Mesh MQTT:连接Symi网关获取设备列表
|
|
464
|
+
- 品牌MQTT:连接第三方品牌MQTT服务器
|
|
465
|
+
2. **设备自动发现**:品牌MQTT连接后自动发现设备
|
|
466
|
+
3. **双向同步**:品牌设备↔Mesh设备实时状态同步
|
|
467
|
+
4. **配置持久化**:设备列表和映射配置持久保存,断线后仍可显示
|
|
468
|
+
|
|
469
|
+
#### 支持的品牌协议
|
|
470
|
+
|
|
471
|
+
| 品牌 | 设备类型 | 功能码映射 |
|
|
472
|
+
|------|---------|-----------|
|
|
473
|
+
| HYQW(花语前湾) | 灯具(8) | 开关(fn=1)、亮度(fn=2, 0-100) |
|
|
474
|
+
| | 空调(12) | 开关(fn=1)、温度(fn=2, 18-29°C)、模式(fn=3)、风速(fn=4) |
|
|
475
|
+
| | 窗帘(14) | 动作(fn=1, 开/关/停)、位置(fn=2, 0-100%) |
|
|
476
|
+
| | 地暖(16) | 开关(fn=1)、温度(fn=2, 5-35°C) |
|
|
477
|
+
| | 新风(36) | 开关(fn=1)、风速(fn=3) |
|
|
478
|
+
|
|
479
|
+
#### 配置步骤
|
|
480
|
+
|
|
481
|
+
1. **添加品牌MQTT配置节点**:
|
|
482
|
+
- 从左侧拖入`Symi MQTT Brand`配置节点
|
|
483
|
+
- 配置品牌MQTT服务器地址、用户名、密码
|
|
484
|
+
- 选择品牌协议(如HYQW)
|
|
485
|
+
|
|
486
|
+
2. **添加MQTT同步节点**:
|
|
487
|
+
- 从左侧拖入`Symi MQTT Sync`节点
|
|
488
|
+
- 选择Mesh MQTT配置(用于获取Mesh设备)
|
|
489
|
+
- 选择品牌MQTT配置(用于获取品牌设备)
|
|
490
|
+
|
|
491
|
+
3. **配置实体映射**:
|
|
492
|
+
- 点击"添加"按钮
|
|
493
|
+
- 左侧选择Mesh设备(多路开关可选择按键)
|
|
494
|
+
- 右侧选择品牌设备
|
|
495
|
+
- 可添加多组映射
|
|
496
|
+
|
|
497
|
+
4. **部署**:点击部署,开始双向同步
|
|
498
|
+
|
|
499
|
+
#### 注意事项
|
|
500
|
+
|
|
501
|
+
- **设备类型匹配**:建议同步相同类型的设备(灯具对灯具,空调对空调)
|
|
502
|
+
- **离线设备显示**:[离线]标记缓存中但当前不在线的设备
|
|
503
|
+
- **防死循环**:内置2秒防抖机制,避免状态震荡
|
|
504
|
+
- **自动重连**:5秒重连间隔,断线自动恢复
|
|
505
|
+
|
|
425
506
|
|
|
426
507
|
## 协议说明
|
|
427
508
|
|
|
@@ -1332,8 +1413,8 @@ A: 每个房间部署一个云端同步节点,配置对应的酒店ID和房间
|
|
|
1332
1413
|
| **Symi KNX Bridge** | KNX系统双向同步 | [KNX双向同步](#knx双向同步) |
|
|
1333
1414
|
| **Symi KNX-HA Bridge** | KNX与HA实体双向同步 | [KNX-HA双向同步](#knx-ha双向同步) |
|
|
1334
1415
|
| **Symi HA Sync** | HA实体双向同步 | [HA双向同步](#ha双向同步) |
|
|
1335
|
-
| **Symi MQTT Sync** | 第三方MQTT品牌设备同步 |
|
|
1336
|
-
| **Symi MQTT Brand** | 品牌MQTT配置节点 |
|
|
1416
|
+
| **Symi MQTT Sync** | 第三方MQTT品牌设备同步 | [MQTT品牌同步](#mqtt品牌同步) |
|
|
1417
|
+
| **Symi MQTT Brand** | 品牌MQTT配置节点 | [MQTT品牌同步](#mqtt品牌同步) |
|
|
1337
1418
|
| **RS485 Debug** | RS485总线数据抓包调试 | [RS485调试节点](#rs485调试节点) |
|
|
1338
1419
|
| **Symi RS485 Sync** | 两种RS485协议双向同步 | [RS485协议同步](#rs485协议同步) |
|
|
1339
1420
|
|
|
@@ -1503,6 +1584,18 @@ node-red-contrib-symi-mesh/
|
|
|
1503
1584
|
|
|
1504
1585
|
## 更新日志
|
|
1505
1586
|
|
|
1587
|
+
### v1.8.2 (2026-01-05)
|
|
1588
|
+
- **MQTT品牌同步协议修复**:
|
|
1589
|
+
- **窗帘设备协议修复**:修复窗帘fn=1功能码的正确解析(0=关闭, 1=打开, 2=停止),之前错误地当作开关处理
|
|
1590
|
+
- **设备类型独立处理**:重构syncToMesh和syncToMqtt函数,按设备类型(8/12/14/16/36)独立处理功能码映射
|
|
1591
|
+
- **完整功能码支持**:
|
|
1592
|
+
- 灯具(8): fn=1开关, fn=2亮度
|
|
1593
|
+
- 空调(12): fn=1开关, fn=2温度, fn=3模式, fn=4风速
|
|
1594
|
+
- 窗帘(14): fn=1动作(开/关/停), fn=2位置
|
|
1595
|
+
- 地暖(16): fn=1开关, fn=2温度
|
|
1596
|
+
- 新风(36): fn=1开关, fn=3风速
|
|
1597
|
+
- **错误日志增强**:同步失败时输出详细错误信息,便于调试
|
|
1598
|
+
|
|
1506
1599
|
### v1.8.1 (2026-01-05)
|
|
1507
1600
|
- **三合一面板深度集成**:
|
|
1508
1601
|
- **子设备选择**:HA同步节点新增三合一子设备选择功能,可分别选择“空调”、“新风”、“地暖”进行独立映射。
|
|
@@ -1600,7 +1693,7 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1600
1693
|
## 关于
|
|
1601
1694
|
|
|
1602
1695
|
**作者**: SYMI 亖米
|
|
1603
|
-
**版本**: 1.8.
|
|
1696
|
+
**版本**: 1.8.2
|
|
1604
1697
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1605
1698
|
**最后更新**: 2026-01-05
|
|
1606
1699
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
package/nodes/symi-ha-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Symi HA Sync Node - Symi设备与Home Assistant实体双向同步
|
|
3
|
-
* 版本: 1.8.
|
|
3
|
+
* 版本: 1.8.2
|
|
4
4
|
*
|
|
5
5
|
* 支持的实体类型和属性:
|
|
6
6
|
* - light: on/off, brightness (0-255)
|
|
@@ -15,7 +15,7 @@ module.exports = function(RED) {
|
|
|
15
15
|
|
|
16
16
|
// 常量定义
|
|
17
17
|
const LOOP_PREVENTION_MS = 2000; // 防死循环时间窗口
|
|
18
|
-
const DEBOUNCE_MS = 500; //
|
|
18
|
+
const DEBOUNCE_MS = 500; // 通用防抖时间
|
|
19
19
|
const MAX_QUEUE_SIZE = 100;
|
|
20
20
|
const CLEANUP_INTERVAL_MS = 60000;
|
|
21
21
|
const TIMESTAMP_EXPIRE_MS = 60000;
|
|
@@ -23,6 +23,9 @@ module.exports = function(RED) {
|
|
|
23
23
|
// 窗帘专用常量
|
|
24
24
|
const COVER_LOOP_PREVENTION_MS = 30000; // 30秒防死循环
|
|
25
25
|
const COVER_DEBOUNCE_MS = 1500; // 1.5秒防抖
|
|
26
|
+
|
|
27
|
+
// 调光专用常量
|
|
28
|
+
const BRIGHTNESS_DEBOUNCE_MS = 800; // 0.8秒防抖,过滤步进过程
|
|
26
29
|
|
|
27
30
|
// Mesh属性类型
|
|
28
31
|
const ATTR_SWITCH = 0x02;
|
|
@@ -92,6 +95,7 @@ module.exports = function(RED) {
|
|
|
92
95
|
node.lastHaToSymi = {};
|
|
93
96
|
node.pendingDebounce = {}; // 防抖定时器
|
|
94
97
|
node.coverMoving = {}; // 窗帘运动状态跟踪 { loopKey: { direction: 'symi'|'ha', startTime, targetPosition } }
|
|
98
|
+
node.brightnessMoving = {}; // 调光运动状态跟踪 { loopKey: { direction: 'ha', startTime } }
|
|
95
99
|
|
|
96
100
|
node.status({ fill: 'yellow', shape: 'ring', text: '初始化中' });
|
|
97
101
|
|
|
@@ -130,10 +134,17 @@ module.exports = function(RED) {
|
|
|
130
134
|
|
|
131
135
|
// 清理过期的窗帘运动状态
|
|
132
136
|
for (const key in node.coverMoving) {
|
|
133
|
-
if (now - node.coverMoving[key].
|
|
137
|
+
if (now - node.coverMoving[key].startTime > COVER_LOOP_PREVENTION_MS) {
|
|
134
138
|
delete node.coverMoving[key];
|
|
135
139
|
}
|
|
136
140
|
}
|
|
141
|
+
|
|
142
|
+
// 清理过期的调光运动状态
|
|
143
|
+
for (const key in node.brightnessMoving) {
|
|
144
|
+
if (now - node.brightnessMoving[key].startTime > LOOP_PREVENTION_MS) {
|
|
145
|
+
delete node.brightnessMoving[key];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
137
148
|
}, CLEANUP_INTERVAL_MS);
|
|
138
149
|
|
|
139
150
|
// 防死循环检查 - 双向时间戳检查
|
|
@@ -367,14 +378,47 @@ module.exports = function(RED) {
|
|
|
367
378
|
return { type: 'switch', value: isOn };
|
|
368
379
|
};
|
|
369
380
|
|
|
370
|
-
//
|
|
381
|
+
// 处理亮度变化(带防抖,避免步进过程干扰)
|
|
371
382
|
node.handleBrightnessChange = function(device, mapping, state) {
|
|
372
383
|
const brightness = state.brightness !== undefined ? state.brightness : device.state.brightness;
|
|
373
384
|
if (brightness === undefined) return null;
|
|
374
385
|
|
|
386
|
+
const loopKey = `${mapping.symiMac}_${mapping.symiKey}_${mapping.haEntityId}`;
|
|
387
|
+
|
|
388
|
+
// 检查是否是HA发起的调光,如果是则忽略Mesh的亮度反馈(步进码)
|
|
389
|
+
if (node.brightnessMoving && node.brightnessMoving[loopKey]) {
|
|
390
|
+
const elapsed = Date.now() - node.brightnessMoving[loopKey].startTime;
|
|
391
|
+
if (elapsed < LOOP_PREVENTION_MS) {
|
|
392
|
+
node.debug(`[Symi->HA] 亮度忽略(HA发起调光中): ${loopKey}`);
|
|
393
|
+
return null;
|
|
394
|
+
} else {
|
|
395
|
+
delete node.brightnessMoving[loopKey];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 防抖处理:取消之前的定时器,设置新的
|
|
400
|
+
const debounceKey = `brightness_${mapping.symiMac}_${mapping.symiKey}`;
|
|
401
|
+
if (node.pendingDebounce[debounceKey]) {
|
|
402
|
+
clearTimeout(node.pendingDebounce[debounceKey]);
|
|
403
|
+
}
|
|
404
|
+
|
|
375
405
|
// Mesh亮度0-100,HA亮度0-255
|
|
376
406
|
const haBrightness = Math.round(brightness * 255 / 100);
|
|
377
|
-
|
|
407
|
+
|
|
408
|
+
// 延迟同步,等待亮度稳定(过滤步进过程)
|
|
409
|
+
node.pendingDebounce[debounceKey] = setTimeout(() => {
|
|
410
|
+
delete node.pendingDebounce[debounceKey];
|
|
411
|
+
|
|
412
|
+
node.queueCommand({
|
|
413
|
+
direction: 'symi-to-ha',
|
|
414
|
+
mapping: mapping,
|
|
415
|
+
syncData: { type: 'brightness', value: haBrightness, meshValue: brightness },
|
|
416
|
+
key: loopKey,
|
|
417
|
+
skipLoopCheck: true
|
|
418
|
+
});
|
|
419
|
+
}, BRIGHTNESS_DEBOUNCE_MS);
|
|
420
|
+
|
|
421
|
+
return null; // 不立即同步,由定时器处理
|
|
378
422
|
};
|
|
379
423
|
|
|
380
424
|
// 处理窗帘变化(带防抖,避免步进反馈干扰)
|
|
@@ -487,19 +531,47 @@ module.exports = function(RED) {
|
|
|
487
531
|
}
|
|
488
532
|
});
|
|
489
533
|
|
|
490
|
-
// 方式B: 尝试订阅HA Server事件总线
|
|
491
|
-
|
|
492
|
-
node.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
534
|
+
// 方式B: 尝试订阅HA Server事件总线 (静默订阅机制)
|
|
535
|
+
const subscribeToHaEvents = () => {
|
|
536
|
+
if (!node.haServer) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// node-red-contrib-home-assistant-websocket 不同版本可能暴露不同的事件总线
|
|
541
|
+
let eventBus = node.haServer.eventBus;
|
|
542
|
+
if (!eventBus && node.haServer.controller) {
|
|
543
|
+
eventBus = node.haServer.controller.events;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (eventBus) {
|
|
547
|
+
// 防止重复订阅
|
|
548
|
+
if (node.haSubscribed) return;
|
|
549
|
+
|
|
550
|
+
node.haEventHandler = (evt) => {
|
|
551
|
+
if (evt && evt.event_type === 'state_changed' && evt.data) {
|
|
552
|
+
node.handleHaStateChange(evt.data.entity_id, evt.data.new_state, evt.data.old_state);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
eventBus.on('ha_events:all', node.haEventHandler);
|
|
556
|
+
node.haSubscribed = true;
|
|
557
|
+
node.log('[HA同步] 已成功订阅HA事件总线');
|
|
558
|
+
node.status({ fill: 'green', shape: 'dot', text: '已连接' });
|
|
559
|
+
} else {
|
|
560
|
+
// 静默重试,不打印 info 日志,只在后台运行
|
|
561
|
+
node.haSubscribedRetryCount = (node.haSubscribedRetryCount || 0) + 1;
|
|
562
|
+
|
|
563
|
+
// 每 5 秒检查一次,直到成功为止(不设上限,因为这是后台静默检查)
|
|
564
|
+
// 只有在前 3 次且是 debug 模式下才打印
|
|
565
|
+
if (node.haSubscribedRetryCount <= 3) {
|
|
566
|
+
node.debug(`[HA同步] 等待 HA 事件总线就绪... (${node.haSubscribedRetryCount})`);
|
|
496
567
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
568
|
+
|
|
569
|
+
setTimeout(subscribeToHaEvents, 5000);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// 启动后立即开始静默尝试
|
|
574
|
+
subscribeToHaEvents();
|
|
503
575
|
|
|
504
576
|
node.handleHaStateChange = function(entityId, newState, oldState) {
|
|
505
577
|
if (!newState) return;
|
|
@@ -545,6 +617,8 @@ module.exports = function(RED) {
|
|
|
545
617
|
if (!oldState || oldAttrs.brightness !== attrs.brightness) {
|
|
546
618
|
// HA亮度0-255,Mesh亮度0-100
|
|
547
619
|
const meshBrightness = Math.round(attrs.brightness * 100 / 255);
|
|
620
|
+
// 标记HA发起的调光,防止Mesh步进反馈回传
|
|
621
|
+
node.brightnessMoving[loopKey] = { direction: 'ha', startTime: Date.now() };
|
|
548
622
|
syncDataList.push({ type: 'brightness', value: meshBrightness });
|
|
549
623
|
}
|
|
550
624
|
}
|
|
@@ -831,6 +905,16 @@ module.exports = function(RED) {
|
|
|
831
905
|
const channels = device.channels;
|
|
832
906
|
const targetChannel = mapping.symiKey;
|
|
833
907
|
|
|
908
|
+
// 检查是否是三合一设备(symiKey为字符串类型)
|
|
909
|
+
const isThreeInOne = typeof targetChannel === 'string' &&
|
|
910
|
+
['aircon', 'fresh_air', 'floor_heating'].includes(targetChannel);
|
|
911
|
+
|
|
912
|
+
if (isThreeInOne) {
|
|
913
|
+
// 三合一设备使用专门的控制函数
|
|
914
|
+
await node.syncThreeInOne(cmd, device, networkAddr);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
834
918
|
try {
|
|
835
919
|
let attrType, param;
|
|
836
920
|
|
|
@@ -881,23 +965,6 @@ module.exports = function(RED) {
|
|
|
881
965
|
|
|
882
966
|
node.log(`[HA->Symi] ${mapping.haEntityId} -> ${device.name || mapping.symiMac}: ${syncData.type}=${JSON.stringify(syncData.value)}`);
|
|
883
967
|
|
|
884
|
-
// 仅输出到debug,不再重复send
|
|
885
|
-
/*
|
|
886
|
-
node.send({
|
|
887
|
-
topic: 'ha-sync/ha-to-symi',
|
|
888
|
-
payload: {
|
|
889
|
-
direction: 'HA→Symi',
|
|
890
|
-
haEntityId: mapping.haEntityId,
|
|
891
|
-
symiMac: mapping.symiMac,
|
|
892
|
-
symiKey: mapping.symiKey,
|
|
893
|
-
syncType: syncData.type,
|
|
894
|
-
value: syncData.value,
|
|
895
|
-
attrType: attrType,
|
|
896
|
-
timestamp: Date.now()
|
|
897
|
-
}
|
|
898
|
-
});
|
|
899
|
-
*/
|
|
900
|
-
|
|
901
968
|
} catch (err) {
|
|
902
969
|
node.error(`[HA->Symi] 控制失败: ${err.message}`);
|
|
903
970
|
}
|
|
@@ -982,7 +1049,8 @@ module.exports = function(RED) {
|
|
|
982
1049
|
clearTimeout(node.pendingDebounce[key]);
|
|
983
1050
|
}
|
|
984
1051
|
node.pendingDebounce = {};
|
|
985
|
-
node.coverMoving = {};
|
|
1052
|
+
node.coverMoving = {}; // 清理窗帘运动状态
|
|
1053
|
+
node.brightnessMoving = {}; // 清理调光运动状态
|
|
986
1054
|
|
|
987
1055
|
if (gateway) {
|
|
988
1056
|
gateway.removeListener('device-state-changed', node.handleSymiStateChange);
|
package/nodes/symi-mqtt-sync.js
CHANGED
|
@@ -293,33 +293,86 @@ module.exports = function(RED) {
|
|
|
293
293
|
|
|
294
294
|
let property = '', value = null;
|
|
295
295
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
296
|
+
// 窗帘设备特殊处理 (typeId=14)
|
|
297
|
+
if (deviceType === 14) {
|
|
298
|
+
if (fn === 1) {
|
|
299
|
+
// fn=1: 窗帘动作控制 (fv: 0=关闭, 1=打开, 2=停止)
|
|
300
|
+
if (fv === 1) {
|
|
301
|
+
property = 'curtainAction'; value = 'open';
|
|
302
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'curtainAction', 'open');
|
|
303
|
+
} else if (fv === 0) {
|
|
304
|
+
property = 'curtainAction'; value = 'close';
|
|
305
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'curtainAction', 'close');
|
|
306
|
+
} else if (fv === 2) {
|
|
307
|
+
property = 'curtainAction'; value = 'stop';
|
|
308
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'curtainAction', 'stop');
|
|
309
|
+
}
|
|
310
|
+
} else if (fn === 2) {
|
|
311
|
+
// fn=2: 窗帘位置 (fv: 0-100%)
|
|
312
|
+
property = 'position'; value = fv;
|
|
313
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'position', fv);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// 灯具设备 (typeId=8)
|
|
317
|
+
else if (deviceType === 8) {
|
|
318
|
+
if (fn === 1) {
|
|
319
|
+
property = 'switch'; value = fv === 1;
|
|
320
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'switch', fv === 1);
|
|
321
|
+
} else if (fn === 2) {
|
|
322
|
+
property = 'brightness'; value = fv;
|
|
323
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'brightness', fv);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// 空调设备 (typeId=12)
|
|
327
|
+
else if (deviceType === 12) {
|
|
328
|
+
if (fn === 1) {
|
|
329
|
+
property = 'switch'; value = fv === 1;
|
|
330
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'switch', fv === 1);
|
|
331
|
+
} else if (fn === 2) {
|
|
332
|
+
property = 'temperature'; value = fv;
|
|
333
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'temperature', fv);
|
|
334
|
+
} else if (fn === 3) {
|
|
306
335
|
const mode = AC_MODE_MAP[fv];
|
|
307
336
|
if (mode) {
|
|
308
337
|
property = 'mode'; value = mode;
|
|
309
338
|
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'mode', mode);
|
|
310
339
|
}
|
|
311
|
-
} else if (
|
|
340
|
+
} else if (fn === 4) {
|
|
341
|
+
const speed = FAN_SPEED_MAP[fv];
|
|
342
|
+
if (speed) {
|
|
343
|
+
property = 'fanSpeed'; value = speed;
|
|
344
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'fanSpeed', speed);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// 地暖设备 (typeId=16)
|
|
349
|
+
else if (deviceType === 16) {
|
|
350
|
+
if (fn === 1) {
|
|
351
|
+
property = 'switch'; value = fv === 1;
|
|
352
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'switch', fv === 1);
|
|
353
|
+
} else if (fn === 2) {
|
|
354
|
+
property = 'temperature'; value = fv;
|
|
355
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'temperature', fv);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// 新风设备 (typeId=36)
|
|
359
|
+
else if (deviceType === 36) {
|
|
360
|
+
if (fn === 1) {
|
|
361
|
+
property = 'switch'; value = fv === 1;
|
|
362
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'switch', fv === 1);
|
|
363
|
+
} else if (fn === 3) {
|
|
312
364
|
const speed = FAN_SPEED_MAP[fv];
|
|
313
365
|
if (speed) {
|
|
314
366
|
property = 'fanSpeed'; value = speed;
|
|
315
367
|
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'fanSpeed', speed);
|
|
316
368
|
}
|
|
317
369
|
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
370
|
+
}
|
|
371
|
+
// 其他设备类型的通用处理
|
|
372
|
+
else {
|
|
373
|
+
if (fn === 1) {
|
|
374
|
+
property = 'switch'; value = fv === 1;
|
|
375
|
+
node._gateway.deviceManager.controlDevice(meshMac, meshChannel, 'switch', fv === 1);
|
|
323
376
|
}
|
|
324
377
|
}
|
|
325
378
|
|
|
@@ -335,7 +388,9 @@ module.exports = function(RED) {
|
|
|
335
388
|
}
|
|
336
389
|
});
|
|
337
390
|
}
|
|
338
|
-
} catch (e) {
|
|
391
|
+
} catch (e) {
|
|
392
|
+
node.warn(`[Brand→Mesh] 同步失败: ${e.message}`);
|
|
393
|
+
}
|
|
339
394
|
}
|
|
340
395
|
|
|
341
396
|
// ===== 同步到MQTT =====
|
|
@@ -353,18 +408,86 @@ module.exports = function(RED) {
|
|
|
353
408
|
const typeInfo = node._brandProtocol.deviceTypes[deviceType];
|
|
354
409
|
if (!typeInfo) return;
|
|
355
410
|
|
|
356
|
-
//
|
|
357
|
-
let fn;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
411
|
+
// 根据设备类型和属性确定功能码
|
|
412
|
+
let fn, fv;
|
|
413
|
+
|
|
414
|
+
// 窗帘设备特殊处理 (typeId=14)
|
|
415
|
+
if (deviceType === 14) {
|
|
416
|
+
if (property === 'curtainAction' || property === 'action') {
|
|
417
|
+
fn = 1;
|
|
418
|
+
if (value === 'open' || value === 1) fv = 1;
|
|
419
|
+
else if (value === 'close' || value === 0) fv = 0;
|
|
420
|
+
else if (value === 'stop' || value === 2) fv = 2;
|
|
421
|
+
else return;
|
|
422
|
+
} else if (property === 'position' || property === 'curtainPosition') {
|
|
423
|
+
fn = 2;
|
|
424
|
+
fv = parseInt(value) || 0;
|
|
425
|
+
} else {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// 灯具设备 (typeId=8)
|
|
430
|
+
else if (deviceType === 8) {
|
|
431
|
+
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
432
|
+
fn = 1;
|
|
433
|
+
fv = value ? 1 : 0;
|
|
434
|
+
} else if (property === 'brightness') {
|
|
435
|
+
fn = 2;
|
|
436
|
+
fv = parseInt(value) || 0;
|
|
437
|
+
} else {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// 空调设备 (typeId=12)
|
|
442
|
+
else if (deviceType === 12) {
|
|
443
|
+
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
444
|
+
fn = 1;
|
|
445
|
+
fv = value ? 1 : 0;
|
|
446
|
+
} else if (property === 'temperature' || property === 'targetTemp') {
|
|
447
|
+
fn = 2;
|
|
448
|
+
fv = parseInt(value) || 24;
|
|
449
|
+
} else if (property === 'mode' || property === 'hvacMode' || property === 'climateMode') {
|
|
450
|
+
fn = 3;
|
|
451
|
+
fv = AC_MODE_REVERSE[value] ?? 0;
|
|
452
|
+
} else if (property === 'fanSpeed' || property === 'fanMode') {
|
|
453
|
+
fn = 4;
|
|
454
|
+
fv = FAN_SPEED_REVERSE[value] ?? 0;
|
|
455
|
+
} else {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// 地暖设备 (typeId=16)
|
|
460
|
+
else if (deviceType === 16) {
|
|
461
|
+
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
462
|
+
fn = 1;
|
|
463
|
+
fv = value ? 1 : 0;
|
|
464
|
+
} else if (property === 'temperature' || property === 'targetTemp') {
|
|
465
|
+
fn = 2;
|
|
466
|
+
fv = parseInt(value) || 20;
|
|
467
|
+
} else {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// 新风设备 (typeId=36)
|
|
472
|
+
else if (deviceType === 36) {
|
|
473
|
+
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
474
|
+
fn = 1;
|
|
475
|
+
fv = value ? 1 : 0;
|
|
476
|
+
} else if (property === 'fanSpeed' || property === 'fanMode') {
|
|
477
|
+
fn = 3;
|
|
478
|
+
fv = FAN_SPEED_REVERSE[value] ?? 0;
|
|
479
|
+
} else {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// 其他设备类型的通用处理
|
|
484
|
+
else {
|
|
485
|
+
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
486
|
+
fn = 1;
|
|
487
|
+
fv = value ? 1 : 0;
|
|
488
|
+
} else {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
368
491
|
}
|
|
369
492
|
|
|
370
493
|
// 防死循环 - 检查两个方向的时间戳
|
|
@@ -381,19 +504,6 @@ module.exports = function(RED) {
|
|
|
381
504
|
node._syncTimestamps.set(meshSyncKey, now);
|
|
382
505
|
node._syncTimestamps.set(mqttSyncKey, now);
|
|
383
506
|
|
|
384
|
-
let fv;
|
|
385
|
-
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
386
|
-
fv = value ? 1 : 0;
|
|
387
|
-
} else if (property === 'brightness' || property === 'temperature' || property === 'position') {
|
|
388
|
-
fv = parseInt(value) || 0;
|
|
389
|
-
} else if (property === 'mode' || property === 'hvacMode') {
|
|
390
|
-
fv = AC_MODE_REVERSE[value] ?? 0;
|
|
391
|
-
} else if (property === 'fanSpeed' || property === 'fanMode') {
|
|
392
|
-
fv = FAN_SPEED_REVERSE[value] ?? 0;
|
|
393
|
-
} else {
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
507
|
const topic = node._brandProtocol.getDownTopic(node);
|
|
398
508
|
const payload = node._brandProtocol.buildMessage(deviceType, deviceId, fn, fv);
|
|
399
509
|
|
|
@@ -410,7 +520,9 @@ module.exports = function(RED) {
|
|
|
410
520
|
timestamp: Date.now()
|
|
411
521
|
}
|
|
412
522
|
});
|
|
413
|
-
} catch (e) {
|
|
523
|
+
} catch (e) {
|
|
524
|
+
node.warn(`[Mesh→Brand] 同步失败: ${e.message}`);
|
|
525
|
+
}
|
|
414
526
|
}
|
|
415
527
|
|
|
416
528
|
// ===== 网关事件监听 =====
|