node-red-contrib-symi-mesh 1.6.9 → 1.7.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.
- package/README.md +89 -47
- package/lib/device-manager.js +56 -5
- package/nodes/symi-485-bridge.html +206 -32
- package/nodes/symi-485-bridge.js +264 -60
- package/nodes/symi-knx-bridge.js +1 -1
- package/nodes/symi-mqtt.js +17 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -382,6 +382,7 @@ npm install node-red-contrib-home-assistant-websocket
|
|
|
382
382
|
- 支持与HA中任意实体同步
|
|
383
383
|
- 配置灵活,易于调整
|
|
384
384
|
|
|
385
|
+
|
|
385
386
|
## 协议说明
|
|
386
387
|
|
|
387
388
|
### 核心协议格式
|
|
@@ -603,7 +604,7 @@ Tab分隔,每行一个实体:
|
|
|
603
604
|
|
|
604
605
|
#### 注意事项
|
|
605
606
|
|
|
606
|
-
1. **
|
|
607
|
+
1. **10秒初始化延迟**:部署后前10秒不同步,等待Mesh设备发现完成
|
|
607
608
|
2. **首次状态缓存**:启动后第一次状态仅缓存,第二次操作才会同步
|
|
608
609
|
3. **防死循环**:KNX控制后1秒内,Mesh状态变化不会反向同步
|
|
609
610
|
|
|
@@ -742,34 +743,67 @@ Mesh 四键开关 第2路 ↔ RS485 六键开关 第5路 地址:2 ✓ 支持
|
|
|
742
743
|
| 通用Modbus | 各类标准Modbus设备 |
|
|
743
744
|
| 自定义协议 | 任意485码匹配(开关/窗帘/场景) |
|
|
744
745
|
|
|
745
|
-
###
|
|
746
|
+
### 自定义协议模式(v1.7.0增强)
|
|
747
|
+
|
|
748
|
+
当内置协议无法满足需求时,可使用"自定义协议"模式,手动录入RS485十六进制码进行双向匹配。
|
|
749
|
+
|
|
750
|
+
#### 自定义开关(4组码)
|
|
751
|
+
|
|
752
|
+
- **发开码**:Mesh开关打开时,发送此码到RS485总线
|
|
753
|
+
- **发关码**:Mesh开关关闭时,发送此码到RS485总线
|
|
754
|
+
- **收开码**:RS485总线收到此码时,触发Mesh开关打开
|
|
755
|
+
- **收关码**:RS485总线收到此码时,触发Mesh开关关闭
|
|
746
756
|
|
|
747
|
-
|
|
757
|
+
**翻转模式**:当收开码=收关码时,收到后自动翻转开关状态
|
|
748
758
|
|
|
749
|
-
####
|
|
750
|
-
- **打开码**:当Mesh开关打开时发送此码,收到此码时触发Mesh开关打开
|
|
751
|
-
- **关闭码**:当Mesh开关关闭时发送此码,收到此码时触发Mesh开关关闭
|
|
759
|
+
#### 自定义窗帘(6组码)
|
|
752
760
|
|
|
753
|
-
|
|
754
|
-
-
|
|
755
|
-
|
|
756
|
-
|
|
761
|
+
- **发开码**、**发关码**、**发停码**:Mesh→RS485
|
|
762
|
+
- **收开码**、**收关码**、**收停码**:RS485→Mesh
|
|
763
|
+
|
|
764
|
+
#### 自定义空调(完整收发码)
|
|
765
|
+
|
|
766
|
+
- **开关**:发开码、发关码、收开码、收关码
|
|
767
|
+
- **风速**:发高风、发中风、发低风、收高风、收中风、收低风
|
|
768
|
+
- **模式**:发制冷、发制热、发除湿、发送风、收制冷、收制热、收除湿、收送风
|
|
769
|
+
- **温度**:发温度码、收温度码(支持16-30度,使用{TEMP}占位符)
|
|
757
770
|
|
|
758
771
|
#### 配置示例
|
|
772
|
+
|
|
773
|
+
**自定义开关(花语前湾大屏)**:
|
|
774
|
+
```
|
|
775
|
+
品牌: 自定义协议
|
|
776
|
+
类型: 自定义开关
|
|
777
|
+
发开码: 01 20 10 14 00 01 00 7F 82 B6
|
|
778
|
+
发关码: 01 20 10 14 00 01 00 FF 83 16
|
|
779
|
+
收开码: 01 20
|
|
780
|
+
收关码: 01 20
|
|
781
|
+
```
|
|
782
|
+
说明:收开码=收关码,收到后翻转开关状态
|
|
783
|
+
|
|
784
|
+
**自定义窗帘**:
|
|
759
785
|
```
|
|
760
786
|
品牌: 自定义协议
|
|
761
787
|
类型: 自定义窗帘
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
788
|
+
发开码: 55 02 02 03 01 49 44
|
|
789
|
+
发关码: 55 02 02 03 02 09 45
|
|
790
|
+
发停码: 55 02 02 03 03 C8 85
|
|
791
|
+
收开码: 55 02 02 03 01
|
|
792
|
+
收关码: 55 02 02 03 02
|
|
793
|
+
收停码: 55 02 02 03 03
|
|
765
794
|
```
|
|
766
795
|
|
|
767
796
|
#### 工作原理
|
|
768
|
-
1. **Mesh→RS485**:Mesh
|
|
769
|
-
2. **RS485→Mesh**:收到的RS485
|
|
770
|
-
3.
|
|
797
|
+
1. **Mesh→RS485**:Mesh设备状态变化时,发送对应的发码
|
|
798
|
+
2. **RS485→Mesh**:收到的RS485帧与收码匹配,触发对应Mesh设备
|
|
799
|
+
3. **翻转逻辑**:开关的收开码=收关码时,收到后翻转当前状态
|
|
800
|
+
4. **防死循环**:500ms内不重复触发,避免Mesh→RS485→Mesh循环
|
|
771
801
|
|
|
772
|
-
|
|
802
|
+
#### 注意事项
|
|
803
|
+
- 支持最多24字节(72个十六进制字符)数据录入
|
|
804
|
+
- 支持空格分隔(如 `55 01 01` 或 `550101`)
|
|
805
|
+
- 自定义模式不需要填写地址字段(自动隐藏)
|
|
806
|
+
- 配置持久化保存,重启后保持
|
|
773
807
|
|
|
774
808
|
### 注意事项
|
|
775
809
|
|
|
@@ -1221,6 +1255,42 @@ node-red-contrib-symi-mesh/
|
|
|
1221
1255
|
|
|
1222
1256
|
## 更新日志
|
|
1223
1257
|
|
|
1258
|
+
### v1.7.1 (2025-12-21)
|
|
1259
|
+
- **自定义协议全面修复**:完善自定义开关/窗帘/空调双向同步
|
|
1260
|
+
- **空调风速控制**:修复风速变化事件触发机制
|
|
1261
|
+
- 添加fanMode字段支持(mesh空调实际使用的字段)
|
|
1262
|
+
- 修复DeviceInfo事件触发,正确发送device-state-changed事件
|
|
1263
|
+
- 支持风速值1-4(1=高, 2=中, 3=低, 4=自动)
|
|
1264
|
+
- 自动识别温控器0x02消息中的风速控制
|
|
1265
|
+
- **空调开关控制**:修复开关状态变化事件触发
|
|
1266
|
+
- 温控器0x02开关消息正确触发device-state-changed事件
|
|
1267
|
+
- 自定义空调开关码(acSendOn/acSendOff)正确发送到RS485总线
|
|
1268
|
+
- **RS485收码匹配**:修复hexStr格式处理
|
|
1269
|
+
- hexStr完全去掉空格,确保标准格式(如030610330001BD27)
|
|
1270
|
+
- 用户录入支持带空格格式(如01 06 10 34 00 01 0D 04)
|
|
1271
|
+
- 自动匹配并触发mesh实体动作
|
|
1272
|
+
- **事件系统优化**:
|
|
1273
|
+
- DeviceInfo添加manager引用,正确触发事件
|
|
1274
|
+
- 修复事件名称不匹配问题(stateChange -> device-state-changed)
|
|
1275
|
+
- 确保所有自定义码双向同步正常
|
|
1276
|
+
- **队列处理**:
|
|
1277
|
+
- 命令队列顺序处理,防止并发冲突
|
|
1278
|
+
- 500ms防死循环机制
|
|
1279
|
+
- 队列限制100条,防止内存溢出
|
|
1280
|
+
- **调试日志**:添加详细调试日志,方便排查问题
|
|
1281
|
+
|
|
1282
|
+
### v1.7.0 (2025-12-21)
|
|
1283
|
+
- **自定义协议增强**:RS485桥接节点自定义协议功能全面升级
|
|
1284
|
+
- 自定义开关:添加发开、发关、收开、收关4组码
|
|
1285
|
+
- 翻转模式:收开码=收关码时,收到后自动翻转开关状态
|
|
1286
|
+
- 自定义窗帘:添加发开、发关、发停、收开、收关、收停6组码
|
|
1287
|
+
- 自定义空调:新增完整收发码支持(开关、风速、模式、温度)
|
|
1288
|
+
- 支持最多24字节(72个十六进制字符)数据录入
|
|
1289
|
+
- 自定义模式自动隐藏地址字段
|
|
1290
|
+
- 配置持久化保存,重启后保持
|
|
1291
|
+
- 防死循环机制优化
|
|
1292
|
+
- 内存优化,防止日志溢出
|
|
1293
|
+
|
|
1224
1294
|
### v1.6.9 (2025-12-20)
|
|
1225
1295
|
- **KNX-HA双向同步**:新增`symi-knx-ha-bridge`节点
|
|
1226
1296
|
- 直接连接KNX与HA实体,实现双向同步
|
|
@@ -1269,34 +1339,6 @@ node-red-contrib-symi-mesh/
|
|
|
1269
1339
|
- 多设备类型:开关、调光灯、窗帘、空调、新风、地暖
|
|
1270
1340
|
- 智能通道选择:根据Mesh设备实际路数显示可选通道
|
|
1271
1341
|
|
|
1272
|
-
### v1.6.6 (2025-12-09)
|
|
1273
|
-
- **三合一面板完整双向同步**:支持空调+新风+地暖独立RS485映射
|
|
1274
|
-
- 三合一0x94协议完整解析:空调(开关/模式/风速/温度)、地暖(开关/温度)、新风(开关/风速)
|
|
1275
|
-
- Mesh→RS485:三合一状态变化自动发送对应RS485帧
|
|
1276
|
-
- RS485→Mesh:RS485帧自动同步到三合一面板对应功能
|
|
1277
|
-
- 支持climateSwitch/climateMode/fanMode/floorHeatingSwitch/freshAirSwitch等字段
|
|
1278
|
-
- **开关双向同步修复**:修复米家App全开/全关只触发部分按键的问题
|
|
1279
|
-
- 命令队列防抖逻辑修复:只有完全相同映射才合并,不同映射独立处理
|
|
1280
|
-
- 支持同一Mesh设备多通道分别绑定不同RS485设备
|
|
1281
|
-
- **RS485多映射匹配**:修复同一从机地址多个映射时只匹配第一个的问题
|
|
1282
|
-
- findAllRS485Mappings返回所有匹配的映射
|
|
1283
|
-
- rs485Channel精确匹配:根据寄存器地址确定通道
|
|
1284
|
-
- **网络异常处理增强**:彻底修复Node.js 18+ AggregateError导致崩溃的问题
|
|
1285
|
-
- 全局uncaughtException/unhandledRejection处理器
|
|
1286
|
-
- TCP连接过程同步错误捕获
|
|
1287
|
-
- 只捕获网络相关错误,不影响其他异常
|
|
1288
|
-
- **三合一面板自动识别**:设备列表API返回isThreeInOne标志
|
|
1289
|
-
- **窗帘双协议兼容**:同时支持米家和小程序两种控制协议
|
|
1290
|
-
- 米家协议: `subOpcode=0x06, status: 0=打开中, 1=关闭中, 2=停止`
|
|
1291
|
-
- 小程序协议: `subOpcode=0x05, status: 1=打开, 2=关闭, 3=停止`
|
|
1292
|
-
- **话语前湾协议完善**:
|
|
1293
|
-
- 空调:客厅/主卧/次卧1/次卧2,寄存器0x0FA0-0x0FAF
|
|
1294
|
-
- 地暖:客餐厅(60)/主卧(61)/次卧1(62)/次卧2(63),寄存器0x0039/0x0043
|
|
1295
|
-
- 新风:从机60,开关0x0039(开=1),风速0x004B(高=2,低=0)
|
|
1296
|
-
- **地址输入修复**:允许从机地址为0(话语前湾空调地址是0)
|
|
1297
|
-
- **内存安全**:命令队列限制100条,节点关闭时清理所有缓存
|
|
1298
|
-
- **文档更新**:添加三合一面板RS485配置说明和协议对照表
|
|
1299
|
-
|
|
1300
1342
|
## 许可证
|
|
1301
1343
|
|
|
1302
1344
|
MIT License
|
|
@@ -1308,8 +1350,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1308
1350
|
## 关于
|
|
1309
1351
|
|
|
1310
1352
|
**作者**: SYMI 亖米
|
|
1311
|
-
**版本**: 1.
|
|
1353
|
+
**版本**: 1.7.1
|
|
1312
1354
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1313
|
-
**最后更新**: 2025-12-
|
|
1355
|
+
**最后更新**: 2025-12-21
|
|
1314
1356
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
1315
1357
|
**npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
|
package/lib/device-manager.js
CHANGED
|
@@ -13,7 +13,7 @@ const DEVICE_TYPE_NAMES = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
class DeviceInfo {
|
|
16
|
-
constructor(data) {
|
|
16
|
+
constructor(data, manager) {
|
|
17
17
|
this.macAddress = data.macAddress;
|
|
18
18
|
this.networkAddress = data.networkAddress;
|
|
19
19
|
this.deviceType = data.deviceType;
|
|
@@ -25,6 +25,7 @@ class DeviceInfo {
|
|
|
25
25
|
this.state = {};
|
|
26
26
|
this.lastSeen = Date.now();
|
|
27
27
|
this.isThreeInOne = false;
|
|
28
|
+
this.manager = manager; // 保存DeviceManager引用
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
generateName() {
|
|
@@ -199,7 +200,16 @@ class DeviceInfo {
|
|
|
199
200
|
case 0x1C:
|
|
200
201
|
// 风速设置(温控器和三合一空调都使用)
|
|
201
202
|
if (parameters.length > 0) {
|
|
203
|
+
const oldFanMode = this.state.fanMode;
|
|
202
204
|
this.state.fanMode = parameters[0];
|
|
205
|
+
// 触发状态变化事件
|
|
206
|
+
if (oldFanMode !== this.state.fanMode) {
|
|
207
|
+
this.emit('stateChange', {
|
|
208
|
+
device: this,
|
|
209
|
+
state: { fanMode: this.state.fanMode },
|
|
210
|
+
attrType: attrType
|
|
211
|
+
});
|
|
212
|
+
}
|
|
203
213
|
}
|
|
204
214
|
break;
|
|
205
215
|
case 0x1D:
|
|
@@ -316,8 +326,10 @@ class DeviceInfo {
|
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
// byte2: bit[0-2]=空调风速 (协议值: 0=自动, 1=低, 2=中, 4=高)
|
|
329
|
+
// 统一转换为标准Mesh值: 1=高, 2=中, 3=低, 4=自动
|
|
319
330
|
const fanLevel = byte2 & 0x07;
|
|
320
|
-
|
|
331
|
+
const fanMap = { 0: 4, 1: 3, 2: 2, 4: 1 };
|
|
332
|
+
this.state.fanMode = fanMap[fanLevel] || 3; // 默认低风
|
|
321
333
|
|
|
322
334
|
// byte3: 空调设定温度 (16-30°C)
|
|
323
335
|
if (byte3 >= 16 && byte3 <= 30) {
|
|
@@ -353,8 +365,10 @@ class DeviceInfo {
|
|
|
353
365
|
this.state.freshAirMode = freshMode;
|
|
354
366
|
|
|
355
367
|
// byte7: bit[0-2]=新风风速 (协议值: 0=自动, 1=低, 2=中, 4=高)
|
|
368
|
+
// 统一转换为标准Mesh值: 1=高, 2=中, 3=低, 4=自动
|
|
356
369
|
const freshFan = byte7 & 0x07;
|
|
357
|
-
|
|
370
|
+
const freshFanMap = { 0: 4, 1: 3, 2: 2, 4: 1 };
|
|
371
|
+
this.state.freshAirSpeed = freshFanMap[freshFan] || 4; // 默认自动
|
|
358
372
|
|
|
359
373
|
}
|
|
360
374
|
|
|
@@ -385,7 +399,44 @@ class DeviceInfo {
|
|
|
385
399
|
if (this.channels === 1) {
|
|
386
400
|
// 单路开关
|
|
387
401
|
const value = parameters[0];
|
|
388
|
-
this.
|
|
402
|
+
console.log(`[DeviceManager] handleSwitchState: deviceType=${this.deviceType}, channels=${this.channels}, value=${value}, name=${this.name}`);
|
|
403
|
+
// 温控器设备:0x02消息可能是开关或风速
|
|
404
|
+
if (this.deviceType === 10) {
|
|
405
|
+
console.log(`[DeviceManager] 温控器设备,检查风速: value=${value}`);
|
|
406
|
+
// 如果值是1-4,是风速控制
|
|
407
|
+
if (value >= 1 && value <= 4) {
|
|
408
|
+
const oldFanMode = this.state.fanMode;
|
|
409
|
+
this.state.fanMode = value;
|
|
410
|
+
console.log(`[DeviceManager] 风速变化: ${oldFanMode} -> ${this.state.fanMode}`);
|
|
411
|
+
// 总是触发事件,即使值没变
|
|
412
|
+
console.log(`[DeviceManager] 触发fanMode事件`);
|
|
413
|
+
// 注意:这里不能直接emit,因为这是DeviceInfo实例,不是DeviceManager
|
|
414
|
+
// 需要通过DeviceManager来触发事件
|
|
415
|
+
if (this.manager) {
|
|
416
|
+
this.manager.emit('device-state-changed', {
|
|
417
|
+
device: this,
|
|
418
|
+
state: { fanMode: this.state.fanMode },
|
|
419
|
+
attrType: 0x1C
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// 否则是开关控制
|
|
424
|
+
console.log(`[DeviceManager] 开关控制: value=${value}`);
|
|
425
|
+
const oldSwitch = this.state.switch;
|
|
426
|
+
this.state.switch = value === 0x02;
|
|
427
|
+
// 触发开关状态变化事件
|
|
428
|
+
if (oldSwitch !== this.state.switch && this.manager) {
|
|
429
|
+
console.log(`[DeviceManager] 触发switch事件: ${oldSwitch} -> ${this.state.switch}`);
|
|
430
|
+
this.manager.emit('device-state-changed', {
|
|
431
|
+
device: this,
|
|
432
|
+
state: { switch: this.state.switch },
|
|
433
|
+
attrType: 0x02
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
this.state.switch = value === 0x02;
|
|
439
|
+
}
|
|
389
440
|
} else if (this.channels <= 4) {
|
|
390
441
|
// 1-4路开关:通常1字节,但米家/面板操作可能发送2字节(0x45类型)
|
|
391
442
|
let value;
|
|
@@ -601,7 +652,7 @@ class DeviceManager extends EventEmitter {
|
|
|
601
652
|
let nameChanged = false;
|
|
602
653
|
|
|
603
654
|
if (!device) {
|
|
604
|
-
device = new DeviceInfo(deviceData);
|
|
655
|
+
device = new DeviceInfo(deviceData, this);
|
|
605
656
|
this.devices.set(mac, device);
|
|
606
657
|
isNew = true;
|
|
607
658
|
} else {
|
|
@@ -160,9 +160,11 @@
|
|
|
160
160
|
|
|
161
161
|
mappings.forEach(function(m, idx) {
|
|
162
162
|
var row = $('<div class="mapping-row" data-idx="' + idx + '" style="flex-wrap:wrap;"></div>');
|
|
163
|
-
//
|
|
163
|
+
// 地址输入框:杜亚2字节地址,自定义模式隐藏,其他显示
|
|
164
164
|
var addrHtml = '';
|
|
165
|
-
if (m.brand === '
|
|
165
|
+
if (m.brand === 'custom') {
|
|
166
|
+
addrHtml = '<div class="addr-col" style="width:60px;visibility:hidden;"></div>';
|
|
167
|
+
} else if (m.brand === 'duya') {
|
|
166
168
|
addrHtml = '<div class="addr-col duya-addr" style="display:flex;gap:2px;">' +
|
|
167
169
|
'<input type="number" class="addr-high" value="' + (m.addrHigh || 1) + '" min="0" max="255" style="width:40px;" title="地址高字节" placeholder="高">' +
|
|
168
170
|
'<input type="number" class="addr-low" value="' + (m.addrLow || 1) + '" min="0" max="255" style="width:40px;" title="地址低字节" placeholder="低">' +
|
|
@@ -279,33 +281,88 @@
|
|
|
279
281
|
renderMappings();
|
|
280
282
|
});
|
|
281
283
|
|
|
282
|
-
// 自定义码输入事件
|
|
283
|
-
container.find('.custom-
|
|
284
|
+
// 自定义码输入事件 - 开关
|
|
285
|
+
container.find('.custom-send-on').off('change').on('change', function() {
|
|
284
286
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
285
287
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
286
|
-
mappings[idx].customCodes.
|
|
288
|
+
mappings[idx].customCodes.sendOn = $(this).val();
|
|
287
289
|
});
|
|
288
|
-
container.find('.custom-
|
|
290
|
+
container.find('.custom-send-off').off('change').on('change', function() {
|
|
289
291
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
290
292
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
291
|
-
mappings[idx].customCodes.
|
|
293
|
+
mappings[idx].customCodes.sendOff = $(this).val();
|
|
292
294
|
});
|
|
293
|
-
container.find('.custom-
|
|
295
|
+
container.find('.custom-recv-on').off('change').on('change', function() {
|
|
294
296
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
295
297
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
296
|
-
mappings[idx].customCodes.
|
|
298
|
+
mappings[idx].customCodes.recvOn = $(this).val();
|
|
297
299
|
});
|
|
298
|
-
container.find('.custom-
|
|
300
|
+
container.find('.custom-recv-off').off('change').on('change', function() {
|
|
299
301
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
300
302
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
301
|
-
mappings[idx].customCodes.
|
|
303
|
+
mappings[idx].customCodes.recvOff = $(this).val();
|
|
302
304
|
});
|
|
303
|
-
|
|
305
|
+
// 窗帘码
|
|
306
|
+
container.find('.custom-send-open, .custom-send-close, .custom-send-stop, .custom-recv-open, .custom-recv-close, .custom-recv-stop').off('change').on('change', function() {
|
|
304
307
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
305
308
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
306
|
-
mappings[idx].customCodes.
|
|
309
|
+
if ($(this).hasClass('custom-send-open')) mappings[idx].customCodes.sendOpen = $(this).val();
|
|
310
|
+
if ($(this).hasClass('custom-send-close')) mappings[idx].customCodes.sendClose = $(this).val();
|
|
311
|
+
if ($(this).hasClass('custom-send-stop')) mappings[idx].customCodes.sendStop = $(this).val();
|
|
312
|
+
if ($(this).hasClass('custom-recv-open')) mappings[idx].customCodes.recvOpen = $(this).val();
|
|
313
|
+
if ($(this).hasClass('custom-recv-close')) mappings[idx].customCodes.recvClose = $(this).val();
|
|
314
|
+
if ($(this).hasClass('custom-recv-stop')) mappings[idx].customCodes.recvStop = $(this).val();
|
|
307
315
|
});
|
|
308
|
-
|
|
316
|
+
// 空调码
|
|
317
|
+
container.find('[class^="custom-ac-"], [class^="custom-fan-"], [class^="custom-mode-"], [class^="custom-temp-"]').off('change').on('change', function() {
|
|
318
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
319
|
+
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
320
|
+
var cls = $(this).attr('class').split(' ')[0];
|
|
321
|
+
|
|
322
|
+
// 处理空调开关、风速、模式
|
|
323
|
+
if (cls.includes('ac-send-on')) mappings[idx].customCodes.acSendOn = $(this).val();
|
|
324
|
+
else if (cls.includes('ac-send-off')) mappings[idx].customCodes.acSendOff = $(this).val();
|
|
325
|
+
else if (cls.includes('ac-recv-on')) mappings[idx].customCodes.acRecvOn = $(this).val();
|
|
326
|
+
else if (cls.includes('ac-recv-off')) mappings[idx].customCodes.acRecvOff = $(this).val();
|
|
327
|
+
else if (cls.includes('fan-send-high')) mappings[idx].customCodes.fanSendHigh = $(this).val();
|
|
328
|
+
else if (cls.includes('fan-send-mid')) mappings[idx].customCodes.fanSendMid = $(this).val();
|
|
329
|
+
else if (cls.includes('fan-send-low')) mappings[idx].customCodes.fanSendLow = $(this).val();
|
|
330
|
+
else if (cls.includes('fan-recv-high')) mappings[idx].customCodes.fanRecvHigh = $(this).val();
|
|
331
|
+
else if (cls.includes('fan-recv-mid')) mappings[idx].customCodes.fanRecvMid = $(this).val();
|
|
332
|
+
else if (cls.includes('fan-recv-low')) mappings[idx].customCodes.fanRecvLow = $(this).val();
|
|
333
|
+
else if (cls.includes('mode-send-cool')) mappings[idx].customCodes.modeSendCool = $(this).val();
|
|
334
|
+
else if (cls.includes('mode-send-heat')) mappings[idx].customCodes.modeSendHeat = $(this).val();
|
|
335
|
+
else if (cls.includes('mode-send-dry')) mappings[idx].customCodes.modeSendDry = $(this).val();
|
|
336
|
+
else if (cls.includes('mode-send-fan')) mappings[idx].customCodes.modeSendFan = $(this).val();
|
|
337
|
+
else if (cls.includes('mode-recv-cool')) mappings[idx].customCodes.modeRecvCool = $(this).val();
|
|
338
|
+
else if (cls.includes('mode-recv-heat')) mappings[idx].customCodes.modeRecvHeat = $(this).val();
|
|
339
|
+
else if (cls.includes('mode-recv-dry')) mappings[idx].customCodes.modeRecvDry = $(this).val();
|
|
340
|
+
else if (cls.includes('mode-recv-fan')) mappings[idx].customCodes.modeRecvFan = $(this).val();
|
|
341
|
+
|
|
342
|
+
// 处理 16-30 度独立温度码
|
|
343
|
+
else if (cls.startsWith('custom-temp-')) {
|
|
344
|
+
// 提取 key, 如 tempSendCool16
|
|
345
|
+
var parts = cls.split('-'); // ["custom", "temp", "send", "cool", "16"]
|
|
346
|
+
if (parts.length === 5) {
|
|
347
|
+
var type = parts[2]; // send/recv
|
|
348
|
+
var mode = parts[3]; // cool/heat
|
|
349
|
+
var temp = parts[4]; // 16-30
|
|
350
|
+
var key = 'temp' + type.charAt(0).toUpperCase() + type.slice(1) +
|
|
351
|
+
mode.charAt(0).toUpperCase() + mode.slice(1) + temp;
|
|
352
|
+
mappings[idx].customCodes[key] = $(this).val();
|
|
353
|
+
} else {
|
|
354
|
+
// 兼容旧的占位符模式
|
|
355
|
+
if (cls.includes('temp-send-cool')) mappings[idx].customCodes.tempSendCool = $(this).val();
|
|
356
|
+
else if (cls.includes('temp-recv-cool')) mappings[idx].customCodes.tempRecvCool = $(this).val();
|
|
357
|
+
else if (cls.includes('temp-send-heat')) mappings[idx].customCodes.tempSendHeat = $(this).val();
|
|
358
|
+
else if (cls.includes('temp-recv-heat')) mappings[idx].customCodes.tempRecvHeat = $(this).val();
|
|
359
|
+
else if (cls.includes('temp-send')) mappings[idx].customCodes.tempSend = $(this).val();
|
|
360
|
+
else if (cls.includes('temp-recv')) mappings[idx].customCodes.tempRecv = $(this).val();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
// 场景码
|
|
365
|
+
container.find('.custom-trigger').off('change').on('change', function() {
|
|
309
366
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
310
367
|
mappings[idx].customCodes = mappings[idx].customCodes || {};
|
|
311
368
|
mappings[idx].customCodes.trigger = $(this).val();
|
|
@@ -322,15 +379,79 @@
|
|
|
322
379
|
html += '<div style="font-size:11px;color:#f57c00;margin-bottom:6px;"><i class="fa fa-code"></i> 自定义RS485码(十六进制,如: 55 01 01 03 01 B9 00)</div>';
|
|
323
380
|
|
|
324
381
|
if (deviceId === 'custom_switch') {
|
|
325
|
-
html += '<div style="display:
|
|
326
|
-
html += '<label style="font-size:11px;"
|
|
327
|
-
html += '<label style="font-size:11px;"
|
|
382
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;">';
|
|
383
|
+
html += '<label style="font-size:11px;">发开码: <input type="text" class="custom-code-send-on" value="' + (codes.sendOn || '') + '" placeholder="01 20 10 14 00 01 00 7F..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
384
|
+
html += '<label style="font-size:11px;">发关码: <input type="text" class="custom-code-send-off" value="' + (codes.sendOff || '') + '" placeholder="01 20 10 14 00 01 00 FF..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
385
|
+
html += '<label style="font-size:11px;">收开码: <input type="text" class="custom-code-recv-on" value="' + (codes.recvOn || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
386
|
+
html += '<label style="font-size:11px;">收关码: <input type="text" class="custom-code-recv-off" value="' + (codes.recvOff || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
328
387
|
html += '</div>';
|
|
388
|
+
html += '<div style="font-size:10px;color:#666;margin-top:4px;">提示:收开码=收关码时,收到后翻转开关状态</div>';
|
|
329
389
|
} else if (deviceId === 'custom_curtain') {
|
|
330
|
-
html += '<div style="display:
|
|
331
|
-
html += '<label style="font-size:11px;"
|
|
332
|
-
html += '<label style="font-size:11px;"
|
|
333
|
-
html += '<label style="font-size:11px;"
|
|
390
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;">';
|
|
391
|
+
html += '<label style="font-size:11px;">发开码: <input type="text" class="custom-code-send-open" value="' + (codes.sendOpen || '') + '" placeholder="55 02 02 03 01..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
392
|
+
html += '<label style="font-size:11px;">发关码: <input type="text" class="custom-code-send-close" value="' + (codes.sendClose || '') + '" placeholder="55 02 02 03 02..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
393
|
+
html += '<label style="font-size:11px;">发停码: <input type="text" class="custom-code-send-stop" value="' + (codes.sendStop || '') + '" placeholder="55 02 02 03 03..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
394
|
+
html += '<label style="font-size:11px;">收开码: <input type="text" class="custom-code-recv-open" value="' + (codes.recvOpen || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
395
|
+
html += '<label style="font-size:11px;">收关码: <input type="text" class="custom-code-recv-close" value="' + (codes.recvClose || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
396
|
+
html += '<label style="font-size:11px;">收停码: <input type="text" class="custom-code-recv-stop" value="' + (codes.recvStop || '') + '" placeholder="01 20..." maxlength="72" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:10px;font-family:monospace;"></label>';
|
|
397
|
+
html += '</div>';
|
|
398
|
+
} else if (deviceId === 'custom_climate') {
|
|
399
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">开关控制</div>';
|
|
400
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:6px;">';
|
|
401
|
+
html += '<label style="font-size:10px;">发开码: <input type="text" class="custom-ac-send-on" value="' + (codes.acSendOn || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
402
|
+
html += '<label style="font-size:10px;">发关码: <input type="text" class="custom-ac-send-off" value="' + (codes.acSendOff || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
403
|
+
html += '<label style="font-size:10px;">收开码: <input type="text" class="custom-ac-recv-on" value="' + (codes.acRecvOn || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
404
|
+
html += '<label style="font-size:10px;">收关码: <input type="text" class="custom-ac-recv-off" value="' + (codes.acRecvOff || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
405
|
+
html += '</div>';
|
|
406
|
+
|
|
407
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">风速控制</div>';
|
|
408
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;margin-bottom:6px;">';
|
|
409
|
+
html += '<label style="font-size:10px;">发高风: <input type="text" class="custom-fan-send-high" value="' + (codes.fanSendHigh || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
410
|
+
html += '<label style="font-size:10px;">发中风: <input type="text" class="custom-fan-send-mid" value="' + (codes.fanSendMid || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
411
|
+
html += '<label style="font-size:10px;">发低风: <input type="text" class="custom-fan-send-low" value="' + (codes.fanSendLow || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
412
|
+
html += '<label style="font-size:10px;">收高风: <input type="text" class="custom-fan-recv-high" value="' + (codes.fanRecvHigh || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
413
|
+
html += '<label style="font-size:10px;">收中风: <input type="text" class="custom-fan-recv-mid" value="' + (codes.fanRecvMid || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
414
|
+
html += '<label style="font-size:10px;">收低风: <input type="text" class="custom-fan-recv-low" value="' + (codes.fanRecvLow || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
415
|
+
html += '</div>';
|
|
416
|
+
|
|
417
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">模式控制</div>';
|
|
418
|
+
html += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px;margin-bottom:6px;">';
|
|
419
|
+
html += '<label style="font-size:10px;">发制冷: <input type="text" class="custom-mode-send-cool" value="' + (codes.modeSendCool || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
420
|
+
html += '<label style="font-size:10px;">发制热: <input type="text" class="custom-mode-send-heat" value="' + (codes.modeSendHeat || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
421
|
+
html += '<label style="font-size:10px;">发除湿: <input type="text" class="custom-mode-send-dry" value="' + (codes.modeSendDry || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
422
|
+
html += '<label style="font-size:10px;">发送风: <input type="text" class="custom-mode-send-fan" value="' + (codes.modeSendFan || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
423
|
+
html += '<label style="font-size:10px;">收制冷: <input type="text" class="custom-mode-recv-cool" value="' + (codes.modeRecvCool || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
424
|
+
html += '<label style="font-size:10px;">收制热: <input type="text" class="custom-mode-recv-heat" value="' + (codes.modeRecvHeat || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
425
|
+
html += '<label style="font-size:10px;">收除湿: <input type="text" class="custom-mode-recv-dry" value="' + (codes.modeRecvDry || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
426
|
+
html += '<label style="font-size:10px;">收送风: <input type="text" class="custom-mode-recv-fan" value="' + (codes.modeRecvFan || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
427
|
+
html += '</div>';
|
|
428
|
+
|
|
429
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;margin-top:4px;">制冷温度发码 (16-30度)</div>';
|
|
430
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
431
|
+
for (var t=16; t<=30; t++) {
|
|
432
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-send-cool-' + t + '" value="' + (codes['tempSendCool'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
433
|
+
}
|
|
434
|
+
html += '</div>';
|
|
435
|
+
|
|
436
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">制冷温度收码 (16-30度)</div>';
|
|
437
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
438
|
+
for (var t=16; t<=30; t++) {
|
|
439
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-recv-cool-' + t + '" value="' + (codes['tempRecvCool'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
440
|
+
}
|
|
441
|
+
html += '</div>';
|
|
442
|
+
|
|
443
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">制热温度发码 (16-30度)</div>';
|
|
444
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
445
|
+
for (var t=16; t<=30; t++) {
|
|
446
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-send-heat-' + t + '" value="' + (codes['tempSendHeat'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
447
|
+
}
|
|
448
|
+
html += '</div>';
|
|
449
|
+
|
|
450
|
+
html += '<div style="font-size:10px;color:#f57c00;margin-bottom:4px;">制热温度收码 (16-30度)</div>';
|
|
451
|
+
html += '<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:4px;margin-bottom:6px;">';
|
|
452
|
+
for (var t=16; t<=30; t++) {
|
|
453
|
+
html += '<label style="font-size:9px;">' + t + '°C: <input type="text" class="custom-temp-recv-heat-' + t + '" value="' + (codes['tempRecvHeat'+t] || '') + '" placeholder="" maxlength="128" style="width:100%;padding:2px 4px;border:1px solid #ffb74d;border-radius:2px;font-size:9px;font-family:monospace;"></label>';
|
|
454
|
+
}
|
|
334
455
|
html += '</div>';
|
|
335
456
|
} else if (deviceId === 'custom_scene') {
|
|
336
457
|
html += '<div style="display:flex;gap:8px;">';
|
|
@@ -374,18 +495,71 @@
|
|
|
374
495
|
}
|
|
375
496
|
// 保存自定义码
|
|
376
497
|
if (m.brand === 'custom') {
|
|
498
|
+
var row = $(this);
|
|
377
499
|
m.customCodes = {};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
var
|
|
381
|
-
var
|
|
382
|
-
var
|
|
383
|
-
var
|
|
384
|
-
|
|
385
|
-
if (
|
|
386
|
-
if (
|
|
387
|
-
if (
|
|
388
|
-
if (
|
|
500
|
+
|
|
501
|
+
// 通用/开关
|
|
502
|
+
var sendOn = row.find('.custom-code-send-on').val() || row.find('.custom-ac-send-on').val() || row.find('.custom-send-on').val();
|
|
503
|
+
var sendOff = row.find('.custom-code-send-off').val() || row.find('.custom-ac-send-off').val() || row.find('.custom-send-off').val();
|
|
504
|
+
var recvOn = row.find('.custom-code-recv-on').val() || row.find('.custom-ac-recv-on').val() || row.find('.custom-recv-on').val();
|
|
505
|
+
var recvOff = row.find('.custom-code-recv-off').val() || row.find('.custom-ac-recv-off').val() || row.find('.custom-recv-off').val();
|
|
506
|
+
|
|
507
|
+
if (sendOn) m.customCodes.sendOn = sendOn;
|
|
508
|
+
if (sendOff) m.customCodes.sendOff = sendOff;
|
|
509
|
+
if (recvOn) m.customCodes.recvOn = recvOn;
|
|
510
|
+
if (recvOff) m.customCodes.recvOff = recvOff;
|
|
511
|
+
|
|
512
|
+
// 兼容旧字段名 (空调开关)
|
|
513
|
+
if (sendOn) m.customCodes.acSendOn = sendOn;
|
|
514
|
+
if (sendOff) m.customCodes.acSendOff = sendOff;
|
|
515
|
+
if (recvOn) m.customCodes.acRecvOn = recvOn;
|
|
516
|
+
if (recvOff) m.customCodes.acRecvOff = recvOff;
|
|
517
|
+
|
|
518
|
+
// 窗帘
|
|
519
|
+
var sendOpen = row.find('.custom-code-send-open').val() || row.find('.custom-send-open').val();
|
|
520
|
+
var sendClose = row.find('.custom-code-send-close').val() || row.find('.custom-send-close').val();
|
|
521
|
+
var sendStop = row.find('.custom-code-send-stop').val() || row.find('.custom-send-stop').val();
|
|
522
|
+
var recvOpen = row.find('.custom-code-recv-open').val() || row.find('.custom-recv-open').val();
|
|
523
|
+
var recvClose = row.find('.custom-code-recv-close').val() || row.find('.custom-recv-close').val();
|
|
524
|
+
var recvStop = row.find('.custom-code-recv-stop').val() || row.find('.custom-recv-stop').val();
|
|
525
|
+
|
|
526
|
+
if (sendOpen) m.customCodes.sendOpen = sendOpen;
|
|
527
|
+
if (sendClose) m.customCodes.sendClose = sendClose;
|
|
528
|
+
if (sendStop) m.customCodes.sendStop = sendStop;
|
|
529
|
+
if (recvOpen) m.customCodes.recvOpen = recvOpen;
|
|
530
|
+
if (recvClose) m.customCodes.recvClose = recvClose;
|
|
531
|
+
if (recvStop) m.customCodes.recvStop = recvStop;
|
|
532
|
+
|
|
533
|
+
// 动态提取所有空调相关的自定义码 (ac-, fan-, mode-, temp-)
|
|
534
|
+
row.find('[class^="custom-ac-"], [class^="custom-fan-"], [class^="custom-mode-"], [class^="custom-temp-"]').each(function() {
|
|
535
|
+
var cls = $(this).attr('class').split(' ')[0];
|
|
536
|
+
var val = $(this).val();
|
|
537
|
+
if (!val) return;
|
|
538
|
+
|
|
539
|
+
if (cls.startsWith('custom-temp-')) {
|
|
540
|
+
var parts = cls.split('-');
|
|
541
|
+
if (parts.length === 5) {
|
|
542
|
+
// 独立温度码: custom-temp-send-cool-16
|
|
543
|
+
var type = parts[2];
|
|
544
|
+
var mode = parts[3];
|
|
545
|
+
var temp = parts[4];
|
|
546
|
+
var key = 'temp' + type.charAt(0).toUpperCase() + type.slice(1) +
|
|
547
|
+
mode.charAt(0).toUpperCase() + mode.slice(1) + temp;
|
|
548
|
+
m.customCodes[key] = val;
|
|
549
|
+
} else {
|
|
550
|
+
// 模板码: custom-temp-send-cool
|
|
551
|
+
var key = cls.replace('custom-', '').replace(/-([a-z])/g, function(g) { return g[1].toUpperCase(); });
|
|
552
|
+
m.customCodes[key] = val;
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
// 其他码: custom-ac-send-on -> acSendOn
|
|
556
|
+
var key = cls.replace('custom-', '').replace(/-([a-z])/g, function(g) { return g[1].toUpperCase(); });
|
|
557
|
+
m.customCodes[key] = val;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// 场景
|
|
562
|
+
var trigger = row.find('.custom-code-trigger').val() || row.find('.custom-trigger').val();
|
|
389
563
|
if (trigger) m.customCodes.trigger = trigger;
|
|
390
564
|
}
|
|
391
565
|
mappings.push(m);
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -309,7 +309,11 @@ module.exports = function(RED) {
|
|
|
309
309
|
name: '自定义窗帘',
|
|
310
310
|
type: 'cover',
|
|
311
311
|
customMode: true,
|
|
312
|
-
|
|
312
|
+
},
|
|
313
|
+
'custom_climate': {
|
|
314
|
+
name: '自定义空调',
|
|
315
|
+
type: 'climate',
|
|
316
|
+
customMode: true,
|
|
313
317
|
},
|
|
314
318
|
'custom_scene': {
|
|
315
319
|
name: '自定义场景',
|
|
@@ -520,13 +524,15 @@ module.exports = function(RED) {
|
|
|
520
524
|
|
|
521
525
|
// 状态缓存 - 用于检测真正变化的开关
|
|
522
526
|
node.stateCache = {};
|
|
527
|
+
// 空调模式缓存 - 用于温度控制时确定当前模式
|
|
528
|
+
node.climateCache = {};
|
|
523
529
|
// 首次启动标记 - 跳过初始状态同步
|
|
524
530
|
node.initializing = true;
|
|
525
531
|
// 启动后延迟20秒再开始同步(Mesh网关需要15秒以上完成设备发现)
|
|
526
532
|
setTimeout(() => {
|
|
527
533
|
node.initializing = false;
|
|
528
534
|
node.log('[RS485 Bridge] 初始化完成,开始同步');
|
|
529
|
-
},
|
|
535
|
+
}, 10000); // 10秒初始化延迟
|
|
530
536
|
|
|
531
537
|
// Mesh设备状态变化处理(事件驱动)
|
|
532
538
|
const handleMeshStateChange = (eventData) => {
|
|
@@ -708,13 +714,12 @@ module.exports = function(RED) {
|
|
|
708
714
|
}).catch(err => {
|
|
709
715
|
node.error(`[Mesh->杜亚] 位置同步失败: ${err.message}`);
|
|
710
716
|
});
|
|
711
|
-
}, 100);
|
|
717
|
+
}, 100);
|
|
712
718
|
}
|
|
713
719
|
continue;
|
|
714
720
|
}
|
|
715
721
|
|
|
716
722
|
// 【自定义窗帘】处理Mesh面板控制同步到485(逻辑同杜亚)
|
|
717
|
-
// 必须检查isUserControl,只有NODE_ACK(subOpcode=0x05)才是用户控制
|
|
718
723
|
if (mapping.brand === 'custom' && mapping.device === 'custom_curtain' && mapping.customCodes) {
|
|
719
724
|
const now = Date.now();
|
|
720
725
|
const curtainKey = `custom_curtain_${mac}`;
|
|
@@ -801,12 +806,22 @@ module.exports = function(RED) {
|
|
|
801
806
|
);
|
|
802
807
|
const hasACChange = isAC && (
|
|
803
808
|
changed.targetTemp !== undefined ||
|
|
809
|
+
changed.acTargetTemp !== undefined ||
|
|
810
|
+
changed.temperature !== undefined ||
|
|
804
811
|
changed.acMode !== undefined ||
|
|
812
|
+
changed.climateMode !== undefined ||
|
|
813
|
+
changed.mode !== undefined ||
|
|
805
814
|
changed.acFanSpeed !== undefined ||
|
|
806
|
-
|
|
815
|
+
changed.fanSpeed !== undefined ||
|
|
816
|
+
changed.climateFanSpeed !== undefined ||
|
|
817
|
+
changed.fanMode !== undefined ||
|
|
818
|
+
changed.fan_speed !== undefined ||
|
|
819
|
+
changed.fanLevel !== undefined ||
|
|
820
|
+
changed.fan_level !== undefined ||
|
|
821
|
+
changed.speed !== undefined ||
|
|
822
|
+
changed.acSwitch !== undefined ||
|
|
807
823
|
changed.climateSwitch !== undefined ||
|
|
808
|
-
changed.
|
|
809
|
-
changed.fanMode !== undefined
|
|
824
|
+
changed.switch !== undefined
|
|
810
825
|
);
|
|
811
826
|
// 新风设备检测
|
|
812
827
|
const isFreshAir = device.includes('fresh_air');
|
|
@@ -1169,8 +1184,8 @@ module.exports = function(RED) {
|
|
|
1169
1184
|
// 开关类型
|
|
1170
1185
|
if (mapping.device === 'custom_switch') {
|
|
1171
1186
|
for (const [key, value] of Object.entries(state)) {
|
|
1172
|
-
if (key === 'switch' || key === 'acSwitch') {
|
|
1173
|
-
const hexCode = value ? codes.
|
|
1187
|
+
if (key === 'switch' || key === 'acSwitch' || key.startsWith('switch_')) {
|
|
1188
|
+
const hexCode = value ? codes.sendOn : codes.sendOff;
|
|
1174
1189
|
if (hexCode) {
|
|
1175
1190
|
await node.sendCustomCode(hexCode);
|
|
1176
1191
|
node.log(`[Mesh->自定义] 开关: ${value ? '开' : '关'}, 发送: ${hexCode}`);
|
|
@@ -1190,41 +1205,29 @@ module.exports = function(RED) {
|
|
|
1190
1205
|
// 1. 优先检查动作命令
|
|
1191
1206
|
if (state.curtainAction !== undefined || state.action !== undefined) {
|
|
1192
1207
|
const action = state.curtainAction || state.action;
|
|
1193
|
-
if (action === 1 || action === 'open') {
|
|
1194
|
-
hexCode = codes.
|
|
1208
|
+
if (action === 1 || action === 'open' || action === 'opening') {
|
|
1209
|
+
hexCode = codes.sendOpen;
|
|
1195
1210
|
actionName = '打开';
|
|
1196
|
-
} else if (action === 2 || action === 'close') {
|
|
1197
|
-
hexCode = codes.
|
|
1211
|
+
} else if (action === 2 || action === 'close' || action === 'closing') {
|
|
1212
|
+
hexCode = codes.sendClose;
|
|
1198
1213
|
actionName = '关闭';
|
|
1199
|
-
} else if (action === 3 || action === 'stop') {
|
|
1200
|
-
hexCode = codes.
|
|
1214
|
+
} else if (action === 3 || action === 'stop' || action === 'stopped') {
|
|
1215
|
+
hexCode = codes.sendStop;
|
|
1201
1216
|
actionName = '停止';
|
|
1202
1217
|
}
|
|
1203
1218
|
}
|
|
1204
|
-
// 2. 其次检查运行状态
|
|
1205
1219
|
else if (state.curtainStatus !== undefined) {
|
|
1206
1220
|
if (state.curtainStatus === 1) {
|
|
1207
|
-
hexCode = codes.
|
|
1208
|
-
actionName = '打开
|
|
1221
|
+
hexCode = codes.sendOpen;
|
|
1222
|
+
actionName = '打开';
|
|
1209
1223
|
} else if (state.curtainStatus === 2) {
|
|
1210
|
-
hexCode = codes.
|
|
1211
|
-
actionName = '关闭
|
|
1212
|
-
} else if (state.curtainStatus === 0 && codes.
|
|
1213
|
-
|
|
1214
|
-
hexCode = codes.stop;
|
|
1224
|
+
hexCode = codes.sendClose;
|
|
1225
|
+
actionName = '关闭';
|
|
1226
|
+
} else if (state.curtainStatus === 0 && codes.sendStop) {
|
|
1227
|
+
hexCode = codes.sendStop;
|
|
1215
1228
|
actionName = '停止';
|
|
1216
1229
|
}
|
|
1217
1230
|
}
|
|
1218
|
-
// 3. 最后检查位置(仅在极端位置时)
|
|
1219
|
-
else if (state.curtainPosition !== undefined) {
|
|
1220
|
-
if (state.curtainPosition >= 95) {
|
|
1221
|
-
hexCode = codes.open;
|
|
1222
|
-
actionName = '打开(位置>=95)';
|
|
1223
|
-
} else if (state.curtainPosition <= 5) {
|
|
1224
|
-
hexCode = codes.close;
|
|
1225
|
-
actionName = '关闭(位置<=5)';
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
1231
|
|
|
1229
1232
|
if (hexCode) {
|
|
1230
1233
|
// 防抖:500ms内不重复发送相同命令
|
|
@@ -1240,6 +1243,76 @@ module.exports = function(RED) {
|
|
|
1240
1243
|
}
|
|
1241
1244
|
}
|
|
1242
1245
|
}
|
|
1246
|
+
// 空调类型
|
|
1247
|
+
else if (mapping.device === 'custom_climate') {
|
|
1248
|
+
node.log(`[自定义空调] 处理状态: ${JSON.stringify(state)}, codes存在: ${!!codes}`);
|
|
1249
|
+
for (const [key, value] of Object.entries(state)) {
|
|
1250
|
+
let hexCode = null;
|
|
1251
|
+
|
|
1252
|
+
// 1. 开关控制 (处理 acSwitch, climateSwitch, switch)
|
|
1253
|
+
if (key === 'acSwitch' || key === 'climateSwitch' || key === 'switch') {
|
|
1254
|
+
const isOn = value === true || value === 1 || value === '1' || value === 'on' || value === 'ON';
|
|
1255
|
+
hexCode = isOn ? (codes.acSendOn || codes.sendOn) : (codes.acSendOff || codes.sendOff);
|
|
1256
|
+
node.log(`[自定义空调] 开关控制: ${key}=${value}, isOn=${isOn}, hexCode=${hexCode}`);
|
|
1257
|
+
}
|
|
1258
|
+
// 2. 风速控制
|
|
1259
|
+
else if (['acFanSpeed', 'fanSpeed', 'climateFanSpeed', 'fanMode', 'fan_speed', 'fanLevel', 'fan_level', 'speed'].includes(key)) {
|
|
1260
|
+
const val = parseInt(value);
|
|
1261
|
+
node.log(`[自定义空调] 风速控制: ${key}=${value}, val=${val}`);
|
|
1262
|
+
// 标准Mesh协议: 1=高, 2=中, 3=低, 4=自动
|
|
1263
|
+
if (val === 1) {
|
|
1264
|
+
hexCode = codes.fanSendHigh;
|
|
1265
|
+
node.log(`[自定义空调] 高风, hexCode=${hexCode}`);
|
|
1266
|
+
}
|
|
1267
|
+
else if (val === 2) {
|
|
1268
|
+
hexCode = codes.fanSendMid;
|
|
1269
|
+
node.log(`[自定义空调] 中风, hexCode=${hexCode}`);
|
|
1270
|
+
}
|
|
1271
|
+
else if (val === 3 || val === 0 || val === 4) {
|
|
1272
|
+
hexCode = codes.fanSendLow;
|
|
1273
|
+
node.log(`[自定义空调] 低风, hexCode=${hexCode}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
// 3. 模式控制
|
|
1277
|
+
else if (key === 'acMode' || key === 'climateMode' || key === 'mode') {
|
|
1278
|
+
if (value === 1) hexCode = codes.modeSendCool;
|
|
1279
|
+
else if (value === 2) hexCode = codes.modeSendHeat;
|
|
1280
|
+
else if (value === 4) hexCode = codes.modeSendDry;
|
|
1281
|
+
else if (value === 3) hexCode = codes.modeSendFan;
|
|
1282
|
+
|
|
1283
|
+
// 更新缓存中的模式
|
|
1284
|
+
if (!node.climateCache[mapping.meshMac]) node.climateCache[mapping.meshMac] = {};
|
|
1285
|
+
node.climateCache[mapping.meshMac].mode = value;
|
|
1286
|
+
}
|
|
1287
|
+
// 4. 温度控制
|
|
1288
|
+
else if (key === 'targetTemp' || key === 'acTargetTemp' || key === 'temperature') {
|
|
1289
|
+
const temp = Math.round(value);
|
|
1290
|
+
// 优先从当前变化中获取模式,否则从缓存中获取,默认制冷(1)
|
|
1291
|
+
const mode = state.acMode || state.climateMode || (node.climateCache[mapping.meshMac] ? node.climateCache[mapping.meshMac].mode : 1);
|
|
1292
|
+
|
|
1293
|
+
// 1. 优先尝试特定温度码,如 tempSendCool16
|
|
1294
|
+
const modeSuffix = (mode === 2) ? 'Heat' : 'Cool';
|
|
1295
|
+
const type = 'Send';
|
|
1296
|
+
const specificKey = 'temp' + type + modeSuffix + temp;
|
|
1297
|
+
hexCode = codes[specificKey];
|
|
1298
|
+
|
|
1299
|
+
// 2. 如果没有特定温度码,尝试模板
|
|
1300
|
+
if (!hexCode) {
|
|
1301
|
+
let template = (mode === 2) ? codes.tempSendHeat : codes.tempSendCool;
|
|
1302
|
+
if (!template) template = codes.tempSend; // 回退到通用模板
|
|
1303
|
+
|
|
1304
|
+
if (template && template.includes('{TEMP}')) {
|
|
1305
|
+
hexCode = template.replace('{TEMP}', temp.toString(16).toUpperCase().padStart(2, '0'));
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if (hexCode) {
|
|
1311
|
+
await node.sendCustomCode(hexCode);
|
|
1312
|
+
node.log(`[Mesh->自定义] 空调 ${key}=${value}, 发送: ${hexCode}`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1243
1316
|
// 场景类型
|
|
1244
1317
|
else if (mapping.device === 'custom_scene') {
|
|
1245
1318
|
if (state.trigger && codes.trigger) {
|
|
@@ -1493,19 +1566,44 @@ module.exports = function(RED) {
|
|
|
1493
1566
|
for (const [key, value] of Object.entries(state)) {
|
|
1494
1567
|
try {
|
|
1495
1568
|
// 开关类型
|
|
1496
|
-
if (key === 'switch') {
|
|
1569
|
+
if (key === 'switch' || key.startsWith('switch_')) {
|
|
1570
|
+
const ch = key.startsWith('switch_') ? parseInt(key.replace('switch_', '')) : channel;
|
|
1497
1571
|
const onOff = value ? 0x02 : 0x01;
|
|
1498
|
-
const param = Buffer.from([
|
|
1572
|
+
const param = Buffer.from([ch - 1, onOff]);
|
|
1573
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
1574
|
+
node.log(`[自定义->Mesh] 开关${ch}: ${value ? '开' : '关'}`);
|
|
1575
|
+
}
|
|
1576
|
+
// 空调开关
|
|
1577
|
+
else if (key === 'acSwitch' || key === 'climateSwitch') {
|
|
1578
|
+
const param = Buffer.from([value ? 0x02 : 0x01]);
|
|
1499
1579
|
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
1500
|
-
node.log(`[自定义->Mesh]
|
|
1580
|
+
node.log(`[自定义->Mesh] 空调开关: ${value ? '开' : '关'}`);
|
|
1581
|
+
}
|
|
1582
|
+
// 空调模式
|
|
1583
|
+
else if (key === 'acMode' || key === 'climateMode' || key === 'mode') {
|
|
1584
|
+
const param = Buffer.from([value]);
|
|
1585
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1D, param);
|
|
1586
|
+
node.log(`[自定义->Mesh] 空调模式: ${value}`);
|
|
1587
|
+
}
|
|
1588
|
+
// 空调风速
|
|
1589
|
+
else if (key === 'acFanSpeed' || key === 'fanMode' || key === 'fanSpeed') {
|
|
1590
|
+
const param = Buffer.from([value]);
|
|
1591
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1C, param);
|
|
1592
|
+
node.log(`[自定义->Mesh] 空调风速: ${value}`);
|
|
1593
|
+
}
|
|
1594
|
+
// 目标温度
|
|
1595
|
+
else if (key === 'acTargetTemp' || key === 'targetTemp') {
|
|
1596
|
+
const param = Buffer.from([Math.round(value)]);
|
|
1597
|
+
await node.gateway.sendControl(meshDevice.networkAddress, 0x1B, param);
|
|
1598
|
+
node.log(`[自定义->Mesh] 目标温度: ${value}°C`);
|
|
1501
1599
|
}
|
|
1502
1600
|
// 窗帘类型
|
|
1503
|
-
else if (key === 'action' || key === 'position') {
|
|
1601
|
+
else if (key === 'action' || key === 'position' || key === 'curtainAction' || key === 'curtainPosition') {
|
|
1504
1602
|
// 窗帘动作: 1=打开, 2=关闭, 3=停止
|
|
1505
1603
|
let action = 0x03; // 停止
|
|
1506
|
-
if (value === 'open') action = 0x01; // 打开
|
|
1507
|
-
else if (value === 'close') action = 0x02; // 关闭
|
|
1508
|
-
else if (value === 'stop') action = 0x03; // 停止
|
|
1604
|
+
if (value === 'open' || value === 1) action = 0x01; // 打开
|
|
1605
|
+
else if (value === 'close' || value === 2) action = 0x02; // 关闭
|
|
1606
|
+
else if (value === 'stop' || value === 3) action = 0x03; // 停止
|
|
1509
1607
|
|
|
1510
1608
|
const param = Buffer.from([action]);
|
|
1511
1609
|
// 0x05是窗帘动作控制属性
|
|
@@ -1715,7 +1813,7 @@ module.exports = function(RED) {
|
|
|
1715
1813
|
node.parseRS485Frame = function(frame) {
|
|
1716
1814
|
if (frame.length < 4) return;
|
|
1717
1815
|
|
|
1718
|
-
const hexStr = frame.toString('hex').toUpperCase();
|
|
1816
|
+
const hexStr = frame.toString('hex').toUpperCase().replace(/\s/g, ''); // 确保完全去掉空格
|
|
1719
1817
|
const hexFormatted = hexStr.match(/.{1,2}/g)?.join(' ') || '';
|
|
1720
1818
|
|
|
1721
1819
|
node.log(`[RS485收到] ${hexFormatted} (${frame.length}字节)`);
|
|
@@ -1793,34 +1891,140 @@ module.exports = function(RED) {
|
|
|
1793
1891
|
const codes = mapping.customCodes;
|
|
1794
1892
|
let matchedAction = null;
|
|
1795
1893
|
|
|
1796
|
-
node.
|
|
1894
|
+
node.log(`[自定义码检测] 检查映射: device=${mapping.device}, meshMac=${mapping.meshMac}`);
|
|
1797
1895
|
|
|
1798
|
-
// 开关类型:匹配
|
|
1896
|
+
// 开关类型:匹配recvOn/recvOff
|
|
1799
1897
|
if (mapping.device === 'custom_switch') {
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1898
|
+
const recvOn = (codes.recvOn || '').replace(/\s/g, '').toUpperCase();
|
|
1899
|
+
const recvOff = (codes.recvOff || '').replace(/\s/g, '').toUpperCase();
|
|
1900
|
+
node.log(`[自定义开关] recvOn=${recvOn}, recvOff=${recvOff}, hexStr=${hexStr}`);
|
|
1901
|
+
node.log(`[自定义开关] 包含recvOn? ${hexStr.includes(recvOn)}, 包含recvOff? ${hexStr.includes(recvOff)}`);
|
|
1902
|
+
|
|
1903
|
+
// 翻转模式:收开码=收关码
|
|
1904
|
+
if (recvOn && recvOff && recvOn === recvOff && hexStr.includes(recvOn)) {
|
|
1905
|
+
const device = node.gateway.deviceManager.getDeviceByMac(mapping.meshMac);
|
|
1906
|
+
const channel = mapping.meshChannel || 1;
|
|
1907
|
+
const stateKey = mapping.meshChannel > 1 ? `switch_${channel}` : 'switch';
|
|
1908
|
+
const currentState = device?.state?.[stateKey] || false;
|
|
1909
|
+
matchedAction = { switch: !currentState };
|
|
1910
|
+
node.log(`[自定义开关] 翻转: ${stateKey} ${currentState} -> ${!currentState}`);
|
|
1911
|
+
} else {
|
|
1912
|
+
if (recvOn && hexStr.includes(recvOn)) {
|
|
1913
|
+
matchedAction = { switch: true };
|
|
1914
|
+
node.log(`[自定义开关] 匹配到收开码`);
|
|
1915
|
+
} else if (recvOff && hexStr.includes(recvOff)) {
|
|
1916
|
+
matchedAction = { switch: false };
|
|
1917
|
+
node.log(`[自定义开关] 匹配到收关码`);
|
|
1918
|
+
}
|
|
1804
1919
|
}
|
|
1805
1920
|
}
|
|
1806
|
-
// 窗帘类型:匹配
|
|
1921
|
+
// 窗帘类型:匹配recvOpen/recvClose/recvStop
|
|
1807
1922
|
else if (mapping.device === 'custom_curtain') {
|
|
1808
|
-
const
|
|
1809
|
-
const
|
|
1810
|
-
const
|
|
1811
|
-
node.debug(`[自定义窗帘匹配] 帧hex=${hexStr}, open=${openCode}, close=${closeCode}, stop=${stopCode}`);
|
|
1923
|
+
const recvOpen = (codes.recvOpen || '').replace(/\s/g, '').toUpperCase();
|
|
1924
|
+
const recvClose = (codes.recvClose || '').replace(/\s/g, '').toUpperCase();
|
|
1925
|
+
const recvStop = (codes.recvStop || '').replace(/\s/g, '').toUpperCase();
|
|
1812
1926
|
|
|
1813
|
-
if (
|
|
1927
|
+
if (recvOpen && hexStr.includes(recvOpen)) {
|
|
1814
1928
|
matchedAction = { action: 'open' };
|
|
1815
|
-
node.log(`[
|
|
1816
|
-
} else if (
|
|
1929
|
+
node.log(`[自定义窗帘] 收开码`);
|
|
1930
|
+
} else if (recvClose && hexStr.includes(recvClose)) {
|
|
1817
1931
|
matchedAction = { action: 'close' };
|
|
1818
|
-
node.log(`[
|
|
1819
|
-
} else if (
|
|
1932
|
+
node.log(`[自定义窗帘] 收关码`);
|
|
1933
|
+
} else if (recvStop && hexStr.includes(recvStop)) {
|
|
1820
1934
|
matchedAction = { action: 'stop' };
|
|
1821
|
-
node.log(`[
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1935
|
+
node.log(`[自定义窗帘] 收停码`);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
// 空调类型:匹配收码
|
|
1939
|
+
else if (mapping.device === 'custom_climate') {
|
|
1940
|
+
if (codes.acRecvOn && hexStr.includes(codes.acRecvOn.replace(/\s/g, '').toUpperCase())) {
|
|
1941
|
+
matchedAction = { acSwitch: true };
|
|
1942
|
+
} else if (codes.acRecvOff && hexStr.includes(codes.acRecvOff.replace(/\s/g, '').toUpperCase())) {
|
|
1943
|
+
matchedAction = { acSwitch: false };
|
|
1944
|
+
}
|
|
1945
|
+
else if (codes.fanRecvHigh && hexStr.includes(codes.fanRecvHigh.replace(/\s/g, '').toUpperCase())) {
|
|
1946
|
+
matchedAction = { acFanSpeed: 1 };
|
|
1947
|
+
} else if (codes.fanRecvMid && hexStr.includes(codes.fanRecvMid.replace(/\s/g, '').toUpperCase())) {
|
|
1948
|
+
matchedAction = { acFanSpeed: 2 };
|
|
1949
|
+
} else if (codes.fanRecvLow && hexStr.includes(codes.fanRecvLow.replace(/\s/g, '').toUpperCase())) {
|
|
1950
|
+
matchedAction = { acFanSpeed: 3 };
|
|
1951
|
+
}
|
|
1952
|
+
else if (codes.modeRecvCool && hexStr.includes(codes.modeRecvCool.replace(/\s/g, '').toUpperCase())) {
|
|
1953
|
+
matchedAction = { acMode: 1 };
|
|
1954
|
+
} else if (codes.modeRecvHeat && hexStr.includes(codes.modeRecvHeat.replace(/\s/g, '').toUpperCase())) {
|
|
1955
|
+
matchedAction = { acMode: 2 };
|
|
1956
|
+
} else if (codes.modeRecvDry && hexStr.includes(codes.modeRecvDry.replace(/\s/g, '').toUpperCase())) {
|
|
1957
|
+
matchedAction = { acMode: 4 };
|
|
1958
|
+
} else if (codes.modeRecvFan && hexStr.includes(codes.modeRecvFan.replace(/\s/g, '').toUpperCase())) {
|
|
1959
|
+
matchedAction = { acMode: 3 };
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// 温度匹配 (独立于模式匹配,因为一帧可能同时包含模式和温度,或者只有温度)
|
|
1963
|
+
if (!matchedAction) {
|
|
1964
|
+
let foundTemp = false;
|
|
1965
|
+
const climateModes = [
|
|
1966
|
+
{ suffix: 'Cool', val: 1 },
|
|
1967
|
+
{ suffix: 'Heat', val: 2 }
|
|
1968
|
+
];
|
|
1969
|
+
|
|
1970
|
+
// 1. 优先匹配 16-30度 独立码
|
|
1971
|
+
for (const m of climateModes) {
|
|
1972
|
+
for (let t = 16; t <= 30; t++) {
|
|
1973
|
+
const key = `tempRecv${m.suffix}${t}`;
|
|
1974
|
+
const code = (codes[key] || '').replace(/\s/g, '').toUpperCase();
|
|
1975
|
+
if (code && hexStr.includes(code)) {
|
|
1976
|
+
matchedAction = { acTargetTemp: t, acMode: m.val };
|
|
1977
|
+
foundTemp = true;
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (foundTemp) break;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// 2. 如果没匹配到独立码,匹配模板
|
|
1985
|
+
if (!foundTemp) {
|
|
1986
|
+
const tempTemplates = [
|
|
1987
|
+
{ template: codes.tempRecvCool, mode: 1 },
|
|
1988
|
+
{ template: codes.tempRecvHeat, mode: 2 },
|
|
1989
|
+
{ template: codes.tempRecv, mode: 0 } // 通用
|
|
1990
|
+
];
|
|
1991
|
+
|
|
1992
|
+
for (const item of tempTemplates) {
|
|
1993
|
+
if (item.template && item.template.includes('{TEMP}')) {
|
|
1994
|
+
const template = item.template.replace(/\s/g, '').toUpperCase();
|
|
1995
|
+
const parts = template.split('{TEMP}');
|
|
1996
|
+
if (parts.length === 2) {
|
|
1997
|
+
const prefix = parts[0];
|
|
1998
|
+
const suffix = parts[1];
|
|
1999
|
+
if (hexStr.includes(prefix)) {
|
|
2000
|
+
// 尝试从 hexStr 中提取温度
|
|
2001
|
+
const startIdx = hexStr.indexOf(prefix) + prefix.length;
|
|
2002
|
+
const tempHex = hexStr.substring(startIdx, startIdx + 2);
|
|
2003
|
+
const remaining = hexStr.substring(startIdx + 2);
|
|
2004
|
+
|
|
2005
|
+
if (tempHex.length === 2 && (!suffix || remaining.includes(suffix))) {
|
|
2006
|
+
const temp = parseInt(tempHex, 16);
|
|
2007
|
+
if (temp >= 16 && temp <= 30) {
|
|
2008
|
+
matchedAction = { acTargetTemp: temp };
|
|
2009
|
+
if (item.mode > 0) matchedAction.acMode = item.mode;
|
|
2010
|
+
foundTemp = true;
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
if (matchedAction) {
|
|
2022
|
+
// 更新缓存中的模式
|
|
2023
|
+
if (matchedAction.acMode !== undefined) {
|
|
2024
|
+
if (!node.climateCache[mapping.meshMac]) node.climateCache[mapping.meshMac] = {};
|
|
2025
|
+
node.climateCache[mapping.meshMac].mode = matchedAction.acMode;
|
|
2026
|
+
}
|
|
2027
|
+
node.log(`[自定义空调] 匹配: ${JSON.stringify(matchedAction)}`);
|
|
1824
2028
|
}
|
|
1825
2029
|
}
|
|
1826
2030
|
// 场景类型:匹配trigger
|
package/nodes/symi-knx-bridge.js
CHANGED
|
@@ -160,7 +160,7 @@ module.exports = function(RED) {
|
|
|
160
160
|
node.initTimer = setTimeout(() => {
|
|
161
161
|
node.initializing = false;
|
|
162
162
|
node.log('[KNX Bridge] 初始化完成,开始同步');
|
|
163
|
-
},
|
|
163
|
+
}, 10000); // 10秒初始化延迟
|
|
164
164
|
|
|
165
165
|
if (node.mappings.length === 0) {
|
|
166
166
|
node.status({ fill: 'grey', shape: 'ring', text: '请添加实体映射' });
|
package/nodes/symi-mqtt.js
CHANGED
|
@@ -1342,11 +1342,27 @@ module.exports = function(RED) {
|
|
|
1342
1342
|
|
|
1343
1343
|
} else if (topic.includes('/fan_mode/set')) {
|
|
1344
1344
|
// 温控器/三合一空调风速 (0x1C协议: 1=高, 2=中, 3=低, 4=自动)
|
|
1345
|
+
node.log(`[MQTT] 收到风速控制: topic=${topic}, payload=${payload}`);
|
|
1345
1346
|
const fans = { 'high': 1, 'medium': 2, 'low': 3, 'auto': 4 };
|
|
1346
1347
|
const fan = fans[payload];
|
|
1348
|
+
node.log(`[MQTT] 风速映射: ${payload} -> ${fan}`);
|
|
1347
1349
|
if (fan !== undefined) {
|
|
1348
1350
|
commands.push({ attrType: 0x1C, param: Buffer.from([fan]) });
|
|
1349
|
-
|
|
1351
|
+
node.log(`[MQTT解析] 空调风速命令: 0x1C=0x${fan.toString(16).toUpperCase()} (${payload})`);
|
|
1352
|
+
|
|
1353
|
+
// 立即触发状态变化事件,通知RS485桥接节点
|
|
1354
|
+
if (device && node.gateway && node.gateway.deviceManager) {
|
|
1355
|
+
node.log(`[MQTT] 触发风速状态变化事件: fanMode=${fan}`);
|
|
1356
|
+
device.state.fanMode = fan;
|
|
1357
|
+
node.gateway.deviceManager.emit('stateChange', {
|
|
1358
|
+
device: device,
|
|
1359
|
+
state: { fanMode: fan },
|
|
1360
|
+
attrType: 0x1C,
|
|
1361
|
+
isUserControl: true
|
|
1362
|
+
});
|
|
1363
|
+
} else {
|
|
1364
|
+
node.warn(`[MQTT] 无法触发事件: device=${!!device}, gateway=${!!node.gateway}, deviceManager=${!!node.gateway?.deviceManager}`);
|
|
1365
|
+
}
|
|
1350
1366
|
}
|
|
1351
1367
|
|
|
1352
1368
|
} else if (topic.endsWith('/cover/set')) {
|