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 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()`函数中的数组检查逻辑
@@ -117,15 +117,14 @@
117
117
  }
118
118
 
119
119
  // 判断是否需要按键选择
120
- function needsKeySelection(device) {
121
- if (!device) return false;
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"><i class="fa fa-arrows-h"></i></div>' +
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('[symi-ha-sync] Render row error:', err, m);
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 20px; text-align: center; color: #999; }
379
- .ha-col { flex: 1 1 45%; }
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 20px"></span>
412
- <span style="flex:1 1 45%">HA实体</span>
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%">
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Symi HA Sync Node - Symi设备与Home Assistant实体双向同步
3
- * 版本: 1.7.9
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
  // 根据实体类型提取变化
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.7.9",
3
+ "version": "1.8.0",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {