node-red-contrib-symi-mesh 1.2.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.
@@ -0,0 +1,659 @@
1
+ /**
2
+ * MQTT Helper for Home Assistant Discovery
3
+ * 基于协议文档和HA MQTT Discovery标准
4
+ */
5
+
6
+ // 设备类型到图标的映射
7
+ const DEVICE_ICONS = {
8
+ 1: 'mdi:light-switch',
9
+ 2: 'mdi:light-switch',
10
+ 3: 'mdi:power-socket-eu',
11
+ 4: 'mdi:lightbulb',
12
+ 5: 'mdi:window-shutter',
13
+ 6: 'mdi:gesture-tap-button',
14
+ 7: 'mdi:door',
15
+ 8: 'mdi:motion-sensor',
16
+ 9: 'mdi:card',
17
+ 10: 'mdi:thermostat',
18
+ 11: 'mdi:thermometer',
19
+ 12: 'mdi:gesture-tap-button',
20
+ 0x14: 'mdi:router-wireless',
21
+ 0x18: 'mdi:lightbulb-multiple',
22
+ 0x94: 'mdi:hvac'
23
+ };
24
+
25
+ // 设备类型到设备类别的映射
26
+ const DEVICE_CLASSES = {
27
+ 7: 'door', // 门磁传感器
28
+ 8: 'motion', // 人体感应
29
+ 11: 'temperature' // 温湿度传感器(主要传感器)
30
+ };
31
+
32
+ function generateDiscoveryConfig(device, mqttPrefix = 'homeassistant') {
33
+ const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
34
+ const entityType = device.getEntityType();
35
+ const configs = [];
36
+
37
+ // 开关设备:每个继电器一个实体
38
+ if (entityType === 'switch') {
39
+ // 插卡取电器特殊处理
40
+ if (device.deviceType === 9) {
41
+ const objectId = `${macClean}_switch`;
42
+ configs.push({
43
+ topic: `${mqttPrefix}/switch/${objectId}/config`,
44
+ payload: JSON.stringify({
45
+ name: `${device.name} 电源`,
46
+ unique_id: objectId,
47
+ state_topic: `symi_mesh/${macClean}/switch/state`,
48
+ command_topic: `symi_mesh/${macClean}/switch/set`,
49
+ payload_on: 'ON',
50
+ payload_off: 'OFF',
51
+ optimistic: false,
52
+ availability_topic: `symi_mesh/${macClean}/availability`,
53
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:card',
54
+ device: {
55
+ identifiers: [macClean],
56
+ name: device.name,
57
+ model: 'Symi 插卡取电器',
58
+ manufacturer: 'SYMI 亖米',
59
+ sw_version: device.state.softwareVersion || 'Unknown'
60
+ }
61
+ }),
62
+ retain: true
63
+ });
64
+
65
+ // 插卡检测传感器
66
+ const cardSensorId = `${macClean}_card_sensor`;
67
+ configs.push({
68
+ topic: `${mqttPrefix}/binary_sensor/${cardSensorId}/config`,
69
+ payload: JSON.stringify({
70
+ name: `${device.name} 插卡检测`,
71
+ unique_id: cardSensorId,
72
+ state_topic: `symi_mesh/${macClean}/card_sensor/state`,
73
+ payload_on: 'ON',
74
+ payload_off: 'OFF',
75
+ device_class: 'plug',
76
+ availability_topic: `symi_mesh/${macClean}/availability`,
77
+ icon: 'mdi:card-account-details',
78
+ entity_category: 'diagnostic',
79
+ device: {
80
+ identifiers: [macClean],
81
+ name: device.name,
82
+ model: 'Symi 插卡取电器',
83
+ manufacturer: 'SYMI 亖米',
84
+ sw_version: device.state.softwareVersion || 'Unknown'
85
+ }
86
+ }),
87
+ retain: true
88
+ });
89
+ } else {
90
+ // 普通开关设备:为每个继电器创建一个按钮实体(button,非toggle)
91
+ for (let i = 1; i <= device.channels; i++) {
92
+ const objectId = device.channels === 1 ? `${macClean}_switch` : `${macClean}_switch_${i}`;
93
+ const channelName = device.channels > 1 ? ` 第${i}路` : '';
94
+ const stateTopic = device.channels === 1 ? `symi_mesh/${macClean}/switch/state` : `symi_mesh/${macClean}/switch_${i}/state`;
95
+ const commandTopic = device.channels === 1 ? `symi_mesh/${macClean}/switch/set` : `symi_mesh/${macClean}/switch_${i}/set`;
96
+
97
+ configs.push({
98
+ topic: `${mqttPrefix}/switch/${objectId}/config`,
99
+ payload: JSON.stringify({
100
+ name: `${device.name}${channelName}`,
101
+ unique_id: objectId,
102
+ state_topic: stateTopic,
103
+ command_topic: commandTopic,
104
+ payload_on: 'ON',
105
+ payload_off: 'OFF',
106
+ optimistic: false,
107
+ availability_topic: `symi_mesh/${macClean}/availability`,
108
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:light-switch',
109
+ device: {
110
+ identifiers: [macClean],
111
+ name: device.name,
112
+ model: `Symi Type ${device.deviceType} (${device.channels}路)`,
113
+ manufacturer: 'SYMI 亖米',
114
+ sw_version: device.state.softwareVersion || 'Unknown'
115
+ }
116
+ }),
117
+ retain: true
118
+ });
119
+ }
120
+ }
121
+
122
+ // 双色调光灯(支持亮度+色温)
123
+ } else if (entityType === 'light' && device.deviceType === 4) {
124
+ const objectId = `${macClean}_light`;
125
+ configs.push({
126
+ topic: `${mqttPrefix}/light/${objectId}/config`,
127
+ payload: JSON.stringify({
128
+ name: device.name,
129
+ unique_id: objectId,
130
+ schema: 'json',
131
+ state_topic: `symi_mesh/${macClean}/light/state`,
132
+ command_topic: `symi_mesh/${macClean}/light/set`,
133
+ brightness: true,
134
+ supported_color_modes: ['color_temp'],
135
+ min_mireds: 153,
136
+ max_mireds: 500,
137
+ optimistic: false,
138
+ availability_topic: `symi_mesh/${macClean}/availability`,
139
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:lightbulb',
140
+ device: {
141
+ identifiers: [macClean],
142
+ name: device.name,
143
+ model: 'Symi 双色调光灯',
144
+ manufacturer: 'SYMI 亖米',
145
+ sw_version: device.state.softwareVersion || 'Unknown'
146
+ }
147
+ }),
148
+ retain: true
149
+ });
150
+
151
+ // 五色调光灯(支持RGB+亮度+色温)
152
+ } else if (entityType === 'light' && device.deviceType === 0x18) {
153
+ const objectId = `${macClean}_light`;
154
+ configs.push({
155
+ topic: `${mqttPrefix}/light/${objectId}/config`,
156
+ payload: JSON.stringify({
157
+ name: device.name,
158
+ unique_id: objectId,
159
+ schema: 'json',
160
+ state_topic: `symi_mesh/${macClean}/light/state`,
161
+ command_topic: `symi_mesh/${macClean}/light/set`,
162
+ brightness: true,
163
+ supported_color_modes: ['rgb', 'color_temp'],
164
+ min_mireds: 153,
165
+ max_mireds: 500,
166
+ optimistic: false,
167
+ availability_topic: `symi_mesh/${macClean}/availability`,
168
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:lightbulb-multiple',
169
+ device: {
170
+ identifiers: [macClean],
171
+ name: device.name,
172
+ model: 'Symi 五色调光灯',
173
+ manufacturer: 'SYMI 亖米',
174
+ sw_version: device.state.softwareVersion || 'Unknown'
175
+ }
176
+ }),
177
+ retain: true
178
+ });
179
+
180
+ // 窗帘
181
+ } else if (entityType === 'cover') {
182
+ const objectId = `${macClean}_cover`;
183
+ configs.push({
184
+ topic: `${mqttPrefix}/cover/${objectId}/config`,
185
+ payload: JSON.stringify({
186
+ name: device.name,
187
+ unique_id: objectId,
188
+ state_topic: `symi_mesh/${macClean}/cover/state`,
189
+ command_topic: `symi_mesh/${macClean}/cover/set`,
190
+ position_topic: `symi_mesh/${macClean}/cover/position`,
191
+ set_position_topic: `symi_mesh/${macClean}/cover/position/set`,
192
+ payload_open: 'OPEN',
193
+ payload_close: 'CLOSE',
194
+ payload_stop: 'STOP',
195
+ availability_topic: `symi_mesh/${macClean}/availability`,
196
+ optimistic: false,
197
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:window-shutter',
198
+ device: {
199
+ identifiers: [macClean],
200
+ name: device.name,
201
+ model: 'Symi 智能窗帘',
202
+ manufacturer: 'SYMI 亖米',
203
+ sw_version: device.state.softwareVersion || 'Unknown'
204
+ }
205
+ }),
206
+ retain: true
207
+ });
208
+
209
+ // 温控器(空调)
210
+ } else if (entityType === 'climate') {
211
+ const objectId = `${macClean}_climate`;
212
+ configs.push({
213
+ topic: `${mqttPrefix}/climate/${objectId}/config`,
214
+ payload: JSON.stringify({
215
+ name: device.name,
216
+ unique_id: objectId,
217
+ current_temperature_topic: `symi_mesh/${macClean}/climate/current_temp`,
218
+ temperature_state_topic: `symi_mesh/${macClean}/climate/target_temp`,
219
+ temperature_command_topic: `symi_mesh/${macClean}/climate/target_temp/set`,
220
+ mode_state_topic: `symi_mesh/${macClean}/climate/mode`,
221
+ mode_command_topic: `symi_mesh/${macClean}/climate/mode/set`,
222
+ fan_mode_state_topic: `symi_mesh/${macClean}/climate/fan_mode`,
223
+ fan_mode_command_topic: `symi_mesh/${macClean}/climate/fan_mode/set`,
224
+ modes: ['off', 'cool', 'heat', 'fan_only', 'dry'],
225
+ fan_modes: ['low', 'medium', 'high', 'auto'],
226
+ min_temp: 16,
227
+ max_temp: 30,
228
+ temp_step: 1,
229
+ temperature_unit: 'C',
230
+ precision: 1.0,
231
+ availability_topic: `symi_mesh/${macClean}/availability`,
232
+ optimistic: false,
233
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:thermostat',
234
+ device: {
235
+ identifiers: [macClean],
236
+ name: device.name,
237
+ model: 'Symi 温控器',
238
+ manufacturer: 'SYMI 亖米',
239
+ sw_version: device.state.softwareVersion || 'Unknown'
240
+ }
241
+ }),
242
+ retain: true
243
+ });
244
+
245
+ // 人体感应器
246
+ } else if (entityType === 'binary_sensor' && device.deviceType === 8) {
247
+ const objectId = `${macClean}_motion`;
248
+ configs.push({
249
+ topic: `${mqttPrefix}/binary_sensor/${objectId}/config`,
250
+ payload: JSON.stringify({
251
+ name: `${device.name}`,
252
+ unique_id: objectId,
253
+ state_topic: `symi_mesh/${macClean}/binary_sensor/state`,
254
+ payload_on: 'ON',
255
+ payload_off: 'OFF',
256
+ device_class: 'motion',
257
+ availability_topic: `symi_mesh/${macClean}/availability`,
258
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:motion-sensor',
259
+ device: {
260
+ identifiers: [macClean],
261
+ name: device.name,
262
+ model: 'Symi 人体感应器',
263
+ manufacturer: 'SYMI 亖米',
264
+ sw_version: device.state.softwareVersion || 'Unknown'
265
+ }
266
+ }),
267
+ retain: true
268
+ });
269
+
270
+ // 门磁传感器
271
+ } else if (entityType === 'binary_sensor' && device.deviceType === 7) {
272
+ const objectId = `${macClean}_door`;
273
+ configs.push({
274
+ topic: `${mqttPrefix}/binary_sensor/${objectId}/config`,
275
+ payload: JSON.stringify({
276
+ name: `${device.name} 门磁`,
277
+ unique_id: objectId,
278
+ state_topic: `symi_mesh/${macClean}/door/state`,
279
+ payload_on: 'ON',
280
+ payload_off: 'OFF',
281
+ device_class: 'door',
282
+ availability_topic: `symi_mesh/${macClean}/availability`,
283
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:door',
284
+ device: {
285
+ identifiers: [macClean],
286
+ name: device.name,
287
+ model: 'Symi 门磁传感器',
288
+ manufacturer: 'SYMI 亖米',
289
+ sw_version: device.state.softwareVersion || 'Unknown'
290
+ }
291
+ }),
292
+ retain: true
293
+ });
294
+
295
+ // 温湿度传感器
296
+ } else if (entityType === 'sensor' && device.deviceType === 11) {
297
+ // 温度传感器
298
+ const tempObjectId = `${macClean}_temperature`;
299
+ configs.push({
300
+ topic: `${mqttPrefix}/sensor/${tempObjectId}/config`,
301
+ payload: JSON.stringify({
302
+ name: `${device.name} 温度`,
303
+ unique_id: tempObjectId,
304
+ state_topic: `symi_mesh/${macClean}/temperature/state`,
305
+ unit_of_measurement: '°C',
306
+ device_class: 'temperature',
307
+ state_class: 'measurement',
308
+ availability_topic: `symi_mesh/${macClean}/availability`,
309
+ icon: 'mdi:thermometer',
310
+ device: {
311
+ identifiers: [macClean],
312
+ name: device.name,
313
+ model: 'Symi 温湿度传感器',
314
+ manufacturer: 'SYMI 亖米',
315
+ sw_version: device.state.softwareVersion || 'Unknown'
316
+ }
317
+ }),
318
+ retain: true
319
+ });
320
+
321
+ // 湿度传感器
322
+ const humidityObjectId = `${macClean}_humidity`;
323
+ configs.push({
324
+ topic: `${mqttPrefix}/sensor/${humidityObjectId}/config`,
325
+ payload: JSON.stringify({
326
+ name: `${device.name} 湿度`,
327
+ unique_id: humidityObjectId,
328
+ state_topic: `symi_mesh/${macClean}/humidity/state`,
329
+ unit_of_measurement: '%',
330
+ device_class: 'humidity',
331
+ state_class: 'measurement',
332
+ availability_topic: `symi_mesh/${macClean}/availability`,
333
+ icon: 'mdi:water-percent',
334
+ device: {
335
+ identifiers: [macClean],
336
+ name: device.name,
337
+ model: 'Symi 温湿度传感器',
338
+ manufacturer: 'SYMI 亖米',
339
+ sw_version: device.state.softwareVersion || 'Unknown'
340
+ }
341
+ }),
342
+ retain: true
343
+ });
344
+
345
+ // 情景面板/情景开关
346
+ } else if (entityType === 'scene' && (device.deviceType === 6 || device.deviceType === 12 || device.deviceType === 0x14)) {
347
+ // 情景面板通常不需要在HA中创建实体,因为它们主要用于触发场景
348
+ // 但我们可以创建一个事件传感器来监控按键事件
349
+ const objectId = `${macClean}_scene_trigger`;
350
+ configs.push({
351
+ topic: `${mqttPrefix}/sensor/${objectId}/config`,
352
+ payload: JSON.stringify({
353
+ name: `${device.name} 场景触发`,
354
+ unique_id: objectId,
355
+ state_topic: `symi_mesh/${macClean}/scene/state`,
356
+ availability_topic: `symi_mesh/${macClean}/availability`,
357
+ icon: DEVICE_ICONS[device.deviceType] || 'mdi:gesture-tap-button',
358
+ entity_category: 'diagnostic',
359
+ device: {
360
+ identifiers: [macClean],
361
+ name: device.name,
362
+ model: device.deviceType === 6 ? 'Symi 情景面板' :
363
+ device.deviceType === 12 ? 'Symi 情景开关' :
364
+ device.deviceType === 0x14 ? 'Symi 场景面板' : 'Symi 场景设备',
365
+ manufacturer: 'SYMI 亖米',
366
+ sw_version: device.state.softwareVersion || 'Unknown'
367
+ }
368
+ }),
369
+ retain: true
370
+ });
371
+
372
+ // 三合一设备(温控器+新风+地暖)
373
+ } else if (entityType === 'three_in_one') {
374
+ const macSuffix = macClean.slice(-6);
375
+ const deviceInfo = {
376
+ identifiers: [macClean],
377
+ name: `三合一_${macSuffix}`,
378
+ model: 'Symi 三合一控制器',
379
+ manufacturer: 'SYMI 亖米',
380
+ sw_version: device.state.softwareVersion || 'Unknown'
381
+ };
382
+
383
+ // 1. 空调实体 - climate类型,温度范围16-30°C
384
+ const climateObjectId = `${macClean}_climate`;
385
+ configs.push({
386
+ topic: `${mqttPrefix}/climate/${climateObjectId}/config`,
387
+ payload: JSON.stringify({
388
+ name: `空调_${macSuffix}`,
389
+ unique_id: climateObjectId,
390
+ current_temperature_topic: `symi_mesh/${macClean}/climate/current_temp`,
391
+ temperature_state_topic: `symi_mesh/${macClean}/climate/target_temp`,
392
+ temperature_command_topic: `symi_mesh/${macClean}/climate/target_temp/set`,
393
+ mode_state_topic: `symi_mesh/${macClean}/climate/mode`,
394
+ mode_command_topic: `symi_mesh/${macClean}/climate/mode/set`,
395
+ fan_mode_state_topic: `symi_mesh/${macClean}/climate/fan_mode`,
396
+ fan_mode_command_topic: `symi_mesh/${macClean}/climate/fan_mode/set`,
397
+ modes: ['off', 'cool', 'heat', 'fan_only', 'dry'],
398
+ fan_modes: ['low', 'medium', 'high', 'auto'],
399
+ min_temp: 16,
400
+ max_temp: 30,
401
+ temp_step: 1,
402
+ temperature_unit: 'C',
403
+ precision: 1.0,
404
+ availability_topic: `symi_mesh/${macClean}/availability`,
405
+ optimistic: false,
406
+ icon: 'mdi:air-conditioner',
407
+ device: deviceInfo
408
+ }),
409
+ retain: true
410
+ });
411
+
412
+ // 2. 新风实体 - fan类型,支持送风forward/排风reverse方向
413
+ const freshAirObjectId = `${macClean}_fresh_air`;
414
+ configs.push({
415
+ topic: `${mqttPrefix}/fan/${freshAirObjectId}/config`,
416
+ payload: JSON.stringify({
417
+ name: `新风_${macSuffix}`,
418
+ unique_id: freshAirObjectId,
419
+ state_topic: `symi_mesh/${macClean}/fresh_air/state`,
420
+ command_topic: `symi_mesh/${macClean}/fresh_air/set`,
421
+ percentage_state_topic: `symi_mesh/${macClean}/fresh_air/speed`,
422
+ percentage_command_topic: `symi_mesh/${macClean}/fresh_air/speed/set`,
423
+ preset_mode_state_topic: `symi_mesh/${macClean}/fresh_air/mode`,
424
+ preset_mode_command_topic: `symi_mesh/${macClean}/fresh_air/mode/set`,
425
+ direction_state_topic: `symi_mesh/${macClean}/fresh_air/direction`,
426
+ direction_command_topic: `symi_mesh/${macClean}/fresh_air/direction/set`,
427
+ preset_modes: ['low', 'medium', 'high', 'auto'],
428
+ payload_on: 'ON',
429
+ payload_off: 'OFF',
430
+ availability_topic: `symi_mesh/${macClean}/availability`,
431
+ optimistic: false,
432
+ icon: 'mdi:air-filter',
433
+ device: deviceInfo
434
+ }),
435
+ retain: true
436
+ });
437
+
438
+ // 3. 地暖实体 - climate类型,温度范围18-32°C
439
+ const floorHeatingObjectId = `${macClean}_floor_heating`;
440
+ configs.push({
441
+ topic: `${mqttPrefix}/climate/${floorHeatingObjectId}/config`,
442
+ payload: JSON.stringify({
443
+ name: `地暖_${macSuffix}`,
444
+ unique_id: floorHeatingObjectId,
445
+ current_temperature_topic: `symi_mesh/${macClean}/floor_heating/current_temp`,
446
+ temperature_state_topic: `symi_mesh/${macClean}/floor_heating/target_temp`,
447
+ temperature_command_topic: `symi_mesh/${macClean}/floor_heating/target_temp/set`,
448
+ mode_state_topic: `symi_mesh/${macClean}/floor_heating/mode`,
449
+ mode_command_topic: `symi_mesh/${macClean}/floor_heating/mode/set`,
450
+ modes: ['off', 'heat'],
451
+ min_temp: 18,
452
+ max_temp: 32,
453
+ temp_step: 1,
454
+ temperature_unit: 'C',
455
+ precision: 1.0,
456
+ availability_topic: `symi_mesh/${macClean}/availability`,
457
+ optimistic: false,
458
+ icon: 'mdi:radiator',
459
+ device: deviceInfo
460
+ }),
461
+ retain: true
462
+ });
463
+ }
464
+
465
+ return configs;
466
+ }
467
+
468
+ function generateStateTopics(device) {
469
+ const macClean = device.macAddress.replace(/:/g, '').toLowerCase();
470
+ const entityType = device.getEntityType();
471
+ const topics = { command: [], state: [] };
472
+
473
+ if (entityType === 'switch') {
474
+ if (device.deviceType === 9) {
475
+ // 插卡取电器
476
+ topics.command.push(`symi_mesh/${macClean}/switch/set`);
477
+ topics.state.push(`symi_mesh/${macClean}/switch/state`);
478
+ topics.state.push(`symi_mesh/${macClean}/card_sensor/state`);
479
+ } else {
480
+ // 普通开关:每个继电器的topic
481
+ for (let i = 1; i <= device.channels; i++) {
482
+ const channelSuffix = device.channels === 1 ? '' : `_${i}`;
483
+ topics.command.push(`symi_mesh/${macClean}/switch${channelSuffix}/set`);
484
+ topics.state.push(`symi_mesh/${macClean}/switch${channelSuffix}/state`);
485
+ }
486
+ }
487
+
488
+ } else if (entityType === 'light') {
489
+ topics.command.push(`symi_mesh/${macClean}/light/set`);
490
+ topics.state.push(`symi_mesh/${macClean}/light/state`);
491
+
492
+ if (device.deviceType === 0x18) {
493
+ // 五色调光灯支持RGB
494
+ topics.command.push(`symi_mesh/${macClean}/light/rgb/set`);
495
+ topics.state.push(`symi_mesh/${macClean}/light/rgb/state`);
496
+ }
497
+
498
+ } else if (entityType === 'cover') {
499
+ topics.command.push(
500
+ `symi_mesh/${macClean}/cover/set`,
501
+ `symi_mesh/${macClean}/cover/position/set`
502
+ );
503
+ topics.state.push(
504
+ `symi_mesh/${macClean}/cover/state`,
505
+ `symi_mesh/${macClean}/cover/position`
506
+ );
507
+
508
+ } else if (entityType === 'climate') {
509
+ topics.command.push(
510
+ `symi_mesh/${macClean}/climate/mode/set`,
511
+ `symi_mesh/${macClean}/climate/target_temp/set`,
512
+ `symi_mesh/${macClean}/climate/fan_mode/set`
513
+ );
514
+ topics.state.push(
515
+ `symi_mesh/${macClean}/climate/current_temp`,
516
+ `symi_mesh/${macClean}/climate/target_temp`,
517
+ `symi_mesh/${macClean}/climate/mode`,
518
+ `symi_mesh/${macClean}/climate/fan_mode`
519
+ );
520
+
521
+ } else if (entityType === 'binary_sensor') {
522
+ // 人体感应器和门磁传感器统一使用binary_sensor/state
523
+ topics.state.push(`symi_mesh/${macClean}/binary_sensor/state`);
524
+
525
+ } else if (entityType === 'sensor') {
526
+ if (device.deviceType === 11) {
527
+ // 温湿度传感器
528
+ topics.state.push(
529
+ `symi_mesh/${macClean}/temperature/state`,
530
+ `symi_mesh/${macClean}/humidity/state`
531
+ );
532
+ }
533
+
534
+ } else if (entityType === 'scene') {
535
+ // 情景面板/开关
536
+ topics.state.push(`symi_mesh/${macClean}/scene/state`);
537
+
538
+ } else if (entityType === 'three_in_one') {
539
+ // 三合一设备:温控器(空调)+ 新风 + 地暖
540
+ // 空调使用climate主题(与温控器一致)
541
+ topics.command.push(
542
+ `symi_mesh/${macClean}/climate/mode/set`,
543
+ `symi_mesh/${macClean}/climate/target_temp/set`,
544
+ `symi_mesh/${macClean}/climate/fan_mode/set`
545
+ );
546
+ topics.state.push(
547
+ `symi_mesh/${macClean}/climate/current_temp`,
548
+ `symi_mesh/${macClean}/climate/target_temp`,
549
+ `symi_mesh/${macClean}/climate/mode`,
550
+ `symi_mesh/${macClean}/climate/fan_mode`
551
+ );
552
+
553
+ // 新风命令和状态(包含送风/排风方向)
554
+ topics.command.push(
555
+ `symi_mesh/${macClean}/fresh_air/set`,
556
+ `symi_mesh/${macClean}/fresh_air/speed/set`,
557
+ `symi_mesh/${macClean}/fresh_air/mode/set`,
558
+ `symi_mesh/${macClean}/fresh_air/direction/set`
559
+ );
560
+ topics.state.push(
561
+ `symi_mesh/${macClean}/fresh_air/state`,
562
+ `symi_mesh/${macClean}/fresh_air/speed`,
563
+ `symi_mesh/${macClean}/fresh_air/mode`,
564
+ `symi_mesh/${macClean}/fresh_air/direction`
565
+ );
566
+
567
+ // 地暖命令和状态
568
+ topics.command.push(
569
+ `symi_mesh/${macClean}/floor_heating/mode/set`,
570
+ `symi_mesh/${macClean}/floor_heating/target_temp/set`
571
+ );
572
+ topics.state.push(
573
+ `symi_mesh/${macClean}/floor_heating/current_temp`,
574
+ `symi_mesh/${macClean}/floor_heating/target_temp`,
575
+ `symi_mesh/${macClean}/floor_heating/mode`
576
+ );
577
+ }
578
+
579
+ // 所有设备都有可用性主题
580
+ topics.state.push(`symi_mesh/${macClean}/availability`);
581
+
582
+ return topics;
583
+ }
584
+
585
+ // 状态值转换函数
586
+ function convertStateValue(entityType, attrType, value, deviceType = null) {
587
+ switch (entityType) {
588
+ case 'switch':
589
+ // 开关状态:true/false -> ON/OFF
590
+ return value ? 'ON' : 'OFF';
591
+
592
+ case 'light':
593
+ if (attrType === 0x03) {
594
+ // 亮度:0-100% -> 0-255
595
+ return Math.round(value * 2.55);
596
+ } else if (attrType === 0x04) {
597
+ // 色温:0-100% -> mireds (153-500)
598
+ // 协议:0%=暖白(高mireds), 100%=冷白(低mireds)
599
+ // HA: 153mireds=冷白(6500K), 500mireds=暖白(2000K)
600
+ const mireds = 500 - (value / 100) * (500 - 153);
601
+ return Math.round(mireds);
602
+ }
603
+ return value;
604
+
605
+ case 'cover':
606
+ if (attrType === 0x05) {
607
+ // 窗帘动作:1=OPEN, 2=CLOSE, 3=STOP
608
+ const actions = { 1: 'OPEN', 2: 'CLOSE', 3: 'STOP' };
609
+ return actions[value] || 'STOP';
610
+ } else if (attrType === 0x06) {
611
+ // 窗帘位置:0-100
612
+ return Math.max(0, Math.min(100, value));
613
+ }
614
+ return value;
615
+
616
+ case 'climate':
617
+ if (attrType === 0x16) {
618
+ // 当前温度:温度*100 -> 温度
619
+ return (value / 100).toFixed(1);
620
+ } else if (attrType === 0x1B) {
621
+ // 目标温度:温度*100 -> 温度
622
+ return (value / 100).toFixed(1);
623
+ } else if (attrType === 0x1C) {
624
+ // 风速:1=high, 2=medium, 3=low, 4=auto
625
+ const fanModes = { 1: 'high', 2: 'medium', 3: 'low', 4: 'auto' };
626
+ return fanModes[value] || 'auto';
627
+ } else if (attrType === 0x1D) {
628
+ // 模式:1=cool, 2=heat, 3=fan_only, 4=dry
629
+ const modes = { 1: 'cool', 2: 'heat', 3: 'fan_only', 4: 'dry' };
630
+ return modes[value] || 'off';
631
+ }
632
+ return value;
633
+
634
+ case 'binary_sensor':
635
+ // 二进制传感器:true/false -> ON/OFF
636
+ return value ? 'ON' : 'OFF';
637
+
638
+ case 'sensor':
639
+ if (attrType === 0x16) {
640
+ // 温度传感器:温度*100 -> 温度
641
+ return (value / 100).toFixed(1);
642
+ } else if (attrType === 0x17) {
643
+ // 湿度传感器:0-100
644
+ return Math.max(0, Math.min(100, value));
645
+ }
646
+ return value;
647
+
648
+ default:
649
+ return value;
650
+ }
651
+ }
652
+
653
+ module.exports = {
654
+ generateDiscoveryConfig,
655
+ generateStateTopics,
656
+ convertStateValue,
657
+ DEVICE_ICONS,
658
+ DEVICE_CLASSES
659
+ };