node-red-contrib-symi-mesh 1.8.2 → 1.8.3

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
@@ -393,8 +393,8 @@ Symi HA同步节点 (`symi-ha-sync`) 用于实现Symi蓝牙Mesh设备与Home Ass
393
393
  - 复用`symi-mqtt`配置节点获取Symi设备信息
394
394
  - 复用`server`配置节点连接Home Assistant
395
395
  2. **双向同步**:
396
- - **Symi -> HA**:Mesh设备状态变化 -> 更新HA实体状态
397
- - **HA -> Symi**:HA实体状态变化 -> 控制Mesh设备
396
+ - **Symi -> HA**:Mesh设备状态变化 -> 更新HA实体状态(自动工作)
397
+ - **HA -> Symi**:HA实体状态变化 -> 控制Mesh设备(需连接HA事件节点)
398
398
  3. **防死循环**:
399
399
  - 内置2秒冷却机制(窗帘30秒)
400
400
  - 区分Symi触发和HA触发,避免信号震荡
@@ -403,6 +403,37 @@ Symi HA同步节点 (`symi-ha-sync`) 用于实现Symi蓝牙Mesh设备与Home Ass
403
403
  - 支持按键通道选择(1-4键)
404
404
  - 支持实体ID搜索和下拉选择
405
405
 
406
+ #### ⚠️ 双向同步连接方式(重要)
407
+
408
+ 要实现完整的双向同步,必须连接HA事件节点到`symi-ha-sync`节点的输入端:
409
+
410
+ **方式1:使用 server-events 节点(推荐)**
411
+ ```
412
+ [events: all] → [symi-ha-sync]
413
+ (事件类型: state_changed)
414
+ ```
415
+
416
+ 配置步骤:
417
+ 1. 添加 `events: all` 节点(Home Assistant 分类下)
418
+ 2. 事件类型(Event Type)填写: `state_changed`
419
+ 3. 将输出连接到 symi-ha-sync 节点的输入端
420
+
421
+ **方式2:使用 server-state-changed 节点**
422
+ ```
423
+ [events: state] → [symi-ha-sync]
424
+ ```
425
+
426
+ 配置步骤:
427
+ 1. 添加 `events: state` 节点
428
+ 2. 实体ID可留空(监听所有实体)或指定特定实体
429
+ 3. 将输出连接到 symi-ha-sync 节点的输入端
430
+
431
+ #### 状态指示
432
+
433
+ - **蓝色 "Mesh→HA (N组)"**:仅 Mesh 到 HA 方向工作(未连接 HA 事件节点)
434
+ - **绿色 "双向同步 (N组)"**:双向同步正常工作
435
+ - **红色**:配置错误
436
+
406
437
  #### 配置步骤
407
438
 
408
439
  1. **添加节点**:从左侧拖入`Symi HA Sync`节点
@@ -413,14 +444,17 @@ Symi HA同步节点 (`symi-ha-sync`) 用于实现Symi蓝牙Mesh设备与Home Ass
413
444
  - **Symi设备**:下拉选择Mesh设备(显示名称和MAC)
414
445
  - **按键**:选择控制通道(按键1-4)
415
446
  - **HA实体**:输入或选择要同步的HA实体ID(如`switch.living_room_light`)
416
- 5. **部署**:点击部署,立即生效
447
+ 5. **连接HA事件节点**:添加`events: all`或`events: state`节点,连接到输入端
448
+ 6. **部署**:点击部署,立即生效
417
449
 
418
450
  #### 注意事项
419
451
 
420
452
  - **MQTT配置**:必须选择`symi-mqtt`配置节点,用于获取设备列表和接收Mesh事件
421
453
  - **HA连接**:必须确保HA服务器节点连接正常
454
+ - **HA事件节点**:必须连接HA事件节点才能实现HA→Symi方向同步
422
455
  - **实体类型**:建议同步相同类型的实体(如开关对开关,调光灯对灯光)
423
456
  - **多通道设备**:对于多键开关,请分别为每个按键添加一条映射
457
+ - **event实体过滤**:系统自动过滤`event.`开头的实体(如按键点击事件),不会同步
424
458
 
