node-red-contrib-symi-mesh 1.7.7 → 1.7.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 +90 -73
- package/nodes/symi-485-bridge.html +69 -13
- package/nodes/symi-ha-sync.html +451 -0
- package/nodes/symi-ha-sync.js +787 -0
- package/nodes/symi-mqtt-brand.js +243 -59
- package/nodes/symi-mqtt-sync.html +151 -42
- package/nodes/symi-mqtt-sync.js +45 -18
- package/package.json +2 -1
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
mqttConfig: { value: '', type: 'symi-mqtt', required: true },
|
|
8
8
|
brandMqttConfig: { value: '', type: 'symi-mqtt-brand', required: true },
|
|
9
9
|
autoDiscover: { value: true },
|
|
10
|
-
mappings: { value: '[]' }
|
|
10
|
+
mappings: { value: '[]' },
|
|
11
|
+
// 持久化缓存:保存设备列表,断线后仍可显示
|
|
12
|
+
cachedMeshDevices: { value: '[]' },
|
|
13
|
+
cachedBrandDevices: { value: '[]' }
|
|
11
14
|
},
|
|
12
15
|
inputs: 1,
|
|
13
16
|
outputs: 1,
|
|
@@ -27,11 +30,14 @@
|
|
|
27
30
|
var mappings = [];
|
|
28
31
|
var meshDevices = [];
|
|
29
32
|
var brandDevices = [];
|
|
33
|
+
var cachedMeshDevices = [];
|
|
34
|
+
var cachedBrandDevices = [];
|
|
30
35
|
|
|
31
|
-
//
|
|
36
|
+
// 设置编辑面板更宽更高
|
|
32
37
|
var panel = $('#dialog-form').parent();
|
|
33
|
-
if (panel.length
|
|
34
|
-
panel.css('width', '900px');
|
|
38
|
+
if (panel.length) {
|
|
39
|
+
if (panel.width() < 920) panel.css('width', '900px');
|
|
40
|
+
if (panel.height() < 700) panel.css('min-height', '700px');
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
var deviceTypes = {
|
|
@@ -42,28 +48,70 @@
|
|
|
42
48
|
36: { name: '新风' }
|
|
43
49
|
};
|
|
44
50
|
|
|
51
|
+
// 加载已保存的配置
|
|
45
52
|
try { mappings = JSON.parse(node.mappings || '[]'); } catch(e) { mappings = []; }
|
|
53
|
+
try { cachedMeshDevices = JSON.parse(node.cachedMeshDevices || '[]'); } catch(e) { cachedMeshDevices = []; }
|
|
54
|
+
try { cachedBrandDevices = JSON.parse(node.cachedBrandDevices || '[]'); } catch(e) { cachedBrandDevices = []; }
|
|
55
|
+
|
|
56
|
+
// 合并设备列表:在线设备 + 缓存设备(去重)
|
|
57
|
+
function mergeDevices(onlineDevices, cachedDevices, keyField) {
|
|
58
|
+
var merged = [];
|
|
59
|
+
var keys = new Set();
|
|
60
|
+
|
|
61
|
+
// 先添加在线设备
|
|
62
|
+
(onlineDevices || []).forEach(function(d) {
|
|
63
|
+
var key = keyField === 'mac' ? (d.mac || '').toLowerCase().replace(/:/g, '') :
|
|
64
|
+
(d.deviceType + '_' + d.deviceId);
|
|
65
|
+
if (!keys.has(key)) {
|
|
66
|
+
keys.add(key);
|
|
67
|
+
d._online = true;
|
|
68
|
+
merged.push(d);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 再添加缓存中不在线的设备
|
|
73
|
+
(cachedDevices || []).forEach(function(d) {
|
|
74
|
+
var key = keyField === 'mac' ? (d.mac || '').toLowerCase().replace(/:/g, '') :
|
|
75
|
+
(d.deviceType + '_' + d.deviceId);
|
|
76
|
+
if (!keys.has(key)) {
|
|
77
|
+
keys.add(key);
|
|
78
|
+
d._online = false;
|
|
79
|
+
merged.push(d);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
46
85
|
|
|
47
86
|
// 加载Mesh设备列表
|
|
48
87
|
function loadMeshDevices(callback) {
|
|
49
88
|
var mqttConfigId = $('#node-input-mqttConfig').val();
|
|
50
89
|
if (!mqttConfigId) {
|
|
51
|
-
|
|
90
|
+
// 无配置时使用缓存
|
|
91
|
+
meshDevices = mergeDevices([], cachedMeshDevices, 'mac');
|
|
52
92
|
if (callback) callback();
|
|
53
93
|
return;
|
|
54
94
|
}
|
|
55
95
|
var mqttConfigNode = RED.nodes.node(mqttConfigId);
|
|
56
96
|
var gatewayId = mqttConfigNode ? mqttConfigNode.gateway : null;
|
|
57
97
|
if (!gatewayId) {
|
|
58
|
-
meshDevices = [];
|
|
98
|
+
meshDevices = mergeDevices([], cachedMeshDevices, 'mac');
|
|
59
99
|
if (callback) callback();
|
|
60
100
|
return;
|
|
61
101
|
}
|
|
62
102
|
$.getJSON('symi-gateway/devices/' + gatewayId, function(devices) {
|
|
63
|
-
|
|
103
|
+
// 合并在线设备和缓存设备
|
|
104
|
+
meshDevices = mergeDevices(devices || [], cachedMeshDevices, 'mac');
|
|
105
|
+
// 更新缓存(只保存有效设备)
|
|
106
|
+
if (devices && devices.length > 0) {
|
|
107
|
+
cachedMeshDevices = devices.map(function(d) {
|
|
108
|
+
return { mac: d.mac, name: d.name, channels: d.channels };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
64
111
|
if (callback) callback();
|
|
65
112
|
}).fail(function() {
|
|
66
|
-
|
|
113
|
+
// 请求失败时使用缓存
|
|
114
|
+
meshDevices = mergeDevices([], cachedMeshDevices, 'mac');
|
|
67
115
|
if (callback) callback();
|
|
68
116
|
});
|
|
69
117
|
}
|
|
@@ -72,38 +120,56 @@
|
|
|
72
120
|
function loadBrandDevices(callback) {
|
|
73
121
|
var brandConfigId = $('#node-input-brandMqttConfig').val();
|
|
74
122
|
if (!brandConfigId) {
|
|
75
|
-
brandDevices = [];
|
|
123
|
+
brandDevices = mergeDevices([], cachedBrandDevices, 'brand');
|
|
76
124
|
if (callback) callback();
|
|
77
125
|
return;
|
|
78
126
|
}
|
|
79
127
|
$.getJSON('symi-mqtt-brand/devices/' + brandConfigId, function(devices) {
|
|
80
|
-
brandDevices = devices || [];
|
|
128
|
+
brandDevices = mergeDevices(devices || [], cachedBrandDevices, 'brand');
|
|
129
|
+
// 更新缓存
|
|
130
|
+
if (devices && devices.length > 0) {
|
|
131
|
+
cachedBrandDevices = devices.map(function(d) {
|
|
132
|
+
return { deviceType: d.deviceType, deviceId: d.deviceId, typeName: d.typeName };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
81
135
|
if (callback) callback();
|
|
82
136
|
}).fail(function() {
|
|
83
|
-
brandDevices = [];
|
|
137
|
+
brandDevices = mergeDevices([], cachedBrandDevices, 'brand');
|
|
84
138
|
if (callback) callback();
|
|
85
139
|
});
|
|
86
140
|
}
|
|
87
141
|
|
|
88
|
-
// 构建Mesh
|
|
89
|
-
function getMeshOptions(selectedMac) {
|
|
142
|
+
// 构建Mesh设备选项(支持显示离线设备)
|
|
143
|
+
function getMeshOptions(selectedMac, savedName) {
|
|
90
144
|
var html = '<option value="">-- 选择Mesh设备 --</option>';
|
|
91
145
|
var selMacNorm = (selectedMac || '').toLowerCase().replace(/:/g, '');
|
|
146
|
+
var found = false;
|
|
147
|
+
|
|
92
148
|
meshDevices.forEach(function(d) {
|
|
93
149
|
var devMacNorm = (d.mac || '').toLowerCase().replace(/:/g, '');
|
|
94
150
|
var selected = (devMacNorm === selMacNorm && selMacNorm !== '') ? ' selected' : '';
|
|
95
|
-
|
|
151
|
+
if (selected) found = true;
|
|
152
|
+
var statusIcon = d._online === false ? ' [离线]' : '';
|
|
153
|
+
var style = d._online === false ? ' style="color:#999;"' : '';
|
|
154
|
+
html += '<option value="' + d.mac + '" data-channels="' + (d.channels || 1) + '" data-name="' + (d.name || '') + '"' + selected + style + '>' + (d.name || d.mac) + statusIcon + '</option>';
|
|
96
155
|
});
|
|
156
|
+
|
|
157
|
+
// 如果已选择的设备不在列表中,添加它(使用保存的名称)
|
|
158
|
+
if (selMacNorm && !found) {
|
|
159
|
+
var displayName = savedName || selectedMac;
|
|
160
|
+
html += '<option value="' + selectedMac + '" selected style="color:#c00;">' + displayName + ' [未找到]</option>';
|
|
161
|
+
}
|
|
162
|
+
|
|
97
163
|
return html;
|
|
98
164
|
}
|
|
99
165
|
|
|
100
166
|
// 构建Mesh按键选项
|
|
101
|
-
function getMeshChannelOptions(mac, selectedChannel) {
|
|
167
|
+
function getMeshChannelOptions(mac, selectedChannel, savedChannels) {
|
|
102
168
|
var macNorm = (mac || '').toLowerCase().replace(/:/g, '');
|
|
103
169
|
var device = meshDevices.find(function(d) {
|
|
104
170
|
return (d.mac || '').toLowerCase().replace(/:/g, '') === macNorm;
|
|
105
171
|
});
|
|
106
|
-
var channels = device ? (device.channels || 1) :
|
|
172
|
+
var channels = device ? (device.channels || 1) : (savedChannels || 1);
|
|
107
173
|
if (channels <= 1) return '';
|
|
108
174
|
var html = '<select class="mesh-channel">';
|
|
109
175
|
for (var i = 1; i <= channels; i++) {
|
|
@@ -114,27 +180,26 @@
|
|
|
114
180
|
return html;
|
|
115
181
|
}
|
|
116
182
|
|
|
117
|
-
//
|
|
118
|
-
function getBrandOptions(selectedType, selectedId) {
|
|
183
|
+
// 构建品牌设备选项(支持显示离线设备)
|
|
184
|
+
function getBrandOptions(selectedType, selectedId, savedTypeName) {
|
|
119
185
|
var html = '<option value="">-- 选择品牌设备 --</option>';
|
|
120
186
|
var selectedKey = selectedType + '_' + selectedId;
|
|
187
|
+
var found = false;
|
|
121
188
|
|
|
122
|
-
//
|
|
189
|
+
// 从设备列表生成选项
|
|
123
190
|
brandDevices.forEach(function(d) {
|
|
124
191
|
var key = d.deviceType + '_' + d.deviceId;
|
|
125
192
|
var selected = (key === selectedKey) ? ' selected' : '';
|
|
126
|
-
|
|
193
|
+
if (selected) found = true;
|
|
194
|
+
var statusIcon = d._online === false ? ' [离线]' : '';
|
|
195
|
+
var style = d._online === false ? ' style="color:#999;"' : '';
|
|
196
|
+
html += '<option value="' + key + '" data-typename="' + (d.typeName || '') + '"' + selected + style + '>' + (d.typeName || '设备') + ' (ID:' + d.deviceId + ')' + statusIcon + '</option>';
|
|
127
197
|
});
|
|
128
198
|
|
|
129
199
|
// 如果选中的设备不在列表中,添加它
|
|
130
|
-
if (selectedType && selectedId) {
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
if (!exists) {
|
|
135
|
-
var typeName = deviceTypes[selectedType] ? deviceTypes[selectedType].name : '类型' + selectedType;
|
|
136
|
-
html += '<option value="' + selectedKey + '" selected>' + typeName + ' (ID:' + selectedId + ')</option>';
|
|
137
|
-
}
|
|
200
|
+
if (selectedType && selectedId && !found) {
|
|
201
|
+
var typeName = savedTypeName || (deviceTypes[selectedType] ? deviceTypes[selectedType].name : '类型' + selectedType);
|
|
202
|
+
html += '<option value="' + selectedKey + '" selected style="color:#c00;">' + typeName + ' (ID:' + selectedId + ') [未找到]</option>';
|
|
138
203
|
}
|
|
139
204
|
|
|
140
205
|
return html;
|
|
@@ -142,7 +207,6 @@
|
|
|
142
207
|
|
|
143
208
|
// 构建品牌设备通道选项(灯具支持多路)
|
|
144
209
|
function getBrandChannelOptions(deviceType, selectedChannel) {
|
|
145
|
-
// 灯具(8)支持多路,其他设备单路
|
|
146
210
|
var channels = (parseInt(deviceType) === 8) ? 8 : 1;
|
|
147
211
|
if (channels <= 1) return '';
|
|
148
212
|
var html = '<select class="brand-channel">';
|
|
@@ -169,12 +233,12 @@
|
|
|
169
233
|
row.html(
|
|
170
234
|
'<div class="mapping-main">' +
|
|
171
235
|
'<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>' +
|
|
236
|
+
' <select class="mesh-select">' + getMeshOptions(m.meshMac, m.meshName) + '</select>' +
|
|
237
|
+
' <span class="mesh-ch-wrap">' + getMeshChannelOptions(m.meshMac, m.meshChannel || 1, m.meshChannels) + '</span>' +
|
|
174
238
|
'</div>' +
|
|
175
239
|
'<div class="arrow-col"><i class="fa fa-arrows-h"></i></div>' +
|
|
176
240
|
'<div class="brand-col">' +
|
|
177
|
-
' <select class="brand-select">' + getBrandOptions(m.brandDeviceType, m.brandDeviceId) + '</select>' +
|
|
241
|
+
' <select class="brand-select">' + getBrandOptions(m.brandDeviceType, m.brandDeviceId, m.brandTypeName) + '</select>' +
|
|
178
242
|
' <span class="brand-ch-wrap">' + getBrandChannelOptions(m.brandDeviceType, m.brandChannel || 1) + '</span>' +
|
|
179
243
|
'</div>' +
|
|
180
244
|
'<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>' +
|
|
@@ -194,9 +258,14 @@
|
|
|
194
258
|
var row = $(this).closest('.mapping-row');
|
|
195
259
|
var idx = row.data('idx');
|
|
196
260
|
var mac = $(this).val();
|
|
261
|
+
var opt = $(this).find('option:selected');
|
|
262
|
+
|
|
197
263
|
mappings[idx].meshMac = mac || '';
|
|
264
|
+
mappings[idx].meshName = opt.data('name') || opt.text().replace(' [离线]', '').replace(' [未找到]', '');
|
|
265
|
+
mappings[idx].meshChannels = parseInt(opt.data('channels')) || 1;
|
|
198
266
|
mappings[idx].meshChannel = 1;
|
|
199
|
-
|
|
267
|
+
|
|
268
|
+
row.find('.mesh-ch-wrap').html(getMeshChannelOptions(mac, 1, mappings[idx].meshChannels));
|
|
200
269
|
bindEvents();
|
|
201
270
|
});
|
|
202
271
|
|
|
@@ -209,17 +278,20 @@
|
|
|
209
278
|
var row = $(this).closest('.mapping-row');
|
|
210
279
|
var idx = row.data('idx');
|
|
211
280
|
var val = $(this).val();
|
|
281
|
+
var opt = $(this).find('option:selected');
|
|
282
|
+
|
|
212
283
|
if (val) {
|
|
213
284
|
var parts = val.split('_');
|
|
214
285
|
mappings[idx].brandDeviceType = parseInt(parts[0]) || 8;
|
|
215
286
|
mappings[idx].brandDeviceId = parseInt(parts[1]) || 1;
|
|
287
|
+
mappings[idx].brandTypeName = opt.data('typename') || opt.text().split(' (ID:')[0].replace(' [离线]', '').replace(' [未找到]', '');
|
|
216
288
|
mappings[idx].brandChannel = 1;
|
|
217
|
-
// 更新通道选择器
|
|
218
289
|
row.find('.brand-ch-wrap').html(getBrandChannelOptions(mappings[idx].brandDeviceType, 1));
|
|
219
290
|
bindEvents();
|
|
220
291
|
} else {
|
|
221
292
|
mappings[idx].brandDeviceType = null;
|
|
222
293
|
mappings[idx].brandDeviceId = null;
|
|
294
|
+
mappings[idx].brandTypeName = null;
|
|
223
295
|
mappings[idx].brandChannel = 1;
|
|
224
296
|
row.find('.brand-ch-wrap').empty();
|
|
225
297
|
}
|
|
@@ -239,12 +311,30 @@
|
|
|
239
311
|
|
|
240
312
|
// 刷新品牌设备
|
|
241
313
|
$('#refresh-brand-btn').on('click', function() {
|
|
242
|
-
|
|
314
|
+
var btn = $(this);
|
|
315
|
+
btn.prop('disabled', true).find('i').addClass('fa-spin');
|
|
316
|
+
loadBrandDevices(function() {
|
|
317
|
+
renderMappings();
|
|
318
|
+
btn.prop('disabled', false).find('i').removeClass('fa-spin');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// 刷新Mesh设备
|
|
323
|
+
$('#refresh-mesh-btn').on('click', function() {
|
|
324
|
+
var btn = $(this);
|
|
325
|
+
btn.prop('disabled', true).find('i').addClass('fa-spin');
|
|
326
|
+
loadMeshDevices(function() {
|
|
327
|
+
renderMappings();
|
|
328
|
+
btn.prop('disabled', false).find('i').removeClass('fa-spin');
|
|
329
|
+
});
|
|
243
330
|
});
|
|
244
331
|
|
|
245
332
|
// 添加映射按钮
|
|
246
333
|
$('#btn-add-mapping').on('click', function() {
|
|
247
|
-
mappings.push({
|
|
334
|
+
mappings.push({
|
|
335
|
+
meshMac: '', meshName: '', meshChannel: 1, meshChannels: 1,
|
|
336
|
+
brandDeviceType: 8, brandDeviceId: 1, brandTypeName: '灯具', brandChannel: 1
|
|
337
|
+
});
|
|
248
338
|
renderMappings();
|
|
249
339
|
});
|
|
250
340
|
|
|
@@ -261,10 +351,14 @@
|
|
|
261
351
|
}, 100);
|
|
262
352
|
});
|
|
263
353
|
|
|
264
|
-
//
|
|
265
|
-
node.
|
|
354
|
+
// 保存时更新所有数据
|
|
355
|
+
node._saveAll = function() {
|
|
266
356
|
node.mappings = JSON.stringify(mappings);
|
|
357
|
+
node.cachedMeshDevices = JSON.stringify(cachedMeshDevices);
|
|
358
|
+
node.cachedBrandDevices = JSON.stringify(cachedBrandDevices);
|
|
267
359
|
$('#node-input-mappings').val(node.mappings);
|
|
360
|
+
$('#node-input-cachedMeshDevices').val(node.cachedMeshDevices);
|
|
361
|
+
$('#node-input-cachedBrandDevices').val(node.cachedBrandDevices);
|
|
268
362
|
};
|
|
269
363
|
|
|
270
364
|
// 初始加载
|
|
@@ -282,7 +376,7 @@
|
|
|
282
376
|
$('#mapping-list').css('max-height', Math.max(150, height) + 'px');
|
|
283
377
|
},
|
|
284
378
|
oneditsave: function() {
|
|
285
|
-
if (this.
|
|
379
|
+
if (this._saveAll) { this._saveAll(); }
|
|
286
380
|
}
|
|
287
381
|
});
|
|
288
382
|
</script>
|
|
@@ -292,8 +386,10 @@
|
|
|
292
386
|
.bridge-section { margin: 12px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #fafafa; }
|
|
293
387
|
.bridge-section h4 { margin: 0 0 10px 0; padding-bottom: 6px; border-bottom: 1px solid #eee; color: #333; font-size: 13px; }
|
|
294
388
|
.bridge-section h4 i { margin-right: 6px; color: #666; }
|
|
389
|
+
.section-btns { float: right; }
|
|
390
|
+
.section-btns button { margin-left: 4px; }
|
|
295
391
|
|
|
296
|
-
#mapping-list { max-height:
|
|
392
|
+
#mapping-list { max-height: 600px; min-height: 400px; overflow-y: auto; }
|
|
297
393
|
.mapping-empty { padding: 15px; text-align: center; color: #999; font-size: 12px; }
|
|
298
394
|
.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
395
|
.mapping-main { display: flex; align-items: center; width: 100%; gap: 6px; min-width: 0; }
|
|
@@ -331,7 +427,10 @@
|
|
|
331
427
|
|
|
332
428
|
<div class="bridge-section">
|
|
333
429
|
<h4><i class="fa fa-exchange"></i> 实体映射(Mesh ↔ 品牌MQTT)
|
|
334
|
-
<
|
|
430
|
+
<span class="section-btns">
|
|
431
|
+
<button type="button" id="refresh-mesh-btn" class="red-ui-button red-ui-button-small" title="刷新Mesh设备"><i class="fa fa-refresh"></i> Mesh</button>
|
|
432
|
+
<button type="button" id="refresh-brand-btn" class="red-ui-button red-ui-button-small" title="刷新品牌设备"><i class="fa fa-refresh"></i> 品牌</button>
|
|
433
|
+
</span>
|
|
335
434
|
</h4>
|
|
336
435
|
<div style="display:flex; padding:4px 8px; font-size:11px; color:#666; border-bottom:1px solid #eee; margin-bottom:6px; gap:6px;">
|
|
337
436
|
<span style="flex:1 1 40%">Mesh设备/按键</span>
|
|
@@ -345,6 +444,8 @@
|
|
|
345
444
|
</div>
|
|
346
445
|
|
|
347
446
|
<input type="hidden" id="node-input-mappings">
|
|
447
|
+
<input type="hidden" id="node-input-cachedMeshDevices">
|
|
448
|
+
<input type="hidden" id="node-input-cachedBrandDevices">
|
|
348
449
|
</script>
|
|
349
450
|
|
|
350
451
|
<script type="text/html" data-help-name="symi-mqtt-sync">
|
|
@@ -354,6 +455,7 @@
|
|
|
354
455
|
<ul>
|
|
355
456
|
<li><strong>双MQTT配置节点</strong>:Mesh MQTT + 品牌MQTT独立配置</li>
|
|
356
457
|
<li><strong>设备自动发现</strong>:品牌MQTT连接后自动发现设备</li>
|
|
458
|
+
<li><strong>配置持久化</strong>:设备列表和映射配置持久保存,断线后仍可显示</li>
|
|
357
459
|
<li><strong>实体映射</strong>:左边选择Mesh设备,右边选择品牌设备</li>
|
|
358
460
|
<li><strong>双向同步</strong>:MQTT↔Mesh双向状态实时同步</li>
|
|
359
461
|
<li><strong>防死循环</strong>:内置2秒防抖机制</li>
|
|
@@ -367,9 +469,16 @@
|
|
|
367
469
|
<dt>品牌MQTT</dt>
|
|
368
470
|
<dd>选择品牌MQTT配置节点(如HYQW),用于获取品牌设备列表</dd>
|
|
369
471
|
<dt>实体映射</dt>
|
|
370
|
-
<dd>配置Mesh
|
|
472
|
+
<dd>配置Mesh设备与品牌设备的对应关系(配置会持久保存)</dd>
|
|
371
473
|
</dl>
|
|
372
474
|
|
|
475
|
+
<h3>离线设备显示</h3>
|
|
476
|
+
<p>当MQTT断开时,已配置的设备仍会显示在列表中:</p>
|
|
477
|
+
<ul>
|
|
478
|
+
<li><strong>[离线]</strong>:设备在缓存中但当前不在线</li>
|
|
479
|
+
<li><strong>[未找到]</strong>:设备既不在线也不在缓存中</li>
|
|
480
|
+
</ul>
|
|
481
|
+
|
|
373
482
|
<h3>支持的设备类型(HYQW)</h3>
|
|
374
483
|
<ul>
|
|
375
484
|
<li><strong>灯具(8)</strong>:开关、亮度</li>
|
package/nodes/symi-mqtt-sync.js
CHANGED
|
@@ -268,18 +268,25 @@ module.exports = function(RED) {
|
|
|
268
268
|
);
|
|
269
269
|
if (!mapping || !mapping.meshMac) return;
|
|
270
270
|
|
|
271
|
-
// 防死循环
|
|
272
|
-
const syncKey = `mqtt_${deviceType}_${deviceId}_${fn}`;
|
|
273
|
-
const lastSync = node._syncTimestamps.get(syncKey) || 0;
|
|
274
|
-
if (Date.now() - lastSync < SYNC_DEBOUNCE_MS) return;
|
|
275
|
-
|
|
276
271
|
const meshMac = mapping.meshMac;
|
|
277
272
|
const meshChannel = parseInt(mapping.meshChannel) || 1;
|
|
273
|
+
|
|
274
|
+
// 防死循环 - 检查两个方向的时间戳
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
const mqttSyncKey = `mqtt_${deviceType}_${deviceId}_${fn}`;
|
|
277
|
+
const meshSyncKey = `mesh_${meshMac}_${meshChannel}_${fn}`;
|
|
278
|
+
const lastMqttSync = node._syncTimestamps.get(mqttSyncKey) || 0;
|
|
279
|
+
const lastMeshSync = node._syncTimestamps.get(meshSyncKey) || 0;
|
|
280
|
+
|
|
281
|
+
// 如果任一方向在防抖时间内有同步,跳过
|
|
282
|
+
if (now - lastMqttSync < SYNC_DEBOUNCE_MS || now - lastMeshSync < SYNC_DEBOUNCE_MS) return;
|
|
283
|
+
|
|
278
284
|
const device = node._gateway.deviceManager.getDeviceByMac(meshMac);
|
|
279
285
|
if (!device) return;
|
|
280
286
|
|
|
281
|
-
|
|
282
|
-
node._syncTimestamps.set(
|
|
287
|
+
// 记录时间戳 - 同时标记两个方向
|
|
288
|
+
node._syncTimestamps.set(mqttSyncKey, now);
|
|
289
|
+
node._syncTimestamps.set(meshSyncKey, now);
|
|
283
290
|
|
|
284
291
|
const typeInfo = node._brandProtocol.deviceTypes[deviceType];
|
|
285
292
|
if (!typeInfo) return;
|
|
@@ -346,21 +353,42 @@ module.exports = function(RED) {
|
|
|
346
353
|
const typeInfo = node._brandProtocol.deviceTypes[deviceType];
|
|
347
354
|
if (!typeInfo) return;
|
|
348
355
|
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
const lastSync = node._syncTimestamps.get(syncKey) || 0;
|
|
352
|
-
if (Date.now() - lastSync < SYNC_DEBOUNCE_MS) return;
|
|
353
|
-
node._syncTimestamps.set(syncKey, Date.now());
|
|
354
|
-
|
|
355
|
-
let fn, fv;
|
|
356
|
+
// 根据属性确定功能码
|
|
357
|
+
let fn;
|
|
356
358
|
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
357
|
-
fn = 1;
|
|
359
|
+
fn = 1;
|
|
358
360
|
} else if (property === 'brightness' || property === 'temperature' || property === 'position') {
|
|
359
|
-
fn = 2;
|
|
361
|
+
fn = 2;
|
|
360
362
|
} else if (property === 'mode' || property === 'hvacMode') {
|
|
361
|
-
fn = 3;
|
|
363
|
+
fn = 3;
|
|
362
364
|
} else if (property === 'fanSpeed' || property === 'fanMode') {
|
|
363
365
|
fn = typeInfo.meshType === 'climate' ? 4 : 3;
|
|
366
|
+
} else {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 防死循环 - 检查两个方向的时间戳
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
const meshSyncKey = `mesh_${meshMac}_${channel}_${fn}`;
|
|
373
|
+
const mqttSyncKey = `mqtt_${deviceType}_${deviceId}_${fn}`;
|
|
374
|
+
const lastMeshSync = node._syncTimestamps.get(meshSyncKey) || 0;
|
|
375
|
+
const lastMqttSync = node._syncTimestamps.get(mqttSyncKey) || 0;
|
|
376
|
+
|
|
377
|
+
// 如果任一方向在防抖时间内有同步,跳过
|
|
378
|
+
if (now - lastMeshSync < SYNC_DEBOUNCE_MS || now - lastMqttSync < SYNC_DEBOUNCE_MS) return;
|
|
379
|
+
|
|
380
|
+
// 记录时间戳 - 同时标记两个方向
|
|
381
|
+
node._syncTimestamps.set(meshSyncKey, now);
|
|
382
|
+
node._syncTimestamps.set(mqttSyncKey, now);
|
|
383
|
+
|
|
384
|
+
let fv;
|
|
385
|
+
if (property === 'switch' || property === 'on' || property === 'isOn') {
|
|
386
|
+
fv = value ? 1 : 0;
|
|
387
|
+
} else if (property === 'brightness' || property === 'temperature' || property === 'position') {
|
|
388
|
+
fv = parseInt(value) || 0;
|
|
389
|
+
} else if (property === 'mode' || property === 'hvacMode') {
|
|
390
|
+
fv = AC_MODE_REVERSE[value] ?? 0;
|
|
391
|
+
} else if (property === 'fanSpeed' || property === 'fanMode') {
|
|
364
392
|
fv = FAN_SPEED_REVERSE[value] ?? 0;
|
|
365
393
|
} else {
|
|
366
394
|
return;
|
|
@@ -371,7 +399,6 @@ module.exports = function(RED) {
|
|
|
371
399
|
|
|
372
400
|
try {
|
|
373
401
|
node._mqttClient.publish(topic, payload, { qos: 0 });
|
|
374
|
-
node._syncTimestamps.set(`mqtt_${deviceType}_${deviceId}_${fn}`, Date.now());
|
|
375
402
|
|
|
376
403
|
// 输出到debug
|
|
377
404
|
node.send({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-mesh",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.8",
|
|
4
4
|
"description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
|
|
5
5
|
"main": "nodes/symi-gateway.js",
|
|
6
6
|
"scripts": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"rs485-debug": "nodes/rs485-debug.js",
|
|
38
38
|
"symi-knx-bridge": "nodes/symi-knx-bridge.js",
|
|
39
39
|
"symi-knx-ha-bridge": "nodes/symi-knx-ha-bridge.js",
|
|
40
|
+
"symi-ha-sync": "nodes/symi-ha-sync.js",
|
|
40
41
|
"symi-rs485-sync": "nodes/symi-rs485-sync.js",
|
|
41
42
|
"symi-mqtt-sync": "nodes/symi-mqtt-sync.js",
|
|
42
43
|
"symi-mqtt-brand": "nodes/symi-mqtt-brand.js"
|