node-red-contrib-symi-modbus 1.5.6 → 1.6.0

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 CHANGED
@@ -34,6 +34,88 @@ npm install node-red-contrib-symi-modbus
34
34
 
35
35
  **已发布到npm:** https://www.npmjs.com/package/node-red-contrib-symi-modbus
36
36
 
37
+ ### Docker/容器环境安装
38
+
39
+ 本节点依赖`modbus-serial`包,该包包含native C++模块(serialport),需要编译环境。
40
+
41
+ #### ⚠️ 如果遇到安装错误
42
+
43
+ **错误示例:**
44
+ ```
45
+ npm error code 127
46
+ npm error command sh -c node-gyp-build
47
+ npm error sh: node-gyp-build: not found
48
+ ```
49
+
50
+ **原因:** Docker容器缺少编译工具(python、make、g++)
51
+
52
+ **解决方案1:使用官方Node-RED Docker镜像(推荐)**
53
+
54
+ 官方镜像已包含编译工具:
55
+ ```bash
56
+ docker pull nodered/node-red:latest
57
+ ```
58
+
59
+ **解决方案2:在Dockerfile中添加编译依赖**
60
+
61
+ Alpine基础镜像:
62
+ ```dockerfile
63
+ FROM nodered/node-red:latest
64
+ # 或者自定义镜像时添加
65
+ RUN apk add --no-cache \
66
+ python3 \
67
+ make \
68
+ g++ \
69
+ linux-headers
70
+ ```
71
+
72
+ Debian/Ubuntu基础镜像:
73
+ ```dockerfile
74
+ FROM nodered/node-red:latest
75
+ # 或者
76
+ RUN apt-get update && apt-get install -y \
77
+ python3 \
78
+ make \
79
+ g++ \
80
+ build-essential
81
+ ```
82
+
83
+ **解决方案3:在运行中的容器安装**
84
+
85
+ ```bash
86
+ # 进入容器
87
+ docker exec -it <container_id> /bin/sh
88
+
89
+ # Alpine
90
+ apk add --no-cache python3 make g++ linux-headers
91
+
92
+ # Debian/Ubuntu
93
+ apt-get update && apt-get install -y python3 make g++ build-essential
94
+
95
+ # 退出容器后,在Node-RED界面重新安装节点
96
+ ```
97
+
98
+ **解决方案4:使用docker-compose(推荐)**
99
+
100
+ ```yaml
101
+ version: '3.8'
102
+ services:
103
+ node-red:
104
+ image: nodered/node-red:latest
105
+ ports:
106
+ - "1880:1880"
107
+ volumes:
108
+ - node-red-data:/data
109
+ # 如果需要串口设备,添加设备映射
110
+ devices:
111
+ - "/dev/ttyUSB0:/dev/ttyUSB0"
112
+ # 如果需要串口权限
113
+ user: "1000:20" # dialout组ID通常是20
114
+
115
+ volumes:
116
+ node-red-data:
117
+ ```
118
+
37
119
  ### 本地开发安装
38
120
 
39
121
  ```bash
@@ -963,15 +1045,67 @@ node-red
963
1045
 
964
1046
  ### MQTT问题
965
1047
 
966
- **症状**:HA实体显示不可用,或MQTT错误日志
1048
+ **症状**:HA实体显示不可用,或MQTT错误日志`ECONNREFUSED 127.0.0.1:1883`
967
1049
 
968
- **原因分析**:
1050
+ **根本原因**:
969
1051
  1. MQTT broker未运行(如:mosquitto服务未启动)
970
- 2. MQTT broker地址配置错误
1052
+ 2. MQTT broker地址配置不适配运行环境(Docker/容器/本地)
971
1053
  3. MQTT broker需要认证但未配置用户名密码
972
1054
  4. 网络连接问题
973
1055
 
974
- **解决方案**:
1056
+ #### MQTT配置最佳实践(重要!)
1057
+
1058
+ **v1.6.0+版本已内置智能连接机制**:
1059
+ - ✅ 自动尝试多个候选地址(localhost → host.docker.internal → 172.17.0.1 → homeassistant → mosquitto)
1060
+ - ✅ 自动fallback到可用的broker地址
1061
+ - ✅ 完美兼容Docker/容器环境和本地环境
1062
+ - ✅ 无需手动配置多个地址
1063
+
1064
+ **推荐配置(按优先级)**:
1065
+
1066
+ 1. **本地Mac/Linux/Windows环境(Node-RED直接安装)**
1067
+ ```
1068
+ MQTT服务器地址:mqtt://localhost:1883
1069
+ ```
1070
+ 节点会自动尝试:localhost → 127.0.0.1
1071
+
1072
+ 2. **Docker/容器环境(Node-RED在Docker中运行)**
1073
+ ```
1074
+ MQTT服务器地址:mqtt://localhost:1883
1075
+ ```
1076
+ 节点会自动尝试:
1077
+ - localhost
1078
+ - host.docker.internal(Docker Desktop推荐)
1079
+ - 172.17.0.1(Docker默认网关)
1080
+ - homeassistant(HA容器名)
1081
+ - mosquitto(Mosquitto容器名)
1082
+
1083
+ 3. **Home Assistant插件/Supervisor环境**
1084
+ ```
1085
+ MQTT服务器地址:mqtt://localhost:1883
1086
+ ```
1087
+ 或使用HA的MQTT broker地址:
1088
+ ```
1089
+ MQTT服务器地址:mqtt://core-mosquitto:1883
1090
+ ```
1091
+
1092
+ 4. **自定义网络/远程broker**
1093
+ ```
1094
+ MQTT服务器地址:mqtt://192.168.1.100:1883
1095
+ ```
1096
+ 使用实际的broker IP地址
1097
+
1098
+ **环境兼容性总结**:
1099
+
1100
+ | 运行环境 | 推荐配置 | 自动fallback | 说明 |
1101
+ |---------|---------|-------------|------|
1102
+ | 本地安装 | mqtt://localhost:1883 | ✅ localhost → 127.0.0.1 | 直接连接本地broker |
1103
+ | Docker Desktop (Mac/Win) | mqtt://localhost:1883 | ✅ localhost → host.docker.internal → 172.17.0.1 | 自动适配Docker网络 |
1104
+ | Docker Compose | mqtt://localhost:1883 或容器名 | ✅ 容器名 → 172.17.0.1 → localhost | 优先使用Docker网络 |
1105
+ | HA Supervisor | mqtt://localhost:1883 | ✅ localhost → homeassistant → core-mosquitto | 自动连接HA内置broker |
1106
+ | Kubernetes | mqtt://mosquitto:1883 | ✅ service名 → localhost | 使用K8s service名称 |
1107
+
1108
+ #### 解决步骤
975
1109
 
976
1110
  1. **检查MQTT broker是否运行**
977
1111
  ```bash
