node-red-contrib-symi-modbus 2.6.7 → 2.6.8

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
@@ -17,6 +17,7 @@ Node-RED的Modbus继电器控制节点,支持TCP/串口通信和MQTT集成,
17
17
  - Modbus RTU over TCP(TCP转RS485网关)
18
18
  - Telnet ASCII(推荐用于TCP转RS485网关)
19
19
  - **Symi开关集成**:自动识别并处理Symi私有协议按键事件,实现开关面板与继电器的双向同步
20
+ - **HomeKit网桥**:一键桥接到Apple HomeKit,支持Siri语音控制,自动同步主站配置,名称可自定义
20
21
  - **多设备轮询**:支持最多10台Modbus从站设备,每台32路继电器
21
22
  - **智能轮询机制**:从站上报时自动暂停轮询,优先处理数据,避免冲突
22
23
  - **稳定可靠**:完整的内存管理、错误处理、断线重连,适合7x24小时长期运行
@@ -146,6 +147,43 @@ node-red-restart
146
147
  5. **无需连线**:主站和从站通过内部事件自动通信,无需手动连线
147
148
  6. 部署流程
148
149
 
150
+ ### 6. 配置HomeKit网桥节点(可选)
151
+
152
+ 将Modbus继电器桥接到Apple HomeKit,实现Siri语音控制:
153
+
154
+ **配置步骤**:
155
+
156
+ 1. **添加网桥节点**
157
+ - 拖拽 **HomeKit网桥** 节点到流程画布
158
+ - 双击节点打开配置界面
159
+
160
+ 2. **基础配置**
161
+ - 网桥名称: `Modbus继电器网桥`(在HomeKit中显示)
162
+ - 选择主站节点: 从下拉框选择已配置的主站
163
+ - 配对码: `031-45-154`(添加到HomeKit时使用)
164
+ - 端口: `51828`(保持默认)
165
+
166
+ 3. **配置继电器名称**(推荐)
167
+ - 选择主站后,下方会自动显示所有继电器
168
+ - 为每个继电器输入友好的中文名称
169
+ - 例如:`客厅灯`、`卧室灯`、`空调插座`等
170
+ - 配置后在HomeKit中直接显示,无需再修改
171
+
172
+ 4. **部署并添加到HomeKit**
173
+ - 点击"完成"并部署流程
174
+ - 打开iPhone/iPad的"家庭"App
175
+ - 点击右上角"+" → "添加配件"
176
+ - 选择"更多选项"
177
+ - 找到"Modbus继电器网桥"
178
+ - 输入配对码:`031-45-154`
179
+ - 完成配对
180
+
181
+ **使用说明**:
182
+ - 线圈0-15显示为开关,线圈16-31显示为插座
183
+ - 支持Siri语音控制:"嘿Siri,打开客厅灯"
184
+ - 支持HomeKit自动化和场景
185
+ - 配置会自动保存,重启后无需重新配对
186
+
149
187
  ## 核心特性说明
150
188
 
151
189
  ### Symi开关自动识别
@@ -578,6 +616,40 @@ msg.payload = "ON"; // 或 "OFF"
578
616
  msg.payload = 1; // 或 0
