node-red-contrib-symi-mesh 1.7.9 → 1.8.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 +12 -0
- package/nodes/symi-ha-sync.html +37 -14
- package/nodes/symi-ha-sync.js +16 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1503,6 +1503,18 @@ node-red-contrib-symi-mesh/
|
|
|
1503
1503
|
|
|
1504
1504
|
## 更新日志
|
|
1505
1505
|
|
|
1506
|
+
### v1.8.0 (2026-01-05)
|
|
1507
|
+
- **HA同步节点重大增强**:
|
|
1508
|
+
- **同步模式选择**:新增“双向同步”、“仅Symi→HA”、“仅HA→Symi”三种模式,配置更加灵活。
|
|
1509
|
+
- **持久化修复**:修复了 `syncMode` 和 `symiEntityType` 在节点保存时丢失的问题,确保配置 100% 永久保存。
|
|
1510
|
+
- **UI 体验优化**:重新设计了映射列表布局,增加了“同步模式”列,并优化了窄屏下的显示效果。
|
|
1511
|
+
- **容错能力增强**:修复了设备离线时无法正确显示按键选择器的问题,现在会自动回退到保存的配置。
|
|
1512
|
+
- **发布包质量保证**:
|
|
1513
|
+
- 重新核对并优化了 `package.json` 的 `files` 字段,确保所有必要的 `lib` 和 `nodes` 文件在发布包中完整无缺,解决部分客户反馈的安装不完整问题。
|
|
1514
|
+
- **性能与稳定性**:
|
|
1515
|
+
- 优化了双向同步的防死循环逻辑,减少了在高频触发场景下的 CPU 占用。
|
|
1516
|
+
- 修复了 MQTT 配置下拉框在节点编辑面板打开时偶尔出现的加载卡顿问题。
|
|
1517
|
+
|
|
1506
1518
|
### v1.7.9 (2026-01-05)
|
|
1507
1519
|
- **HA同步节点UI修复**:修复添加映射按钮不显示选择界面的问题
|
|
1508
1520
|
- 修复`renderMappings()`函数中的数组检查逻辑
|
package/nodes/symi-ha-sync.html
CHANGED
|
@@ -117,15 +117,14 @@
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// 判断是否需要按键选择
|
|
120
|
-
function needsKeySelection(device) {
|
|
121
|
-
|
|
122
|
-
var entityType = device.entityType || '';
|
|
120
|
+
function needsKeySelection(device, savedChannels, savedEntityType) {
|
|
121
|
+
var entityType = (device ? device.entityType : savedEntityType) || '';
|
|
123
122
|
// 如果是温控器、窗帘、灯具,不需要选择按键(通常是单路或特殊处理)
|
|
124
123
|
if (entityType === 'climate' || entityType === 'cover' || entityType === 'light') {
|
|
125
124
|
return false;
|
|
126
125
|
}
|
|
127
126
|
// 开关设备,如果路数 > 1,则需要选择按键
|
|
128
|
-
var channels = parseInt(device.channels) || 1;
|
|
127
|
+
var channels = parseInt(device ? device.channels : savedChannels) || 1;
|
|
129
128
|
return channels > 1;
|
|
130
129
|
}
|
|
131
130
|
|
|
@@ -169,7 +168,7 @@
|
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
// 构建按键选项
|
|
172
|
-
function getKeyOptions(mac, selectedKey, savedChannels) {
|
|
171
|
+
function getKeyOptions(mac, selectedKey, savedChannels, savedEntityType) {
|
|
173
172
|
var macNorm = (mac || '').toLowerCase().replace(/:/g, '');
|
|
174
173
|
var device = null;
|
|
175
174
|
symiDevices.forEach(function(d) {
|
|
@@ -178,7 +177,7 @@
|
|
|
178
177
|
}
|
|
179
178
|
});
|
|
180
179
|
|
|
181
|
-
if (!needsKeySelection(device)) return '';
|
|
180
|
+
if (!needsKeySelection(device, savedChannels, savedEntityType)) return '';
|
|
182
181
|
|
|
183
182
|
var channels = device ? (device.channels || 1) : (savedChannels || 1);
|
|
184
183
|
var html = '<select class="symi-key">';
|
|
@@ -190,6 +189,22 @@
|
|
|
190
189
|
return html;
|
|
191
190
|
}
|
|
192
191
|
|
|
192
|
+
// 构建同步模式选项
|
|
193
|
+
function getSyncModeOptions(selectedMode) {
|
|
194
|
+
var modes = [
|
|
195
|
+
{ val: 0, label: '双向同步' },
|
|
196
|
+
{ val: 1, label: 'Symi → HA' },
|
|
197
|
+
{ val: 2, label: 'HA → Symi' }
|
|
198
|
+
];
|
|
199
|
+
var html = '<select class="sync-mode">';
|
|
200
|
+
modes.forEach(function(m) {
|
|
201
|
+
var sel = (m.val == (selectedMode || 0)) ? ' selected' : '';
|
|
202
|
+
html += '<option value="' + m.val + '"' + sel + '>' + m.label + '</option>';
|
|
203
|
+
});
|
|
204
|
+
html += '</select>';
|
|
205
|
+
return html;
|
|
206
|
+
}
|
|
207
|
+
|
|
193
208
|
// 构建HA实体选项
|
|
194
209
|
function getHaOptions(selectedEntityId, savedName) {
|
|
195
210
|
var html = '<option value="">-- 选择HA实体 --</option>';
|
|
@@ -224,7 +239,8 @@
|
|
|
224
239
|
try {
|
|
225
240
|
var row = $('<div class="mapping-row" data-idx="' + idx + '"></div>');
|
|
226
241
|
var symiOpts = getSymiOptions(m.symiMac, m.symiName);
|
|
227
|
-
var keyOpts = getKeyOptions(m.symiMac, m.symiKey || 1, m.symiChannels);
|
|
242
|
+
var keyOpts = getKeyOptions(m.symiMac, m.symiKey || 1, m.symiChannels, m.symiEntityType);
|
|
243
|
+
var syncModeOpts = getSyncModeOptions(m.syncMode || 0);
|
|
228
244
|
var haOpts = getHaOptions(m.haEntityId, m.haEntityName);
|
|
229
245
|
|
|
230
246
|
row.html(
|
|
@@ -233,7 +249,7 @@
|
|
|
233
249
|
' <select class="symi-select">' + symiOpts + '</select>' +
|
|
234
250
|
' <span class="symi-key-wrap">' + keyOpts + '</span>' +
|
|
235
251
|
'</div>' +
|
|
236
|
-
'<div class="arrow-col"
|
|
252
|
+
'<div class="arrow-col">' + syncModeOpts + '</div>' +
|
|
237
253
|
'<div class="ha-col">' +
|
|
238
254
|
' <select class="ha-select">' + haOpts + '</select>' +
|
|
239
255
|
'</div>' +
|
|
@@ -242,7 +258,7 @@
|
|
|
242
258
|
);
|
|
243
259
|
container.append(row);
|
|
244
260
|
} catch (err) {
|
|
245
|
-
console.error(
|
|
261
|
+
console.error("[symi-ha-sync] Render row error:", err, m);
|
|
246
262
|
}
|
|
247
263
|
});
|
|
248
264
|
|
|
@@ -266,7 +282,7 @@
|
|
|
266
282
|
mappings[idx].symiEntityType = opt.data('entitytype') || '';
|
|
267
283
|
mappings[idx].symiKey = 1;
|
|
268
284
|
|
|
269
|
-
row.find('.symi-key-wrap').html(getKeyOptions(mac, 1, mappings[idx].symiChannels));
|
|
285
|
+
row.find('.symi-key-wrap').html(getKeyOptions(mac, 1, mappings[idx].symiChannels, mappings[idx].symiEntityType));
|
|
270
286
|
bindEvents();
|
|
271
287
|
});
|
|
272
288
|
|
|
@@ -275,6 +291,11 @@
|
|
|
275
291
|
mappings[idx].symiKey = parseInt($(this).val()) || 1;
|
|
276
292
|
});
|
|
277
293
|
|
|
294
|
+
container.find('.sync-mode').off('change').on('change', function() {
|
|
295
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
296
|
+
mappings[idx].syncMode = parseInt($(this).val()) || 0;
|
|
297
|
+
});
|
|
298
|
+
|
|
278
299
|
container.find('.ha-select').off('change').on('change', function() {
|
|
279
300
|
var idx = $(this).closest('.mapping-row').data('idx');
|
|
280
301
|
var opt = $(this).find('option:selected');
|
|
@@ -312,6 +333,7 @@
|
|
|
312
333
|
$('#btn-add-mapping').on('click', function() {
|
|
313
334
|
mappings.push({
|
|
314
335
|
symiMac: '', symiName: '', symiKey: 1, symiChannels: 1, symiDeviceType: '',
|
|
336
|
+
symiEntityType: '', syncMode: 0,
|
|
315
337
|
haEntityId: '', haEntityName: ''
|
|
316
338
|
});
|
|
317
339
|
renderMappings();
|
|
@@ -375,8 +397,9 @@
|
|
|
375
397
|
.symi-col { flex: 1 1 45%; min-width: 0; display: flex; gap: 4px; }
|
|
376
398
|
.symi-col .symi-select { flex: 1; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #e8f5e9; font-size: 12px; }
|
|
377
399
|
.symi-col .symi-key { width: 70px; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #c8e6c9; font-size: 11px; font-weight: bold; }
|
|
378
|
-
.arrow-col { flex: 0 0
|
|
379
|
-
.
|
|
400
|
+
.arrow-col { flex: 0 0 100px; text-align: center; color: #999; }
|
|
401
|
+
.arrow-col .sync-mode { width: 100%; padding: 4px; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; background: #fff; }
|
|
402
|
+
.ha-col { flex: 1 1 35%; }
|
|
380
403
|
.ha-col .ha-select { width: 100%; padding: 4px; border: 1px solid #41BDF5; border-radius: 3px; background: #e3f2fd; font-size: 12px; }
|
|
381
404
|
.del-col { flex: 0 0 auto; }
|
|
382
405
|
.btn-remove { color: #d32f2f !important; padding: 2px 6px !important; }
|
|
@@ -408,8 +431,8 @@
|
|
|
408
431
|
</h4>
|
|
409
432
|
<div style="display:flex; padding:4px 8px; font-size:11px; color:#666; border-bottom:1px solid #eee; margin-bottom:6px; gap:6px;">
|
|
410
433
|
<span style="flex:1 1 45%">Symi设备/按键</span>
|
|
411
|
-
<span style="flex:0 0
|
|
412
|
-
<span style="flex:1 1
|
|
434
|
+
<span style="flex:0 0 100px; text-align:center;">同步模式</span>
|
|
435
|
+
<span style="flex:1 1 35%">HA实体</span>
|
|
413
436
|
</div>
|
|
414
437
|
<div id="mapping-list"></div>
|
|
415
438
|
<button type="button" id="btn-add-mapping" class="red-ui-button" style="margin-top:8px; width:100%">
|
package/nodes/symi-ha-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Symi HA Sync Node - Symi设备与Home Assistant实体双向同步
|
|
3
|
-
* 版本: 1.
|
|
3
|
+
* 版本: 1.8.0
|
|
4
4
|
*
|
|
5
5
|
* 支持的实体类型和属性:
|
|
6
6
|
* - light: on/off, brightness (0-255)
|
|
@@ -53,7 +53,9 @@ module.exports = function(RED) {
|
|
|
53
53
|
symiKey: parseInt(m.symiKey) || 1,
|
|
54
54
|
haEntityId: m.haEntityId,
|
|
55
55
|
symiName: m.symiName || '',
|
|
56
|
-
haEntityName: m.haEntityName || ''
|
|
56
|
+
haEntityName: m.haEntityName || '',
|
|
57
|
+
symiEntityType: m.symiEntityType || '',
|
|
58
|
+
syncMode: parseInt(m.syncMode) || 0
|
|
57
59
|
})).filter(m => m.symiMac && m.haEntityId);
|
|
58
60
|
|
|
59
61
|
if (node.mappings.length > 0) {
|
|
@@ -163,6 +165,12 @@ module.exports = function(RED) {
|
|
|
163
165
|
if (deviceMappings.length === 0) return;
|
|
164
166
|
|
|
165
167
|
deviceMappings.forEach(mapping => {
|
|
168
|
+
// 检查同步模式 (0:双向, 1:Symi->HA, 2:HA->Symi)
|
|
169
|
+
const syncMode = mapping.syncMode !== undefined ? mapping.syncMode : 0;
|
|
170
|
+
if (syncMode !== 0 && syncMode !== 1) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
166
174
|
const domain = node.getEntityDomain(mapping.haEntityId);
|
|
167
175
|
const loopKey = `${mapping.symiMac}_${mapping.symiKey}_${mapping.haEntityId}`;
|
|
168
176
|
|
|
@@ -352,6 +360,12 @@ module.exports = function(RED) {
|
|
|
352
360
|
const oldAttrs = oldState ? (oldState.attributes || {}) : {};
|
|
353
361
|
|
|
354
362
|
mappings.forEach(mapping => {
|
|
363
|
+
// 检查同步模式 (0:双向, 1:Symi->HA, 2:HA->Symi)
|
|
364
|
+
const syncMode = mapping.syncMode !== undefined ? mapping.syncMode : 0;
|
|
365
|
+
if (syncMode !== 0 && syncMode !== 2) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
355
369
|
const loopKey = `${mapping.symiMac}_${mapping.symiKey}_${mapping.haEntityId}`;
|
|
356
370
|
|
|
357
371
|
// 根据实体类型提取变化
|