node-red-contrib-symi-mesh 1.2.3 → 1.3.1

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
@@ -15,6 +15,7 @@
15
15
  - **多设备类型**:支持开关、灯光、窗帘、温控器、传感器等12+种设备
16
16
  - **三合一面板**:完整支持空调+新风+地暖三合一控制面板
17
17
  - **KNX集成**:支持与KNX系统双向同步
18
+ - **云端同步**:从酒店云云平台自动获取设备名称和场景信息
18
19
  - **稳定可靠**:完善的错误处理和自动重连机制
19
20
 
20
21
  ## 快速开始
@@ -48,22 +49,31 @@ node-red-restart
48
49
 
49
50
  添加"Symi Gateway"配置节点:
50
51
 
51
- **TCP模式**(推荐):
52
- - 主机地址: 网关IP(如 192.168.2.110)
53
- - 端口: 4196(默认)
52
+ **网关连接配置**:
53
+ - **连接方式**: TCP/IP 或 串口
54
+ - **TCP模式**(推荐):
55
+ - 主机地址: 网关IP(如 192.168.2.110)
56
+ - 端口: 4196(默认)
57
+ - **串口模式**:
58
+ - 串口路径: /dev/ttyUSB0
59
+ - 波特率: 115200
54
60
 
55
- **串口模式**:
56
- - 串口路径: /dev/ttyUSB0
57
- - 波特率: 115200
61
+ **MQTT配置**(用于Home Assistant集成):
62
+ - **MQTT地址**: mqtt://localhost:1883(默认,可修改)
63
+ - **用户名**: MQTT认证用户名(可选)
64
+ - **密码**: MQTT认证密码(可选)
65
+ - **HA前缀**: homeassistant(默认,可修改)
58
66
 
59
- ### 3. 配置MQTT节点
67
+ > **提示**: MQTT配置已集成到网关节点中,所有使用该网关的MQTT节点会自动使用这些配置。
60
68
 
61
- 添加"Symi MQTT"节点:
69
+ ### 3. 添加MQTT桥接节点
70
+
71
+ 添加"Symi MQTT"节点到流程中:
62
72
 
63
73
  - **网关**: 选择已配置的网关节点
64
- - **MQTT Broker**: mqtt://localhost:1883
65
- - **用户名/密码**: MQTT认证信息(可选)
66
- - **Discovery前缀**: homeassistant(默认)
74
+ - **名称**: 可选,用于标识节点
75
+
76
+ > **注意**: MQTT连接参数从网关节点获取,无需重复配置。
67
77
 
68
78
  ### 4. 部署并验证
69
79
 
@@ -146,6 +156,8 @@ node-red-restart
146
156
  | 0x92 | 设备列表响应 | 网关→主机 | 返回设备信息 |
147
157
  | 0x30 | 设备控制 | 主机→网关 | 控制指定设备 |
148
158
  | 0xB0 | 控制响应 | 网关→主机 | 控制结果 |
159
+ | 0x34 | 场景控制 | 主机→网关 | 执行场景(群控) |
160
+ | 0xB4 | 场景控制响应 | 网关→主机 | 场景控制结果 |
149
161
  | 0x80 | 状态事件 | 网关→主机 | 设备状态主动上报 |
150
162
  | 0x32 | 状态查询 | 主机→网关 | 查询设备状态 |
151
163
 
@@ -177,6 +189,7 @@ node-red-restart
177
189
  | 目标温度 | 0x1B | 2字节LE | 16-30°C, *100 |
178
190
  | 风速 | 0x1C | 1字节 | 1=高/2=中/3=低/4=自动 |
179
191
  | 模式 | 0x1D | 1字节 | 1=制冷/2=制热/3=送风/4=除湿 |
192
+ | 6键开关状态 | 0x45 | 2字节LE | 场景执行后状态上报 |
180
193
 
181
194
  ## MQTT主题结构
182
195
 