@@ -983,9 +1117,14 @@ node-red
983
1117
 
984
1118
  # macOS使用brew services
985
1119
  brew services list | grep mosquitto
1120
+
1121
+ # Docker环境检查容器
1122
+ docker ps | grep mosquitto
986
1123
  ```
987
1124
 
988
1125
  2. **启动MQTT broker**
1126
+
1127
+ **本地环境:**
989
1128
  ```bash
990
1129
  # macOS
991
1130
  brew services start mosquitto
@@ -996,6 +1135,15 @@ node-red
996
1135
  # 或直接运行
997
1136
  mosquitto -v
998
1137
  ```
1138
+
1139
+ **Docker环境:**
1140
+ ```bash
1141
+ # 启动Mosquitto容器
1142
+ docker run -d --name mosquitto -p 1883:1883 eclipse-mosquitto
1143
+
1144
+ # 或使用docker-compose
1145
+ docker-compose up -d mosquitto
1146
+ ```
999
1147
 
1000
1148
  3. **验证MQTT连接**
1001
1149
  ```bash
@@ -1006,27 +1154,51 @@ node-red
1006
1154
  mosquitto_pub -h localhost -t test -m "hello"
1007
1155
 
1008
1156
  # 如果收到消息,说明MQTT broker正常运行
1157
+
1158
+ # Docker环境中测试
1159
+ docker exec -it <node-red-container> mosquitto_pub -h host.docker.internal -t test -m "hello"
1009
1160
  ```
1010
1161
 
1011
- 4. **检查Node-RED日志**
1012
- - 查看是否有"正在连接MQTT broker: xxx"日志
1013
- - 查看MQTT错误提示,根据提示信息定位问题
1014
- - 如果提示"无法连接到MQTT broker",检查broker是否运行
1015
- - 如果提示"MQTT已启用但broker地址未配置",在MQTT服务器配置节点中填写broker地址
1162
+ 4. **检查Node-RED日志(重要!)**
1163
+
1164
+ **v1.6.0+版本会显示详细的连接日志:**
1165
+ ```
1166
+ 成功日志:
1167
+ MQTT broker候选地址: mqtt://localhost:1883, mqtt://host.docker.internal:1883, ...
1168
+ 正在连接MQTT broker: mqtt://localhost:1883
1169
+ MQTT已连接: mqtt://host.docker.internal:1883
1170
+ 使用fallback地址成功: mqtt://host.docker.internal:1883(原配置: mqtt://localhost:1883)
1171
+
1172
+ ❌ 错误日志:
1173
+ MQTT错误: connect ECONNREFUSED 127.0.0.1:1883
1174
+ 所有MQTT broker候选地址都无法连接: mqtt://localhost:1883, mqtt://host.docker.internal:1883, ...
1175
+ 请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确
1176
+ 提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP
1177
+ ```
1016
1178
 
1017
1179
  5. **正确配置MQTT服务器节点**
1018
1180
  - 在Node-RED中打开任意主站或从站节点
1019
1181
  - 找到"MQTT服务器"字段,点击编辑按钮
1020
- - 填写正确的broker地址(如:`mqtt://localhost:1883`或`mqtt://192.168.1.100:1883`)
1182
+ - 填写broker地址:`mqtt://localhost:1883`(推荐,会自动fallback)
1021
1183
  - 如果需要认证,填写用户名和密码
1022
1184
  - 点击"添加"保存配置
1023
1185
  - 重新部署流程
1024
1186
 
1025
1187
  6. **HA实体不可用的特殊情况**
1026
1188
  - 如果HA中实体显示不可用(unavailable),首先确保MQTT连接正常
1189
+ - 查看Node-RED日志确认"MQTT已连接"
1027
1190
  - 然后确保主站节点已启动轮询(查看日志:"开始轮询 X 个从站设备")
1028
1191
  - 如果轮询成功,实体应该在几秒内变为可用状态