425
459
  #### 三合一面板配置
426
460
 
@@ -1501,6 +1535,28 @@ node-red-contrib-symi-mesh/
1501
1535
  - 无TODO/FIXME/HACK标记
1502
1536
  - 代码无重复,模块化设计
1503
1537
 
1538
+ ## 更新日志
1539
+
1540
+ ### v1.8.3 (2026-01-05)
1541
+
1542
+ **HA同步节点重大修复**:
1543
+ - 修复HA→Symi方向不工作的问题(input监听器未正确注册)
1544
+ - 支持多种HA节点消息格式(server-events、server-state-changed、trigger:state等)
1545
+ - 自动过滤event类型实体(如按键点击事件),避免无效同步
1546
+ - 优化状态显示:蓝色"Mesh→HA"表示单向,绿色"双向同步"表示双向正常
1547
+ - 精简日志输出,只记录映射中的实体状态变化
1548
+ - 更新HTML帮助文档,详细说明双向同步连接方式
1549
+ - 更新README文档,添加完整的HA同步配置指南
1550
+
1551
+ **连接方式说明**:
1552
+ - Symi→HA:自动工作,无需额外配置
1553
+ - HA→Symi:需连接`events: all`或`events: state`节点到symi-ha-sync输入端
1554
+
1555
+ ### v1.8.2 (2025-12-xx)
1556
+
1557
+ - 窗帘和调光灯智能防抖,只同步最终状态
1558
+ - 三合一面板完整双向同步支持
1559
+
1504
1560
  ## 技术支持
1505
1561
 
1506
1562
  如遇问题,请提供以下信息:
@@ -1584,6 +1640,21 @@ node-red-contrib-symi-mesh/
1584
1640
 
1585
1641
  ## 更新日志
1586
1642
 
1643
+ ### v1.8.3 (2026-01-05)
1644
+
1645
+ **HA同步节点重大修复**:
1646
+ - 修复HA→Symi方向不工作的问题(input监听器未正确注册)
1647
+ - 支持多种HA节点消息格式(server-events、server-state-changed、trigger:state等)
1648
+ - 自动过滤event类型实体(如按键点击事件),避免无效同步
1649
+ - 优化状态显示:蓝色"Mesh→HA"表示单向,绿色"双向同步"表示双向正常
1650
+ - 精简日志输出,只记录映射中的实体状态变化
1651
+ - 更新HTML帮助文档,详细说明双向同步连接方式
1652
+ - 更新README文档,添加完整的HA同步配置指南
1653
+
1654
+ **连接方式说明**:
1655
+ - Symi→HA:自动工作,无需额外配置
1656
+ - HA→Symi:需连接`events: all`或`events: state`节点到symi-ha-sync输入端
1657
+
1587
1658
  ### v1.8.2 (2026-01-05)
1588
1659
  - **MQTT品牌同步协议修复**:
1589
1660
  - **窗帘设备协议修复**:修复窗帘fn=1功能码的正确解析(0=关闭, 1=打开, 2=停止),之前错误地当作开关处理
@@ -1626,62 +1697,6 @@ node-red-contrib-symi-mesh/
1626
1697
  - 优化了双向同步的防死循环逻辑,减少了在高频触发场景下的 CPU 占用。
1627
1698
  - 修复了 MQTT 配置下拉框在节点编辑面板打开时偶尔出现的加载卡顿问题。
1628
1699
 
