meross-cli 0.1.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/cli/commands/control/execute.js +23 -0
  5. package/cli/commands/control/index.js +12 -0
  6. package/cli/commands/control/menu.js +193 -0
  7. package/cli/commands/control/params/generic.js +229 -0
  8. package/cli/commands/control/params/index.js +56 -0
  9. package/cli/commands/control/params/light.js +188 -0
  10. package/cli/commands/control/params/thermostat.js +166 -0
  11. package/cli/commands/control/params/timer.js +242 -0
  12. package/cli/commands/control/params/trigger.js +206 -0
  13. package/cli/commands/dump.js +35 -0
  14. package/cli/commands/index.js +34 -0
  15. package/cli/commands/info.js +221 -0
  16. package/cli/commands/list.js +112 -0
  17. package/cli/commands/mqtt.js +187 -0
  18. package/cli/commands/sniffer/device-sniffer.js +217 -0
  19. package/cli/commands/sniffer/fake-app.js +233 -0
  20. package/cli/commands/sniffer/index.js +7 -0
  21. package/cli/commands/sniffer/message-queue.js +65 -0
  22. package/cli/commands/sniffer/sniffer-menu.js +676 -0
  23. package/cli/commands/stats.js +90 -0
  24. package/cli/commands/status/device-status.js +1403 -0
  25. package/cli/commands/status/hub-status.js +72 -0
  26. package/cli/commands/status/index.js +50 -0
  27. package/cli/commands/status/subdevices/hub-smoke-detector.js +82 -0
  28. package/cli/commands/status/subdevices/hub-temp-hum-sensor.js +43 -0
  29. package/cli/commands/status/subdevices/hub-thermostat-valve.js +83 -0
  30. package/cli/commands/status/subdevices/hub-water-leak-sensor.js +27 -0
  31. package/cli/commands/status/subdevices/index.js +23 -0
  32. package/cli/commands/test/index.js +185 -0
  33. package/cli/config/users.js +108 -0
  34. package/cli/control-registry.js +875 -0
  35. package/cli/helpers/client.js +89 -0
  36. package/cli/helpers/meross.js +106 -0
  37. package/cli/menu/index.js +10 -0
  38. package/cli/menu/main.js +648 -0
  39. package/cli/menu/settings.js +789 -0
  40. package/cli/meross-cli.js +547 -0
  41. package/cli/tests/README.md +365 -0
  42. package/cli/tests/test-alarm.js +144 -0
  43. package/cli/tests/test-child-lock.js +248 -0
  44. package/cli/tests/test-config.js +133 -0
  45. package/cli/tests/test-control.js +189 -0
  46. package/cli/tests/test-diffuser.js +505 -0
  47. package/cli/tests/test-dnd.js +246 -0
  48. package/cli/tests/test-electricity.js +209 -0
  49. package/cli/tests/test-encryption.js +281 -0
  50. package/cli/tests/test-garage.js +259 -0
  51. package/cli/tests/test-helper.js +313 -0
  52. package/cli/tests/test-hub-mts100.js +355 -0
  53. package/cli/tests/test-hub-sensors.js +489 -0
  54. package/cli/tests/test-light.js +253 -0
  55. package/cli/tests/test-presence.js +497 -0
  56. package/cli/tests/test-registry.js +419 -0
  57. package/cli/tests/test-roller-shutter.js +628 -0
  58. package/cli/tests/test-runner.js +415 -0
  59. package/cli/tests/test-runtime.js +234 -0
  60. package/cli/tests/test-screen.js +133 -0
  61. package/cli/tests/test-sensor-history.js +146 -0
  62. package/cli/tests/test-smoke-config.js +138 -0
  63. package/cli/tests/test-spray.js +131 -0
  64. package/cli/tests/test-temp-unit.js +133 -0
  65. package/cli/tests/test-template.js +238 -0
  66. package/cli/tests/test-thermostat.js +919 -0
  67. package/cli/tests/test-timer.js +372 -0
  68. package/cli/tests/test-toggle.js +342 -0
  69. package/cli/tests/test-trigger.js +279 -0
  70. package/cli/utils/display.js +86 -0
  71. package/cli/utils/terminal.js +137 -0
  72. package/package.json +53 -0