@@ -395,6 +408,209 @@ return {
395
408
  4. **双向同步**:避免循环控制,使用`outputRBE: true`防抖
396
409
  5. **测试验证**:先测试单向同步,确认正常后再启用双向
397
410
 
411
+ ## 云端数据同步
412
+
413
+ 本节点支持从酒店云云平台自动获取设备名称和场景信息,实现本地化配置的自动同步。
414
+
415
+ ### 使用场景
416
+
417
+ - **酒店项目**:自动同步房间设备的本地化名称(如"客厅窗帘"、"床头灯"等)
418
+ - **场景管理**:获取云端配置的场景列表(如"全关"、"睡眠模式"等)并本地化调用
419
+ - **批量部署**:统一管理多个房间的设备名称,无需手动配置
420
+
421
+ ### 配置步骤
422
+
423
+ #### 1. 添加云端同步节点
424
+
425
+ 在Node-RED中添加"Symi Cloud Sync"节点:
426
+
427
+ **基础配置**:
428
+ - **网关**:选择已配置的Symi Gateway节点
429
+ - **MQTT节点**:选择Symi MQTT节点(用于更新Discovery配置)
430
+
431
+ **云端认证**:
432
+ - **App ID**:`cee70459`(默认已填写)
433
+ - **App Secret**:`a719a4b58e2e57e50d758ee3762507e7`(默认已填写)
434
+ - 点击"测试连接"按钮验证认证信息
435
+
436
+ **房间配置**:
437
+ - **酒店ID**:输入酒店ID或点击"加载酒店列表"自动获取
438
+ - **房间号**:输入房间号(如:1001)
439
+ - **房间UUID**:可选,如果有UUID可直接填写
440
+
441
+ **同步选项**:
442
+ - **自动同步**:勾选后,启动时自动从云端获取数据(推荐)
443
+
444
+ #### 2. 部署并验证
445
+
446
+ 1. 点击右上角"部署"按钮
447
+ 2. 检查调试面板确认同步状态:
448
+ - `开始从云端获取数据` - 开始同步
449
+ - `云端数据获取成功: X个设备, Y个场景` - 同步成功
450
+ - `设备名称已更新: 旧名称 -> 新名称` - 名称更新
451
+ - `重新发布MQTT Discovery配置` - 更新HA配置
452
+ 3. 在Home Assistant中查看设备名称是否已更新
453
+
454
+ #### 3. 手动触发同步
455
+
456
+ 如需手动触发同步,可以使用inject节点:
457
+
458
+ ```
459
+ [inject] --> [cloud-sync]
460
+ payload: "sync"
461
+ ```
462
+
463
+ #### 4. 调用场景
464
+
465
+ 云端同步后,可以通过inject节点调用场景:
466
+
467
+ ```
468
+ [inject] --> [cloud-sync]
469
+ payload: { scene_id: 4 } // 执行场景ID为4的场景(如"全关")
470
+ ```
471
+
472
+ ### 工作原理
473
+
474
+ #### 数据获取流程
475
+
476
+ ```
477
+ 启动/部署
478
+
479
+ 调用云端API(房间信息接口)
480
+
481
+ 解析设备列表和场景列表
482
+
483
+ 根据MAC地址匹配本地设备
484
+
485
+ 更新设备名称(device.name)
486
+
487
+ 保存场景列表到本地
488
+
489
+ 重新发布MQTT Discovery配置
490
+
491
+ 持久化存储(缓存)
492
+
493
+ 完成(不再调用)
494
+ ```
495
+
496
+ #### MAC地址匹配规则
497
+
498
+ 云端返回的设备MAC地址会自动反转后与本地设备MAC地址匹配:
499
+
500
+ ```javascript
501
+ // 云端设备(MAC地址为反序存储)
502
+ {
503
+ "device_name": "窗帘",
504
+ "nick_name": "客厅窗帘",
505
+ "mac": "14d881c086d4", // 云端反序存储
506
+ "naddr": 256
507
+ }
508
+
509
+ // 本地设备匹配
510
+ 1. 将云端MAC转换为小写并去除分隔符: "14d881c086d4"
511
+ 2. 反转MAC地址(每2位反转): "d486c081d814"
512
+ 3. 与本地设备MAC进行精确匹配
513
+ 4. 匹配成功后更新 device.name = "客厅窗帘"
514
+ 5. 如果云端名称为"未命名"或空,则保留设备类型标准名称(如"智能窗帘")
515
+ 6. 重新发布MQTT Discovery(使用新名称)
516
+ ```
517
+
518
+ #### 场景本地化调用
519
+
520
+ 云端场景信息会保存到本地,可以通过网关直接调用:
521
+
522
+ ```javascript
523
+ // 云端场景
524
+ {
525
+ "scene_name": "全关",
526
+ "scene_id": 4
527
+ }
528
+
529
+ // 本地调用
530
+ 通过inject节点发送: { scene_id: 4 }
531
+ 节点会查找场景名称并输出到下游节点
532
+ 可以连接到其他节点进行进一步处理
533
+ ```
534
+
535
+ ### 数据持久化
536
+
537
+ 云端同步节点会将获取的数据持久化存储到Node-RED的context中:
538
+
539
+ - **存储内容**:设备列表、场景列表、最后同步时间
540
+ - **存储位置**:节点的context存储(重启后保留)
541
+ - **使用场景**:
542
+ - 云端API调用失败时,自动使用缓存数据
543
+ - 禁用自动同步后,仍可使用上次获取的数据
544
+ - 网络故障时,不影响已同步的设备名称
545
+
546
+ ### 常见问题
547
+
548
+ #### Q: 云端同步会影响性能吗?
549
+
550
+ A: 不会。云端同步仅在启动/部署时执行一次,不会持续轮询。同步完成后,节点处于空闲状态,不占用资源。
551
+
552
+ #### Q: 如果云端API调用失败怎么办?
553
+
554
+ A: 节点会自动使用上次成功获取的缓存数据。如果从未成功获取过,设备将保持原有名称。
555
+
556
+ #### Q: 可以禁用云端同步节点吗?
557
+
558
+ A: 可以。禁用节点不会影响其他功能(网关、MQTT、设备控制)的正常运行。已同步的设备名称会保留。
559
+
560
+ #### Q: 云端名称和本地设备如何匹配?
561
+
562
+ A: 通过MAC地址精确匹配。云端返回的设备MAC地址会自动反转后与本地设备MAC匹配(因为云端存储为反序)。如果云端名称为"未命名"或空,则自动保留设备类型标准名称(如"智能窗帘"、"零火开关"等)。
563
+
564
+ #### Q: 场景如何调用?
565
+
566
+ A: 云端同步后,场景会自动发布为Home Assistant按钮实体。在HA中点击场景按钮即可触发本地场景执行(使用0x34协议,不经过云端)。场景执行后,所有相关设备的状态会自动同步到HA,确保状态一致性。也可以通过inject节点发送 `{ scene_id: X }` 来调用场景。
567
+
568
+ #### Q: 设备列表中的勾选框是做什么用的?
569
+
570
+ A: 勾选的设备会同步云端名称到本地,未勾选的设备保持原有名称不变。如果不勾选任何设备,则默认同步所有设备。场景勾选同理,勾选的场景会发布到Home Assistant。支持一键全选功能。
571
+
572
+ #### Q: 开关设备的按键名称如何显示?
573
+
574
+ A: 对于多键开关(1-6键),云端同步节点会显示每个按键的名称(如"床头灯"、"睡眠"等),这些名称来自云端的sub_device配置,并会同步到MQTT实体名称中。
575
+
576
+ #### Q: MQTT配置在哪里?
577
+
578
+ A: MQTT配置统一在**网关节点**中配置。所有使用该网关的节点(包括云端同步节点)都会自动使用网关中配置的MQTT客户端,无需重复配置。
579
+
580
+ #### Q: HA中的实体名称格式是什么?
581
+
582
+ A: HA实体名称为纯名称,不带MAC地址和ID。例如"客厅灯 床头灯"而不是"客厅灯_14d881c086d4_1"。MAC地址仅用作unique_id确保实体唯一性。
583
+
584
+ #### Q: 多个房间如何管理?
585
+
586
+ A: 每个房间部署一个云端同步节点,配置对应的酒店ID和房间号即可。
587
+
588
+ ### 故障排除
589
+
590
+ **症状**: 云端同步失败
591
+
592
+ **解决方法**:
593
+ 1. 检查App ID和App Secret是否正确
594
+ 2. 点击"测试连接"按钮验证认证信息
595
+ 3. 检查网络连接是否正常
596
+ 4. 查看调试日志中的详细错误信息
597
+
598
+ **症状**: 设备名称未更新
599
+
600
+ **解决方法**:
601
+ 1. 检查云端设备MAC地址是否与本地设备一致
602
+ 2. 查看调试日志中的"设备匹配完成"信息
603
+ 3. 确认MQTT节点已正确配置
604
+ 4. 手动触发同步(发送 `msg.payload = "sync"`)
605
+
606
+ **症状**: 场景无法调用
607
+
608
+ **解决方法**:
609
+ 1. 检查云端是否返回了场景列表
610
+ 2. 查看调试日志中的"场景列表已保存"信息
611
+ 3. 确认场景ID是否正确
612
+ 4. 检查网关连接是否正常
613
+
398
614
  ## 故障排除
399
615
 
400
616
  ### 网关连接失败
@@ -457,6 +673,7 @@ return {
457
673
  配置节点,管理与Symi蓝牙Mesh网关的连接。
458
674
 
459
675
  **配置项**:
676
+ - MQTT配置:选择MQTT配置节点(用于Home Assistant Discovery)
460
677
  - 连接类型:TCP或串口
461
678
  - TCP模式:IP地址和端口(默认4196)
462
679
  - 串口模式:串口路径和波特率(默认115200)
@@ -494,6 +711,51 @@ return {
494
711
  - 发送控制命令
495
712
  - 支持KNX双向同步
496
713
 
714
+ ### Symi Cloud Sync(云端同步节点)
715
+
716
+ 从酒店云云平台同步设备名称和场景信息到本地。
717
+
718
+ **配置项**:
719
+ - 网关:选择已配置的网关节点(自动使用网关中配置的MQTT)
720
+ - App ID / App Secret:云端API认证信息(已预填)
721
+ - 酒店ID / 房间号:指定要同步的房间(支持下拉选择或手动输入)
722
+ - 设备列表:显示云端设备列表(含按键名称),支持一键全选
723
+ - 场景列表:显示云端场景列表,支持一键全选
724
+ - 自动同步:启动时自动从云端获取数据
725
+
726
+ **功能**:
727
+ - 启动时自动从云端获取设备名称和场景列表
728
+ - 根据MAC地址精确匹配本地设备并更新名称(云端MAC地址自动反转匹配)
729
+ - 支持开关设备的按键名称同步(1-6键的sub_device名称)
730
+ - 云端"未命名"设备自动保留设备类型标准名称(如"智能窗帘"、"零火开关"等)
731
+ - 酒店和房间选择自动持久化保存,重新打开配置无需重新选择
732
+ - 自动重新发布MQTT Discovery配置(使用云端名称)
733
+ - HA实体名称为纯名称,不带MAC地址和ID
734
+ - 将选中的场景发布为Home Assistant按钮实体
735
+ - 在HA中点击场景按钮即可触发本地场景执行(使用0x31协议)
736
+ - 场景触发通过事件机制路由,确保可靠执行
737
+ - 数据持久化存储,获取失败时使用缓存
738
+ - 支持手动触发同步(发送 `msg.payload = "sync"`)
739
+ - 支持场景调用(发送 `msg.payload = { scene_id: 4 }`)
740
+
741
+ **使用场景**:
742
+ - 酒店项目:自动同步房间设备的本地化名称
743
+ - 场景管理:将云端场景发布到Home Assistant进行控制
744
+ - 批量部署:统一管理多个房间的设备名称和场景
745
+
746
+ **注意事项**:
747
+ - 仅在启动/部署时获取一次,不会持续轮询
748
+ - 云端MAC地址自动反转匹配本地设备(云端存储为反序)
749
+ - 云端"未命名"设备自动保留设备类型标准名称
750
+ - 酒店和房间选择自动持久化保存,重新打开配置无需重新选择
751
+ - 勾选的设备和场景才会同步,未勾选则同步全部(支持一键全选)
752
+ - 场景按钮发布到HA后,点击即可触发本地场景执行(0x34协议)
753
+ - 场景执行是本地化控制,不经过云端
754
+ - 场景执行后,所有相关设备状态自动同步到HA(队列处理,确保可靠)
755
+ - 按键名称会同步到MQTT实体名称,MAC地址仍作为唯一ID
756
+ - 获取失败时自动使用上次缓存的数据
757
+ - 可以禁用节点,不影响其他功能正常运行
758
+
497
759
  ## 开发者信息
498
760
 
499
761
  ### 项目结构
@@ -563,15 +825,48 @@ node-red-contrib-symi-mesh/
563
825
 
564
826
  ## 更新日志
565
827
 
566
- ### v1.2.3 (2025-10-28)
567
- - 修复三合一设备新风方向状态同步问题
568
- - 优化日志输出,提升系统性能
569
- - 完善Home Assistant MQTT Discovery兼容性
570
-
571
- ### v1.2.0 (2025-10-27)
572
- - 完整支持三合一设备(空调+新风+地暖)
573
- - 新增MQTT Discovery自动发现功能
574
- - 优化设备识别和状态同步机制
828
+ ### v1.3.1 (2025-11-07)
829
+ - **重要修复**:场景控制协议修正(从0x31改为0x34,符合最新协议规范)
830
+ - **重要修复**:场景执行状态同步队列处理(确保场景触发后所有设备状态正确同步)
831
+ - **重要修复**:6键开关状态同步(新增0x45消息类型支持)
832
+ - **性能优化**:状态事件队列化处理,避免高频状态更新时的CPU占用
833
+ - **稳定性提升**:场景执行2秒超时机制,确保状态同步窗口准确
834
+ - **日志优化**:场景执行期间的状态更新使用log级别,便于调试和验证
835
+ - 场景控制帧格式:`53 34 06 BC C0 00 05 01 [场景ID] [校验]`
836
+ - 场景执行响应:`53 B4 00 00 E7`(网关确认)
837
+ - 场景执行通知:`53 80 11 ...`(设备执行通知)
838
+ - 支持场景执行后的批量设备状态同步(队列处理,非阻塞)
839
+ - 6键开关场景状态同步(0x45消息类型,2字节小端序)
840
+ - 所有设备类型的场景状态同步验证通过
841
+
842
+ ### v1.3.0 (2025-11-06)
843
+ - **新功能**:新增云端同步节点(Symi Cloud Sync)
844
+ - **重要修复**:MAC地址反序处理(修复设备匹配问题)
845
+ - **重要修复**:场景触发事件路由机制(修复场景无法执行问题)
846
+ - **重要修复**:设备控制日志级别(从debug改为log,便于调试)
847
+ - **重要修复**:酒店和房间选择持久化保存(重新打开配置无需重新选择)
848
+ - **重要修复**:云端"未命名"设备自动保留设备类型标准名称
849
+ - **架构优化**:MQTT配置统一到网关节点,简化配置流程
850
+ - **节点优化**:Symi MQTT改为config node,不再显示在左侧面板
851
+ - 支持从酒店云云平台自动获取设备名称和场景信息
852
+ - 云端MAC地址自动反转匹配本地设备(云端存储为反序)
853
+ - 支持开关设备的按键名称同步(sub_device)
854
+ - 显示云端设备列表(所有设备都有名称)
855
+ - 自动重新发布MQTT Discovery配置(使用云端名称)
856
+ - HA实体名称为纯名称,不带MAC地址和ID
857
+ - 场景自动发布为Home Assistant按钮实体
858
+ - 支持在HA中直接触发场景(本地化控制)
859
+ - 场景触发通过事件机制路由,确保可靠执行
860
+ - 可选择性同步设备和场景(勾选功能,支持一键全选)
861
+ - 数据持久化存储,获取失败时使用缓存
862
+ - 仅在启动时获取一次,不影响系统性能
863
+
864
+ ### v1.2.4 (2025-11-04)
865
+ - **重大优化**:大幅减少日志输出,解决长期运行日志膨胀问题
866
+ - 将高频日志(Frame接收/发送、状态事件、MQTT消息等)改为debug级别
867
+ - 保留关键日志(连接/断开、设备发现、错误)用于问题排查
868
+ - 提升系统性能和稳定性,适合7x24小时长期运行
869
+ - 修复网关或MQTT服务器离线时的错误处理
575
870
 
576
871
 
577
872
  ## 许可证
@@ -585,8 +880,8 @@ Copyright (c) 2025 SYMI 亖米
585
880
  ## 关于
586
881
 
587
882
  **作者**: SYMI 亖米
588
- **版本**: 1.2.3
883
+ **版本**: 1.3.1
589
884
  **协议**: 蓝牙MESH网关(初级版)串口协议V1.0
590
- **最后更新**: 2025-10-28
885
+ **最后更新**: 2025-11-07
591
886
  **仓库**: https://github.com/symi-daguo/node-red-contrib-symi-mesh
592
887
  **npm包**: https://www.npmjs.com/package/node-red-contrib-symi-mesh
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Cloud API Client for Symi Hotel Cloud Platform
3
+ * 酒店云云接口封装
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+
8
+ class CloudAPI {
9
+ constructor(appId, appSecret, logger = console) {
10
+ this.baseUrl = 'https://hotel.beancomm.net/hapi/open/';
11
+ this.appId = appId;
12
+ this.appSecret = appSecret;
13
+ this.logger = logger;
14
+ }
15
+
16
+ generateSignature(body, timestamp) {
17
+ const raw = body + timestamp + this.appSecret;
18
+ return crypto.createHash('md5').update(raw).digest('hex');
19
+ }
20
+
21
+ async request(endpoint, data = {}) {
22
+ const timestamp = Math.floor(Date.now() / 1000).toString();
23
+ const body = JSON.stringify(data);
24
+ const signature = this.generateSignature(body, timestamp);
25
+
26
+ const url = this.baseUrl + endpoint;
27
+ const headers = {
28
+ 'Content-Type': 'application/json',
29
+ 'X-Beancomm-Timestamp': timestamp,
30
+ 'X-Beancomm-AppId': this.appId,
31
+ 'X-Beancomm-Signature': signature
32
+ };
33
+
34
+ try {
35
+ const axios = require('axios');
36
+ const response = await axios.post(url, body, {
37
+ headers,
38
+ timeout: 10000
39
+ });
40
+
41
+ if (response.data.code === 0) {
42
+ return response.data.data;
43
+ } else {
44
+ throw new Error(`API Error: ${response.data.message || 'Unknown error'}`);
45
+ }
46
+ } catch (error) {
47
+ if (error.response) {
48
+ throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
49
+ } else if (error.request) {
50
+ throw new Error('Network error: No response from server');
51
+ } else {
52
+ throw error;
53
+ }
54
+ }
55
+ }
56
+
57
+ async getHotelList(page = 1, limit = 100) {
58
+ return await this.request('hotel/list', { page, limit });
59
+ }
60
+
61
+ async getRoomList(hotelId, page = 1, limit = 100) {
62
+ return await this.request('room/list', {
63
+ hotel_id: hotelId,
64
+ page,
65
+ limit
66
+ });
67
+ }
68
+
69
+ async getRoomInfo(params) {
70
+ if (params.room_uuid) {
71
+ return await this.request('room/info', {
72
+ room_uuid: params.room_uuid
73
+ });
74
+ } else if (params.hotel_id && params.room_no) {
75
+ return await this.request('room/info', {
76
+ hotel_id: params.hotel_id,
77
+ room_no: params.room_no
78
+ });
79
+ } else {
80
+ throw new Error('Must provide room_uuid or (hotel_id + room_no)');
81
+ }
82
+ }
83
+
84
+ async executeScene(roomUuid, sceneId) {
85
+ return await this.request('room/control', {
86
+ room_uuid: roomUuid,
87
+ action: 'execute_scene',
88
+ payload: {
89
+ scene_id: sceneId
90
+ }
91
+ });
92
+ }
93
+ }
94
+
95
+ module.exports = CloudAPI;
96
+
@@ -82,6 +82,13 @@ class DeviceInfo {
82
82
  this.handleSwitchState(parameters);
83
83
  }
84
84
  break;
85
+ case 0x45:
86
+ // 6键开关状态上报(2字节,小端序)
87
+ // 用于场景执行后的状态同步
88
+ if (this.channels === 6) {
89
+ this.handleSwitchState(parameters);
90
+ }
91
+ break;
85
92
  case 0x03:
86
93
  if (parameters.length > 0) {
87
94
  this.state.brightness = parameters[0];
@@ -90,10 +90,17 @@ function generateDiscoveryConfig(device, mqttPrefix = 'homeassistant') {
90
90
  // 普通开关设备:为每个继电器创建一个按钮实体(button,非toggle)
91
91
  for (let i = 1; i <= device.channels; i++) {
92
92
  const objectId = device.channels === 1 ? `${macClean}_switch` : `${macClean}_switch_${i}`;
93
- const channelName = device.channels > 1 ? ` 第${i}路` : '';
93
+
94
+ let channelName = '';
95
+ if (device.subDeviceNames && device.subDeviceNames[i - 1]) {
96
+ channelName = ` ${device.subDeviceNames[i - 1]}`;
97
+ } else if (device.channels > 1) {
98
+ channelName = ` 第${i}路`;
99
+ }
100
+
94
101
  const stateTopic = device.channels === 1 ? `symi_mesh/${macClean}/switch/state` : `symi_mesh/${macClean}/switch_${i}/state`;
95
102
  const commandTopic = device.channels === 1 ? `symi_mesh/${macClean}/switch/set` : `symi_mesh/${macClean}/switch_${i}/set`;
96
-
103
+
97
104
  configs.push({
98
105
  topic: `${mqttPrefix}/switch/${objectId}/config`,
99
106
  payload: JSON.stringify({
@@ -650,10 +657,34 @@ function convertStateValue(entityType, attrType, value, deviceType = null) {
650
657
  }
651
658
  }
652
659
 
660
+ function generateSceneButtonConfig(scene, roomNo, mqttPrefix = 'homeassistant') {
661
+ const sceneId = `scene_${scene.scene_id}`;
662
+ const objectId = `symi_room_${roomNo}_${sceneId}`;
663
+
664
+ return {
665
+ topic: `${mqttPrefix}/button/${objectId}/config`,
666
+ payload: JSON.stringify({
667
+ name: `${scene.scene_name}`,
668
+ unique_id: objectId,
669
+ command_topic: `symi_mesh/room_${roomNo}/scene/${scene.scene_id}/trigger`,
670
+ payload_press: 'PRESS',
671
+ icon: 'mdi:play-circle',
672
+ device: {
673
+ identifiers: [`symi_room_${roomNo}`],
674
+ name: `房间${roomNo}场景控制`,
675
+ model: 'Symi 场景控制器',
676
+ manufacturer: 'SYMI 亖米'
677
+ }
678
+ }),
679
+ retain: true
680
+ };
681
+ }
682
+
653
683
  module.exports = {
654
684
  generateDiscoveryConfig,
655
685
  generateStateTopics,
656
686
  convertStateValue,
687
+ generateSceneButtonConfig,
657
688
  DEVICE_ICONS,
658
689
  DEVICE_CLASSES
659
690
  };
package/lib/protocol.js CHANGED
@@ -9,12 +9,13 @@ const MIN_FRAME_LENGTH = 4;
9
9
  // Operation codes
10
10
  const OP_READ_DEVICE_LIST = 0x12;
11
11
  const OP_DEVICE_CONTROL = 0x30;
12
+ const OP_SCENE_CONTROL = 0x34; // 场景控制(使用0x34,不是0x31)
12
13
  const OP_TRANSPARENT_CONTROL = 0x40;
13
14
  const OP_EVENT_NODE_STATUS = 0x80;
14
15
  const OP_EVENT_TRANSPARENT_MSG = 0xC0;
15
16
  const OP_RESP_DEVICE_LIST = 0x92;
16
17
  const OP_RESP_DEVICE_CONTROL = 0xB0;
17
- const OP_SCENE_CONTROL = 0x31;
18
+ const OP_RESP_SCENE_CONTROL = 0xB4; // 场景控制响应
18
19
  const OP_DEVICE_STATUS_QUERY = 0x32;
19
20
 
20
21
  class ProtocolFrame {
@@ -410,7 +411,23 @@ class ProtocolHandler {
410
411
  }
411
412
 
412
413
  buildSceneControlFrame(sceneId) {
413
- return this.buildFrame(OP_SCENE_CONTROL, Buffer.from([sceneId]));
414
+ // 场景控制协议格式:
415
+ // 53 34 06 bc c0 00 05 01 [场景ID] [校验]
416
+ // - 地址:0xC0BC(小端序:bc c0)- 广播地址,确保所有设备收到
417
+ // - ACK:0x00(不需要ACK)
418
+ // - 重传:0x05(重传5次,持续1.2秒)
419
+ // - msgtype:0x01(场景执行)
420
+ // - 场景ID:1字节
421
+
422
+ const addr = 0xC0BC; // 广播地址
423
+ const addrLow = addr & 0xFF; // 0xBC
424
+ const addrHigh = (addr >> 8) & 0xFF; // 0xC0
425
+ const ack = 0x00; // 不需要ACK
426
+ const retrans = 0x05; // 重传5次
427
+ const msgtype = 0x01; // 场景执行
428
+
429
+ const data = Buffer.from([addrLow, addrHigh, ack, retrans, msgtype, sceneId]);
430
+ return this.buildFrame(OP_SCENE_CONTROL, data);
414
431
  }
415
432
 
416
433
  buildDeviceStatusQueryFrame(networkAddr, msgType = 0x00) {
package/lib/tcp-client.js CHANGED
@@ -236,9 +236,9 @@ class TCPClient extends EventEmitter {
236
236
  return;
237
237
  }
238
238
 
239
- // 添加发送日志
239
+ // 使用debug记录发送帧,避免日志泛滥
240
240
  const frameHex = frame.toString('hex').toUpperCase();
241
- this.logger.log(`[TCP Frame] 发送: ${frameHex}`);
241
+ this.logger.debug(`[TCP Frame] 发送: ${frameHex}`);
242
242
 
243
243
  this.client.write(frame, (error) => {
244
244
  if (error) {