1629
- ### v1.7.9 (2026-01-05)
1630
- - **HA同步节点UI修复**:修复添加映射按钮不显示选择界面的问题
1631
- - 修复`renderMappings()`函数中的数组检查逻辑
1632
- - 修复`mergeDevices()`函数中的空值检查
1633
- - 使用`entityType`字段判断设备类型(climate/cover/light不显示按键选择)
1634
- - 添加设备类型标签显示:[温控器]、[窗帘]、[灯具]、[N路开关]、[单路开关]
1635
- - 添加错误捕获和日志输出,便于调试
1636
-
1637
- ### v1.7.8 (2026-01-05)
1638
- - **配置持久化增强**:所有同步节点的设备列表和映射配置持久保存
1639
- - **MQTT同步节点**:Mesh设备和品牌设备列表持久化,断线后仍可显示已配置的映射
1640
- - **HA同步节点**:Symi设备和HA实体列表持久化,断线后仍可显示已配置的映射
1641
- - **RS485桥接节点**:Mesh设备列表持久化,断线后仍可显示已配置的映射
1642
- - **离线设备显示**:[离线]标记缓存中但当前不在线的设备,[未找到]标记不在缓存中的设备
1643
- - **刷新按钮**:各节点添加独立刷新按钮,可手动刷新设备列表
1644
- - **MQTT品牌同步增强**:完整对接HYQW(花语前湾)MQTT协议
1645
- - **完整设备类型支持**:灯具(8)、空调(12)、窗帘(14)、地暖(16)、新风(36)
1646
- - **功能码映射**:完整的fn/fv到Mesh属性双向转换
1647
- - 灯具:开关(fn=1)、亮度(fn=2, 0-100)
1648
- - 空调:开关(fn=1)、温度(fn=2, 18-29°C)、模式(fn=3)、风速(fn=4)
1649
- - 窗帘:动作(fn=1, 开/关/停)、位置(fn=2, 0-100%)
1650
- - 地暖:开关(fn=1)、温度(fn=2, 5-35°C)
1651
- - 新风:开关(fn=1)、风速(fn=3)
1652
- - **可扩展架构**:BRAND_PROTOCOLS对象支持添加新品牌协议
1653
- - **双向状态同步**:HYQW↔Mesh实时同步,2秒防抖防死循环
1654
- - **自动设备发现**:自动发现品牌MQTT设备,限制200个
1655
- - **错误日志限流**:每60秒最多记录一次错误,避免日志爆炸
1656
- - **自动重连**:5秒重连间隔,断线自动恢复
1657
- - **资源清理**:完善的定时器和事件监听器清理机制
1658
- - **HA同步节点完整重构**:实现所有实体类型的完美双向同步
1659
- - **完整实体类型支持**:
1660
- - switch/input_boolean:开关状态 (on/off)
1661
- - light:开关 + 亮度 (0-255 ↔ 0-100)
1662
- - cover:开/关/停 + 位置 (0-100%)
1663
- - climate:开关 + 温度 + 模式(cool/heat/fan_only/dry) + 风速(high/medium/low/auto)
1664
- - fan:开关 + 风速
1665
- - **Mesh属性完整映射**:
1666
- - 0x02 开关状态(单路/多路)
1667
- - 0x03 亮度 (0-100)
1668
- - 0x05 窗帘运行状态
1669
- - 0x06 窗帘位置 (0-100)
1670
- - 0x1B 目标温度 (16-30°C)
1671
- - 0x1C 风速 (1=高/2=中/3=低/4=自动)
1672
- - 0x1D 空调模式 (1=制冷/2=制热/3=送风/4=除湿)
1673
- - **智能按键选择**:只有多路开关才显示按键选择,温控器/窗帘/调光灯显示"-"
1674
- - **设备类型标签**:自动识别并显示设备类型([温控器]、[窗帘]、[调光灯]、[N路开关])
1675
- - **智能防抖机制**:窗帘和调光灯使用500ms防抖,只同步最终位置/亮度
1676
- - **防死循环增强**:2秒冷却时间 + 双向时间戳检查
1677
- - **配置持久化**:设备列表和映射配置持久保存
1678
- - **内存泄漏防护**:定时清理过期时间戳和防抖定时器
1679
- - **代码质量优化**:
1680
- - 同步时间戳Map自动清理60秒以上的条目
1681
- - 设备发现数量限制,防止内存溢出
1682
- - 完善的节点关闭清理逻辑
1683
- - 防死循环机制增强:双向时间戳检查
1684
-
1685
1700
  ## 许可证
1686
1701
 
1687
1702
  MIT License
@@ -1693,7 +1708,7 @@ Copyright (c) 2025 SYMI 亖米
1693
1708
  ## 关于
1694
1709
 
1695
1710
  **作者**: SYMI 亖米
1696
- **版本**: 1.8.2
1711
+ **版本**: 1.8.3
1697
1712
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
1698
1713
  **最后更新**: 2026-01-05
