node-red-contrib-symi-mesh 1.7.0 → 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 +26 -29
- package/lib/device-manager.js +56 -5
- package/nodes/symi-485-bridge.js +115 -25
- 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
|
### 核心协议格式
|
|
@@ -1254,6 +1255,30 @@ node-red-contrib-symi-mesh/
|
|
|
1254
1255
|
|
|
1255
1256
|
## 更新日志
|
|
1256
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
|
+
|
|
1257
1282
|
### v1.7.0 (2025-12-21)
|
|
1258
1283
|
- **自定义协议增强**:RS485桥接节点自定义协议功能全面升级
|
|
1259
1284
|
- 自定义开关:添加发开、发关、收开、收关4组码
|
|
@@ -1314,34 +1339,6 @@ node-red-contrib-symi-mesh/
|
|
|
1314
1339
|
- 多设备类型:开关、调光灯、窗帘、空调、新风、地暖
|
|
1315
1340
|
- 智能通道选择:根据Mesh设备实际路数显示可选通道
|
|
1316
1341
|
|
|
1317
|
-
### v1.6.6 (2025-12-09)
|
|
1318
|
-
- **三合一面板完整双向同步**:支持空调+新风+地暖独立RS485映射
|
|
1319
|
-
- 三合一0x94协议完整解析:空调(开关/模式/风速/温度)、地暖(开关/温度)、新风(开关/风速)
|
|
1320
|
-
- Mesh→RS485:三合一状态变化自动发送对应RS485帧
|
|
1321
|
-
- RS485→Mesh:RS485帧自动同步到三合一面板对应功能
|
|
1322
|
-
- 支持climateSwitch/climateMode/fanMode/floorHeatingSwitch/freshAirSwitch等字段
|
|
1323
|
-
- **开关双向同步修复**:修复米家App全开/全关只触发部分按键的问题
|
|
1324
|
-
- 命令队列防抖逻辑修复:只有完全相同映射才合并,不同映射独立处理
|
|
1325
|
-
- 支持同一Mesh设备多通道分别绑定不同RS485设备
|
|
1326
|
-
- **RS485多映射匹配**:修复同一从机地址多个映射时只匹配第一个的问题
|
|
1327
|
-
- findAllRS485Mappings返回所有匹配的映射
|
|
1328
|
-
- rs485Channel精确匹配:根据寄存器地址确定通道
|
|
1329
|
-
- **网络异常处理增强**:彻底修复Node.js 18+ AggregateError导致崩溃的问题
|
|
1330
|
-
- 全局uncaughtException/unhandledRejection处理器
|
|
1331
|
-
- TCP连接过程同步错误捕获
|
|
1332
|
-
- 只捕获网络相关错误,不影响其他异常
|
|
1333
|
-
- **三合一面板自动识别**:设备列表API返回isThreeInOne标志
|
|
1334
|
-
- **窗帘双协议兼容**:同时支持米家和小程序两种控制协议
|
|
1335
|
-
- 米家协议: `subOpcode=0x06, status: 0=打开中, 1=关闭中, 2=停止`
|
|
1336
|
-
- 小程序协议: `subOpcode=0x05, status: 1=打开, 2=关闭, 3=停止`
|
|
1337
|
-
- **话语前湾协议完善**:
|
|
1338
|
-
- 空调:客厅/主卧/次卧1/次卧2,寄存器0x0FA0-0x0FAF
|
|
1339
|
-
- 地暖:客餐厅(60)/主卧(61)/次卧1(62)/次卧2(63),寄存器0x0039/0x0043
|
|
1340
|
-
- 新风:从机60,开关0x0039(开=1),风速0x004B(高=2,低=0)
|
|
1341
|
-
- **地址输入修复**:允许从机地址为0(话语前湾空调地址是0)
|
|
1342
|
-
- **内存安全**:命令队列限制100条,节点关闭时清理所有缓存
|
|
1343
|
-
- **文档更新**:添加三合一面板RS485配置说明和协议对照表
|
|
1344
|
-
|
|
1345
1342
|
## 许可证
|
|
1346
1343
|
|
|
1347
1344
|
MIT License
|
|
@@ -1353,7 +1350,7 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1353
1350
|
## 关于
|
|
1354
1351
|
|
|
1355
1352
|
**作者**: SYMI 亖米
|
|
1356
|
-
**版本**: 1.7.
|
|
1353
|
+
**版本**: 1.7.1
|
|
1357
1354
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1358
1355
|
**最后更新**: 2025-12-21
|
|
1359
1356
|
**仓库**: https://github.com/symi-daguo/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 {
|
package/nodes/symi-485-bridge.js
CHANGED
|
@@ -524,6 +524,8 @@ module.exports = function(RED) {
|
|
|
524
524
|
|
|
525
525
|
// 状态缓存 - 用于检测真正变化的开关
|
|
526
526
|
node.stateCache = {};
|
|
527
|
+
// 空调模式缓存 - 用于温度控制时确定当前模式
|
|
528
|
+
node.climateCache = {};
|
|
527
529
|
// 首次启动标记 - 跳过初始状态同步
|
|
528
530
|
node.initializing = true;
|
|
529
531
|
// 启动后延迟20秒再开始同步(Mesh网关需要15秒以上完成设备发现)
|
|
@@ -804,12 +806,22 @@ module.exports = function(RED) {
|
|
|
804
806
|
);
|
|
805
807
|
const hasACChange = isAC && (
|
|
806
808
|
changed.targetTemp !== undefined ||
|
|
809
|
+
changed.acTargetTemp !== undefined ||
|
|
810
|
+
changed.temperature !== undefined ||
|
|
807
811
|
changed.acMode !== undefined ||
|
|
812
|
+
changed.climateMode !== undefined ||
|
|
813
|
+
changed.mode !== undefined ||
|
|
808
814
|
changed.acFanSpeed !== undefined ||
|
|
809
|
-
|
|
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 ||
|
|
810
823
|
changed.climateSwitch !== undefined ||
|
|
811
|
-
changed.
|
|
812
|
-
changed.fanMode !== undefined
|
|
824
|
+
changed.switch !== undefined
|
|
813
825
|
);
|
|
814
826
|
// 新风设备检测
|
|
815
827
|
const isFreshAir = device.includes('fresh_air');
|
|
@@ -1233,25 +1245,50 @@ module.exports = function(RED) {
|
|
|
1233
1245
|
}
|
|
1234
1246
|
// 空调类型
|
|
1235
1247
|
else if (mapping.device === 'custom_climate') {
|
|
1248
|
+
node.log(`[自定义空调] 处理状态: ${JSON.stringify(state)}, codes存在: ${!!codes}`);
|
|
1236
1249
|
for (const [key, value] of Object.entries(state)) {
|
|
1237
1250
|
let hexCode = null;
|
|
1238
|
-
|
|
1239
|
-
|
|
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}`);
|
|
1240
1257
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
+
}
|
|
1245
1275
|
}
|
|
1246
|
-
|
|
1276
|
+
// 3. 模式控制
|
|
1277
|
+
else if (key === 'acMode' || key === 'climateMode' || key === 'mode') {
|
|
1247
1278
|
if (value === 1) hexCode = codes.modeSendCool;
|
|
1248
1279
|
else if (value === 2) hexCode = codes.modeSendHeat;
|
|
1249
1280
|
else if (value === 4) hexCode = codes.modeSendDry;
|
|
1250
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;
|
|
1251
1286
|
}
|
|
1252
|
-
|
|
1287
|
+
// 4. 温度控制
|
|
1288
|
+
else if (key === 'targetTemp' || key === 'acTargetTemp' || key === 'temperature') {
|
|
1253
1289
|
const temp = Math.round(value);
|
|
1254
|
-
|
|
1290
|
+
// 优先从当前变化中获取模式,否则从缓存中获取,默认制冷(1)
|
|
1291
|
+
const mode = state.acMode || state.climateMode || (node.climateCache[mapping.meshMac] ? node.climateCache[mapping.meshMac].mode : 1);
|
|
1255
1292
|
|
|
1256
1293
|
// 1. 优先尝试特定温度码,如 tempSendCool16
|
|
1257
1294
|
const modeSuffix = (mode === 2) ? 'Heat' : 'Cool';
|
|
@@ -1529,19 +1566,44 @@ module.exports = function(RED) {
|
|
|
1529
1566
|
for (const [key, value] of Object.entries(state)) {
|
|
1530
1567
|
try {
|
|
1531
1568
|
// 开关类型
|
|
1532
|
-
if (key === 'switch') {
|
|
1569
|
+
if (key === 'switch' || key.startsWith('switch_')) {
|
|
1570
|
+
const ch = key.startsWith('switch_') ? parseInt(key.replace('switch_', '')) : channel;
|
|
1533
1571
|
const onOff = value ? 0x02 : 0x01;
|
|
1534
|
-
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]);
|
|
1535
1579
|
await node.gateway.sendControl(meshDevice.networkAddress, 0x02, param);
|
|
1536
|
-
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`);
|
|
1537
1599
|
}
|
|
1538
1600
|
// 窗帘类型
|
|
1539
|
-
else if (key === 'action' || key === 'position') {
|
|
1601
|
+
else if (key === 'action' || key === 'position' || key === 'curtainAction' || key === 'curtainPosition') {
|
|
1540
1602
|
// 窗帘动作: 1=打开, 2=关闭, 3=停止
|
|
1541
1603
|
let action = 0x03; // 停止
|
|
1542
|
-
if (value === 'open') action = 0x01; // 打开
|
|
1543
|
-
else if (value === 'close') action = 0x02; // 关闭
|
|
1544
|
-
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; // 停止
|
|
1545
1607
|
|
|
1546
1608
|
const param = Buffer.from([action]);
|
|
1547
1609
|
// 0x05是窗帘动作控制属性
|
|
@@ -1751,7 +1813,7 @@ module.exports = function(RED) {
|
|
|
1751
1813
|
node.parseRS485Frame = function(frame) {
|
|
1752
1814
|
if (frame.length < 4) return;
|
|
1753
1815
|
|
|
1754
|
-
const hexStr = frame.toString('hex').toUpperCase();
|
|
1816
|
+
const hexStr = frame.toString('hex').toUpperCase().replace(/\s/g, ''); // 确保完全去掉空格
|
|
1755
1817
|
const hexFormatted = hexStr.match(/.{1,2}/g)?.join(' ') || '';
|
|
1756
1818
|
|
|
1757
1819
|
node.log(`[RS485收到] ${hexFormatted} (${frame.length}字节)`);
|
|
@@ -1829,24 +1891,30 @@ module.exports = function(RED) {
|
|
|
1829
1891
|
const codes = mapping.customCodes;
|
|
1830
1892
|
let matchedAction = null;
|
|
1831
1893
|
|
|
1832
|
-
node.
|
|
1894
|
+
node.log(`[自定义码检测] 检查映射: device=${mapping.device}, meshMac=${mapping.meshMac}`);
|
|
1833
1895
|
|
|
1834
1896
|
// 开关类型:匹配recvOn/recvOff
|
|
1835
1897
|
if (mapping.device === 'custom_switch') {
|
|
1836
1898
|
const recvOn = (codes.recvOn || '').replace(/\s/g, '').toUpperCase();
|
|
1837
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)}`);
|
|
1838
1902
|
|
|
1839
1903
|
// 翻转模式:收开码=收关码
|
|
1840
1904
|
if (recvOn && recvOff && recvOn === recvOff && hexStr.includes(recvOn)) {
|
|
1841
1905
|
const device = node.gateway.deviceManager.getDeviceByMac(mapping.meshMac);
|
|
1842
|
-
const
|
|
1906
|
+
const channel = mapping.meshChannel || 1;
|
|
1907
|
+
const stateKey = mapping.meshChannel > 1 ? `switch_${channel}` : 'switch';
|
|
1908
|
+
const currentState = device?.state?.[stateKey] || false;
|
|
1843
1909
|
matchedAction = { switch: !currentState };
|
|
1844
|
-
node.log(`[自定义开关] 翻转: ${currentState} -> ${!currentState}`);
|
|
1910
|
+
node.log(`[自定义开关] 翻转: ${stateKey} ${currentState} -> ${!currentState}`);
|
|
1845
1911
|
} else {
|
|
1846
1912
|
if (recvOn && hexStr.includes(recvOn)) {
|
|
1847
1913
|
matchedAction = { switch: true };
|
|
1914
|
+
node.log(`[自定义开关] 匹配到收开码`);
|
|
1848
1915
|
} else if (recvOff && hexStr.includes(recvOff)) {
|
|
1849
1916
|
matchedAction = { switch: false };
|
|
1917
|
+
node.log(`[自定义开关] 匹配到收关码`);
|
|
1850
1918
|
}
|
|
1851
1919
|
}
|
|
1852
1920
|
}
|
|
@@ -1882,7 +1950,25 @@ module.exports = function(RED) {
|
|
|
1882
1950
|
matchedAction = { acFanSpeed: 3 };
|
|
1883
1951
|
}
|
|
1884
1952
|
else if (codes.modeRecvCool && hexStr.includes(codes.modeRecvCool.replace(/\s/g, '').toUpperCase())) {
|
|
1885
|
-
|
|
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) {
|
|
1886
1972
|
for (let t = 16; t <= 30; t++) {
|
|
1887
1973
|
const key = `tempRecv${m.suffix}${t}`;
|
|
1888
1974
|
const code = (codes[key] || '').replace(/\s/g, '').toUpperCase();
|
|
@@ -1912,7 +1998,6 @@ module.exports = function(RED) {
|
|
|
1912
1998
|
const suffix = parts[1];
|
|
1913
1999
|
if (hexStr.includes(prefix)) {
|
|
1914
2000
|
// 尝试从 hexStr 中提取温度
|
|
1915
|
-
// 简单起见,如果包含前缀且后面有2位十六进制+后缀
|
|
1916
2001
|
const startIdx = hexStr.indexOf(prefix) + prefix.length;
|
|
1917
2002
|
const tempHex = hexStr.substring(startIdx, startIdx + 2);
|
|
1918
2003
|
const remaining = hexStr.substring(startIdx + 2);
|
|
@@ -1934,6 +2019,11 @@ module.exports = function(RED) {
|
|
|
1934
2019
|
}
|
|
1935
2020
|
|
|
1936
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
|
+
}
|
|
1937
2027
|
node.log(`[自定义空调] 匹配: ${JSON.stringify(matchedAction)}`);
|
|
1938
2028
|
}
|
|
1939
2029
|
}
|
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')) {
|