node-red-contrib-symi-mesh 1.6.5 → 1.6.6
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 +219 -27
- package/lib/device-manager.js +30 -3
- package/lib/mqtt-helper.js +3 -3
- package/lib/tcp-client.js +29 -13
- package/nodes/symi-485-bridge.html +15 -3
- package/nodes/symi-485-bridge.js +747 -206
- package/nodes/symi-485-config.js +54 -4
- package/nodes/symi-device.js +7 -7
- package/nodes/symi-gateway.js +86 -14
- package/nodes/symi-mqtt.js +22 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ node-red-restart
|
|
|
202
202
|
|
|
203
203
|
### 开关状态编码(重要)
|
|
204
204
|
|
|
205
|
-
**协议规则**: 每2位表示1
|
|
205
|
+
**协议规则**: 每2位表示1路,`01`=关,`10`=开,从最低位开始
|
|
206
206
|
|
|
207
207
|
| 路数 | 全关 | 全开 | 示例 |
|
|
208
208
|
|-----|------|------|------|
|
|
@@ -211,24 +211,57 @@ node-red-restart
|
|
|
211
211
|
| 3路 | 0x15 | 0x2A | 从低位开始 |
|
|
212
212
|
| 4路 | 0x55 | 0xAA | 每2位1路 |
|
|
213
213
|
| 6路 | 0x5555 | 0xAAAA | 2字节小端序 |
|
|
214
|
+
| 8路 | 0x5555 | 0xAAAA | 2字节小端序 |
|
|
215
|
+
|
|
216
|
+
#### 8路开关状态详解
|
|
217
|
+
|
|
218
|
+
8路开关使用2字节(16位)表示状态,小端序传输。实测帧示例:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
帧格式: 53 80 [Status] [Length] [网络地址LE] [AttrType] [状态LE] [Checksum]
|
|
222
|
+
示例帧: 53 80 05 05 E4 01 45 56 55 70
|
|
223
|
+
│ │ │ │ └─────┘ │ └───┘ └─ 校验和
|
|
224
|
+
│ │ │ │ │ │ └─ 状态值 0x5556 (第1路开)
|
|
225
|
+
│ │ │ │ │ └─ AttrType=0x45 (8路开关)
|
|
226
|
+
│ │ │ │ └─ 网络地址 0x01E4
|
|
227
|
+
│ │ │ └─ 数据长度=5字节
|
|
228
|
+
│ │ └─ 状态码
|
|
229
|
+
│ └─ Opcode=0x80 (事件上报)
|
|
230
|
+
└─ 帧头
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**状态值对照表**(小端序):
|
|
234
|
+
| 状态数据 | 16位值 | 二进制 | 含义 |
|
|
235
|
+
|---------|--------|--------|------|
|
|
236
|
+
| 55 55 | 0x5555 | 01010101 01010101 | 全关 |
|
|
237
|
+
| 56 55 | 0x5556 | 01010101 01010110 | 第1路开 |
|
|
238
|
+
| 59 55 | 0x5559 | 01010101 01011001 | 第2路开 |
|
|
239
|
+
| 65 55 | 0x5565 | 01010101 01100101 | 第3路开 |
|
|
240
|
+
| 95 55 | 0x5595 | 01010101 10010101 | 第4路开 |
|
|
241
|
+
| 55 56 | 0x5655 | 01010110 01010101 | 第5路开 |
|
|
242
|
+
| 55 59 | 0x5955 | 01011001 01010101 | 第6路开 |
|
|
243
|
+
| 55 65 | 0x6555 | 01100101 01010101 | 第7路开 |
|
|
244
|
+
| 55 95 | 0x9555 | 10010101 01010101 | 第8路开 |
|
|
245
|
+
| AA AA | 0xAAAA | 10101010 10101010 | 全开 |
|
|
214
246
|
|
|
215
247
|
### 常用消息类型
|
|
216
248
|
|
|
217
249
|
| 消息类型 | 代码 | 数据格式 | 范围/说明 |
|
|
218
250
|
|---------|------|---------|----------|
|
|
219
|
-
| 开关状态 | 0x02 | 1
|
|
251
|
+
| 开关状态 | 0x02 | 1字节 | 1-4路开关,见上表 |
|
|
220
252
|
| 亮度 | 0x03 | 1字节 | 0-100% |
|
|
221
253
|
| 色温 | 0x04 | 1字节 | 0%=暖白, 100%=冷白 |
|
|
222
254
|
| 窗帘动作 | 0x05 | 1字节 | 1=开/2=关/3=停 |
|
|
223
255
|
| 窗帘位置 | 0x06 | 1字节 | 0-100% |
|
|
224
256
|
| 人体感应 | 0x0C | 1字节 | 0=无活动/1=检测到 |
|
|
225
257
|
| 插卡状态 | 0x0E | 1字节 | 0=无卡/1=有卡 |
|
|
226
|
-
|
|
|
227
|
-
| 当前温度 | 0x16 | 2字节LE | 温度*100
|
|
228
|
-
| 目标温度 | 0x1B |
|
|
258
|
+
| 湿度 | 0x17 | 1字节 | 0-100% |
|
|
259
|
+
| 当前温度 | 0x16 | 2字节LE | 温度*100(如2500=25.00°C)|
|
|
260
|
+
| 目标温度 | 0x1B | 1字节 | 16-30°C(直接温度值)|
|
|
229
261
|
| 风速 | 0x1C | 1字节 | 1=高/2=中/3=低/4=自动 |
|
|
230
262
|
| 模式 | 0x1D | 1字节 | 1=制冷/2=制热/3=送风/4=除湿 |
|
|
231
|
-
| 6
|
|
263
|
+
| 6-8路开关 | 0x45 | 2字节LE | 多路开关状态,见上表 |
|
|
264
|
+
| 五色RGB | 0x4C | 5字节 | R/G/B/WW/CW (各0-255) |
|
|
232
265
|
|
|
233
266
|
## MQTT主题结构
|
|
234
267
|
|
|
@@ -531,10 +564,35 @@ Mesh 四键开关 第2路 ↔ RS485 六键开关 第5路 地址:2 ✓ 支持
|
|
|
531
564
|
|-------------|--------------|---------|
|
|
532
565
|
| 开关-按键N | 开关 | 开/关状态 |
|
|
533
566
|
| 温控器 | 空调 | 开关、温度、模式、风速 |
|
|
567
|
+
| 三合一面板 | 空调+新风+地暖 | 分别配置,独立同步 |
|
|
534
568
|
| 调光灯 | 调光器 | 开关、亮度 |
|
|
535
569
|
|
|
536
570
|
**部分同步**:如果RS485协议不提供某些功能点,只同步双方都支持的内容
|
|
537
571
|
|
|
572
|
+
### 三合一面板配置
|
|
573
|
+
|
|
574
|
+
三合一面板(温控器类型)可同时控制空调、新风、地暖,需要创建3个独立映射:
|
|
575
|
+
|
|
576
|
+
#### 配置示例(话语前湾协议)
|
|
577
|
+
|
|
578
|
+
| Mesh设备 | RS485设备类型 | 从机地址 | 说明 |
|
|
579
|
+
|---------|--------------|---------|------|
|
|
580
|
+
| 三合一面板_xxx | 次卧1空调 | 0 | 空调寄存器0x0FA4-0x0FA7 |
|
|
581
|
+
| 三合一面板_xxx | 新风 | 60 | 开关0x0039,风速0x004B |
|
|
582
|
+
| 三合一面板_xxx | 次卧1地暖 | 62 | 开关0x0039,温度0x0043 |
|
|
583
|
+
|
|
584
|
+
#### 话语前湾地暖/新风从机地址
|
|
585
|
+
|
|
586
|
+
| 设备 | 从机地址 | 十六进制 |
|
|
587
|
+
|------|---------|---------|
|
|
588
|
+
| 客餐厅地暖 | 60 | 0x3C |
|
|
589
|
+
| 新风 | 60 | 0x3C |
|
|
590
|
+
| 主卧地暖 | 61 | 0x3D |
|
|
591
|
+
| 次卧1地暖 | 62 | 0x3E |
|
|
592
|
+
| 次卧2地暖 | 63 | 0x3F |
|
|
593
|
+
|
|
594
|
+
> **注意**:新风和客餐厅地暖共用从机地址60,但开关值不同(新风开=1,地暖开=2),请分别创建映射。
|
|
595
|
+
|
|
538
596
|
### 内置协议支持
|
|
539
597
|
|
|
540
598
|
| 品牌 | 支持设备 |
|
|
@@ -992,6 +1050,131 @@ A: 每个房间部署一个云端同步节点,配置对应的酒店ID和房间
|
|
|
992
1050
|
- 获取失败时自动使用上次缓存的数据
|
|
993
1051
|
- 可以禁用节点,不影响其他功能正常运行
|
|
994
1052
|
|
|
1053
|
+
## RS485网桥节点
|
|
1054
|
+
|
|
1055
|
+
### 功能概述
|
|
1056
|
+
|
|
1057
|
+
RS485网桥节点实现Mesh网关与第三方RS485设备的**双向同步**:
|
|
1058
|
+
|
|
1059
|
+
- **事件驱动**:状态变化时立即同步,无轮询
|
|
1060
|
+
- **多协议支持**:标准Modbus RTU、杜亚窗帘、自定义十六进制码
|
|
1061
|
+
- **按键对按键**:Mesh开关的每一路可独立映射到RS485的任意按键
|
|
1062
|
+
- **防循环保护**:智能防抖机制,避免无限循环
|
|
1063
|
+
- **内存安全**:命令队列限制100条,缓存自动清理
|
|
1064
|
+
|
|
1065
|
+
### 配置说明
|
|
1066
|
+
|
|
1067
|
+
#### 1. 添加RS485配置节点
|
|
1068
|
+
|
|
1069
|
+
首先配置RS485连接(串口或TCP):
|
|
1070
|
+
|
|
1071
|
+
- **连接方式**: 串口 或 TCP
|
|
1072
|
+
- **串口模式**: 选择串口路径,波特率默认9600
|
|
1073
|
+
- **TCP模式**: 输入IP地址和端口
|
|
1074
|
+
|
|
1075
|
+
#### 2. 添加RS485桥接节点
|
|
1076
|
+
|
|
1077
|
+
配置实体映射:
|
|
1078
|
+
|
|
1079
|
+
| 配置项 | 说明 |
|
|
1080
|
+
|-------|------|
|
|
1081
|
+
| **Mesh实体** | 选择要同步的Mesh设备 |
|
|
1082
|
+
| **Mesh按键** | 选择具体哪一路(多路开关) |
|
|
1083
|
+
| **品牌** | 选择协议品牌(话语前湾、杜亚、自定义等) |
|
|
1084
|
+
| **设备类型** | 选择设备类型(决定寄存器配置) |
|
|
1085
|
+
| **RS485按键** | 选择RS485端的哪一路 |
|
|
1086
|
+
| **从机地址** | Modbus从机地址(0-255,空调填0) |
|
|
1087
|
+
|
|
1088
|
+
### 话语前湾协议配置
|
|
1089
|
+
|
|
1090
|
+
话语前湾项目使用以下协议:
|
|
1091
|
+
|
|
1092
|
+
#### 开关设备 (A4B3)
|
|
1093
|
+
|
|
1094
|
+
| 设备 | 从机地址 | 配置方式 |
|
|
1095
|
+
|-----|---------|---------|
|
|
1096
|
+
| 客厅主灯 | 3 | 选择"话语前湾" → "一键开关",地址填3 |
|
|
1097
|
+
| 餐厅主灯 | 3 | 选择"话语前湾" → "三键开关",按键3,地址填3 |
|
|
1098
|
+
| 玄关灯带 | 1 | 选择"话语前湾" → "四键开关",按键4,地址填1 |
|
|
1099
|
+
|
|
1100
|
+
**寄存器映射**:
|
|
1101
|
+
- 按键1: 0x1031
|
|
1102
|
+
- 按键2: 0x1032
|
|
1103
|
+
- 按键3: 0x1033
|
|
1104
|
+
- 按键4: 0x1034
|
|
1105
|
+
- 按键5: 0x1035
|
|
1106
|
+
- 按键6: 0x1036
|
|
1107
|
+
|
|
1108
|
+
#### 空调设备 (A5B5)
|
|
1109
|
+
|
|
1110
|
+
**所有空调从机地址都是0**,通过设备类型区分房间:
|
|
1111
|
+
|
|
1112
|
+
| 房间 | 设备类型 | 寄存器范围 |
|
|
1113
|
+
|-----|---------|-----------|
|
|
1114
|
+
| 客厅空调 | ac_living | 0x0FA0-0x0FA3 |
|
|
1115
|
+
| 次卧1空调 | ac_bedroom2_1 | 0x0FA4-0x0FA7 |
|
|
1116
|
+
| 次卧2空调 | ac_bedroom2_2 | 0x0FA8-0x0FAB |
|
|
1117
|
+
| 主卧空调 | ac_master | 0x0FAC-0x0FAF |
|
|
1118
|
+
|
|
1119
|
+
**配置示例**:选择"话语前湾" → "主卧空调",地址填**0**
|
|
1120
|
+
|
|
1121
|
+
#### 地暖设备 (A3B3)
|
|
1122
|
+
|
|
1123
|
+
| 房间 | 从机地址 | 说明 |
|
|
1124
|
+
|-----|---------|------|
|
|
1125
|
+
| 客餐厅 | 60 (0x3C) | 开关:0x0039, 温度:0x0043 |
|
|
1126
|
+
| 主卧 | 61 (0x3D) | 开关:0x0039, 温度:0x0043 |
|
|
1127
|
+
| 次卧1 | 62 (0x3E) | 开关:0x0039, 温度:0x0043 |
|
|
1128
|
+
| 次卧2 | 63 (0x3F) | 开关:0x0039, 温度:0x0043 |
|
|
1129
|
+
|
|
1130
|
+
**配置示例**:选择"话语前湾" → "地暖",地址填60/61/62/63
|
|
1131
|
+
|
|
1132
|
+
#### 新风设备 (A3B3)
|
|
1133
|
+
|
|
1134
|
+
| 参数 | 从机地址 | 说明 |
|
|
1135
|
+
|-----|---------|------|
|
|
1136
|
+
| 新风 | 60 (0x3C) | 开关:0x0039(值01), 风速:0x004B |
|
|
1137
|
+
|
|
1138
|
+
**配置示例**:选择"话语前湾" → "新风",地址填60
|
|
1139
|
+
|
|
1140
|
+
#### 杜亚窗帘 (A6B6)
|
|
1141
|
+
|
|
1142
|
+
| 窗帘 | 地址高 | 地址低 |
|
|
1143
|
+
|-----|-------|-------|
|
|
1144
|
+
| 主卧窗帘 | 2 | 2 |
|
|
1145
|
+
| 主卧窗纱 | 1 | 1 |
|
|
1146
|
+
|
|
1147
|
+
**配置示例**:选择"杜亚窗帘" → "窗帘",地址高填2,地址低填2
|
|
1148
|
+
|
|
1149
|
+
### 双向同步流程
|
|
1150
|
+
|
|
1151
|
+
```
|
|
1152
|
+
Mesh面板按键 → 网关 → device-state-changed事件
|
|
1153
|
+
↓
|
|
1154
|
+
RS485桥接节点检测状态变化
|
|
1155
|
+
↓
|
|
1156
|
+
匹配映射配置 + 检查isUserControl
|
|
1157
|
+
↓
|
|
1158
|
+
生成RS485帧 → 发送到485总线
|
|
1159
|
+
↓
|
|
1160
|
+
485设备执行动作
|
|
1161
|
+
|
|
1162
|
+
485面板/遥控器 → 485总线 → RS485桥接节点接收帧
|
|
1163
|
+
↓
|
|
1164
|
+
解析帧 + 匹配映射配置
|
|
1165
|
+
↓
|
|
1166
|
+
调用gateway.sendControl()
|
|
1167
|
+
↓
|
|
1168
|
+
Mesh设备执行动作 → HA状态更新
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
### 注意事项
|
|
1172
|
+
|
|
1173
|
+
1. **HA只对接Mesh设备**:485设备不会发布到HA,避免重复实体
|
|
1174
|
+
2. **窗帘防抖**:窗帘操作有1-2秒防抖,防止电机反馈误触发
|
|
1175
|
+
3. **初始化延迟**:启动后20秒才开始同步,等待Mesh设备发现完成
|
|
1176
|
+
4. **场景执行**:场景触发时多设备状态会排队同步,每命令间隔50ms
|
|
1177
|
+
|
|
995
1178
|
## 开发者信息
|
|
996
1179
|
|
|
997
1180
|
### 项目结构
|
|
@@ -1061,6 +1244,34 @@ node-red-contrib-symi-mesh/
|
|
|
1061
1244
|
|
|
1062
1245
|
## 更新日志
|
|
1063
1246
|
|
|
1247
|
+
### v1.6.6 (2025-12-09)
|
|
1248
|
+
- **三合一面板完整双向同步**:支持空调+新风+地暖独立RS485映射
|
|
1249
|
+
- 三合一0x94协议完整解析:空调(开关/模式/风速/温度)、地暖(开关/温度)、新风(开关/风速)
|
|
1250
|
+
- Mesh→RS485:三合一状态变化自动发送对应RS485帧
|
|
1251
|
+
- RS485→Mesh:RS485帧自动同步到三合一面板对应功能
|
|
1252
|
+
- 支持climateSwitch/climateMode/fanMode/floorHeatingSwitch/freshAirSwitch等字段
|
|
1253
|
+
- **开关双向同步修复**:修复米家App全开/全关只触发部分按键的问题
|
|
1254
|
+
- 命令队列防抖逻辑修复:只有完全相同映射才合并,不同映射独立处理
|
|
1255
|
+
- 支持同一Mesh设备多通道分别绑定不同RS485设备
|
|
1256
|
+
- **RS485多映射匹配**:修复同一从机地址多个映射时只匹配第一个的问题
|
|
1257
|
+
- findAllRS485Mappings返回所有匹配的映射
|
|
1258
|
+
- rs485Channel精确匹配:根据寄存器地址确定通道
|
|
1259
|
+
- **网络异常处理增强**:彻底修复Node.js 18+ AggregateError导致崩溃的问题
|
|
1260
|
+
- 全局uncaughtException/unhandledRejection处理器
|
|
1261
|
+
- TCP连接过程同步错误捕获
|
|
1262
|
+
- 只捕获网络相关错误,不影响其他异常
|
|
1263
|
+
- **三合一面板自动识别**:设备列表API返回isThreeInOne标志
|
|
1264
|
+
- **窗帘双协议兼容**:同时支持米家和小程序两种控制协议
|
|
1265
|
+
- 米家协议: `subOpcode=0x06, status: 0=打开中, 1=关闭中, 2=停止`
|
|
1266
|
+
- 小程序协议: `subOpcode=0x05, status: 1=打开, 2=关闭, 3=停止`
|
|
1267
|
+
- **话语前湾协议完善**:
|
|
1268
|
+
- 空调:客厅/主卧/次卧1/次卧2,寄存器0x0FA0-0x0FAF
|
|
1269
|
+
- 地暖:客餐厅(60)/主卧(61)/次卧1(62)/次卧2(63),寄存器0x0039/0x0043
|
|
1270
|
+
- 新风:从机60,开关0x0039(开=1),风速0x004B(高=2,低=0)
|
|
1271
|
+
- **地址输入修复**:允许从机地址为0(话语前湾空调地址是0)
|
|
1272
|
+
- **内存安全**:命令队列限制100条,节点关闭时清理所有缓存
|
|
1273
|
+
- **文档更新**:添加三合一面板RS485配置说明和协议对照表
|
|
1274
|
+
|
|
1064
1275
|
### v1.6.5 (2025-12-06)
|
|
1065
1276
|
- **杜亚窗帘协议**:原生支持杜亚窗帘协议(A6B6),2字节地址,自动CRC16计算
|
|
1066
1277
|
- 帧格式:55 [地址高] [地址低] 03 [动作/位置] [CRC16低] [CRC16高]
|
|
@@ -1078,25 +1289,6 @@ node-red-contrib-symi-mesh/
|
|
|
1078
1289
|
- **RS485调试增强**:新增协议测试发送功能
|
|
1079
1290
|
- **初始化延迟**:20秒,避免部署时误发命令
|
|
1080
1291
|
|
|
1081
|
-
### v1.6.4 (2025-12-05)
|
|
1082
|
-
- **代码优化**:移除未使用的 `three-in-one-detected` 事件监听器(死代码清理)
|
|
1083
|
-
- **TCP连接优化**:移除可能导致连接问题的keep-alive设置
|
|
1084
|
-
- **多从机支持修复**:修复只能配置从机ID1的问题,现在支持任意从机地址(1-255)
|
|
1085
|
-
- **映射数值类型修复**:确保address/meshChannel/rs485Channel字段为正确的数字类型
|
|
1086
|
-
|
|
1087
|
-
### v1.6.3 (2025-12-05)
|
|
1088
|
-
- **自定义协议模式**:支持任意RS485十六进制码双向匹配
|
|
1089
|
-
- **按键寄存器优化**:只处理按键寄存器(0x1031-0x1036),移除指示灯寄存器处理
|
|
1090
|
-
- **CRC校验正确**:发送帧与协议文档完全匹配
|
|
1091
|
-
|
|
1092
|
-
### v1.6.2 (2025-12-05)
|
|
1093
|
-
- **MQTT订阅修复**:修复闭包问题导致的设备MAC映射错误,确保HA实体可控
|
|
1094
|
-
- **内存泄漏修复**:节点关闭时正确移除gateway事件监听器,防止内存累积
|
|
1095
|
-
- **三合一面板完善**:空调/新风/地暖控制和状态反馈全面优化
|
|
1096
|
-
- **RS485双向同步**:支持Mesh与第三方485设备双向状态同步
|
|
1097
|
-
- **代码优化**:中文注释规范化,移除冗余代码,提升可维护性
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
1292
|
## 许可证
|
|
1101
1293
|
|
|
1102
1294
|
MIT License
|
|
@@ -1108,8 +1300,8 @@ Copyright (c) 2025 SYMI 亖米
|
|
|
1108
1300
|
## 关于
|
|
1109
1301
|
|
|
1110
1302
|
**作者**: SYMI 亖米
|
|
1111
|
-
**版本**: 1.6.
|
|
1303
|
+
**版本**: 1.6.6
|
|
1112
1304
|
**协议**: 蓝牙MESH网关(初级版)串口协议V1.0
|
|
1113
|
-
**最后更新**: 2025-12-
|
|
1305
|
+
**最后更新**: 2025-12-09
|
|
1114
1306
|
**仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
|
|
1115
1307
|
**npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
|
package/lib/device-manager.js
CHANGED
|
@@ -100,9 +100,36 @@ class DeviceInfo {
|
|
|
100
100
|
break;
|
|
101
101
|
case 0x05:
|
|
102
102
|
// CURT_RUN_STATUS - 窗帘运行状态
|
|
103
|
-
//
|
|
103
|
+
// 【兼容两种协议】:
|
|
104
|
+
// 米家协议: 0=打开中, 1=关闭中, 2=停止
|
|
105
|
+
// 小程序协议: 1=打开, 2=关闭, 3=停止
|
|
106
|
+
// 通过status=0(仅米家)和status=3(仅小程序)来区分协议类型
|
|
104
107
|
if (parameters.length > 0) {
|
|
105
|
-
|
|
108
|
+
const status = parameters[0];
|
|
109
|
+
this.state.curtainStatus = status;
|
|
110
|
+
|
|
111
|
+
// 检测协议类型:status=0只在米家出现,status=3只在小程序出现
|
|
112
|
+
if (status === 0) {
|
|
113
|
+
this.state.curtainProtocol = 'mijia';
|
|
114
|
+
this.state.curtainAction = 'opening';
|
|
115
|
+
} else if (status === 3) {
|
|
116
|
+
this.state.curtainProtocol = 'miniprogram';
|
|
117
|
+
this.state.curtainAction = 'stopped';
|
|
118
|
+
} else if (status === 1) {
|
|
119
|
+
// status=1: 米家=关闭中, 小程序=打开
|
|
120
|
+
if (this.state.curtainProtocol === 'mijia') {
|
|
121
|
+
this.state.curtainAction = 'closing';
|
|
122
|
+
} else {
|
|
123
|
+
this.state.curtainAction = 'opening';
|
|
124
|
+
}
|
|
125
|
+
} else if (status === 2) {
|
|
126
|
+
// status=2: 米家=停止, 小程序=关闭
|
|
127
|
+
if (this.state.curtainProtocol === 'mijia') {
|
|
128
|
+
this.state.curtainAction = 'stopped';
|
|
129
|
+
} else {
|
|
130
|
+
this.state.curtainAction = 'closing';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
106
133
|
}
|
|
107
134
|
break;
|
|
108
135
|
case 0x06:
|
|
@@ -161,7 +188,7 @@ class DeviceInfo {
|
|
|
161
188
|
// 目标温度TMPC_TEMP:1字节,直接温度值(16-30°C)
|
|
162
189
|
if (parameters.length > 0) {
|
|
163
190
|
const temp = parameters[0];
|
|
164
|
-
if (temp >=
|
|
191
|
+
if (temp >= 16 && temp <= 30) {
|
|
165
192
|
this.state.targetTemp = temp;
|
|
166
193
|
// 不推断开关状态,完全由0x02消息决定
|
|
167
194
|
}
|
package/lib/mqtt-helper.js
CHANGED
|
@@ -627,11 +627,11 @@ function convertStateValue(entityType, attrType, value, deviceType = null) {
|
|
|
627
627
|
|
|
628
628
|
case 'climate':
|
|
629
629
|
if (attrType === 0x16) {
|
|
630
|
-
// 当前温度:温度*100 -> 温度
|
|
630
|
+
// 当前温度:温度*100 -> 温度 (2字节小端序)
|
|
631
631
|
return (value / 100).toFixed(1);
|
|
632
632
|
} else if (attrType === 0x1B) {
|
|
633
|
-
//
|
|
634
|
-
return
|
|
633
|
+
// 目标温度:直接温度值 (1字节,16-30°C)
|
|
634
|
+
return value.toString();
|
|
635
635
|
} else if (attrType === 0x1C) {
|
|
636
636
|
// 风速:1=high, 2=medium, 3=low, 4=auto
|
|
637
637
|
const fanModes = { 1: 'high', 2: 'medium', 3: 'low', 4: 'auto' };
|
package/lib/tcp-client.js
CHANGED
|
@@ -57,21 +57,33 @@ class TCPClient extends EventEmitter {
|
|
|
57
57
|
}
|
|
58
58
|
}, 10000);
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// 捕获connect过程中的同步错误(如AggregateError)
|
|
61
|
+
try {
|
|
62
|
+
this.client.connect(this.port, this.host, () => {
|
|
63
|
+
if (!resolved && !rejected) {
|
|
64
|
+
resolved = true;
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
this.connected = true;
|
|
67
|
+
this.logger.log(`Connected to gateway at ${this.host}:${this.port}`);
|
|
68
|
+
this.emit('connected');
|
|
69
|
+
|
|
70
|
+
if (!this.processingQueue) {
|
|
71
|
+
this.startQueueProcessor();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} catch (connectError) {
|
|
78
|
+
// 捕获同步错误(包括AggregateError)
|
|
61
79
|
if (!resolved && !rejected) {
|
|
62
|
-
|
|
80
|
+
rejected = true;
|
|
63
81
|
clearTimeout(timeout);
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!this.processingQueue) {
|
|
69
|
-
this.startQueueProcessor();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
resolve();
|
|
82
|
+
this.logger.warn(`TCP连接同步错误: ${connectError.message || connectError}`);
|
|
83
|
+
this.handleDisconnect();
|
|
84
|
+
reject(connectError);
|
|
73
85
|
}
|
|
74
|
-
}
|
|
86
|
+
}
|
|
75
87
|
|
|
76
88
|
this.client.on('data', (data) => {
|
|
77
89
|
try {
|
|
@@ -88,9 +100,13 @@ class TCPClient extends EventEmitter {
|
|
|
88
100
|
|
|
89
101
|
this.client.on('error', (error) => {
|
|
90
102
|
clearTimeout(timeout);
|
|
103
|
+
// AggregateError特殊处理(Node.js 18+的IPv4/IPv6连接失败)
|
|
104
|
+
if (error.name === 'AggregateError' || error.errors) {
|
|
105
|
+
this.logger.warn(`TCP连接失败: 无法连接到 ${this.host}:${this.port}`);
|
|
106
|
+
}
|
|
91
107
|
// 只在首次连接或重要错误时记录,避免大量重复日志
|
|
92
108
|
// ECONNRESET通常是网络波动,不记录错误
|
|
93
|
-
if (!this.connected && error.code !== 'ECONNREFUSED' && error.code !== 'ECONNRESET') {
|
|
109
|
+
else if (!this.connected && error.code !== 'ECONNREFUSED' && error.code !== 'ECONNRESET') {
|
|
94
110
|
this.logger.error('TCP client error:', error.message);
|
|
95
111
|
}
|
|
96
112
|
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
'</div>';
|
|
170
170
|
} else {
|
|
171
171
|
addrHtml = '<div class="addr-col normal-addr">' +
|
|
172
|
-
'<input type="number" class="addr-input" value="' + (m.address
|
|
172
|
+
'<input type="number" class="addr-input" value="' + (m.address !== undefined ? m.address : 1) + '" min="0" max="255" title="Modbus从机地址(0-255)">' +
|
|
173
173
|
'</div>';
|
|
174
174
|
}
|
|
175
175
|
row.html(
|
|
@@ -258,7 +258,9 @@
|
|
|
258
258
|
|
|
259
259
|
container.find('.addr-input').off('change').on('change', function() {
|
|
260
260
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
261
|
-
|
|
261
|
+
var val = $(this).val();
|
|
262
|
+
mappings[idx].address = (val !== '' && val !== undefined) ? parseInt(val) : 1;
|
|
263
|
+
if (isNaN(mappings[idx].address)) mappings[idx].address = 1;
|
|
262
264
|
});
|
|
263
265
|
|
|
264
266
|
// 杜亚2字节地址输入
|
|
@@ -352,14 +354,24 @@
|
|
|
352
354
|
oneditsave: function() {
|
|
353
355
|
var mappings = [];
|
|
354
356
|
$('#mapping-list .mapping-row').each(function() {
|
|
357
|
+
// 获取地址值,允许0(话语前湾空调地址是0)
|
|
358
|
+
var addrVal = $(this).find('.addr-input').val();
|
|
359
|
+
var address = (addrVal !== '' && addrVal !== undefined) ? parseInt(addrVal) : 1;
|
|
360
|
+
if (isNaN(address)) address = 1;
|
|
361
|
+
|
|
355
362
|
var m = {
|
|
356
363
|
meshMac: $(this).find('.mesh-select').val() || '',
|
|
357
364
|
meshChannel: parseInt($(this).find('.mesh-channel').val()) || 1,
|
|
358
365
|
brand: $(this).find('.brand-select').val() || '',
|
|
359
366
|
device: $(this).find('.device-select').val() || '',
|
|
360
|
-
address:
|
|
367
|
+
address: address,
|
|
361
368
|
rs485Channel: parseInt($(this).find('.rs485-channel').val()) || 1
|
|
362
369
|
};
|
|
370
|
+
// 保存杜亚窗帘2字节地址
|
|
371
|
+
if (m.brand === 'duya') {
|
|
372
|
+
m.addrHigh = parseInt($(this).find('.addr-high').val()) || 1;
|
|
373
|
+
m.addrLow = parseInt($(this).find('.addr-low').val()) || 1;
|
|
374
|
+
}
|
|
363
375
|
// 保存自定义码
|
|
364
376
|
if (m.brand === 'custom') {
|
|
365
377
|
m.customCodes = {};
|