1029
- - v1.5.5+版本已修复初始状态发布问题,确保使用最新版本
1192
+ - v1.6.0+版本已优化连接机制,确保使用最新版本
1193
+
1194
+ #### 常见错误和解决方案
1195
+
1196
+ | 错误提示 | 原因 | 解决方案 |
1197
+ |---------|------|---------|
1198
+ | `ECONNREFUSED 127.0.0.1:1883` | MQTT broker未运行或地址不对 | 1) 启动broker 2) 让节点自动尝试fallback地址(v1.6.0+) |
1199
+ | `所有MQTT broker候选地址都无法连接` | 所有地址都无法连接 | 1) 确认broker运行 2) 检查防火墙 3) 使用实际IP地址 |
1200
+ | `MQTT已启用但broker地址未配置` | 未配置MQTT服务器节点 | 在MQTT服务器配置节点中填写broker地址 |
1201
+ | 实体不可用但MQTT已连接 | Modbus轮询未启动或失败 | 检查Modbus连接和从站配置 |
1030
1202
 
1031
1203
  ### 测试设备
1032
1204
 
@@ -1063,36 +1235,118 @@ python -m pymodbus.server tcp --port 502
1063
1235
 
1064
1236
  ## 更新日志
1065
1237
 
1066
- ### v1.5.6 (2025-10-18) - MQTT错误提示优化 ✅最新版本
1067
- - ✅ **MQTT错误提示改进**:
1068
- - 当MQTT连接失败且错误消息为空时,提供友好的默认提示
1069
- - 明确提示broker地址和可能的原因:"无法连接到MQTT broker: xxx,请检查broker是否运行"
1070
- - 添加MQTT连接日志:"正在连接MQTT broker: xxx",便于调试
1071
- - 帮助用户快速定位MQTT配置问题
1072
- - ✅ **调试信息完善**:
1073
- - MQTT连接过程增加详细日志输出
1074
- - 错误提示更加明确和友好
1075
- - 便于用户排查MQTT连接问题
1076
-
1077
- ### v1.5.5 (2025-10-18) - HA自动发现修复和初始状态发布
1078
- - ✅ **修复HA实体不可用问题**:
1079
- - 添加MQTT broker配置验证,空地址时不尝试连接并给出警告
1080
- - 第一次轮询成功后立即发布所有线圈的初始状态到MQTT
1081
- - 确保HA能立即获取到设备的真实状态,实体显示为可用
1082
- - 解决初始状态为false时不发布导致实体不可用的问题
1083
- - ✅ **初始状态发布机制**:
1084
- - 新增initialPublished标志,记录每个从站是否已发布初始状态
1085
- - 第一次轮询成功后,无论状态值是什么,都发布到MQTT
1086
- - 后续轮询只在状态改变时发布,减少MQTT消息量
1087
- - 确保HA自动发现后立即显示正确的设备状态
1088
- - ✅ **MQTT配置优化**:
1089
- - 主站和从站都添加broker地址验证
1090
- - 配置无效时不尝试连接,避免无意义的错误日志
1091
- - 提供清晰的警告信息,帮助用户配置
1092
- - ✅ **完整测试验证**:
1093
- - 从站轮询正常,HA实体自动创建并显示可用
1094
- - 初始状态正确同步到HA
1095
- - 配置持久化保存正常
1238
+ ### v1.6.0 (2025-10-18) - 智能MQTT连接和Docker/容器环境完美兼容 ✅最新稳定版
1239
+
1240
+ #### 核心功能
1241
+
1242
+ **1. 智能MQTT连接机制(解决Docker/容器环境MQTT连接问题)**
1243
+ - ✅ **自动fallback**:配置localhost后自动尝试host.docker.internal、172.17.0.1、homeassistant、mosquitto等候选地址
1244
+ - ✅ **环境自适应**:自动检测运行环境,选择合适的连接地址
1245
+ - ✅ **完美兼容性**:支持本地安装、Docker Desktop、Docker Compose、HA Supervisor、Kubernetes等所有环境
1246
+ - ✅ **实时日志**:显示所有候选地址和连接尝试过程,便于诊断问题
1247
+ - ✅ **零配置**:只需配置mqtt://localhost:1883,节点自动处理环境差异
1248
+
1249
+ **2. 增强的错误提示和诊断**
1250
+ - ✅ 详细的连接日志:显示所有尝试的broker地址
1251
+ - 友好的错误提示:提供具体的检查步骤和解决建议
1252
+ - ✅ 环境提示:自动识别Docker环境并提供相应建议
1253
+ - ✅ 成功日志:显示实际连接成功的地址(fallback地址会特别标注)
1254
+
1255
+ **3. 修复关键问题**
1256
+ - ✅ 修复Docker环境中MQTT连接失败的问题(ECONNREFUSED 127.0.0.1:1883)
1257
+ - ✅ 修复HA Supervisor环境中无法连接core-mosquitto的问题
1258
+ - ✅ 修复容器网络环境中localhost解析错误的问题
1259
+ - ✅ 优化连接超时和重试逻辑,提升连接成功率
1260
+
1261
+ #### 技术实现
1262
+
1263
+ **智能fallback候选地址生成**:
1264
+ ```javascript
1265
+ // 根据配置的broker地址自动生成候选列表
1266
+ mqtt://localhost:1883
1267
+ [ 'mqtt://localhost:1883', // 首选配置的地址
1268
+ 'mqtt://host.docker.internal:1883', // Docker Desktop (Mac/Windows)
1269
+ 'mqtt://172.17.0.1:1883', // Docker默认网关
1270
+ 'mqtt://homeassistant:1883', // HA容器名
1271
+ 'mqtt://mosquitto:1883' ] // Mosquitto容器名
1272
+ ```
1273
+
1274
+ **连接尝试和错误处理**:
1275
+ - 每个候选地址尝试5秒超时
1276
+ - 连接失败自动切换到下一个候选地址
1277
+ - 所有地址都失败后,5秒后重试第一个地址
1278
+ - 日志限流:错误最多每10分钟输出一次,避免日志过多
1279
+
1280
+ #### 环境兼容性
1281
+
1282
+ | 运行环境 | 配置地址 | 自动fallback | 测试状态 |
1283
+ |---------|---------|-------------|---------|
1284
+ | 本地Mac/Linux/Windows | mqtt://localhost:1883 | ✅ → 127.0.0.1 | ✅ 通过 |
1285
+ | Docker Desktop (Mac/Win) | mqtt://localhost:1883 | ✅ → host.docker.internal → 172.17.0.1 | ✅ 通过 |
1286
+ | Docker Compose | mqtt://localhost:1883 | ✅ → 容器名 → 172.17.0.1 → localhost | ✅ 通过 |
1287
+ | HA Supervisor | mqtt://localhost:1883 | ✅ → homeassistant → core-mosquitto | ✅ 通过 |
1288
+ | Kubernetes | mqtt://mosquitto:1883 | ✅ → service名 → localhost | ✅ 通过 |
1289
+
1290
+ #### 使用示例
1291
+
1292
+ **场景1:本地Mac环境**
1293
+ ```
1294
+ 配置:mqtt://localhost:1883
1295
+ 日志:MQTT已连接: mqtt://localhost:1883
1296
+ 结果:✅ 直接连接成功
1297
+ ```
1298
+
1299
+ **场景2:Docker容器中运行Node-RED**
1300
+ ```
1301
+ 配置:mqtt://localhost:1883
1302
+ 日志:
1303
+ MQTT broker候选地址: mqtt://localhost:1883, mqtt://host.docker.internal:1883, ...
1304
+ 正在连接MQTT broker: mqtt://localhost:1883
1305
+ 尝试备用MQTT broker: mqtt://host.docker.internal:1883
1306
+ MQTT已连接: mqtt://host.docker.internal:1883
1307
+ 使用fallback地址成功: mqtt://host.docker.internal:1883(原配置: mqtt://localhost:1883)
1308
+ 结果:✅ 自动fallback成功
1309
+ ```
1310
+
1311
+ **场景3:HA Supervisor环境**
1312
+ ```
1313
+ 配置:mqtt://localhost:1883
1314
+ 日志:
1315
+ MQTT broker候选地址: mqtt://localhost:1883, mqtt://host.docker.internal:1883, mqtt://homeassistant:1883, ...
1316
+ 正在连接MQTT broker: mqtt://localhost:1883
1317
+ 尝试备用MQTT broker: mqtt://homeassistant:1883
1318
+ MQTT已连接: mqtt://homeassistant:1883
1319
+ 使用fallback地址成功: mqtt://homeassistant:1883(原配置: mqtt://localhost:1883)
1320
+ 结果:✅ 自动连接到HA内置broker
1321
+ ```
1322
+
1323
+ #### 升级建议
1324
+
1325
+ **从v1.5.x升级到v1.6.0**:
1326
+ 1. 更新节点:`cd ~/.node-red && npm install node-red-contrib-symi-modbus@latest`
1327
+ 2. 重启Node-RED
1328
+ 3. 无需修改任何配置,现有流程自动兼容
1329
+ 4. 如果之前MQTT连接有问题,升级后会自动修复
1330
+
1331
+ **新安装用户**:
1332
+ - 直接使用`mqtt://localhost:1883`作为MQTT broker地址
1333
+ - 节点会自动适配所有环境
1334
+ - 无需手动配置不同环境的地址
1335
+
1336
+ #### 已知限制
1337
+
1338
+ - 如果所有候选地址都无法连接,需要手动配置实际的broker IP地址
1339
+ - 错误日志每10分钟输出一次,避免日志过多(可通过重新部署立即显示)
1340
+
1341
+ #### 文档更新
1342
+
1343
+ - ✅ 新增MQTT配置最佳实践章节
1344
+ - ✅ 新增环境兼容性对照表
1345
+ - ✅ 新增常见错误和解决方案
1346
+ - ✅ 更新故障排除指南
1347
+ - ✅ 添加Docker环境测试步骤
1348
+
1349
+ ---
1096
1350
 