@@ -0,0 +1,875 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Control method registry for device control testing.
5
+ * Maps control method names to metadata including parameter schemas and categories.
6
+ */
7
+
8
+ /**
9
+ * Mapping from control method names to required ability namespaces.
10
+ * This is used to filter methods based on what the device actually supports.
11
+ */
12
+ const METHOD_TO_NAMESPACE_MAP = {
13
+ // Power Control
14
+ setToggle: ['Appliance.Control.Toggle'],
15
+ setToggleX: ['Appliance.Control.ToggleX'],
16
+
17
+ // Light Control
18
+ setLight: ['Appliance.Control.Light'],
19
+ setLightColor: ['Appliance.Control.Light'],
20
+ setDiffuserLight: ['Appliance.Control.Diffuser.Light'],
21
+
22
+ // Climate Control
23
+ setThermostatMode: ['Appliance.Control.Thermostat.Mode'],
24
+ setThermostatModeB: ['Appliance.Control.Thermostat.ModeB'],
25
+ setSpray: ['Appliance.Control.Spray'],
26
+ setDiffuserSpray: ['Appliance.Control.Diffuser.Spray'],
27
+
28
+ // Cover Control
29
+ setGarageDoor: ['Appliance.GarageDoor.State'],
30
+ openGarageDoor: ['Appliance.GarageDoor.State'],
31
+ closeGarageDoor: ['Appliance.GarageDoor.State'],
32
+ setGarageDoorConfig: ['Appliance.GarageDoor.Config', 'Appliance.GarageDoor.MultipleConfig'],
33
+ setRollerShutterPosition: ['Appliance.RollerShutter.Position', 'Appliance.RollerShutter.State'],
34
+ setRollerShutterUp: ['Appliance.RollerShutter.Position', 'Appliance.RollerShutter.State'],
35
+ setRollerShutterDown: ['Appliance.RollerShutter.Position', 'Appliance.RollerShutter.State'],
36
+ setRollerShutterStop: ['Appliance.RollerShutter.Position', 'Appliance.RollerShutter.State'],
37
+ openRollerShutter: ['Appliance.RollerShutter.Position', 'Appliance.RollerShutter.State'],
38
+ closeRollerShutter: ['Appliance.RollerShutter.Position', 'Appliance.RollerShutter.State'],
39
+ stopRollerShutter: ['Appliance.RollerShutter.Position', 'Appliance.RollerShutter.State'],
40
+ setRollerShutterConfig: ['Appliance.RollerShutter.Config'],
41
+
42
+ // Configuration
43
+ setChildLock: ['Appliance.Control.PhysicalLock'],
44
+ setSystemLedMode: ['Appliance.System.LedMode'],
45
+ setScreenBrightness: ['Appliance.Control.Screen.Brightness'],
46
+ setTempUnit: ['Appliance.Control.TempUnit'],
47
+ setDNDMode: ['Appliance.System.DNDMode'],
48
+ setConfigOverTemp: ['Appliance.Config.OverTemp'],
49
+ setPresenceConfig: ['Appliance.Control.Presence.Config'],
50
+ setPresenceStudy: ['Appliance.Control.Presence.Study'],
51
+
52
+ // Timer and Trigger
53
+ setTimerX: ['Appliance.Control.TimerX'],
54
+ deleteTimerX: ['Appliance.Control.TimerX'],
55
+ setTriggerX: ['Appliance.Control.TriggerX'],
56
+ deleteTriggerX: ['Appliance.Control.TriggerX']
57
+ };
58
+
59
+ /**
60
+ * Control method metadata registry.
61
+ * Each entry defines:
62
+ * - name: Human-readable name
63
+ * - category: Feature category for grouping
64
+ * - params: Parameter schema for input collection
65
+ * - description: Brief description
66
+ */
67
+ const CONTROL_METHOD_REGISTRY = {
68
+ // Power Control
69
+ setToggle: {
70
+ name: 'Toggle (On/Off)',
71
+ category: 'Power Control',
72
+ description: 'Turn device on or off',
73
+ params: [
74
+ {
75
+ name: 'onoff',
76
+ type: 'boolean',
77
+ label: 'State',
78
+ choices: [
79
+ { name: 'On', value: true },
80
+ { name: 'Off', value: false }
81
+ ],
82
+ required: true
83
+ }
84
+ ]
85
+ },
86
+ setToggleX: {
87
+ name: 'Toggle Channel (On/Off)',
88
+ category: 'Power Control',
89
+ description: 'Turn device channel on or off',
90
+ params: [
91
+ {
92
+ name: 'channel',
93
+ type: 'number',
94
+ label: 'Channel',
95
+ required: true,
96
+ default: 0
97
+ },
98
+ {
99
+ name: 'onoff',
100
+ type: 'boolean',
101
+ label: 'State',
102
+ choices: [
103
+ { name: 'On', value: true },
104
+ { name: 'Off', value: false }
105
+ ],
106
+ required: true
107
+ }
108
+ ]
109
+ },
110
+
111
+ // Light Control
112
+ setLight: {
113
+ name: 'Light Control',
114
+ category: 'Light Control',
115
+ description: 'Control light settings (advanced)',
116
+ params: [
117
+ {
118
+ name: 'light',
119
+ type: 'object',
120
+ label: 'Light Configuration',
121
+ required: true,
122
+ properties: [
123
+ { name: 'channel', type: 'number', default: 0 },
124
+ { name: 'onoff', type: 'number', default: 1 },
125
+ { name: 'rgb', type: 'number' },
126
+ { name: 'luminance', type: 'number', min: 0, max: 100 },
127
+ { name: 'temperature', type: 'number', min: 0, max: 100 }
128
+ ]
129
+ }
130
+ ]
131
+ },
132
+ setLightColor: {
133
+ name: 'Light Color & Brightness',
134
+ category: 'Light Control',
135
+ description: 'Set light color, brightness, and temperature',
136
+ params: [
137
+ {
138
+ name: 'channel',
139
+ type: 'number',
140
+ label: 'Channel',
141
+ default: 0
142
+ },
143
+ {
144
+ name: 'onoff',
145
+ type: 'boolean',
146
+ label: 'Turn On/Off',
147
+ required: false
148
+ },
149
+ {
150
+ name: 'rgb',
151
+ type: 'rgb',
152
+ label: 'RGB Color (r,g,b)',
153
+ required: false
154
+ },
155
+ {
156
+ name: 'luminance',
157
+ type: 'number',
158
+ label: 'Brightness (0-100)',
159
+ min: 0,
160
+ max: 100,
161
+ required: false
162
+ },
163
+ {
164
+ name: 'temperature',
165
+ type: 'number',
166
+ label: 'Temperature (0-100)',
167
+ min: 0,
168
+ max: 100,
169
+ required: false
170
+ },
171
+ {
172
+ name: 'gradual',
173
+ type: 'boolean',
174
+ label: 'Transition Type',
175
+ required: false
176
+ }
177
+ ]
178
+ },
179
+ setDiffuserLight: {
180
+ name: 'Diffuser Light',
181
+ category: 'Light Control',
182
+ description: 'Control diffuser light settings',
183
+ params: [
184
+ {
185
+ name: 'light',
186
+ type: 'object',
187
+ label: 'Light Configuration',
188
+ required: true,
189
+ properties: [
190
+ { name: 'channel', type: 'number', default: 0 },
191
+ { name: 'onoff', type: 'number', default: 1 },
192
+ { name: 'rgb', type: 'number' },
193
+ { name: 'luminance', type: 'number', min: 0, max: 100 }
194
+ ]
195
+ }
196
+ ]
197
+ },
198
+
199
+ // Climate Control
200
+ setThermostatMode: {
201
+ name: 'Thermostat Mode',
202
+ category: 'Climate Control',
203
+ description: 'Set thermostat mode and temperature',
204
+ params: [
205
+ {
206
+ name: 'channel',
207
+ type: 'number',
208
+ label: 'Channel',
209
+ default: 0
210
+ },
211
+ {
212
+ name: 'mode',
213
+ type: 'enum',
214
+ label: 'Mode',
215
+ choices: [
216
+ { name: 'Heat', value: 0 },
217
+ { name: 'Cool', value: 1 },
218
+ { name: 'Economy', value: 2 },
219
+ { name: 'Auto', value: 3 },
220
+ { name: 'Manual', value: 4 }
221
+ ],
222
+ required: false
223
+ },
224
+ {
225
+ name: 'onoff',
226
+ type: 'number',
227
+ label: 'On/Off (0=off, 1=on)',
228
+ required: false
229
+ },
230
+ {
231
+ name: 'heatTemperature',
232
+ type: 'number',
233
+ label: 'Heat Temperature (°C)',
234
+ required: false
235
+ },
236
+ {
237
+ name: 'coolTemperature',
238
+ type: 'number',
239
+ label: 'Cool Temperature (°C)',
240
+ required: false
241
+ },
242
+ {
243
+ name: 'ecoTemperature',
244
+ type: 'number',
245
+ label: 'Eco Temperature (°C)',
246
+ required: false
247
+ },
248
+ {
249
+ name: 'manualTemperature',
250
+ type: 'number',
251
+ label: 'Manual Temperature (°C)',
252
+ required: false
253
+ }
254
+ ]
255
+ },
256
+ setThermostatModeB: {
257
+ name: 'Thermostat Mode B',
258
+ category: 'Climate Control',
259
+ description: 'Set thermostat mode B (advanced)',
260
+ params: [
261
+ {
262
+ name: 'channel',
263
+ type: 'number',
264
+ label: 'Channel',
265
+ default: 0
266
+ },
267
+ {
268
+ name: 'modeData',
269
+ type: 'object',
270
+ label: 'Mode Data',
271
+ required: true
272
+ }
273
+ ]
274
+ },
275
+ setSpray: {
276
+ name: 'Spray/Humidifier',
277
+ category: 'Climate Control',
278
+ description: 'Control spray/humidifier mode',
279
+ params: [
280
+ {
281
+ name: 'channel',
282
+ type: 'number',
283
+ label: 'Channel',
284
+ default: 0
285
+ },
286
+ {
287
+ name: 'mode',
288
+ type: 'enum',
289
+ label: 'Spray Mode',
290
+ choices: [
291
+ { name: 'Off', value: 0 },
292
+ { name: 'Continuous', value: 1 },
293
+ { name: 'Intermittent', value: 2 }
294
+ ],
295
+ required: true
296
+ }
297
+ ]
298
+ },
299
+ setDiffuserSpray: {
300
+ name: 'Diffuser Spray',
301
+ category: 'Climate Control',
302
+ description: 'Control diffuser spray mode',
303
+ params: [
304
+ {
305
+ name: 'channel',
306
+ type: 'number',
307
+ label: 'Channel',
308
+ default: 0
309
+ },
310
+ {
311
+ name: 'mode',
312
+ type: 'enum',
313
+ label: 'Spray Mode',
314
+ choices: [
315
+ { name: 'Off', value: 0 },
316
+ { name: 'Continuous', value: 1 },
317
+ { name: 'Intermittent', value: 2 }
318
+ ],
319
+ required: true
320
+ }
321
+ ]
322
+ },
323
+
324
+ // Cover Control
325
+ setGarageDoor: {
326
+ name: 'Garage Door',
327
+ category: 'Cover Control',
328
+ description: 'Open or close garage door',
329
+ params: [
330
+ {
331
+ name: 'channel',
332
+ type: 'number',
333
+ label: 'Channel',
334
+ default: 0
335
+ },
336
+ {
337
+ name: 'open',
338
+ type: 'boolean',
339
+ label: 'Action',
340
+ choices: [
341
+ { name: 'Open', value: true },
342
+ { name: 'Close', value: false }
343
+ ],
344
+ required: true
345
+ }
346
+ ]
347
+ },
348
+ setRollerShutterPosition: {
349
+ name: 'Roller Shutter Position',
350
+ category: 'Cover Control',
351
+ description: 'Set roller shutter position (0-100, -1 to stop)',
352
+ params: [
353
+ {
354
+ name: 'channel',
355
+ type: 'number',
356
+ label: 'Channel',
357
+ default: 0
358
+ },
359
+ {
360
+ name: 'position',
361
+ type: 'number',
362
+ label: 'Position (0-100, -1=stop)',
363
+ min: -1,
364
+ max: 100,
365
+ required: true
366
+ }
367
+ ]
368
+ },
369
+ setRollerShutterUp: {
370
+ name: 'Roller Shutter Open',
371
+ category: 'Cover Control',
372
+ description: 'Open roller shutter (position 100)',
373
+ params: [
374
+ {
375
+ name: 'channel',
376
+ type: 'number',
377
+ label: 'Channel',
378
+ default: 0
379
+ }
380
+ ]
381
+ },
382
+ setRollerShutterDown: {
383
+ name: 'Roller Shutter Close',
384
+ category: 'Cover Control',
385
+ description: 'Close roller shutter (position 0)',
386
+ params: [
387
+ {
388
+ name: 'channel',
389
+ type: 'number',
390
+ label: 'Channel',
391
+ default: 0
392
+ }
393
+ ]
394
+ },
395
+ setRollerShutterStop: {
396
+ name: 'Roller Shutter Stop',
397
+ category: 'Cover Control',
398
+ description: 'Stop roller shutter movement',
399
+ params: [
400
+ {
401
+ name: 'channel',
402
+ type: 'number',
403
+ label: 'Channel',
404
+ default: 0
405
+ }
406
+ ]
407
+ },
408
+ openRollerShutter: {
409
+ name: 'Roller Shutter Open',
410
+ category: 'Cover Control',
411
+ description: 'Open roller shutter (convenience method)',
412
+ params: [
413
+ {
414
+ name: 'channel',
415
+ type: 'number',
416
+ label: 'Channel',
417
+ default: 0
418
+ }
419
+ ]
420
+ },
421
+ closeRollerShutter: {
422
+ name: 'Roller Shutter Close',
423
+ category: 'Cover Control',
424
+ description: 'Close roller shutter (convenience method)',
425
+ params: [
426
+ {
427
+ name: 'channel',
428
+ type: 'number',
429
+ label: 'Channel',
430
+ default: 0
431
+ }
432
+ ]
433
+ },
434
+ stopRollerShutter: {
435
+ name: 'Roller Shutter Stop',
436
+ category: 'Cover Control',
437
+ description: 'Stop roller shutter (convenience method)',
438
+ params: [
439
+ {
440
+ name: 'channel',
441
+ type: 'number',
442
+ label: 'Channel',
443
+ default: 0
444
+ }
445
+ ]
446
+ },
447
+ setRollerShutterPosition: {
448
+ name: 'Roller Shutter Set Position',
449
+ category: 'Cover Control',
450
+ description: 'Set roller shutter to specific position (convenience method)',
451
+ params: [
452
+ {
453
+ name: 'position',
454
+ type: 'number',
455
+ label: 'Position (0-100)',
456
+ min: 0,
457
+ max: 100,
458
+ required: true
459
+ },
460
+ {
461
+ name: 'channel',
462
+ type: 'number',
463
+ label: 'Channel',
464
+ default: 0
465
+ }
466
+ ]
467
+ },
468
+ setRollerShutterConfig: {
469
+ name: 'Roller Shutter Configuration',
470
+ category: 'Configuration',
471
+ description: 'Configure roller shutter settings',
472
+ params: [
473
+ {
474
+ name: 'config',
475
+ type: 'object',
476
+ label: 'Configuration Object',
477
+ required: true
478
+ }
479
+ ]
480
+ },
481
+ openGarageDoor: {
482
+ name: 'Garage Door Open',
483
+ category: 'Cover Control',
484
+ description: 'Open garage door (convenience method)',
485
+ params: [
486
+ {
487
+ name: 'channel',
488
+ type: 'number',
489
+ label: 'Channel',
490
+ default: 0
491
+ }
492
+ ]
493
+ },
494
+ closeGarageDoor: {
495
+ name: 'Garage Door Close',
496
+ category: 'Cover Control',
497
+ description: 'Close garage door (convenience method)',
498
+ params: [
499
+ {
500
+ name: 'channel',
501
+ type: 'number',
502
+ label: 'Channel',
503
+ default: 0
504
+ }
505
+ ]
506
+ },
507
+ setGarageDoorConfig: {
508
+ name: 'Garage Door Configuration',
509
+ category: 'Configuration',
510
+ description: 'Configure garage door settings',
511
+ params: [
512
+ {
513
+ name: 'configData',
514
+ type: 'object',
515
+ label: 'Configuration Object',
516
+ required: true
517
+ }
518
+ ]
519
+ },
520
+
521
+ // Configuration
522
+ setChildLock: {
523
+ name: 'Child Lock',
524
+ category: 'Configuration',
525
+ description: 'Enable or disable child lock',
526
+ params: [
527
+ {
528
+ name: 'lockData',
529
+ type: 'object',
530
+ label: 'Lock Configuration',
531
+ required: true,
532
+ properties: [
533
+ { name: 'lock', type: 'number', label: 'Lock (0=unlock, 1=lock)', required: true }
534
+ ]
535
+ }
536
+ ]
537
+ },
538
+ setSystemLedMode: {
539
+ name: 'LED Indicator Mode',
540
+ category: 'Configuration',
541
+ description: 'Control LED indicator mode',
542
+ params: [
543
+ {
544
+ name: 'ledModeData',
545
+ type: 'object',
546
+ label: 'LED Mode Configuration',
547
+ required: true
548
+ }
549
+ ]
550
+ },
551
+ setScreenBrightness: {
552
+ name: 'Screen Brightness',
553
+ category: 'Configuration',
554
+ description: 'Set device screen brightness',
555
+ params: [
556
+ {
557
+ name: 'brightnessData',
558
+ type: 'object',
559
+ label: 'Brightness Configuration',
560
+ required: true,
561
+ properties: [
562
+ { name: 'brightness', type: 'number', min: 0, max: 100, required: true }
563
+ ]
564
+ }
565
+ ]
566
+ },
567
+ setTempUnit: {
568
+ name: 'Temperature Unit',
569
+ category: 'Configuration',
570
+ description: 'Set temperature unit (Celsius/Fahrenheit)',
571
+ params: [
572
+ {
573
+ name: 'tempUnitData',
574
+ type: 'object',
575
+ label: 'Temperature Unit Configuration',
576
+ required: true,
577
+ properties: [
578
+ { name: 'unit', type: 'number', label: 'Unit (0=Celsius, 1=Fahrenheit)', required: true }
579
+ ]
580
+ }
581
+ ]
582
+ },
583
+ setDNDMode: {
584
+ name: 'Do Not Disturb Mode',
585
+ category: 'Configuration',
586
+ description: 'Enable or disable Do Not Disturb mode (turns off LED indicator)',
587
+ params: [
588
+ {
589
+ name: 'mode',
590
+ type: 'boolean',
591
+ label: 'DND Mode',
592
+ choices: [
593
+ { name: 'Enable DND', value: true },
594
+ { name: 'Disable DND', value: false }
595
+ ],
596
+ required: true
597
+ }
598
+ ]
599
+ },
600
+ setConfigOverTemp: {
601
+ name: 'Over-Temperature Protection',
602
+ category: 'Configuration',
603
+ description: 'Enable or disable over-temperature protection',
604
+ params: [
605
+ {
606
+ name: 'enable',
607
+ type: 'boolean',
608
+ label: 'Over-Temperature Protection',
609
+ choices: [
610
+ { name: 'On', value: true },
611
+ { name: 'Off', value: false }
612
+ ],
613
+ required: true
614
+ }
615
+ ]
616
+ },
617
+ setPresenceConfig: {
618
+ name: 'Presence Sensor Configuration',
619
+ category: 'Configuration',
620
+ description: 'Configure presence sensor settings (mode, sensitivity, distance, etc.)',
621
+ params: [
622
+ {
623
+ name: 'configData',
624
+ type: 'object',
625
+ label: 'Configuration Object',
626
+ required: true,
627
+ properties: [
628
+ { name: 'channel', type: 'number', label: 'Channel', default: 0 },
629
+ { name: 'mode', type: 'object', label: 'Mode Configuration' },
630
+ { name: 'noBodyTime', type: 'object', label: 'No Body Time Configuration' },
631
+ { name: 'distance', type: 'object', label: 'Distance Configuration' },
632
+ { name: 'sensitivity', type: 'object', label: 'Sensitivity Configuration' },
633
+ { name: 'mthx', type: 'object', label: 'Motion Threshold Configuration' }
634
+ ]
635
+ }
636
+ ]
637
+ },
638
+ setPresenceStudy: {
639
+ name: 'Presence Sensor Study/Calibration',
640
+ category: 'Configuration',
641
+ description: 'Start or stop presence sensor study/calibration mode',
642
+ params: [
643
+ {
644
+ name: 'studyData',
645
+ type: 'object',
646
+ label: 'Study Data Object',
647
+ required: true,
648
+ properties: [
649
+ { name: 'channel', type: 'number', label: 'Channel', default: 0 },
650
+ { name: 'value', type: 'number', label: 'Study Mode Value (typically 1-3)' },
651
+ { name: 'status', type: 'number', label: 'Status (0=stop, 1=start)', required: true }
652
+ ]
653
+ }
654
+ ]
655
+ },
656
+ setTimerX: {
657
+ name: 'Timer Control',
658
+ category: 'Automation',
659
+ description: 'Create or update a timer',
660
+ params: [
661
+ {
662
+ name: 'timerx',
663
+ type: 'object',
664
+ label: 'Timer Configuration',
665
+ required: true,
666
+ properties: [
667
+ { name: 'id', type: 'string', label: 'Timer ID (for updates)' },
668
+ { name: 'channel', type: 'number', default: 0 },
669
+ { name: 'onoff', type: 'number', label: 'Action (0=off, 1=on)', required: true },
670
+ { name: 'type', type: 'number', label: 'Timer Type', required: true },
671
+ { name: 'time', type: 'number', label: 'Time Value', required: true }
672
+ ]
673
+ }
674
+ ]
675
+ },
676
+ deleteTimerX: {
677
+ name: 'Delete Timer',
678
+ category: 'Automation',
679
+ description: 'Delete a timer',
680
+ params: [
681
+ {
682
+ name: 'timerId',
683
+ type: 'string',
684
+ label: 'Timer ID',
685
+ required: true
686
+ },
687
+ {
688
+ name: 'channel',
689
+ type: 'number',
690
+ label: 'Channel',
691
+ default: 0
692
+ }
693
+ ]
694
+ },
695
+ setTriggerX: {
696
+ name: 'Trigger Control',
697
+ category: 'Automation',
698
+ description: 'Create or update a trigger',
699
+ params: [
700
+ {
701
+ name: 'triggerx',
702
+ type: 'object',
703
+ label: 'Trigger Configuration',
704
+ required: true
705
+ }
706
+ ]
707
+ },
708
+ deleteTriggerX: {
709
+ name: 'Delete Trigger',
710
+ category: 'Automation',
711
+ description: 'Delete a trigger',
712
+ params: [
713
+ {
714
+ name: 'triggerId',
715
+ type: 'string',
716
+ label: 'Trigger ID',
717
+ required: true
718
+ },
719
+ {
720
+ name: 'channel',
721
+ type: 'number',
722
+ label: 'Channel',
723
+ required: false,
724
+ default: 0
725
+ }
726
+ ]
727
+ }
728
+ };
729
+
730
+ /**
731
+ * Get metadata for a control method.
732
+ * @param {string} methodName - Name of the control method
733
+ * @returns {Object|null} Method metadata or null if not found
734
+ */
735
+ function getMethodMetadata(methodName) {
736
+ return CONTROL_METHOD_REGISTRY[methodName] || null;
737
+ }
738
+
739
+ /**
740
+ * Get all methods grouped by category.
741
+ * @returns {Object} Methods grouped by category
742
+ */
743
+ function getMethodsByCategory() {
744
+ const categories = {};
745
+ for (const [methodName, metadata] of Object.entries(CONTROL_METHOD_REGISTRY)) {
746
+ const category = metadata.category || 'Other';
747
+ if (!categories[category]) {
748
+ categories[category] = [];
749
+ }
750
+ categories[category].push({
751
+ methodName,
752
+ ...metadata
753
+ });
754
+ }
755
+ return categories;
756
+ }
757
+
758
+ /**
759
+ * Check if device supports a required namespace.
760
+ * @param {Object} device - Device instance
761
+ * @param {string} namespace - Namespace to check
762
+ * @returns {boolean} True if device supports the namespace
763
+ */
764
+ function deviceSupportsNamespace(device, namespace) {
765
+ if (!device._abilities || typeof device._abilities !== 'object') {
766
+ return false;
767
+ }
768
+ return !!device._abilities[namespace];
769
+ }
770
+
771
+ /**
772
+ * Check if device supports any of the required namespaces for a method.
773
+ * @param {Object} device - Device instance
774
+ * @param {Array<string>} namespaces - Array of namespace strings (any match is sufficient)
775
+ * @returns {boolean} True if device supports at least one namespace
776
+ */
777
+ function deviceSupportsAnyNamespace(device, namespaces) {
778
+ if (!namespaces || namespaces.length === 0) {
779
+ return true; // No namespace requirement means always available
780
+ }
781
+ return namespaces.some(namespace => deviceSupportsNamespace(device, namespace));
782
+ }
783
+
784
+ /**
785
+ * Detect available control methods on a device based on device abilities.
786
+ * Only shows methods that are in the registry AND the device supports the required namespaces.
787
+ * @param {Object} device - Device instance
788
+ * @returns {Array} Array of available control methods with metadata
789
+ */
790
+ function detectControlMethods(device) {
791
+ const availableMethods = [];
792
+
793
+ // Only show methods that are in our registry and device supports
794
+ // Iterate through registry instead of device methods to ensure we only show known methods
795
+ for (const [methodName, metadata] of Object.entries(CONTROL_METHOD_REGISTRY)) {
796
+ // Check if method exists on device (check both exact name and common variations)
797
+ const deviceMethod = device[methodName];
798
+ if (typeof deviceMethod !== 'function') {
799
+ // Try alternative naming (e.g., setDNDMode might be on device)
800
+ continue; // Method doesn't exist on device, skip
801
+ }
802
+
803
+ // Check if this method requires specific namespaces
804
+ const requiredNamespaces = METHOD_TO_NAMESPACE_MAP[methodName];
805
+
806
+ // If method has namespace requirements, check if device supports them
807
+ if (requiredNamespaces && requiredNamespaces.length > 0) {
808
+ if (!deviceSupportsAnyNamespace(device, requiredNamespaces)) {
809
+ // Device doesn't support required namespaces, skip this method
810
+ continue;
811
+ }
812
+ } else {
813
+ // Method has no namespace requirement - this shouldn't happen for control methods
814
+ // Skip it to be safe, or we could add it but it's better to be strict
815
+ continue;
816
+ }
817
+
818
+ // Method exists, has namespace requirements, and device supports them - add it
819
+ availableMethods.push({
820
+ methodName,
821
+ ...metadata
822
+ });
823
+ }
824
+
825
+ // Also check for any control/set methods on device that might not be in registry
826
+ // but have namespace requirements (for future extensibility)
827
+ for (const prop in device) {
828
+ if ((prop.startsWith('control') || prop.startsWith('set')) && typeof device[prop] === 'function') {
829
+ // Skip if already in our list
830
+ if (availableMethods.some(m => m.methodName === prop)) {
831
+ continue;
832
+ }
833
+
834
+ // Check if this method has namespace requirements
835
+ const requiredNamespaces = METHOD_TO_NAMESPACE_MAP[prop];
836
+
837
+ // Only include if it has namespace requirements AND device supports them
838
+ if (requiredNamespaces && requiredNamespaces.length > 0) {
839
+ if (deviceSupportsAnyNamespace(device, requiredNamespaces)) {
840
+ // Unknown method but device supports it - add with basic info
841
+ const displayName = prop.replace(/^(control|set)/, '').replace(/([A-Z])/g, ' $1').trim();
842
+ availableMethods.push({
843
+ methodName: prop,
844
+ name: displayName,
845
+ category: 'Other',
846
+ description: 'Control method',
847
+ params: []
848
+ });
849
+ }
850
+ }
851
+ // If no namespace requirement, skip it (don't show unknown methods without namespace checks)
852
+ }
853
+ }
854
+
855
+ // Sort by category, then by name
856
+ availableMethods.sort((a, b) => {
857
+ if (a.category !== b.category) {
858
+ return a.category.localeCompare(b.category);
859
+ }
860
+ return a.name.localeCompare(b.name);
861
+ });
862
+
863
+ return availableMethods;
864
+ }
865
+
866
+ module.exports = {
867
+ CONTROL_METHOD_REGISTRY,
868
+ METHOD_TO_NAMESPACE_MAP,
869
+ getMethodMetadata,
870
+ getMethodsByCategory,
871
+ detectControlMethods,
872
+ deviceSupportsNamespace,
873
+ deviceSupportsAnyNamespace
874
+ };
875
+