1699
1714
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
@@ -477,11 +477,44 @@
477
477
  <h3>功能特性</h3>
478
478
  <ul>
479
479
  <li><strong>完美双向同步</strong>:Symi↔HA实时状态同步</li>
480
- <li><strong>多设备类型支持</strong>:开关、调光灯、窗帘、温控器/空调</li>
480
+ <li><strong>多设备类型支持</strong>:开关、调光灯、窗帘、温控器/空调、新风、地暖</li>
481
481
  <li><strong>智能按键选择</strong>:只有多路开关才显示按键选择</li>
482
482
  <li><strong>配置持久化</strong>:设备列表和映射配置持久保存</li>
483
483
  <li><strong>防死循环</strong>:内置2秒防抖机制</li>
484
- <li><strong>智能防抖</strong>:窗帘/调光灯只同步最终位置</li>
484
+ <li><strong>智能防抖</strong>:窗帘/调光灯只同步最终位置,过程状态不同步</li>
485
+ </ul>
486
+
487
+ <h3>⚠️ 双向同步连接方式(重要)</h3>
488
+ <p>要实现 HA→Symi 方向同步,必须连接 HA 事件节点到本节点输入端:</p>
489
+
490
+ <h4>方式1:使用 server-events 节点(推荐)</h4>
491
+ <pre>
492
+ [server-events] → [symi-ha-sync]
493
+ (事件类型: state_changed)
494
+ </pre>
495
+ <p><strong>配置步骤:</strong></p>
496
+ <ol>
497
+ <li>添加 <code>events: all</code> 节点(Home Assistant 分类下)</li>
498
+ <li>事件类型(Event Type)填写: <code>state_changed</code></li>
499
+ <li>将输出连接到 symi-ha-sync 节点的输入端</li>
500
+ </ol>
501
+
502
+ <h4>方式2:使用 server-state-changed 节点</h4>
503
+ <pre>
504
+ [server-state-changed] → [symi-ha-sync]
505
+ </pre>
506
+ <p><strong>配置步骤:</strong></p>
507
+ <ol>
508
+ <li>添加 <code>events: state</code> 节点</li>
509
+ <li>实体ID可留空(监听所有实体)或指定特定实体</li>
510
+ <li>将输出连接到 symi-ha-sync 节点的输入端</li>
511
+ </ol>
512
+
513
+ <h3>状态指示</h3>
514
+ <ul>
515
+ <li><strong>蓝色 "Mesh→HA"</strong>:仅 Mesh 到 HA 方向工作(未连接 HA 事件节点)</li>
516
+ <li><strong>绿色 "双向同步"</strong>:双向同步正常工作</li>
517
+ <li><strong>红色</strong>:配置错误</li>
485
518
  </ul>
486
519
 
487
520
  <h3>支持的设备类型</h3>
@@ -490,6 +523,15 @@
490
523
  <li><strong>调光灯</strong>:开关 + 亮度 (0-100)</li>
491
524
  <li><strong>窗帘</strong>:开/关/停 + 位置 (0-100%)</li>
492
525
  <li><strong>温控器/空调</strong>:开关 + 温度 + 模式 + 风速</li>
526
+ <li><strong>三合一面板</strong>:空调 + 新风 + 地暖,分别配置</li>
527
+ </ul>
528
+
529
+ <h3>三合一面板配置</h3>
530
+ <p>三合一面板需要分别为每个子设备创建映射:</p>
531
+ <ul>
532
+ <li><strong>空调</strong>:选择子设备"空调",映射到 climate 实体</li>
533
+ <li><strong>新风</strong>:选择子设备"新风",映射到 fan 实体</li>
534
+ <li><strong>地暖</strong>:选择子设备"地暖",映射到 climate 实体</li>
493
535
  </ul>
494
536
 
495
537
  <h3>离线设备显示</h3>
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Symi HA Sync Node - Symi设备与Home Assistant实体双向同步
3
- * 版本: 1.8.2
3
+ * 版本: 1.8.3
4
4
  *
5
5
  * 支持的实体类型和属性:
6
6
  * - light: on/off, brightness (0-255)
