node-red-contrib-symi-mesh 1.7.2 → 1.7.4

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.
@@ -0,0 +1,381 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('symi-mqtt-sync', {
3
+ category: 'Symi Mesh',
4
+ color: '#9370DB',
5
+ defaults: {
6
+ name: { value: '' },
7
+ mqttConfig: { value: '', type: 'symi-mqtt', required: true },
8
+ brandMqttConfig: { value: '', type: 'symi-mqtt-brand', required: true },
9
+ autoDiscover: { value: true },
10
+ mappings: { value: '[]' }
11
+ },
12
+ inputs: 1,
13
+ outputs: 1,
14
+ icon: 'font-awesome/fa-cloud',
15
+ align: 'left',
16
+ paletteLabel: 'MQTT同步',
17
+ label: function() {
18
+ if (this.name) return this.name;
19
+ try {
20
+ var m = JSON.parse(this.mappings || '[]');
21
+ if (m.length > 0) return 'MQTT同步(' + m.length + '组)';
22
+ } catch(e) {}
23
+ return 'MQTT同步';
24
+ },
25
+ oneditprepare: function() {
26
+ var node = this;
27
+ var mappings = [];
28
+ var meshDevices = [];
29
+ var brandDevices = [];
30
+
31
+ // 设置编辑面板更宽
32
+ var panel = $('#dialog-form').parent();
33
+ if (panel.length && panel.width() < 920) {
34
+ panel.css('width', '900px');
35
+ }
36
+
37
+ var deviceTypes = {
38
+ 8: { name: '灯具' },
39
+ 12: { name: '空调' },
40
+ 14: { name: '窗帘' },
41
+ 16: { name: '地暖' },
42
+ 36: { name: '新风' }
43
+ };
44
+
45
+ try { mappings = JSON.parse(node.mappings || '[]'); } catch(e) { mappings = []; }
46
+
47
+ // 加载Mesh设备列表
48
+ function loadMeshDevices(callback) {
49
+ var mqttConfigId = $('#node-input-mqttConfig').val();
50
+ if (!mqttConfigId) {
51
+ meshDevices = [];
52
+ if (callback) callback();
53
+ return;
54
+ }
55
+ var mqttConfigNode = RED.nodes.node(mqttConfigId);
56
+ var gatewayId = mqttConfigNode ? mqttConfigNode.gateway : null;
57
+ if (!gatewayId) {
58
+ meshDevices = [];
59
+ if (callback) callback();
60
+ return;
61
+ }
62
+ $.getJSON('/symi-gateway/devices/' + gatewayId, function(devices) {
63
+ meshDevices = devices || [];
64
+ if (callback) callback();
65
+ }).fail(function() {
66
+ meshDevices = [];
67
+ if (callback) callback();
68
+ });
69
+ }
70
+
71
+ // 加载品牌MQTT设备列表
72
+ function loadBrandDevices(callback) {
73
+ var brandConfigId = $('#node-input-brandMqttConfig').val();
74
+ if (!brandConfigId) {
75
+ brandDevices = [];
76
+ if (callback) callback();
77
+ return;
78
+ }
79
+ $.getJSON('/symi-mqtt-brand/devices/' + brandConfigId, function(devices) {
80
+ brandDevices = devices || [];
81
+ if (callback) callback();
82
+ }).fail(function() {
83
+ brandDevices = [];
84
+ if (callback) callback();
85
+ });
86
+ }
87
+
88
+ // 构建Mesh设备选项
89
+ function getMeshOptions(selectedMac) {
90
+ var html = '<option value="">-- 选择Mesh设备 --</option>';
91
+ var selMacNorm = (selectedMac || '').toLowerCase().replace(/:/g, '');
92
+ meshDevices.forEach(function(d) {
93
+ var devMacNorm = (d.mac || '').toLowerCase().replace(/:/g, '');
94
+ var selected = (devMacNorm === selMacNorm && selMacNorm !== '') ? ' selected' : '';
95
+ html += '<option value="' + d.mac + '" data-channels="' + (d.channels || 1) + '"' + selected + '>' + d.name + '</option>';
96
+ });
97
+ return html;
98
+ }
99
+
100
+ // 构建Mesh按键选项
101
+ function getMeshChannelOptions(mac, selectedChannel) {
102
+ var macNorm = (mac || '').toLowerCase().replace(/:/g, '');
103
+ var device = meshDevices.find(function(d) {
104
+ return (d.mac || '').toLowerCase().replace(/:/g, '') === macNorm;
105
+ });
106
+ var channels = device ? (device.channels || 1) : 0;
107
+ if (channels <= 1) return '';
108
+ var html = '<select class="mesh-channel">';
109
+ for (var i = 1; i <= channels; i++) {
110
+ var sel = (i == selectedChannel) ? ' selected' : '';
111
+ html += '<option value="' + i + '"' + sel + '>第' + i + '路</option>';
112
+ }
113
+ html += '</select>';
114
+ return html;
115
+ }
116
+
117
+ // 构建品牌设备选项
118
+ function getBrandOptions(selectedType, selectedId) {
119
+ var html = '<option value="">-- 选择品牌设备 --</option>';
120
+ var selectedKey = selectedType + '_' + selectedId;
121
+
122
+ // 从已发现设备生成选项
123
+ brandDevices.forEach(function(d) {
124
+ var key = d.deviceType + '_' + d.deviceId;
125
+ var selected = (key === selectedKey) ? ' selected' : '';
126
+ html += '<option value="' + key + '"' + selected + '>' + d.typeName + ' (ID:' + d.deviceId + ')</option>';
127
+ });
128
+
129
+ // 如果选中的设备不在列表中,添加它
130
+ if (selectedType && selectedId) {
131
+ var exists = brandDevices.some(function(d) {
132
+ return d.deviceType == selectedType && d.deviceId == selectedId;
133
+ });
134
+ if (!exists) {
135
+ var typeName = deviceTypes[selectedType] ? deviceTypes[selectedType].name : '类型' + selectedType;
136
+ html += '<option value="' + selectedKey + '" selected>' + typeName + ' (ID:' + selectedId + ')</option>';
137
+ }
138
+ }
139
+
140
+ return html;
141
+ }
142
+
143
+ // 构建品牌设备通道选项(灯具支持多路)
144
+ function getBrandChannelOptions(deviceType, selectedChannel) {
145
+ // 灯具(8)支持多路,其他设备单路
146
+ var channels = (parseInt(deviceType) === 8) ? 8 : 1;
147
+ if (channels <= 1) return '';
148
+ var html = '<select class="brand-channel">';
149
+ for (var i = 1; i <= channels; i++) {
150
+ var sel = (i == selectedChannel) ? ' selected' : '';
151
+ html += '<option value="' + i + '"' + sel + '>第' + i + '路</option>';
152
+ }
153
+ html += '</select>';
154
+ return html;
155
+ }
156
+
157
+ // 渲染映射列表
158
+ function renderMappings() {
159
+ var container = $('#mapping-list');
160
+ container.empty();
161
+
162
+ if (mappings.length === 0) {
163
+ container.append('<div class="mapping-empty">暂无映射,点击下方按钮添加</div>');
164
+ return;
165
+ }
166
+
167
+ mappings.forEach(function(m, idx) {
168
+ var row = $('<div class="mapping-row" data-idx="' + idx + '"></div>');
169
+ row.html(
170
+ '<div class="mapping-main">' +
171
+ '<div class="mesh-col">' +
172
+ ' <select class="mesh-select">' + getMeshOptions(m.meshMac) + '</select>' +
173
+ ' <span class="mesh-ch-wrap">' + getMeshChannelOptions(m.meshMac, m.meshChannel || 1) + '</span>' +
174
+ '</div>' +
175
+ '<div class="arrow-col"><i class="fa fa-arrows-h"></i></div>' +
176
+ '<div class="brand-col">' +
177
+ ' <select class="brand-select">' + getBrandOptions(m.brandDeviceType, m.brandDeviceId) + '</select>' +
178
+ ' <span class="brand-ch-wrap">' + getBrandChannelOptions(m.brandDeviceType, m.brandChannel || 1) + '</span>' +
179
+ '</div>' +
180
+ '<div class="del-col"><button type="button" class="red-ui-button red-ui-button-small btn-remove" title="删除"><i class="fa fa-times"></i></button></div>' +
181
+ '</div>'
182
+ );
183
+ container.append(row);
184
+ });
185
+
186
+ bindEvents();
187
+ }
188
+
189
+ // 绑定事件
190
+ function bindEvents() {
191
+ var container = $('#mapping-list');
192
+
193
+ container.find('.mesh-select').off('change').on('change', function() {
194
+ var row = $(this).closest('.mapping-row');
195
+ var idx = row.data('idx');
196
+ var mac = $(this).val();
197
+ mappings[idx].meshMac = mac || '';
198
+ mappings[idx].meshChannel = 1;
199
+ row.find('.mesh-ch-wrap').html(getMeshChannelOptions(mac, 1));
200
+ bindEvents();
201
+ });
202
+
203
+ container.find('.mesh-channel').off('change').on('change', function() {
204
+ var idx = $(this).closest('.mapping-row').data('idx');
205
+ mappings[idx].meshChannel = parseInt($(this).val()) || 1;
206
+ });
207
+
208
+ container.find('.brand-select').off('change').on('change', function() {
209
+ var row = $(this).closest('.mapping-row');
210
+ var idx = row.data('idx');
211
+ var val = $(this).val();
212
+ if (val) {
213
+ var parts = val.split('_');
214
+ mappings[idx].brandDeviceType = parseInt(parts[0]) || 8;
215
+ mappings[idx].brandDeviceId = parseInt(parts[1]) || 1;
216
+ mappings[idx].brandChannel = 1;
217
+ // 更新通道选择器
218
+ row.find('.brand-ch-wrap').html(getBrandChannelOptions(mappings[idx].brandDeviceType, 1));
219
+ bindEvents();
220
+ } else {
221
+ mappings[idx].brandDeviceType = null;
222
+ mappings[idx].brandDeviceId = null;
223
+ mappings[idx].brandChannel = 1;
224
+ row.find('.brand-ch-wrap').empty();
225
+ }
226
+ });
227
+
228
+ container.find('.brand-channel').off('change').on('change', function() {
229
+ var idx = $(this).closest('.mapping-row').data('idx');
230
+ mappings[idx].brandChannel = parseInt($(this).val()) || 1;
231
+ });
232
+
233
+ container.find('.btn-remove').off('click').on('click', function() {
234
+ var idx = $(this).closest('.mapping-row').data('idx');
235
+ mappings.splice(idx, 1);
236
+ renderMappings();
237
+ });
238
+ }
239
+
240
+ // 刷新品牌设备
241
+ $('#refresh-brand-btn').on('click', function() {
242
+ loadBrandDevices(renderMappings);
243
+ });
244
+
245
+ // 添加映射按钮
246
+ $('#btn-add-mapping').on('click', function() {
247
+ mappings.push({ meshMac: '', meshChannel: 1, brandDeviceType: 8, brandDeviceId: 1, brandChannel: 1 });
248
+ renderMappings();
249
+ });
250
+
251
+ // 配置变化时重新加载
252
+ $('#node-input-mqttConfig').on('change', function() {
253
+ setTimeout(function() {
254
+ loadMeshDevices(renderMappings);
255
+ }, 100);
256
+ });
257
+
258
+ $('#node-input-brandMqttConfig').on('change', function() {
259
+ setTimeout(function() {
260
+ loadBrandDevices(renderMappings);
261
+ }, 100);
262
+ });
263
+
264
+ // 保存时更新mappings
265
+ node._saveMappings = function() {
266
+ node.mappings = JSON.stringify(mappings);
267
+ $('#node-input-mappings').val(node.mappings);
268
+ };
269
+
270
+ // 初始加载
271
+ setTimeout(function() {
272
+ loadMeshDevices(function() {
273
+ loadBrandDevices(renderMappings);
274
+ });
275
+ }, 300);
276
+ },
277
+ oneditresize: function(size) {
278
+ var rows = $('#dialog-form>div:not(.bridge-section)');
279
+ var height = size.height;
280
+ for (var i = 0; i < rows.length; i++) { height -= $(rows[i]).outerHeight(true); }
281
+ height -= 120;
282
+ $('#mapping-list').css('max-height', Math.max(150, height) + 'px');
283
+ },
284
+ oneditsave: function() {
285
+ if (this._saveMappings) { this._saveMappings(); }
286
+ }
287
+ });
288
+ </script>
289
+
290
+ <script type="text/html" data-template-name="symi-mqtt-sync">
291
+ <style>
292
+ .bridge-section { margin: 12px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #fafafa; }
293
+ .bridge-section h4 { margin: 0 0 10px 0; padding-bottom: 6px; border-bottom: 1px solid #eee; color: #333; font-size: 13px; }
294
+ .bridge-section h4 i { margin-right: 6px; color: #666; }
295
+
296
+ #mapping-list { max-height: calc(100vh - 400px); min-height: 150px; overflow-y: auto; }
297
+ .mapping-empty { padding: 15px; text-align: center; color: #999; font-size: 12px; }
298
+ .mapping-row { display: flex; flex-direction: column; padding: 6px 8px; margin-bottom: 6px; background: #fff; border: 1px solid #e0e0e0; border-radius: 4px; gap: 6px; }
299
+ .mapping-main { display: flex; align-items: center; width: 100%; gap: 6px; min-width: 0; }
300
+ .mesh-col { flex: 1 1 40%; min-width: 0; display: flex; gap: 4px; }
301
+ .mesh-col .mesh-select { flex: 1; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #e8f5e9; font-size: 12px; }
302
+ .mesh-col .mesh-channel { width: 58px; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #c8e6c9; font-size: 11px; font-weight: bold; }
303
+ .arrow-col { flex: 0 0 20px; text-align: center; color: #999; }
304
+ .brand-col { flex: 1 1 40%; display: flex; gap: 4px; }
305
+ .brand-col .brand-select { flex: 1; padding: 4px; border: 1px solid #ba68c8; border-radius: 3px; background: #f3e5f5; font-size: 12px; }
306
+ .brand-col .brand-channel { width: 58px; padding: 4px; border: 1px solid #ba68c8; border-radius: 3px; background: #e1bee7; font-size: 11px; font-weight: bold; }
307
+ .del-col { flex: 0 0 auto; }
308
+ .btn-remove { color: #d32f2f !important; padding: 2px 6px !important; }
309
+ </style>
310
+
311
+ <div class="form-row">
312
+ <label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
313
+ <input type="text" id="node-input-name" placeholder="如:客厅MQTT同步">
314
+ </div>
315
+
316
+ <div class="bridge-section">
317
+ <h4><i class="fa fa-server"></i> MQTT配置</h4>
318
+ <div class="form-row">
319
+ <label for="node-input-mqttConfig"><i class="fa fa-wifi"></i> Mesh MQTT</label>
320
+ <input type="text" id="node-input-mqttConfig">
321
+ </div>
322
+ <div class="form-row">
323
+ <label for="node-input-brandMqttConfig"><i class="fa fa-cloud"></i> 品牌MQTT</label>
324
+ <input type="text" id="node-input-brandMqttConfig">
325
+ </div>
326
+ <div class="form-row">
327
+ <label for="node-input-autoDiscover"><i class="fa fa-search"></i> 自动发现</label>
328
+ <input type="checkbox" id="node-input-autoDiscover" style="width:auto;"> 启用
329
+ </div>
330
+ </div>
331
+
332
+ <div class="bridge-section">
333
+ <h4><i class="fa fa-exchange"></i> 实体映射(Mesh ↔ 品牌MQTT)
334
+ <button type="button" id="refresh-brand-btn" class="red-ui-button red-ui-button-small" style="float:right;" title="刷新品牌设备"><i class="fa fa-refresh"></i></button>
335
+ </h4>
336
+ <div style="display:flex; padding:4px 8px; font-size:11px; color:#666; border-bottom:1px solid #eee; margin-bottom:6px; gap:6px;">
337
+ <span style="flex:1 1 40%">Mesh设备/按键</span>
338
+ <span style="flex:0 0 20px"></span>
339
+ <span style="flex:1 1 40%">品牌设备</span>
340
+ </div>
341
+ <div id="mapping-list"></div>
342
+ <button type="button" id="btn-add-mapping" class="red-ui-button" style="margin-top:8px; width:100%">
343
+ <i class="fa fa-plus"></i> 添加映射
344
+ </button>
345
+ </div>
346
+
347
+ <input type="hidden" id="node-input-mappings">
348
+ </script>
349
+
350
+ <script type="text/html" data-help-name="symi-mqtt-sync">
351
+ <p>MQTT同步节点 - 实现第三方MQTT品牌设备与Symi Mesh设备的双向状态同步。</p>
352
+
353
+ <h3>功能特性</h3>
354
+ <ul>
355
+ <li><strong>双MQTT配置节点</strong>:Mesh MQTT + 品牌MQTT独立配置</li>
356
+ <li><strong>设备自动发现</strong>:品牌MQTT连接后自动发现设备</li>
357
+ <li><strong>实体映射</strong>:左边选择Mesh设备,右边选择品牌设备</li>
358
+ <li><strong>双向同步</strong>:MQTT↔Mesh双向状态实时同步</li>
359
+ <li><strong>防死循环</strong>:内置2秒防抖机制</li>
360
+ <li><strong>自动重连</strong>:断线后自动重连</li>
361
+ </ul>
362
+
363
+ <h3>配置说明</h3>
364
+ <dl class="message-properties">
365
+ <dt>Mesh MQTT</dt>
366
+ <dd>选择symi-mqtt配置节点,用于获取Mesh设备列表和状态同步</dd>
367
+ <dt>品牌MQTT</dt>
368
+ <dd>选择品牌MQTT配置节点(如HYQW),用于获取品牌设备列表</dd>
369
+ <dt>实体映射</dt>
370
+ <dd>配置Mesh设备与品牌设备的对应关系</dd>
371
+ </dl>
372
+
373
+ <h3>支持的设备类型(HYQW)</h3>
374
+ <ul>
375
+ <li><strong>灯具(8)</strong>:开关、亮度</li>
376
+ <li><strong>空调(12)</strong>:开关、温度、模式、风速</li>
377
+ <li><strong>窗帘(14)</strong>:开关、位置</li>
378
+ <li><strong>地暖(16)</strong>:开关、温度</li>
379
+ <li><strong>新风(36)</strong>:开关、风速</li>
380
+ </ul>
381
+ </script>