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,497 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Presence Sensor Device Tests
5
+ * Tests presence detection, light readings, and configuration for presence sensor devices
6
+ */
7
+
8
+ const { findDevicesByAbility, waitForDeviceConnection, getDeviceName, OnlineStatus } = require('./test-helper');
9
+
10
+ const metadata = {
11
+ name: 'presence',
12
+ description: 'Tests presence detection, light readings, and configuration for presence sensor devices',
13
+ requiredAbilities: ['Appliance.Control.Sensor.LatestX'],
14
+ minDevices: 1
15
+ };
16
+
17
+ async function runTests(context) {
18
+ const { manager, devices, options = {} } = context;
19
+ const timeout = options.timeout || 30000;
20
+ const results = [];
21
+
22
+ // If no devices provided, discover them
23
+ let presenceDevices = devices || [];
24
+ if (presenceDevices.length === 0) {
25
+ presenceDevices = await findDevicesByAbility(manager, 'Appliance.Control.Sensor.LatestX', OnlineStatus.ONLINE);
26
+ }
27
+
28
+ // Filter out hub devices - presence test is for standalone presence sensors
29
+ presenceDevices = presenceDevices.filter(device => {
30
+ // Skip hub devices - they're tested separately
31
+ return device.constructor.name !== 'MerossHubDevice' && typeof device.getSubdevice !== 'function';
32
+ });
33
+
34
+ // Wait for devices to be connected
35
+ for (const device of presenceDevices) {
36
+ await waitForDeviceConnection(device, timeout);
37
+ await new Promise(resolve => setTimeout(resolve, 1000));
38
+ }
39
+
40
+ // Test 1: Check if devices were found
41
+ if (presenceDevices.length === 0) {
42
+ results.push({
43
+ name: 'should find devices with presence sensor capability',
44
+ passed: false,
45
+ skipped: true,
46
+ error: 'No standalone presence sensor devices found (hub devices are tested separately)',
47
+ device: null
48
+ });
49
+ return results;
50
+ }
51
+
52
+ results.push({
53
+ name: 'should find devices with presence sensor capability',
54
+ passed: true,
55
+ skipped: false,
56
+ error: null,
57
+ device: null
58
+ });
59
+
60
+ const testDevice = presenceDevices[0];
61
+ const deviceName = getDeviceName(testDevice);
62
+
63
+ // Test 2: Get latest sensor readings
64
+ try {
65
+ if (typeof testDevice.getLatestSensorReadings === 'function') {
66
+ const readings = await testDevice.getLatestSensorReadings(['presence', 'light'], timeout);
67
+
68
+ if (!readings || !readings.latest) {
69
+ results.push({
70
+ name: 'should get latest sensor readings',
71
+ passed: false,
72
+ skipped: false,
73
+ error: 'getLatestSensorReadings returned invalid response',
74
+ device: deviceName
75
+ });
76
+ } else {
77
+ results.push({
78
+ name: 'should get latest sensor readings',
79
+ passed: true,
80
+ skipped: false,
81
+ error: null,
82
+ device: deviceName,
83
+ details: {
84
+ hasLatest: !!readings.latest,
85
+ latestLength: readings.latest?.length || 0
86
+ }
87
+ });
88
+ }
89
+ } else {
90
+ results.push({
91
+ name: 'should get latest sensor readings',
92
+ passed: false,
93
+ skipped: true,
94
+ error: 'Device does not support getLatestSensorReadings',
95
+ device: deviceName
96
+ });
97
+ }
98
+ } catch (error) {
99
+ results.push({
100
+ name: 'should get latest sensor readings',
101
+ passed: false,
102
+ skipped: false,
103
+ error: error.message,
104
+ device: deviceName
105
+ });
106
+ }
107
+
108
+ // Wait a bit for state to update after reading
109
+ await new Promise(resolve => setTimeout(resolve, 1000));
110
+
111
+ // Test 3: Get presence data
112
+ try {
113
+ if (typeof testDevice.getPresence === 'function') {
114
+ const presence = testDevice.getPresence();
115
+
116
+ // Presence can be null if no data yet, which is acceptable
117
+ if (presence === null) {
118
+ results.push({
119
+ name: 'should get presence data',
120
+ passed: true,
121
+ skipped: false,
122
+ error: null,
123
+ device: deviceName,
124
+ details: { note: 'No presence data available yet (device may need time to detect)' }
125
+ });
126
+ } else if (typeof presence === 'object') {
127
+ // Validate presence data structure
128
+ const isValid = (
129
+ typeof presence.isPresent === 'boolean' &&
130
+ typeof presence.state === 'string' &&
131
+ (presence.state === 'presence' || presence.state === 'absence')
132
+ );
133
+
134
+ if (!isValid) {
135
+ results.push({
136
+ name: 'should get presence data',
137
+ passed: false,
138
+ skipped: false,
139
+ error: `Invalid presence data structure: ${JSON.stringify(presence)}`,
140
+ device: deviceName
141
+ });
142
+ } else {
143
+ results.push({
144
+ name: 'should get presence data',
145
+ passed: true,
146
+ skipped: false,
147
+ error: null,
148
+ device: deviceName,
149
+ details: {
150
+ state: presence.state,
151
+ isPresent: presence.isPresent,
152
+ hasDistance: presence.distance !== null && presence.distance !== undefined,
153
+ hasTimestamp: presence.timestamp !== null && presence.timestamp !== undefined
154
+ }
155
+ });
156
+ }
157
+ } else {
158
+ results.push({
159
+ name: 'should get presence data',
160
+ passed: false,
161
+ skipped: false,
162
+ error: `getPresence returned unexpected type: ${typeof presence}`,
163
+ device: deviceName
164
+ });
165
+ }
166
+ } else {
167
+ results.push({
168
+ name: 'should get presence data',
169
+ passed: false,
170
+ skipped: true,
171
+ error: 'Device does not support getPresence',
172
+ device: deviceName
173
+ });
174
+ }
175
+ } catch (error) {
176
+ results.push({
177
+ name: 'should get presence data',
178
+ passed: false,
179
+ skipped: false,
180
+ error: error.message,
181
+ device: deviceName
182
+ });
183
+ }
184
+
185
+ // Test 4: Check if present
186
+ try {
187
+ if (typeof testDevice.isPresent === 'function') {
188
+ const isPresent = testDevice.isPresent();
189
+
190
+ // isPresent can return null if no data, which is acceptable
191
+ if (isPresent === null) {
192
+ results.push({
193
+ name: 'should check if presence is detected',
194
+ passed: true,
195
+ skipped: false,
196
+ error: null,
197
+ device: deviceName,
198
+ details: { note: 'No presence data available yet' }
199
+ });
200
+ } else if (typeof isPresent === 'boolean') {
201
+ results.push({
202
+ name: 'should check if presence is detected',
203
+ passed: true,
204
+ skipped: false,
205
+ error: null,
206
+ device: deviceName,
207
+ details: { isPresent: isPresent }
208
+ });
209
+ } else {
210
+ results.push({
211
+ name: 'should check if presence is detected',
212
+ passed: false,
213
+ skipped: false,
214
+ error: `isPresent returned unexpected type: ${typeof isPresent}`,
215
+ device: deviceName
216
+ });
217
+ }
218
+ } else {
219
+ results.push({
220
+ name: 'should check if presence is detected',
221
+ passed: false,
222
+ skipped: true,
223
+ error: 'Device does not support isPresent',
224
+ device: deviceName
225
+ });
226
+ }
227
+ } catch (error) {
228
+ results.push({
229
+ name: 'should check if presence is detected',
230
+ passed: false,
231
+ skipped: false,
232
+ error: error.message,
233
+ device: deviceName
234
+ });
235
+ }
236
+
237
+ // Test 5: Get light reading
238
+ try {
239
+ if (typeof testDevice.getLight === 'function') {
240
+ const light = testDevice.getLight();
241
+
242
+ // Light can be null if no data yet, which is acceptable
243
+ if (light === null) {
244
+ results.push({
245
+ name: 'should get light reading',
246
+ passed: true,
247
+ skipped: false,
248
+ error: null,
249
+ device: deviceName,
250
+ details: { note: 'No light data available yet' }
251
+ });
252
+ } else if (typeof light === 'object' && light.value !== undefined) {
253
+ results.push({
254
+ name: 'should get light reading',
255
+ passed: true,
256
+ skipped: false,
257
+ error: null,
258
+ device: deviceName,
259
+ details: {
260
+ value: light.value,
261
+ hasTimestamp: light.timestamp !== null && light.timestamp !== undefined
262
+ }
263
+ });
264
+ } else {
265
+ results.push({
266
+ name: 'should get light reading',
267
+ passed: false,
268
+ skipped: false,
269
+ error: `getLight returned invalid data: ${JSON.stringify(light)}`,
270
+ device: deviceName
271
+ });
272
+ }
273
+ } else {
274
+ results.push({
275
+ name: 'should get light reading',
276
+ passed: false,
277
+ skipped: true,
278
+ error: 'Device does not support getLight',
279
+ device: deviceName
280
+ });
281
+ }
282
+ } catch (error) {
283
+ results.push({
284
+ name: 'should get light reading',
285
+ passed: false,
286
+ skipped: false,
287
+ error: error.message,
288
+ device: deviceName
289
+ });
290
+ }
291
+
292
+ // Test 6: Get all sensor readings
293
+ try {
294
+ if (typeof testDevice.getAllSensorReadings === 'function') {
295
+ const allReadings = testDevice.getAllSensorReadings();
296
+
297
+ if (!allReadings || typeof allReadings !== 'object') {
298
+ results.push({
299
+ name: 'should get all sensor readings',
300
+ passed: false,
301
+ skipped: false,
302
+ error: 'getAllSensorReadings returned invalid response',
303
+ device: deviceName
304
+ });
305
+ } else {
306
+ results.push({
307
+ name: 'should get all sensor readings',
308
+ passed: true,
309
+ skipped: false,
310
+ error: null,
311
+ device: deviceName,
312
+ details: {
313
+ hasPresence: allReadings.presence !== null && allReadings.presence !== undefined,
314
+ hasLight: allReadings.light !== null && allReadings.light !== undefined
315
+ }
316
+ });
317
+ }
318
+ } else {
319
+ results.push({
320
+ name: 'should get all sensor readings',
321
+ passed: false,
322
+ skipped: true,
323
+ error: 'Device does not support getAllSensorReadings',
324
+ device: deviceName
325
+ });
326
+ }
327
+ } catch (error) {
328
+ results.push({
329
+ name: 'should get all sensor readings',
330
+ passed: false,
331
+ skipped: false,
332
+ error: error.message,
333
+ device: deviceName
334
+ });
335
+ }
336
+
337
+ // Test 7: Get presence configuration
338
+ try {
339
+ if (typeof testDevice.getPresenceConfig === 'function') {
340
+ const config = await testDevice.getPresenceConfig(0, timeout);
341
+
342
+ if (!config) {
343
+ results.push({
344
+ name: 'should get presence configuration',
345
+ passed: false,
346
+ skipped: false,
347
+ error: 'getPresenceConfig returned null or undefined',
348
+ device: deviceName
349
+ });
350
+ } else {
351
+ results.push({
352
+ name: 'should get presence configuration',
353
+ passed: true,
354
+ skipped: false,
355
+ error: null,
356
+ device: deviceName,
357
+ details: { hasConfig: !!config }
358
+ });
359
+ }
360
+ } else {
361
+ results.push({
362
+ name: 'should get presence configuration',
363
+ passed: false,
364
+ skipped: true,
365
+ error: 'Device does not support getPresenceConfig',
366
+ device: deviceName
367
+ });
368
+ }
369
+ } catch (error) {
370
+ // Some devices may not support this, so we'll mark as skipped if it's a not-supported error
371
+ const errorMsg = error.message || String(error);
372
+ if (errorMsg.includes('not supported') || errorMsg.includes('not found') || errorMsg.includes('timeout')) {
373
+ results.push({
374
+ name: 'should get presence configuration',
375
+ passed: false,
376
+ skipped: true,
377
+ error: `getPresenceConfig not supported or timed out: ${errorMsg}`,
378
+ device: deviceName
379
+ });
380
+ } else {
381
+ results.push({
382
+ name: 'should get presence configuration',
383
+ passed: false,
384
+ skipped: false,
385
+ error: error.message,
386
+ device: deviceName
387
+ });
388
+ }
389
+ }
390
+
391
+ // Test 8: Get presence study/calibration status
392
+ try {
393
+ if (typeof testDevice.getPresenceStudy === 'function') {
394
+ const study = await testDevice.getPresenceStudy(timeout);
395
+
396
+ if (!study) {
397
+ results.push({
398
+ name: 'should get presence study status',
399
+ passed: false,
400
+ skipped: false,
401
+ error: 'getPresenceStudy returned null or undefined',
402
+ device: deviceName
403
+ });
404
+ } else {
405
+ results.push({
406
+ name: 'should get presence study status',
407
+ passed: true,
408
+ skipped: false,
409
+ error: null,
410
+ device: deviceName,
411
+ details: { hasStudy: !!study }
412
+ });
413
+ }
414
+ } else {
415
+ results.push({
416
+ name: 'should get presence study status',
417
+ passed: false,
418
+ skipped: true,
419
+ error: 'Device does not support getPresenceStudy',
420
+ device: deviceName
421
+ });
422
+ }
423
+ } catch (error) {
424
+ // Some devices may not support this, so we'll mark as skipped if it's a not-supported error
425
+ const errorMsg = error.message || String(error);
426
+ if (errorMsg.includes('not supported') || errorMsg.includes('not found') || errorMsg.includes('timeout')) {
427
+ results.push({
428
+ name: 'should get presence study status',
429
+ passed: false,
430
+ skipped: true,
431
+ error: `getPresenceStudy not supported or timed out: ${errorMsg}`,
432
+ device: deviceName
433
+ });
434
+ } else {
435
+ results.push({
436
+ name: 'should get presence study status',
437
+ passed: false,
438
+ skipped: false,
439
+ error: error.message,
440
+ device: deviceName
441
+ });
442
+ }
443
+ }
444
+
445
+ // Test 9: Handle presence push notifications
446
+ try {
447
+ // Set up listener for presence push notifications
448
+ let receivedNotification = false;
449
+ const notificationHandler = (notification) => {
450
+ if (notification.namespace === 'Appliance.Control.Sensor.LatestX') {
451
+ receivedNotification = true;
452
+ }
453
+ };
454
+
455
+ testDevice.on('pushNotification', notificationHandler);
456
+
457
+ // Request latest readings (may trigger a push notification)
458
+ if (typeof testDevice.getLatestSensorReadings === 'function') {
459
+ await testDevice.getLatestSensorReadings(['presence', 'light'], timeout);
460
+ }
461
+
462
+ // Wait a bit for potential push notifications
463
+ await new Promise(resolve => setTimeout(resolve, 5000));
464
+
465
+ // Remove listener
466
+ testDevice.removeListener('pushNotification', notificationHandler);
467
+
468
+ // Note: We don't assert on receivedNotification since push notifications
469
+ // are device-initiated and may not occur during testing
470
+ // This test just verifies the listener mechanism works
471
+ results.push({
472
+ name: 'should handle presence push notifications',
473
+ passed: true,
474
+ skipped: false,
475
+ error: null,
476
+ device: deviceName,
477
+ details: { notificationReceived: receivedNotification }
478
+ });
479
+
480
+ } catch (error) {
481
+ results.push({
482
+ name: 'should handle presence push notifications',
483
+ passed: false,
484
+ skipped: false,
485
+ error: error.message,
486
+ device: deviceName
487
+ });
488
+ }
489
+
490
+ return results;
491
+ }
492
+
493
+ module.exports = {
494
+ metadata,
495
+ runTests
496
+ };
497
+