579
617
  ```
580
618
 
619
+ ### HomeKit网桥节点
620
+
621
+ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
622
+
623
+ **配置参数**:
624
+ - **主站节点**:选择要桥接的Modbus主站节点(必填)
625
+ - **网桥名称**:在HomeKit中显示的网桥名称(默认:Modbus继电器网桥)
626
+ - **MAC地址**:HomeKit网桥的唯一标识符(自动生成,无需修改)
627
+ - **配对码**:HomeKit配对时使用的PIN码(格式:XXX-XX-XXX,默认:031-45-154)
628
+ - **端口**:HomeKit网桥监听端口(默认:51828)
629
+ - **继电器名称配置**:为每个继电器配置友好的中文名称(可选)
630
+
631
+ **自动同步规则**:
632
+ - 自动读取主站节点配置的所有从站和线圈
633
+ - 线圈0-15(1-16路):创建为开关(Switch)配件
634
+ - 线圈16-31(17-32路):创建为插座(Outlet)配件
635
+ - 监听主站的状态变化事件,实时同步到HomeKit
636
+ - 接收HomeKit控制命令,发送到主站执行
637
+
638
+ **使用示例**:
639
+ 1. 在Node-RED中配置好主站节点和从站
640
+ 2. 添加HomeKit网桥节点,选择主站节点
641
+ 3. 为每个继电器配置友好的名称(例如:客厅灯、卧室灯)
642
+ 4. 部署流程
643
+ 5. 在iPhone的"家庭"App中添加配件,输入配对码
644
+ 6. 完成配对后,即可在HomeKit中控制继电器,支持Siri语音控制
645
+
646
+ **注意事项**:
647
+ - 确保主站节点已正确配置并运行
648
+ - 配对码格式必须为XXX-XX-XXX(8位数字)
649
+ - 端口不能与其他服务冲突(默认51828)
650
+ - 配置信息持久化存储在~/.node-red/homekit-persist目录
651
+ - 重启Node-RED后自动恢复配对状态,无需重新配对
652
+
581
653
  ## 输出消息格式
582
654
 
583
655
  ### 主站节点
@@ -638,28 +710,33 @@ msg.payload = 1; // 或 0
638
710
 
639
711
  ## 项目信息
640
712
 
641
- **版本**: v2.6.7
713
+ **版本**: v2.6.8
642
714
 
643
715
  **核心功能**:
644
716
  - 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
645
717
  - 多设备轮询(最多10台从站,每台32路继电器,轮询间隔100-10000ms可调)
646
- - Symi私有协议自动识别(支持两种485开关控制方式)
647
- - 智能轮询暂停机制(从站上报时自动暂停,处理完成后恢复)
648
718
  - 🔥 **双模式支持**(本地模式和MQTT模式可选切换,断网也能稳定运行)
649
- - 🔥 **免连线通信**(主站和从站通过内部事件通信,无需连线,支持本地模式和MQTT模式)
650
- - MQTT集成(可选启用,Home Assistant自动发现,实体唯一性保证,QoS=0高性能发布)
651
- - 物理开关面板双向同步(亖米协议支持,LED反馈同步,支持开关模式和场景模式)
652
- - 共享连接架构(多个从站开关节点共享同一个串口/TCP连接,支持500+节点)
653
- - 长期稳定运行(内存管理、智能重连、错误日志限流、异步MQTT发布、TCP永久连接)
719
+ - 🔥 **免连线通信**(主站和从站通过内部事件通信,无需连线)
720
+ - 🔥 **HomeKit网桥**(一键桥接到Apple HomeKit,支持Siri语音控制)
721
+ - MQTT集成(可选启用,Home Assistant自动发现)
722
+ - 物理开关面板双向同步(支持开关模式和场景模式)
723
+ - 长期稳定运行(内存管理、智能重连、异步处理)
654
724
 
655
- **技术栈**:
656
- - modbus-serial: ^8.0.23(内部封装serialport,支持TCP和串口)
657
- - serialport: ^12.0.0(原生串口通信)
658
- - mqtt: ^5.14.1(最新稳定版,可选依赖)
725
+ **技术要求**:
659
726
  - Node.js: >=14.0.0
660
727
  - Node-RED: >=2.0.0
661
728
 
662
- **最新更新(v2.6.7)**:
729
+ **最新更新(v2.6.8)**:
730
+ - **🔥 新增HomeKit网桥节点**:
731
+ - 一键桥接Modbus继电器到Apple HomeKit
732
+ - 自动同步主站配置的所有从站和继电器
733
+ - 支持在Node-RED中配置继电器友好名称
734
+ - 双向同步:HomeKit控制继电器,继电器状态实时同步到HomeKit
735
+ - 支持Siri语音控制和HomeKit自动化场景
736
+ - 持久化存储配对信息,重启后自动恢复
737
+ - 线圈0-15显示为开关,线圈16-31显示为插座(避免误触发)
738
+
739
+ **v2.6.7更新**:
663
740
  - **🔥 修复LED反馈功能**:
664
741
  - 主站轮询检测到状态变化时自动广播事件
665
742
  - 从站开关节点监听状态变化并发送LED反馈到物理面板
@@ -986,7 +1063,7 @@ msg.payload = 1; // 或 0
986
1063
 
987
1064
  ## 项目信息
988
1065
 
989
- **版本**: v2.6.7
1066
+ **版本**: v2.6.8
990
1067
 
991
1068
  **核心功能**:
992
1069
  - 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
@@ -995,6 +1072,7 @@ msg.payload = 1; // 或 0
995
1072
  - 智能轮询暂停机制(从站上报时自动暂停,处理完成后恢复)
996
1073
  - 🔥 **双模式支持**(本地模式和MQTT模式可选切换,断网也能稳定运行)
997
1074
  - 🔥 **免连线通信**(主站和从站通过内部事件通信,无需连线,支持本地模式和MQTT模式)
1075
+ - 🔥 **HomeKit网桥**(一键桥接到Apple HomeKit,支持Siri语音控制,名称可自定义)
998
1076
  - MQTT集成(可选启用,Home Assistant自动发现,实体唯一性保证,QoS=0高性能发布)
999
1077
  - 物理开关面板双向同步(亖米协议支持,LED反馈同步,支持开关模式和场景模式)
1000
1078
  - 共享连接架构(多个从站开关节点共享同一个串口/TCP连接,支持500+节点)
@@ -1004,5 +1082,7 @@ msg.payload = 1; // 或 0
1004
1082
  - modbus-serial: ^8.0.23(内部封装serialport,支持TCP和串口)
1005
1083
  - serialport: ^12.0.0(原生串口通信)
1006
1084
  - mqtt: ^5.14.1(最新稳定版,可选依赖)
1085
+ - hap-nodejs: ^1.2.0(HomeKit桥接)
1086
+ - node-persist: ^4.0.4(持久化存储)
1007
1087
  - Node.js: >=14.0.0
1008
1088
  - Node-RED: >=2.0.0
@@ -0,0 +1,251 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('homekit-bridge', {
3
+ category: 'SYMI-MODBUS',
4
+ color: '#FF9800',
5
+ defaults: {
6
+ name: {value: "HomeKit网桥"},
7
+ bridgeName: {value: "Modbus继电器网桥"},
8
+ username: {value: ""}, // 自动生成MAC地址
9
+ pincode: {value: "031-45-154"},
10
+ port: {value: 51828},
11
+ masterNode: {value: "", required: true},
12
+ // 继电器名称配置(持久化)
13
+ relayNames: {value: {}} // 格式: {"10_0": "客厅灯", "10_1": "卧室灯", ...}
14
+ },
15
+ inputs: 0,
16
+ outputs: 0,
17
+ icon: "home.png",
18
+ label: function() {
19
+ return this.name || "HomeKit网桥";
20
+ },
21
+ oneditprepare: function() {
22
+ var node = this;
23
+
24
+ // 填充主站节点选择器
25
+ var masterNodeSelect = $("#node-input-masterNode");
26
+ masterNodeSelect.empty();
27
+ masterNodeSelect.append('<option value="">请选择主站节点</option>');
28
+
29
+ // 查找所有modbus-master节点
30
+ RED.nodes.eachNode(function(n) {
31
+ if (n.type === "modbus-master") {
32
+ var label = n.name || `主站 ${n.id.substring(0, 8)}`;
33
+ var selected = (n.id === node.masterNode) ? ' selected' : '';
34
+ masterNodeSelect.append(`<option value="${n.id}"${selected}>${label}</option>`);
35
+ }
36
+ });
37
+
38
+ // 自动生成MAC地址(如果未设置)
39
+ if (!node.username || node.username === "") {
40
+ node.username = generateMacAddress();
41
+ $("#node-input-username").val(node.username);
42
+ }
43
+
44
+ // 初始化继电器名称配置
45
+ if (!node.relayNames) {
46
+ node.relayNames = {};
47
+ }
48
+
49
+ // 监听主站节点选择变化
50
+ masterNodeSelect.on("change", function() {
51
+ renderRelayNameConfig();
52
+ });
53
+
54
+ // 初始渲染
55
+ renderRelayNameConfig();
56
+
57
+ // 生成MAC地址函数
58
+ function generateMacAddress() {
59
+ var mac = [];
60
+ for (var i = 0; i < 6; i++) {
61
+ var byte = Math.floor(Math.random() * 256).toString(16).toUpperCase();
62
+ mac.push(byte.length === 1 ? "0" + byte : byte);
63
+ }
64
+ return mac.join(":");
65
+ }
66
+
67
+ // 渲染继电器名称配置
68
+ function renderRelayNameConfig() {
69
+ var masterNodeId = $("#node-input-masterNode").val();
70
+ var container = $("#relay-names-container");
71
+ container.empty();
72
+
73
+ if (!masterNodeId) {
74
+ container.html('<div style="padding: 20px; text-align: center; color: #999;">请先选择主站节点</div>');
75
+ return;
76
+ }
77
+
78
+ // 获取主站节点配置
79
+ var masterNode = RED.nodes.node(masterNodeId);
80
+ if (!masterNode || !masterNode.slaves || masterNode.slaves.length === 0) {
81
+ container.html('<div style="padding: 20px; text-align: center; color: #999;">主站节点未配置从站</div>');
82
+ return;
83
+ }
84
+
85
+ // 遍历所有从站和线圈
86
+ masterNode.slaves.forEach(function(slave) {
87
+ var slaveSection = $('<div class="slave-section" style="margin-bottom: 20px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; background: #f9f9f9;">');
88
+
89
+ slaveSection.html(`
90
+ <div style="font-weight: bold; font-size: 14px; margin-bottom: 10px; color: #333;">
91
+ 从站 ${slave.address} (线圈 ${slave.coilStart}-${slave.coilEnd})
92
+ </div>
93
+ `);
94
+
95
+ var relayGrid = $('<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">');
96
+
97
+ for (var coil = slave.coilStart; coil <= slave.coilEnd; coil++) {
98
+ var key = slave.address + "_" + coil;
99
+ var currentName = node.relayNames[key] || getDefaultRelayName(slave.address, coil);
100
+ var relayType = coil < 16 ? "开关" : "插座";
101
+
102
+ var relayRow = $(`
103
+ <div style="display: flex; align-items: center; gap: 8px; padding: 8px; background: white; border-radius: 4px;">
104
+ <span style="min-width: 80px; font-size: 12px; color: #666;">线圈${coil} (${relayType}):</span>
105
+ <input type="text" class="relay-name-input" data-key="${key}" value="${currentName}"
106
+ style="flex: 1; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;"
107
+ placeholder="输入名称...">
108
+ </div>
109
+ `);
110
+
111
+ relayGrid.append(relayRow);
112
+ }
113
+
114
+ slaveSection.append(relayGrid);
115
+ container.append(slaveSection);
116
+ });
117
+
118
+ // 绑定输入事件
119
+ $(".relay-name-input").on("input", function() {
120
+ var key = $(this).data("key");
121
+ var value = $(this).val().trim();
122
+ if (value) {
123
+ node.relayNames[key] = value;
124
+ } else {
125
+ delete node.relayNames[key];
126
+ }
127
+ });
128
+ }
129
+
130
+ // 获取默认继电器名称
131
+ function getDefaultRelayName(slaveAddr, coil) {
132
+ var type = coil < 16 ? "开关" : "场景";
133
+ return `从站${slaveAddr}_${type}${coil + 1}`;
134
+ }
135
+ },
136
+ oneditsave: function() {
137
+ // 保存时确保relayNames已更新
138
+ // 已在input事件中实时更新,无需额外处理
139
+ }
140
+ });
141
+ </script>
142
+
143
+ <script type="text/html" data-template-name="homekit-bridge">
144
+ <div class="form-row">
145
+ <label for="node-input-name"><i class="fa fa-tag"></i> 节点名称</label>
146
+ <input type="text" id="node-input-name" placeholder="HomeKit网桥">
147
+ </div>
148
+
149
+ <div class="form-row">
150
+ <label for="node-input-masterNode"><i class="fa fa-microchip"></i> 主站节点</label>
151
+ <select id="node-input-masterNode" style="width: 70%;">
152
+ <option value="">请选择主站节点</option>
153
+ </select>
154
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">选择要桥接的Modbus主站节点</div>
155
+ </div>
156
+
157
+ <hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;">
158
+
159
+ <div class="form-row">
160
+ <label for="node-input-bridgeName"><i class="fa fa-home"></i> 网桥名称</label>
161
+ <input type="text" id="node-input-bridgeName" placeholder="Modbus继电器网桥">
162
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">在HomeKit中显示的网桥名称</div>
163
+ </div>
164
+
165
+ <div class="form-row">
166
+ <label for="node-input-username"><i class="fa fa-barcode"></i> MAC地址</label>
167
+ <input type="text" id="node-input-username" placeholder="自动生成" readonly>
168
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">HomeKit网桥的唯一标识符(自动生成)</div>
169
+ </div>
170
+
171
+ <div class="form-row">
172
+ <label for="node-input-pincode"><i class="fa fa-key"></i> 配对码</label>
173
+ <input type="text" id="node-input-pincode" placeholder="031-45-154">
174
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">HomeKit配对时使用的PIN码(格式:XXX-XX-XXX)</div>
175
+ </div>
176
+
177
+ <div class="form-row">
178
+ <label for="node-input-port"><i class="fa fa-plug"></i> 端口</label>
179
+ <input type="number" id="node-input-port" placeholder="51828" min="1024" max="65535">
180
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">HomeKit网桥监听端口(默认:51828)</div>
181
+ </div>
182
+
183
+ <hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;">
184
+
185
+ <div class="form-row">
186
+ <label style="width: 100%; margin-bottom: 10px;">
187
+ <i class="fa fa-list"></i> 继电器名称配置
188
+ </label>
189
+ <div id="relay-names-container" style="width: 100%; max-height: 400px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; background: white;">
190
+ <div style="padding: 20px; text-align: center; color: #999;">请先选择主站节点</div>
191
+ </div>
192
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">
193
+ 配置每个继电器在HomeKit中显示的名称<br>
194
+ 线圈0-15显示为开关,线圈16-31显示为插座(避免误触发场景)
195
+ </div>
196
+ </div>
197
+ </script>
198
+
199
+ <script type="text/html" data-help-name="homekit-bridge">
200
+ <p>HomeKit网桥节点,将Modbus继电器桥接到Apple HomeKit。</p>
201
+
202
+ <h3>功能特性</h3>
203
+ <ul>
204
+ <li>自动同步主站节点配置的所有从站和继电器</li>
205
+ <li>线圈0-15显示为开关(Switch),线圈16-31显示为插座(Outlet)</li>
206
+ <li>支持在Node-RED中配置每个继电器的名称,无需在HomeKit中修改</li>
207
+ <li>双向同步:HomeKit控制继电器,继电器状态同步到HomeKit</li>
208
+ <li>持久化存储配置和配对信息,重启后自动恢复</li>
209
+ </ul>
210
+
211
+ <h3>配置说明</h3>
212
+ <dl class="message-properties">
213
+ <dt>主站节点 <span class="property-type">必填</span></dt>
214
+ <dd>选择要桥接的Modbus主站节点</dd>
215
+
216
+ <dt>网桥名称 <span class="property-type">可选</span></dt>
217
+ <dd>在HomeKit中显示的网桥名称(默认:Modbus继电器网桥)</dd>
218
+
219
+ <dt>MAC地址 <span class="property-type">自动生成</span></dt>
220
+ <dd>HomeKit网桥的唯一标识符,自动生成,无需修改</dd>
221
+
222
+ <dt>配对码 <span class="property-type">必填</span></dt>
223
+ <dd>HomeKit配对时使用的PIN码(格式:XXX-XX-XXX,默认:031-45-154)</dd>
224
+
225
+ <dt>端口 <span class="property-type">可选</span></dt>
226
+ <dd>HomeKit网桥监听端口(默认:51828)</dd>
227
+
228
+ <dt>继电器名称配置 <span class="property-type">可选</span></dt>
229
+ <dd>配置每个继电器在HomeKit中显示的名称,支持中文</dd>
230
+ </dl>
231
+
232
+ <h3>使用步骤</h3>
233
+ <ol>
234
+ <li>选择已配置的Modbus主站节点</li>
235
+ <li>配置网桥名称和配对码</li>
236
+ <li>为每个继电器配置友好的名称(可选)</li>
237
+ <li>部署流程</li>
238
+ <li>在iPhone/iPad的"家庭"App中添加配件,输入配对码</li>
239
+ <li>完成配对后,即可在HomeKit中控制继电器</li>
240
+ </ol>
241
+
242
+ <h3>注意事项</h3>
243
+ <ul>
244
+ <li>确保主站节点已正确配置并运行</li>
245
+ <li>配对码格式必须为XXX-XX-XXX(8位数字)</li>
246
+ <li>端口不能与其他服务冲突</li>
247
+ <li>线圈16-31使用插座类型,避免在HomeKit中误触发所有开关</li>
248
+ <li>配置信息持久化存储在~/.node-red/homekit-persist目录</li>
249
+ </ul>
250
+ </script>
251
+
@@ -0,0 +1,328 @@
1
+ module.exports = function(RED) {
2
+ "use strict";
3
+ const hap = require('hap-nodejs');
4
+ const storage = require('node-persist');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ function HomekitBridgeNode(config) {
9
+ RED.nodes.createNode(this, config);
10
+ const node = this;
11
+
12
+ // 配置参数
13
+ node.config = {
14
+ name: config.name || "HomeKit网桥",
15
+ bridgeName: config.bridgeName || "Modbus继电器网桥",
16
+ username: config.username,
17
+ pincode: config.pincode || "031-45-154",
18
+ port: parseInt(config.port) || 51828,
19
+ masterNodeId: config.masterNode,
20
+ relayNames: config.relayNames || {}
21
+ };
22
+
23
+ // 运行时状态
24
+ node.bridge = null;
25
+ node.accessories = {};
26
+ node.stateChangeListener = null;
27
+ node.masterNode = null;
28
+ node.isInitialized = false;
29
+
30
+ // 初始化持久化存储
31
+ const persistDir = path.join(RED.settings.userDir || os.homedir() + '/.node-red', 'homekit-persist');
32
+
33
+ // 初始化函数
34
+ async function initialize() {
35
+ try {
36
+ // 初始化存储
37
+ await storage.init({
38
+ dir: persistDir,
39
+ stringify: JSON.stringify,
40
+ parse: JSON.parse,
41
+ encoding: 'utf8',
42
+ logging: false,
43
+ ttl: false,
44
+ expiredInterval: 2 * 60 * 1000,
45
+ forgiveParseErrors: true
46
+ });
47
+
48
+ node.log(`持久化存储初始化成功: ${persistDir}`);
49
+
50
+ // 保存继电器名称配置到持久化存储
51
+ await storage.setItem('relayNames', node.config.relayNames);
52
+ node.log(`继电器名称配置已保存: ${Object.keys(node.config.relayNames).length} 个`);
53
+
54
+ // 获取主站节点
55
+ node.masterNode = RED.nodes.getNode(node.config.masterNodeId);
56
+ if (!node.masterNode) {
57
+ node.error("未找到主站节点");
58
+ node.status({fill: "red", shape: "dot", text: "主站节点未找到"});
59
+ return;
60
+ }
61
+
62
+ // 从主站节点的 config.slaves 获取从站配置
63
+ const slaves = node.masterNode.config ? node.masterNode.config.slaves : null;
64
+ if (!slaves || slaves.length === 0) {
65
+ node.error("主站节点未配置从站");
66
+ node.status({fill: "red", shape: "dot", text: "主站未配置从站"});
67
+ return;
68
+ }
69
+
70
+ node.log(`找到主站节点,从站数量: ${slaves.length}`);
71
+
72
+ // 创建HomeKit网桥
73
+ await createBridge();
74
+
75
+ // 注册状态变化监听器
76
+ registerStateChangeListener();
77
+
78
+ node.isInitialized = true;
79
+ node.status({fill: "green", shape: "dot", text: "运行中"});
80
+ node.log(`HomeKit网桥已启动: ${node.config.bridgeName}`);
81
+ node.log(`配对码: ${node.config.pincode}`);
82
+ node.log(`端口: ${node.config.port}`);
83
+
84
+ } catch (err) {
85
+ node.error(`初始化失败: ${err.message}`);
86
+ node.status({fill: "red", shape: "dot", text: "初始化失败"});
87
+ }
88
+ }
89
+
90
+ // 创建HomeKit网桥
91
+ async function createBridge() {
92
+ // 创建网桥配件
93
+ const uuid = hap.uuid.generate('homebridge:modbus-bridge:' + node.config.username);
94
+ node.bridge = new hap.Bridge(node.config.bridgeName, uuid);
95
+
96
+ // 配置网桥信息
97
+ node.bridge.getService(hap.Service.AccessoryInformation)
98
+ .setCharacteristic(hap.Characteristic.Manufacturer, "SYMI")
99
+ .setCharacteristic(hap.Characteristic.Model, "Modbus Relay Bridge")
100
+ .setCharacteristic(hap.Characteristic.SerialNumber, node.config.username)
101
+ .setCharacteristic(hap.Characteristic.FirmwareRevision, "2.6.8");
102
+
103
+ // 遍历所有从站和线圈,创建配件
104
+ let accessoryCount = 0;
105
+ const slaves = node.masterNode.config ? node.masterNode.config.slaves : [];
106
+ slaves.forEach(function(slave) {
107
+ for (let coil = slave.coilStart; coil <= slave.coilEnd; coil++) {
108
+ const key = `${slave.address}_${coil}`;
109
+ const accessory = createAccessory(slave.address, coil);
110
+ if (accessory) {
111
+ node.bridge.addBridgedAccessory(accessory);
112
+ node.accessories[key] = accessory;
113
+ accessoryCount++;
114
+ }
115
+ }
116
+ });
117
+
118
+ node.log(`已创建 ${accessoryCount} 个HomeKit配件`);
119
+
120
+ // 发布网桥
121
+ node.bridge.publish({
122
+ username: node.config.username,
123
+ pincode: node.config.pincode,
124
+ port: node.config.port,
125
+ category: hap.Categories.BRIDGE
126
+ });
127
+
128
+ // 部署后自动更新所有配件名称(确保使用最新配置)
129
+ setTimeout(() => {
130
+ updateAllAccessoryNames();
131
+ }, 1000);
132
+ }
133
+
134
+ // 创建单个配件
135
+ function createAccessory(slaveAddr, coil) {
136
+ const key = `${slaveAddr}_${coil}`;
137
+ const name = node.config.relayNames[key] || getDefaultRelayName(slaveAddr, coil);
138
+ const uuid = hap.uuid.generate(`homebridge:modbus-relay:${slaveAddr}:${coil}`);
139
+
140
+ // 线圈0-15使用开关,线圈16-31使用插座(避免误触发)
141
+ const isSwitch = coil < 16;
142
+ const accessory = new hap.Accessory(name, uuid);
143
+
144
+ // 保存关键信息到配件对象(用于后续更新名称)
145
+ accessory._slaveAddr = slaveAddr;
146
+ accessory._coil = coil;
147
+ accessory._isSwitch = isSwitch;
148
+
149
+ // 配置配件信息
150
+ accessory.getService(hap.Service.AccessoryInformation)
151
+ .setCharacteristic(hap.Characteristic.Manufacturer, "SYMI")
152
+ .setCharacteristic(hap.Characteristic.Model, isSwitch ? "Modbus Switch" : "Modbus Outlet")
153
+ .setCharacteristic(hap.Characteristic.SerialNumber, `${slaveAddr}-${coil}`)
154
+ .setCharacteristic(hap.Characteristic.FirmwareRevision, "2.6.8");
155
+
156
+ // 添加服务(开关或插座)
157
+ const service = isSwitch
158
+ ? accessory.addService(hap.Service.Switch, name)
159
+ : accessory.addService(hap.Service.Outlet, name);
160
+
161
+ // 配置On特性
162
+ service.getCharacteristic(hap.Characteristic.On)
163
+ .on('get', (callback) => {
164
+ // 从主站获取当前状态
165
+ const state = getRelayState(slaveAddr, coil);
166
+ callback(null, state);
167
+ })
168
+ .on('set', (value, callback) => {
169
+ // 发送控制命令到主站
170
+ setRelayState(slaveAddr, coil, value);
171
+ callback(null);
172
+ });
173
+
174
+ // 如果是插座,配置InUse特性(始终为true)
175
+ if (!isSwitch) {
176
+ service.getCharacteristic(hap.Characteristic.OutletInUse)
177
+ .on('get', (callback) => {
178
+ callback(null, true);
179
+ });
180
+ }
181
+
182
+ return accessory;
183
+ }
184
+
185
+ // 更新配件名称(重新部署时调用)
186
+ function updateAccessoryName(accessory, newName) {
187
+ try {
188
+ const isSwitch = accessory._isSwitch;
189
+ const service = isSwitch
190
+ ? accessory.getService(hap.Service.Switch)
191
+ : accessory.getService(hap.Service.Outlet);
192
+
193
+ if (service) {
194
+ // 更新服务名称
195
+ service.setCharacteristic(hap.Characteristic.Name, newName);
196
+ // 更新配件显示名称
197
+ accessory.displayName = newName;
198
+ node.log(`配件名称已更新: ${accessory._slaveAddr}_${accessory._coil} -> ${newName}`);
199
+ }
200
+ } catch (err) {
201
+ node.error(`更新配件名称失败: ${err.message}`);
202
+ }
203
+ }
204
+
205
+ // 获取默认继电器名称
206
+ function getDefaultRelayName(slaveAddr, coil) {
207
+ const type = coil < 16 ? "开关" : "场景";
208
+ return `从站${slaveAddr}_${type}${coil + 1}`;
209
+ }
210
+
211
+ // 获取继电器状态(修复:使用 deviceStates 而不是 coilStates)
212
+ function getRelayState(slaveAddr, coil) {
213
+ if (!node.masterNode || !node.masterNode.deviceStates) {
214
+ return false;
215
+ }
216
+ const deviceState = node.masterNode.deviceStates[slaveAddr];
217
+ if (!deviceState || !deviceState.coils) {
218
+ return false;
219
+ }
220
+ return deviceState.coils[coil] || false;
221
+ }
222
+
223
+ // 设置继电器状态
224
+ function setRelayState(slaveAddr, coil, value) {
225
+ // 发送内部事件到主站
226
+ RED.events.emit('modbus:writeCoil', {
227
+ slave: slaveAddr,
228
+ coil: coil,
229
+ value: value,
230
+ source: 'homekit'
231
+ });
232
+
233
+ node.log(`HomeKit控制: 从站${slaveAddr} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
234
+ }
235
+
236
+ // 注册状态变化监听器
237
+ function registerStateChangeListener() {
238
+ node.stateChangeListener = function(data) {
239
+ try {
240
+ const slave = parseInt(data.slave);
241
+ const coil = parseInt(data.coil);
242
+ const value = Boolean(data.value);
243
+ const key = `${slave}_${coil}`;
244
+
245
+ // 查找对应的配件
246
+ const accessory = node.accessories[key];
247
+ if (!accessory) {
248
+ return;
249
+ }
250
+
251
+ // 更新HomeKit状态
252
+ const isSwitch = coil < 16;
253
+ const service = isSwitch
254
+ ? accessory.getService(hap.Service.Switch)
255
+ : accessory.getService(hap.Service.Outlet);
256
+
257
+ if (service) {
258
+ service.getCharacteristic(hap.Characteristic.On).updateValue(value);
259
+ node.log(`状态同步到HomeKit: 从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
260
+ }
261
+ } catch (err) {
262
+ node.error(`状态变化监听器错误: ${err.message}`);
263
+ }
264
+ };
265
+
266
+ RED.events.on('modbus:coilStateChanged', node.stateChangeListener);
267
+ node.log("已注册状态变化监听器(用于HomeKit同步)");
268
+ }
269
+
270
+ // 监听配置更新(重新部署时)
271
+ node.on('input', function(msg) {
272
+ // 检查是否是配置更新消息
273
+ if (msg.topic === 'updateNames' && node.isInitialized) {
274
+ updateAllAccessoryNames();
275
+ }
276
+ });
277
+
278
+ // 更新所有配件名称
279
+ function updateAllAccessoryNames() {
280
+ try {
281
+ let updateCount = 0;
282
+ Object.keys(node.accessories).forEach(key => {
283
+ const accessory = node.accessories[key];
284
+ const newName = node.config.relayNames[key] || getDefaultRelayName(accessory._slaveAddr, accessory._coil);
285
+ updateAccessoryName(accessory, newName);
286
+ updateCount++;
287
+ });
288
+ node.log(`已更新 ${updateCount} 个配件名称`);
289
+ } catch (err) {
290
+ node.error(`批量更新配件名称失败: ${err.message}`);
291
+ }
292
+ }
293
+
294
+ // 节点关闭时清理
295
+ node.on('close', function(done) {
296
+ node.log("正在停止HomeKit网桥...");
297
+
298
+ // 移除状态变化监听器
299
+ if (node.stateChangeListener) {
300
+ RED.events.removeListener('modbus:coilStateChanged', node.stateChangeListener);
301
+ node.log("状态变化监听器已移除");
302
+ }
303
+
304
+ // 停止网桥
305
+ if (node.bridge) {
306
+ try {
307
+ node.bridge.unpublish();
308
+ node.log("HomeKit网桥已停止");
309
+ } catch (err) {
310
+ node.warn(`停止网桥时出错: ${err.message}`);
311
+ }
312
+ }
313
+
314
+ // 清理资源
315
+ node.accessories = {};
316
+ node.bridge = null;
317
+ node.isInitialized = false;
318
+
319
+ done();
320
+ });
321
+
322
+ // 启动初始化
323
+ initialize();
324
+ }
325
+
326
+ RED.nodes.registerType("homekit-bridge", HomekitBridgeNode);
327
+ };
328
+
@@ -805,7 +805,7 @@ module.exports = function(RED) {
805
805
  }
806
806
 
807
807
  if (!node.isConnected) {
808
- node.warn('轮询已停止(Modbus未连接)');
808
+ node.log('轮询已停止(Modbus未连接)');
809
809
  return;
810
810
  }
811
811
 
@@ -817,7 +817,7 @@ module.exports = function(RED) {
817
817
  // 获取当前从站配置(在轮询前获取,确保使用正确的间隔)
818
818
  const currentSlave = node.config.slaves[node.currentSlaveIndex];
819
819
  if (!currentSlave) {
820
- node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
820
+ node.log(`从站索引${node.currentSlaveIndex}无效,重置为0`);
821
821
  node.currentSlaveIndex = 0;
822
822
  // 继续轮询,不要中断
823
823
  node.pollTimer = setTimeout(pollLoop, 200);
@@ -829,8 +829,8 @@ module.exports = function(RED) {
829
829
  try {
830
830
  await node.pollNextSlave();
831
831
  } catch (err) {
832
- // 捕获所有异常,防止轮询中断(降级为warn,避免日志刷屏)
833
- node.warn(`轮询异常(已忽略,继续轮询): ${err.message}`);
832
+ // 捕获所有异常,防止轮询中断(只记录到日志文件,不输出到调试窗口)
833
+ node.log(`轮询异常(已忽略,继续轮询): ${err.message}`);
834
834
  }
835
835
 
836
836
  // 移动到下一个从站
@@ -877,7 +877,7 @@ module.exports = function(RED) {
877
877
  // 获取当前从站配置
878
878
  const slave = node.config.slaves[node.currentSlaveIndex];
879
879
  if (!slave) {
880
- node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
880
+ node.log(`从站索引${node.currentSlaveIndex}无效,重置为0`);
881
881
  node.currentSlaveIndex = 0;
882
882
  return;
883
883
  }
@@ -993,7 +993,7 @@ module.exports = function(RED) {
993
993
 
994
994
  // 确保deviceState存在(防止未定义错误)
995
995
  if (!node.deviceStates[slaveId]) {
996
- node.warn(`从站${slaveId}状态未初始化,跳过错误记录`);
996
+ node.log(`从站${slaveId}状态未初始化,跳过错误记录`);
997
997
  return;
998
998
  }
999
999
 
@@ -1016,15 +1016,15 @@ module.exports = function(RED) {
1016
1016
  // 增加超时计数(仅用于统计,不影响轮询)
1017
1017
  node.deviceStates[slaveId].timeoutCount++;
1018
1018
 
1019
- // 只记录日志,不再临时忽略从站(确保轮询永不停止)
1019
+ // 只记录日志到文件,不输出到调试窗口(避免日志刷屏)
1020
1020
  if (shouldLog) {
1021
- node.warn(`从站${slaveId}轮询超时: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [连续超时${node.deviceStates[slaveId].timeoutCount}次]`);
1021
+ node.log(`从站${slaveId}轮询超时: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [连续超时${node.deviceStates[slaveId].timeoutCount}次]`);
1022
1022
  node.lastErrorLog[slaveId] = now;
1023
1023
  }
1024
1024
  } else {
1025
- // 非超时错误(如CRC错误、总线干扰),记录日志但不累积计数
1025
+ // 非超时错误(如CRC错误、总线干扰),记录日志到文件,不输出到调试窗口
1026
1026
  if (shouldLog) {
1027
- node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd})`);
1027
+ node.log(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd})`);
1028
1028
  node.lastErrorLog[slaveId] = now;
1029
1029
  }
1030
1030
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-modbus",
3
- "version": "2.6.7",
4
- "description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现和物理开关面板双向同步,工控机长期稳定运行",
3
+ "version": "2.6.8",
4
+ "description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥和物理开关面板双向同步,工控机长期稳定运行",
5
5
  "main": "nodes/modbus-master.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -37,12 +37,15 @@
37
37
  "modbus-server-config": "nodes/modbus-server-config.js",
38
38
  "mqtt-server-config": "nodes/mqtt-server-config.js",
39
39
  "serial-port-config": "nodes/serial-port-config.js",
40
- "modbus-debug": "nodes/modbus-debug.js"
40
+ "modbus-debug": "nodes/modbus-debug.js",
41
+ "homekit-bridge": "nodes/homekit-bridge.js"
41
42
  }
42
43
  },
43
44
  "dependencies": {
45
+ "hap-nodejs": "^1.2.0",
44
46
  "modbus-serial": "^8.0.23",
45
47
  "mqtt": "^5.14.1",
48
+ "node-persist": "^4.0.4",
46
49
  "serialport": "^12.0.0"
47
50
  },
48
51
  "repository": {