@@ -96,28 +96,48 @@ module.exports = function(RED) {
96
96
  node.pendingDebounce = {}; // 防抖定时器
97
97
  node.coverMoving = {}; // 窗帘运动状态跟踪 { loopKey: { direction: 'symi'|'ha', startTime, targetPosition } }
98
98
  node.brightnessMoving = {}; // 调光运动状态跟踪 { loopKey: { direction: 'ha', startTime } }
99
+ node.haInputReceived = false; // 是否收到过HA输入
99
100
 
100
101
  node.status({ fill: 'yellow', shape: 'ring', text: '初始化中' });
102
+
103
+ // 更新状态显示的辅助函数
104
+ node.updateStatus = function() {
105
+ if (configError) {
106
+ node.status({ fill: 'red', shape: 'ring', text: configError });
107
+ } else if (node.haInputReceived) {
108
+ node.status({ fill: 'green', shape: 'dot', text: `双向同步 (${node.mappings.length}组)` });
109
+ } else if (gateway) {
110
+ node.status({ fill: 'blue', shape: 'dot', text: `Mesh→HA (${node.mappings.length}组)` });
111
+ } else {
112
+ node.status({ fill: 'yellow', shape: 'ring', text: '等待连接' });
113
+ }
114
+ };
101
115
 
102
- // 检查配置
116
+ // 检查配置 - 但不要直接return,允许input监听器注册
117
+ let gateway = null;
118
+ let configError = null;
119
+
103
120
  if (!node.mqttNode) {
104
- node.status({ fill: 'red', shape: 'ring', text: '未配置MQTT节点' });
105
- return;
106
- }
107
- if (!node.haServer) {
108
- node.status({ fill: 'red', shape: 'ring', text: '未配置HA服务器' });
109
- return;
121
+ configError = '未配置MQTT节点';
122
+ } else if (!node.haServer) {
123
+ configError = '未配置HA服务器';
124
+ } else {
125
+ // 获取Gateway引用
126
+ gateway = node.mqttNode.gateway;
127
+ if (!gateway) {
128
+ configError = 'MQTT节点未关联网关';
129
+ }
110
130
  }
111
-
112
- // 获取Gateway引用
113
- const gateway = node.mqttNode.gateway;
114
- if (!gateway) {
115
- node.status({ fill: 'red', shape: 'ring', text: 'MQTT节点未关联网关' });
116
- return;
131
+
132
+ if (configError) {
133
+ node.status({ fill: 'red', shape: 'ring', text: configError });
134
+ node.warn(`[HA同步] ${configError}`);
135
+ // 不要return,继续注册input监听器
136
+ } else {
137
+ // 初始状态:只有Mesh→HA方向,等待HA输入连接
138
+ node.updateStatus();
117
139
  }
118
140
 
119
- node.status({ fill: 'green', shape: 'dot', text: `运行中 (${node.mappings.length}组)` });
120
-
121
141
  // 定期清理过期的防抖时间戳
122
142
  node.cleanupInterval = setInterval(function() {
123
143
  const now = Date.now();
@@ -145,6 +165,16 @@ module.exports = function(RED) {
145
165
  delete node.brightnessMoving[key];
146
166
  }
147
167
  }
168
+
169
+ // 尝试重新获取gateway(如果之前没有)
170
+ if (!gateway && node.mqttNode && node.mqttNode.gateway) {
171
+ gateway = node.mqttNode.gateway;
172
+ if (gateway) {
173
+ node.log('[HA同步] 网关已就绪,注册设备状态监听');
174
+ gateway.on('device-state-changed', node.handleSymiStateChange);
175
+ node.status({ fill: 'green', shape: 'dot', text: `运行中 (${node.mappings.length}组)` });
176
+ }
177
+ }
148
178
  }, CLEANUP_INTERVAL_MS);
149
179
 
150
180
  // 防死循环检查 - 双向时间戳检查
@@ -510,74 +540,157 @@ module.exports = function(RED) {
510
540
  return { type: 'hvac_mode', value: haMode, meshValue: mode };
511
541
  };
512
542
 
513
- // 监听网关事件
514
- gateway.on('device-state-changed', node.handleSymiStateChange);
543
+ // 监听网关事件(如果gateway可用)
544
+ if (gateway) {
545
+ gateway.on('device-state-changed', node.handleSymiStateChange);
546
+ }
515
547
 
516
548
  // ========== 2. 监听HA状态变化 (HA -> Symi) ==========
517
549
 
518
- // 方式A: 通过Input输入 (server-state-changed节点)
550
+ // 方式A: 通过Input输入 (支持多种HA节点格式)
519
551
  node.on('input', function(msg) {
520
- if (msg.payload && (msg.payload.entity_id || (msg.data && msg.data.entity_id))) {
521
- const entityId = msg.payload.entity_id || msg.data.entity_id;
522
- const newState = msg.payload.new_state || msg.data.new_state;
523
- const oldState = msg.payload.old_state || msg.data.old_state;
524
-
525
- if (entityId && newState) {
526
- node.handleHaStateChange(entityId, newState, oldState);
552
+ // 首次收到HA输入时更新状态
553
+ if (!node.haInputReceived) {
554
+ node.haInputReceived = true;
555
+ node.updateStatus();
556
+ node.log('[HA同步] 已收到HA输入,双向同步已启用');
557
+ }
558
+
559
+ // 支持多种消息格式
560
+ let entityId, newState, oldState;
561
+
562
+ // 格式1: server-events 节点格式 (msg.payload.event_type + msg.payload.event)
563
+ // 实际格式: { event_type: "state_changed", entity_id: "...", event: { entity_id, new_state, old_state } }
564
+ if (msg.payload && msg.payload.event_type === 'state_changed') {
565
+ entityId = msg.payload.entity_id;
566
+ // event 直接包含 new_state 和 old_state(不是 event.data)
567
+ if (msg.payload.event) {
568
+ newState = msg.payload.event.new_state;
569
+ oldState = msg.payload.event.old_state;
527
570
  }
528
- } else if (msg.data && msg.data.entity_id && msg.data.new_state) {
529
- // 兼容旧格式
530
- node.handleHaStateChange(msg.data.entity_id, msg.data.new_state, msg.data.old_state);
531
571
  }
572
+ // 格式2: server-state-changed 节点的标准格式 (msg.data)
573
+ else if (msg.data && msg.data.entity_id) {
574
+ entityId = msg.data.entity_id;
575
+ newState = msg.data.new_state;
576
+ oldState = msg.data.old_state;
577
+ }
578
+ // 格式3: payload 中直接包含 entity_id 和 new_state
579
+ else if (msg.payload && msg.payload.entity_id && msg.payload.new_state) {
580
+ entityId = msg.payload.entity_id;
581
+ newState = msg.payload.new_state;
582
+ oldState = msg.payload.old_state;
583
+ }
584
+ // 格式4: 直接的 event 数据 (msg.event)
585
+ else if (msg.event && msg.event.new_state) {
586
+ entityId = msg.event.entity_id;
587
+ newState = msg.event.new_state;
588
+ oldState = msg.event.old_state;
589
+ }
590
+ // 格式5: trigger: state 节点格式 (msg.payload 是状态对象)
591
+ else if (msg.payload && typeof msg.payload === 'object' &&
592
+ msg.payload.state !== undefined && msg.payload.attributes !== undefined) {
593
+ entityId = msg.topic || msg.payload.entity_id;
594
+ newState = msg.payload;
595
+ oldState = null;
596
+ }
597
+
598
+ // 过滤非 state_changed 事件和无效数据
599
+ if (!entityId || !newState) {
600
+ return; // 静默忽略无法解析的消息
601
+ }
602
+
603
+ // 过滤 event 类型的实体(如 event.xxx_click_e_xxx),这些不需要同步
604
+ if (entityId.startsWith('event.')) {
605
+ return;
606
+ }
607
+
608
+ // 检查是否在映射列表中
609
+ const mappings = node.findMappingsByHa(entityId);
610
+ if (mappings.length === 0) {
611
+ return; // 不在映射中的实体静默忽略
612
+ }
613
+
614
+ // 只记录映射中的实体状态变化
615
+ node.log(`[HA->Symi] ${entityId}: ${oldState?.state || 'null'} -> ${newState.state}`);
616
+ node.handleHaStateChange(entityId, newState, oldState);
532
617
  });
533
618
 
534
- // 方式B: 尝试订阅HA Server事件总线 (静默订阅机制)
619
+ // 方式B: 尝试订阅HA Server事件总线 (备用方式,大多数情况不可用)
535
620
  const subscribeToHaEvents = () => {
536
621
  if (!node.haServer) {
622
+ // 静默处理,不显示警告
537
623
  return;
538
624
  }
539
625
 
540
- // node-red-contrib-home-assistant-websocket 不同版本可能暴露不同的事件总线
541
- let eventBus = node.haServer.eventBus;
542
- if (!eventBus && node.haServer.controller) {
626
+ // 尝试多种方式获取事件总线
627
+ let eventBus = null;
628
+
629
+ // 方式1: 直接访问 eventBus
630
+ if (node.haServer.eventBus) {
631
+ eventBus = node.haServer.eventBus;
632
+ }
633
+ // 方式2: 通过 controller
634
+ else if (node.haServer.controller && node.haServer.controller.events) {
543
635
  eventBus = node.haServer.controller.events;
544
636
  }
637
+ // 方式3: 通过 websocket
638
+ else if (node.haServer.websocket && node.haServer.websocket.eventBus) {
639
+ eventBus = node.haServer.websocket.eventBus;
640
+ }
641
+ // 方式4: 检查 _events
642
+ else if (node.haServer._events) {
643
+ eventBus = node.haServer;
644
+ }
545
645
 
546
- if (eventBus) {
646
+ if (eventBus && typeof eventBus.on === 'function') {
547
647
  // 防止重复订阅
548
648
  if (node.haSubscribed) return;
549
649
 
550
650
  node.haEventHandler = (evt) => {
551
651
  if (evt && evt.event_type === 'state_changed' && evt.data) {
652
+ if (!node.haInputReceived) {
653
+ node.haInputReceived = true;
654
+ node.updateStatus();
655
+ }
552
656
  node.handleHaStateChange(evt.data.entity_id, evt.data.new_state, evt.data.old_state);
553
657
  }
554
658
  };
555
- eventBus.on('ha_events:all', node.haEventHandler);
556
- node.haSubscribed = true;
557
- node.log('[HA同步] 已成功订阅HA事件总线');
558
- node.status({ fill: 'green', shape: 'dot', text: '已连接' });
559
- } else {
560
- // 静默重试,不打印 info 日志,只在后台运行
561
- node.haSubscribedRetryCount = (node.haSubscribedRetryCount || 0) + 1;
562
659
 
563
- // 每 5 秒检查一次,直到成功为止(不设上限,因为这是后台静默检查)
564
- // 只有在前 3 次且是 debug 模式下才打印
565
- if (node.haSubscribedRetryCount <= 3) {
566
- node.debug(`[HA同步] 等待 HA 事件总线就绪... (${node.haSubscribedRetryCount})`);
660
+ // 尝试订阅不同的事件名
661
+ try {
662
+ eventBus.on('ha_events:all', node.haEventHandler);
663
+ node.haSubscribed = true;
664
+ node.haInputReceived = true;
665
+ node.log('[HA同步] 已成功订阅HA事件总线');
666
+ node.updateStatus();
667
+ } catch (e1) {
668
+ try {
669
+ eventBus.on('state_changed', node.haEventHandler);
670
+ node.haSubscribed = true;
671
+ node.haInputReceived = true;
672
+ node.log('[HA同步] 已成功订阅HA事件总线');
673
+ node.updateStatus();
674
+ } catch (e2) {
675
+ // 静默失败
676
+ }
567
677
  }
568
-
569
- setTimeout(subscribeToHaEvents, 5000);
570
678
  }
679
+ // 事件总线不可用时不再重试,依赖 input 方式
571
680
  };
572
681
 
573
- // 启动后立即开始静默尝试
574
- subscribeToHaEvents();
682
+ // 延迟启动订阅尝试,给HA服务器时间初始化
683
+ setTimeout(subscribeToHaEvents, 3000);
575
684
 
576
685
  node.handleHaStateChange = function(entityId, newState, oldState) {
577
- if (!newState) return;
686
+ if (!newState) {
687
+ return;
688
+ }
578
689
 
579
690
  const mappings = node.findMappingsByHa(entityId);
580
- if (mappings.length === 0) return;
691
+ if (mappings.length === 0) {
692
+ return;
693
+ }
581
694
 
582
695
  const domain = node.getEntityDomain(entityId);
583
696
  const attrs = newState.attributes || {};
@@ -869,22 +982,6 @@ module.exports = function(RED) {
869
982
 
870
983
  node.log(`[Symi->HA] ${mapping.symiName || mapping.symiMac} -> ${mapping.haEntityId}: ${syncData.type}=${JSON.stringify(syncData.value)}`);
871
984
 
872
- // 仅输出到debug,不再重复send
873
- /*
874
- node.send({
875
- topic: 'ha-sync/symi-to-ha',
876
- payload: {
877
- direction: 'Symi→HA',
878
- symiMac: mapping.symiMac,
879
- symiKey: mapping.symiKey,
880
- haEntityId: mapping.haEntityId,
881
- syncType: syncData.type,
882
- value: syncData.value,
883
- timestamp: Date.now()
884
- }
885
- });
886
- */
887
-
888
985
  } catch (err) {
889
986
  node.error(`[Symi->HA] 调用失败: ${err.message}`);
890
987
  }
@@ -895,7 +992,14 @@ module.exports = function(RED) {
895
992
  const { mapping, syncData, key } = cmd;
896
993
  node.recordSyncTime('ha-to-symi', key);
897
994
 
898
- const device = gateway.getDevice(mapping.symiMac);
995
+ // 动态获取gateway(可能在初始化后才可用)
996
+ const currentGateway = gateway || (node.mqttNode && node.mqttNode.gateway);
997
+ if (!currentGateway) {
998
+ node.warn(`[HA->Symi] 网关未就绪,无法控制设备`);
999
+ return;
1000
+ }
1001
+
1002
+ const device = currentGateway.getDevice(mapping.symiMac);
899
1003
  if (!device) {
900
1004
  node.warn(`[HA->Symi] 设备未找到: ${mapping.symiMac}`);
901
1005
  return;
@@ -961,7 +1065,7 @@ module.exports = function(RED) {
961
1065
  return;
962
1066
  }
963
1067
 
964
- await gateway.sendControl(networkAddr, attrType, param);
1068
+ await currentGateway.sendControl(networkAddr, attrType, param);
965
1069
 
966
1070
  node.log(`[HA->Symi] ${mapping.haEntityId} -> ${device.name || mapping.symiMac}: ${syncData.type}=${JSON.stringify(syncData.value)}`);
967
1071
 
@@ -975,6 +1079,13 @@ module.exports = function(RED) {
975
1079
  const { mapping, syncData } = cmd;
976
1080
  const subType = mapping.symiKey;
977
1081
 
1082
+ // 动态获取gateway
1083
+ const currentGateway = gateway || (node.mqttNode && node.mqttNode.gateway);
1084
+ if (!currentGateway) {
1085
+ node.warn(`[HA->Symi] 网关未就绪,无法控制三合一设备`);
1086
+ return;
1087
+ }
1088
+
978
1089
  try {
979
1090
  let attrType, param;
980
1091
 
@@ -1030,7 +1141,7 @@ module.exports = function(RED) {
1030
1141
  }
1031
1142
 
1032
1143
  if (attrType && param) {
1033
- await gateway.sendControl(networkAddr, attrType, param);
1144
+ await currentGateway.sendControl(networkAddr, attrType, param);
1034
1145
  node.log(`[HA->Symi] ${mapping.haEntityId} -> ${mapping.symiName}(${subType}): ${syncData.type}=${JSON.stringify(syncData.value)}`);
1035
1146
  }
1036
1147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-symi-mesh",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "description": "Node-RED节点集合,用于通过TCP/串口连接Symi蓝牙Mesh网关,支持Home Assistant MQTT Discovery自动发现和云端数据同步",
5
5
  "main": "nodes/symi-gateway.js",
6
6
  "scripts": {