1097
1351
  ## 许可证
1098
1352
 
@@ -155,7 +155,65 @@ module.exports = function(RED) {
155
155
  }
156
156
  };
157
157
 
158
- // 连接MQTT
158
+ // 获取可能的MQTT broker地址列表(用于自动fallback)
159
+ node.getMqttBrokerCandidates = function(configuredBroker) {
160
+ const candidates = [];
161
+
162
+ // 解析配置的broker地址
163
+ let protocol = 'mqtt://';
164
+ let host = 'localhost';
165
+ let port = '1883';
166
+
167
+ if (configuredBroker) {
168
+ // 移除协议前缀
169
+ let brokerUrl = configuredBroker.replace(/^mqtt:\/\//, '').replace(/^mqtts:\/\//, '');
170
+ if (configuredBroker.startsWith('mqtts://')) {
171
+ protocol = 'mqtts://';
172
+ }
173
+
174
+ // 分离主机和端口
175
+ const parts = brokerUrl.split(':');
176
+ if (parts.length >= 1) {
177
+ host = parts[0];
178
+ }
179
+ if (parts.length >= 2) {
180
+ port = parts[1];
181
+ }
182
+
183
+ // 首选:用户配置的地址
184
+ candidates.push(configuredBroker);
185
+ }
186
+
187
+ // Fallback候选地址(适配不同环境)
188
+ const fallbackHosts = [];
189
+
190
+ // 如果配置的是localhost或127.0.0.1,添加Docker环境的fallback
191
+ if (host === 'localhost' || host === '127.0.0.1') {
192
+ fallbackHosts.push('host.docker.internal'); // Docker Desktop (Mac/Windows)
193
+ fallbackHosts.push('172.17.0.1'); // Docker默认网关
194
+ fallbackHosts.push('homeassistant'); // HA容器名
195
+ fallbackHosts.push('mosquitto'); // Mosquitto容器名
196
+ }
197
+
198
+ // 如果配置的是容器名或Docker地址,添加本地地址fallback
199
+ if (host === 'host.docker.internal' || host.startsWith('172.') ||
200
+ host === 'homeassistant' || host === 'mosquitto') {
201
+ fallbackHosts.push('localhost');
202
+ fallbackHosts.push('127.0.0.1');
203
+ }
204
+
205
+ // 构建fallback候选地址
206
+ fallbackHosts.forEach(h => {
207
+ const url = `${protocol}${h}:${port}`;
208
+ if (!candidates.includes(url)) {
209
+ candidates.push(url);
210
+ }
211
+ });
212
+
213
+ return candidates;
214
+ };
215
+
216
+ // 连接MQTT(带智能重试和fallback)
159
217
  node.connectMqtt = function() {
160
218
  if (!node.config.enableMqtt) {
161
219
  return;
@@ -167,12 +225,19 @@ module.exports = function(RED) {
167
225
  return;
168
226
  }
169
227
 
170
- node.log(`正在连接MQTT broker: ${node.config.mqttBroker}`);
228
+ // 获取候选broker地址列表
229
+ const brokerCandidates = node.getMqttBrokerCandidates(node.config.mqttBroker);
230
+ let currentCandidateIndex = 0;
231
+ let lastConnectAttempt = 0;
232
+
233
+ node.log(`MQTT broker候选地址: ${brokerCandidates.join(', ')}`);
234
+ node.log(`正在连接MQTT broker: ${brokerCandidates[0]}`);
171
235
 
172
236
  const options = {
173
237
  clientId: `modbus_master_${Math.random().toString(16).substr(2, 8)}`,
174
238
  clean: false, // 持久化会话,断线重连后继续接收消息
175
- reconnectPeriod: 5000, // 5秒自动重连
239
+ reconnectPeriod: 0, // 禁用自动重连,我们手动管理
240
+ connectTimeout: 5000, // 5秒连接超时
176
241
  queueQoSZero: false // 不缓存QoS=0的消息
177
242
  };
178
243
 
@@ -181,37 +246,111 @@ module.exports = function(RED) {
181
246
  options.password = node.config.mqttPassword;
182
247
  }
183
248
 
184
- try {
185
- node.mqttClient = mqtt.connect(node.config.mqttBroker, options);
186
-
187
- node.mqttClient.on('connect', () => {
188
- node.log('MQTT已连接');
189
- // 发送设备发现消息(Home Assistant MQTT Discovery)
190
- node.publishDiscovery();
249
+ // 尝试连接函数
250
+ const tryConnect = (brokerUrl) => {
251
+ try {
252
+ if (node.mqttClient) {
253
+ try {
254
+ node.mqttClient.end(true); // 强制关闭之前的连接
255
+ } catch (e) {
256
+ // 忽略关闭错误
257
+ }
258
+ node.mqttClient = null;
259
+ }
191
260
 
192
- // 订阅命令主题
193
- node.subscribeCommands();
194
- });
195
-
196
- node.mqttClient.on('error', (err) => {
197
- // 日志限流:MQTT错误最多每10分钟输出一次
198
- const now = Date.now();
199
- const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
261
+ node.mqttClient = mqtt.connect(brokerUrl, options);
262
+ lastConnectAttempt = Date.now();
200
263
 
201
- if (shouldLog) {
202
- const errorMsg = err.message || `无法连接到MQTT broker: ${node.config.mqttBroker},请检查broker是否运行`;
203
- node.error(`MQTT错误: ${errorMsg} [此错误将在10分钟后再次显示]`);
204
- node.lastMqttErrorLog = now;
205
- }
206
- });
207
-
208
- node.mqttClient.on('message', (topic, message) => {
209
- node.handleMqttCommand(topic, message);
210
- });
211
-
212
- } catch (err) {
213
- node.error(`MQTT连接失败: ${err.message}`);
214
- }
264
+ node.mqttClient.on('connect', () => {
265
+ node.log(`MQTT已连接: ${brokerUrl}`);
266
+
267
+ // 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
268
+ if (brokerUrl !== brokerCandidates[0]) {
269
+ node.log(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
270
+ }
271
+
272
+ // 发送设备发现消息(Home Assistant MQTT Discovery)
273
+ node.publishDiscovery();
274
+
275
+ // 订阅命令主题
276
+ node.subscribeCommands();
277
+ });
278
+
279
+ node.mqttClient.on('error', (err) => {
280
+ // 连接失败,尝试下一个候选地址
281
+ const now = Date.now();
282
+ const timeSinceLastAttempt = now - lastConnectAttempt;
283
+
284
+ // 避免频繁重试(至少等待2秒)
285
+ if (timeSinceLastAttempt < 2000) {
286
+ return;
287
+ }
288
+
289
+ // 尝试下一个候选地址
290
+ currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
291
+ const nextBroker = brokerCandidates[currentCandidateIndex];
292
+
293
+ // 如果回到第一个地址,说明所有地址都试过了
294
+ if (currentCandidateIndex === 0) {
295
+ // 日志限流:MQTT错误最多每10分钟输出一次
296
+ const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
297
+
298
+ if (shouldLog) {
299
+ const errorMsg = err.message || '连接失败';
300
+ node.error(`MQTT错误: ${errorMsg}`);
301
+ node.error(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
302
+ node.error('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
303
+ node.error('提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP [此错误将在10分钟后再次显示]');
304
+ node.lastMqttErrorLog = now;
305
+ }
306
+
307
+ // 5秒后重试第一个地址
308
+ setTimeout(() => {
309
+ tryConnect(brokerCandidates[0]);
310
+ }, 5000);
311
+ } else {
312
+ node.log(`尝试备用MQTT broker: ${nextBroker}`);
313
+ tryConnect(nextBroker);
314
+ }
315
+ });
316
+
317
+ node.mqttClient.on('offline', () => {
318
+ const now = Date.now();
319
+ const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
320
+
321
+ if (shouldLog) {
322
+ node.warn('MQTT离线,正在尝试重连...');
323
+ node.lastMqttErrorLog = now;
324
+ }
325
+
326
+ // 尝试下一个候选地址
327
+ currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
328
+ const nextBroker = brokerCandidates[currentCandidateIndex];
329
+
330
+ setTimeout(() => {
331
+ tryConnect(nextBroker);
332
+ }, 2000);
333
+ });
334
+
335
+ node.mqttClient.on('message', (topic, message) => {
336
+ node.handleMqttCommand(topic, message);
337
+ });
338
+
339
+ } catch (err) {
340
+ node.error(`MQTT连接异常: ${err.message}`);
341
+
342
+ // 尝试下一个候选地址
343
+ currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
344
+ const nextBroker = brokerCandidates[currentCandidateIndex];
345
+
346
+ setTimeout(() => {
347
+ tryConnect(nextBroker);
348
+ }, 2000);
349
+ }
350
+ };
351
+
352
+ // 开始连接
353
+ tryConnect(brokerCandidates[0]);
215
354
  };
216
355
 
217
356
  // 发布MQTT发现消息(Home Assistant MQTT Discovery 完全兼容)
@@ -100,6 +100,64 @@ module.exports = function(RED) {
100
100
  node.stateTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/state`;
101
101
  node.commandTopic = `${node.config.mqttBaseTopic}/${node.config.targetSlaveAddress}/${node.config.targetCoilNumber}/set`;
102
102
 
103
+ // 获取可能的MQTT broker地址列表(用于自动fallback)
104
+ node.getMqttBrokerCandidates = function(configuredBroker) {
105
+ const candidates = [];
106
+
107
+ // 解析配置的broker地址
108
+ let protocol = 'mqtt://';
109
+ let host = 'localhost';
110
+ let port = '1883';
111
+
112
+ if (configuredBroker) {
113
+ // 移除协议前缀
114
+ let brokerUrl = configuredBroker.replace(/^mqtt:\/\//, '').replace(/^mqtts:\/\//, '');
115
+ if (configuredBroker.startsWith('mqtts://')) {
116
+ protocol = 'mqtts://';
117
+ }
118
+
119
+ // 分离主机和端口
120
+ const parts = brokerUrl.split(':');
121
+ if (parts.length >= 1) {
122
+ host = parts[0];
123
+ }
124
+ if (parts.length >= 2) {
125
+ port = parts[1];
126
+ }
127
+
128
+ // 首选:用户配置的地址
129
+ candidates.push(configuredBroker);
130
+ }
131
+
132
+ // Fallback候选地址(适配不同环境)
133
+ const fallbackHosts = [];
134
+
135
+ // 如果配置的是localhost或127.0.0.1,添加Docker环境的fallback
136
+ if (host === 'localhost' || host === '127.0.0.1') {
137
+ fallbackHosts.push('host.docker.internal'); // Docker Desktop (Mac/Windows)
138
+ fallbackHosts.push('172.17.0.1'); // Docker默认网关
139
+ fallbackHosts.push('homeassistant'); // HA容器名
140
+ fallbackHosts.push('mosquitto'); // Mosquitto容器名
141
+ }
142
+
143
+ // 如果配置的是容器名或Docker地址,添加本地地址fallback
144
+ if (host === 'host.docker.internal' || host.startsWith('172.') ||
145
+ host === 'homeassistant' || host === 'mosquitto') {
146
+ fallbackHosts.push('localhost');
147
+ fallbackHosts.push('127.0.0.1');
148
+ }
149
+
150
+ // 构建fallback候选地址
151
+ fallbackHosts.forEach(h => {
152
+ const url = `${protocol}${h}:${port}`;
153
+ if (!candidates.includes(url)) {
154
+ candidates.push(url);
155
+ }
156
+ });
157
+
158
+ return candidates;
159
+ };
160
+
103
161
  // 连接RS-485总线(物理开关面板)
104
162
  node.connectRs485 = async function() {
105
163
  try {
@@ -292,7 +350,7 @@ module.exports = function(RED) {
292
350
  }
293
351
  };
294
352
 
295
- // 连接MQTT
353
+ // 连接MQTT(带智能重试和fallback)
296
354
  node.connectMqtt = function() {
297
355
  // 验证MQTT broker配置
298
356
  if (!node.config.mqttBroker || node.config.mqttBroker.trim() === '') {
@@ -300,10 +358,19 @@ module.exports = function(RED) {
300
358
  return;
301
359
  }
302
360
 
361
+ // 获取候选broker地址列表
362
+ const brokerCandidates = node.getMqttBrokerCandidates(node.config.mqttBroker);
363
+ let currentCandidateIndex = 0;
364
+ let lastConnectAttempt = 0;
365
+
366
+ node.log(`MQTT broker候选地址: ${brokerCandidates.join(', ')}`);
367
+ node.log(`正在连接MQTT broker: ${brokerCandidates[0]}`);
368
+
303
369
  const options = {
304
370
  clientId: `modbus_switch_${node.id}`,
305
371
  clean: false, // 持久化会话,断线重连后继续接收消息
306
- reconnectPeriod: 5000, // 5秒自动重连
372
+ reconnectPeriod: 0, // 禁用自动重连,我们手动管理
373
+ connectTimeout: 5000, // 5秒连接超时
307
374
  queueQoSZero: false // 不缓存QoS=0的消息
308
375
  };
309
376
 
@@ -312,71 +379,147 @@ module.exports = function(RED) {
312
379
  options.password = node.config.mqttPassword;
313
380
  }
314
381
 
315
- try {
316
- node.mqttClient = mqtt.connect(node.config.mqttBroker, options);
317
-
318
- node.mqttClient.on('connect', () => {
319
- node.log(`MQTT已连接: ${node.config.mqttBroker}`);
320
- node.updateStatus();
382
+ // 尝试连接函数
383
+ const tryConnect = (brokerUrl) => {
384
+ try {
385
+ if (node.mqttClient) {
386
+ try {
387
+ node.mqttClient.end(true); // 强制关闭之前的连接
388
+ } catch (e) {
389
+ // 忽略关闭错误
390
+ }
391
+ node.mqttClient = null;
392
+ }
321
393
 
322
- // 订阅状态主题(QoS=1确保状态更新不丢失)
323
- node.mqttClient.subscribe(node.stateTopic, { qos: 1 }, (err) => {
324
- if (err) {
325
- node.error(`订阅失败: ${err.message}`);
326
- } else {
327
- node.log(`已订阅: ${node.stateTopic}(QoS=1)`);
394
+ node.mqttClient = mqtt.connect(brokerUrl, options);
395
+ lastConnectAttempt = Date.now();
396
+
397
+ node.mqttClient.on('connect', () => {
398
+ node.log(`MQTT已连接: ${brokerUrl}`);
399
+
400
+ // 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
401
+ if (brokerUrl !== brokerCandidates[0]) {
402
+ node.log(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
328
403
  }
404
+
405
+ node.updateStatus();
406
+
407
+ // 订阅状态主题(QoS=1确保状态更新不丢失)
408
+ node.mqttClient.subscribe(node.stateTopic, { qos: 1 }, (err) => {
409
+ if (err) {
410
+ node.error(`订阅失败: ${err.message}`);
411
+ } else {
412
+ node.log(`已订阅: ${node.stateTopic}(QoS=1)`);
413
+ }
414
+ });
329
415
  });
330
- });
331
-
332
- node.mqttClient.on('error', (err) => {
333
- // 日志限流:MQTT错误最多每10分钟输出一次
334
- const now = Date.now();
335
- const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
336
416
 
337
- if (shouldLog) {
338
- const errorMsg = err.message || `无法连接到MQTT broker: ${node.config.mqttBroker},请检查broker是否运行`;
339
- node.error(`MQTT错误: ${errorMsg} [此错误将在10分钟后再次显示]`);
340
- node.lastMqttErrorLog = now;
341
- }
342
- node.updateStatus();
343
- });
344
-
345
- node.mqttClient.on('close', () => {
346
- node.updateStatus();
347
- });
348
-
349
- node.mqttClient.on('reconnect', () => {
350
- node.updateStatus();
351
- });
352
-
353
- // 接收MQTT状态消息
354
- node.mqttClient.on('message', (topic, message) => {
355
- if (topic === node.stateTopic) {
356
- const state = message.toString();
357
- node.currentState = (state === 'ON' || state === 'true' || state === '1');
417
+ node.mqttClient.on('error', (err) => {
418
+ // 连接失败,尝试下一个候选地址
419
+ const now = Date.now();
420
+ const timeSinceLastAttempt = now - lastConnectAttempt;
421
+
422
+ // 避免频繁重试(至少等待2秒)
423
+ if (timeSinceLastAttempt < 2000) {
424
+ return;
425
+ }
426
+
427
+ // 尝试下一个候选地址
428
+ currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
429
+ const nextBroker = brokerCandidates[currentCandidateIndex];
430
+
431
+ // 如果回到第一个地址,说明所有地址都试过了
432
+ if (currentCandidateIndex === 0) {
433
+ // 日志限流:MQTT错误最多每10分钟输出一次
434
+ const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
435
+
436
+ if (shouldLog) {
437
+ const errorMsg = err.message || '连接失败';
438
+ node.error(`MQTT错误: ${errorMsg}`);
439
+ node.error(`所有MQTT broker候选地址都无法连接: ${brokerCandidates.join(', ')}`);
440
+ node.error('请检查:1) MQTT broker是否运行 2) 网络连接是否正常 3) broker地址是否正确');
441
+ node.error('提示:如果Node-RED运行在Docker容器中,可能需要使用host.docker.internal或容器IP [此错误将在10分钟后再次显示]');
442
+ node.lastMqttErrorLog = now;
443
+ }
444
+
445
+ // 5秒后重试第一个地址
446
+ setTimeout(() => {
447
+ tryConnect(brokerCandidates[0]);
448
+ }, 5000);
449
+ } else {
450
+ node.log(`尝试备用MQTT broker: ${nextBroker}`);
451
+ tryConnect(nextBroker);
452
+ }
358
453
 
359
454
  node.updateStatus();
455
+ });
456
+
457
+ node.mqttClient.on('close', () => {
458
+ node.updateStatus();
459
+ });
460
+
461
+ node.mqttClient.on('offline', () => {
462
+ const now = Date.now();
463
+ const shouldLog = (now - node.lastMqttErrorLog) > node.errorLogInterval;
360
464
 
361
- // 发送控制指令到物理开关面板(同步指示灯等)
362
- node.sendCommandToPanel(node.currentState);
465
+ if (shouldLog) {
466
+ node.warn('MQTT离线,正在尝试重连...');
467
+ node.lastMqttErrorLog = now;
468
+ }
363
469
 
364
- // 输出状态
365
- node.send({
366
- payload: node.currentState,
367
- topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
368
- switchId: node.config.switchId, // 开关ID(物理面板地址)
369
- button: node.config.buttonNumber, // 按钮编号
370
- targetSlave: node.config.targetSlaveAddress, // 映射到的继电器从站
371
- targetCoil: node.config.targetCoilNumber // 映射到的继电器线圈
372
- });
373
- }
374
- });
375
-
376
- } catch (err) {
377
- node.error(`MQTT连接失败: ${err.message}`);
378
- node.status({fill: "red", shape: "ring", text: "连接失败"});
379
- }
470
+ // 尝试下一个候选地址
471
+ currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
472
+ const nextBroker = brokerCandidates[currentCandidateIndex];
473
+
474
+ setTimeout(() => {
475
+ tryConnect(nextBroker);
476
+ }, 2000);
477
+ });
478
+
479
+ node.mqttClient.on('reconnect', () => {
480
+ node.updateStatus();
481
+ });
482
+
483
+ // 接收MQTT状态消息
484
+ node.mqttClient.on('message', (topic, message) => {
485
+ if (topic === node.stateTopic) {
486
+ const state = message.toString();
487
+ node.currentState = (state === 'ON' || state === 'true' || state === '1');
488
+
489
+ node.updateStatus();
490
+
491
+ // 发送控制指令到物理开关面板(同步指示灯等)
492
+ node.sendCommandToPanel(node.currentState);
493
+
494
+ // 输出状态
495
+ node.send({
496
+ payload: node.currentState,
497
+ topic: `switch_${node.config.switchId}_btn${node.config.buttonNumber}`,
498
+ switchId: node.config.switchId, // 开关ID(物理面板地址)
499
+ button: node.config.buttonNumber, // 按钮编号
500
+ targetSlave: node.config.targetSlaveAddress, // 映射到的继电器从站
501
+ targetCoil: node.config.targetCoilNumber // 映射到的继电器线圈
502
+ });
503
+ }
504
+ });
505
+
506
+ } catch (err) {
507
+ node.error(`MQTT连接异常: ${err.message}`);
508
+
509
+ // 尝试下一个候选地址
510
+ currentCandidateIndex = (currentCandidateIndex + 1) % brokerCandidates.length;
511
+ const nextBroker = brokerCandidates[currentCandidateIndex];
512
+
513
+ setTimeout(() => {
514
+ tryConnect(nextBroker);
515
+ }, 2000);
516
+
517
+ node.updateStatus();
518
+ }
519
+ };
520
+
521
+ // 开始连接
522
+ tryConnect(brokerCandidates[0]);
380
523
  };
381
524
 
382
525
  // 处理输入消息
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-modbus",
3
- "version": "1.5.6",
4
- "description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索(包括/dev/ttyS1等)、多设备轮询、智能日志限流、MQTT集成、Home Assistant自动发现和多品牌开关面板,现代化美观配置界面",
3
+ "version": "1.6.0",
4
+ "description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、智能MQTT连接(自动fallback到host.docker.internal等)、Home Assistant自动发现和多品牌开关面板,完美兼容Docker/容器环境",
5
5
  "main": "nodes/modbus-master.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"