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,415 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const testRegistry = require('./test-registry');
6
+
7
+ /**
8
+ * Test runner for CLI - lightweight custom async execution engine
9
+ * No Mocha dependency - uses simple async/await with structured results
10
+ */
11
+
12
+ /**
13
+ * Gets device display name for error messages
14
+ * @param {Object} device - Device instance
15
+ * @returns {string} Device display name
16
+ */
17
+ function getDeviceName(device) {
18
+ if (!device) return 'Unknown device';
19
+ return device.name || device.uuid || 'Unknown device';
20
+ }
21
+
22
+ /**
23
+ * Formats error message with device context
24
+ * @param {Error} error - Error object
25
+ * @param {Object} device - Device instance (optional)
26
+ * @param {string} testName - Test name (optional)
27
+ * @returns {string} Formatted error message
28
+ */
29
+ function formatError(error, device = null, testName = null) {
30
+ const parts = [];
31
+
32
+ if (testName) {
33
+ parts.push(`Test: ${testName}`);
34
+ }
35
+
36
+ if (device) {
37
+ parts.push(`Device: ${getDeviceName(device)}`);
38
+ }
39
+
40
+ parts.push(`Error: ${error.message || error}`);
41
+
42
+ if (error.stack) {
43
+ parts.push(`\nStack trace:\n${error.stack}`);
44
+ }
45
+
46
+ return parts.join('\n');
47
+ }
48
+
49
+ /**
50
+ * Runs a single test with timeout and error handling
51
+ * @param {Function} testFn - Test function to execute
52
+ * @param {string} testName - Name of the test
53
+ * @param {number} timeout - Timeout in milliseconds
54
+ * @param {Object} device - Device instance (optional, for context)
55
+ * @returns {Promise<Object>} Test result object
56
+ */
57
+ async function runSingleTest(testFn, testName, timeout = 30000, device = null) {
58
+ const startTime = Date.now();
59
+
60
+ return new Promise(async (resolve) => {
61
+ const timeoutId = setTimeout(() => {
62
+ resolve({
63
+ name: testName,
64
+ passed: false,
65
+ skipped: false,
66
+ error: `Test timed out after ${timeout}ms`,
67
+ duration: Date.now() - startTime,
68
+ device: device ? getDeviceName(device) : null
69
+ });
70
+ }, timeout);
71
+
72
+ try {
73
+ const result = await testFn();
74
+
75
+ clearTimeout(timeoutId);
76
+ const duration = Date.now() - startTime;
77
+
78
+ // Handle test result - can be boolean, object, or void
79
+ if (result === false) {
80
+ resolve({
81
+ name: testName,
82
+ passed: false,
83
+ skipped: false,
84
+ error: 'Test returned false',
85
+ duration: duration,
86
+ device: device ? getDeviceName(device) : null
87
+ });
88
+ } else if (result && typeof result === 'object') {
89
+ // Test returned structured result
90
+ resolve({
91
+ name: testName,
92
+ passed: result.passed !== false,
93
+ skipped: result.skipped === true,
94
+ error: result.error || null,
95
+ duration: duration,
96
+ device: device ? getDeviceName(device) : null,
97
+ ...result
98
+ });
99
+ } else {
100
+ // Test passed (returned true, undefined, or truthy value)
101
+ resolve({
102
+ name: testName,
103
+ passed: true,
104
+ skipped: false,
105
+ error: null,
106
+ duration: duration,
107
+ device: device ? getDeviceName(device) : null
108
+ });
109
+ }
110
+ } catch (error) {
111
+ clearTimeout(timeoutId);
112
+ resolve({
113
+ name: testName,
114
+ passed: false,
115
+ skipped: false,
116
+ error: formatError(error, device, testName),
117
+ duration: Date.now() - startTime,
118
+ device: device ? getDeviceName(device) : null
119
+ });
120
+ }
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Loads and validates a test file
126
+ * @param {string} testFile - Path to test file
127
+ * @returns {Promise<Object>} Test module with metadata and runTests function
128
+ */
129
+ async function loadTestFile(testFile) {
130
+ if (!fs.existsSync(testFile)) {
131
+ throw new Error(`Test file not found: ${testFile}`);
132
+ }
133
+
134
+ // Clear require cache to allow reloading
135
+ delete require.cache[require.resolve(testFile)];
136
+
137
+ const testModule = require(testFile);
138
+
139
+ // For now, allow test files without metadata (they'll use registry metadata)
140
+ // This provides backward compatibility during migration
141
+ // TODO: After migration, require metadata in all test files
142
+
143
+ if (typeof testModule.runTests !== 'function') {
144
+ throw new Error(`Test file ${testFile} must export a 'runTests' function`);
145
+ }
146
+
147
+ return testModule;
148
+ }
149
+
150
+ /**
151
+ * Runs tests from a test file
152
+ * @param {string} testType - Test type name
153
+ * @param {Object} context - Test context: { manager, devices, options }
154
+ * @returns {Promise<Object>} Test results
155
+ */
156
+ async function runTest(testType, context) {
157
+ const { manager, devices = [], options = {} } = context;
158
+
159
+ if (!manager) {
160
+ throw new Error('MerossManager instance is required in context');
161
+ }
162
+
163
+ // Resolve test type (handles aliases)
164
+ const resolvedType = testRegistry.resolveTestType(testType);
165
+ if (!resolvedType) {
166
+ throw new Error(`Unknown test type: ${testType}. Available types: ${getAvailableTestTypes().join(', ')}`);
167
+ }
168
+
169
+ // Get test file path
170
+ const testFile = getTestFile(resolvedType);
171
+ if (!testFile) {
172
+ throw new Error(`Test file not found for type: ${resolvedType}`);
173
+ }
174
+
175
+ const startTime = Date.now();
176
+
177
+ try {
178
+ // Load test module
179
+ const testModule = await loadTestFile(testFile);
180
+ const metadata = testModule.metadata || {};
181
+
182
+ // Get metadata from registry if not in test file
183
+ const registryMetadata = testRegistry.getTestMetadata(resolvedType);
184
+ const minDevices = metadata.minDevices || (registryMetadata ? registryMetadata.minDevices : 1);
185
+ const testName = metadata.name || resolvedType;
186
+ const description = metadata.description || (registryMetadata ? registryMetadata.description : 'No description');
187
+
188
+ // Validate devices
189
+ if (devices.length < minDevices) {
190
+ return {
191
+ success: false,
192
+ passed: 0,
193
+ failed: 0,
194
+ skipped: 1,
195
+ duration: Date.now() - startTime,
196
+ error: `Insufficient devices: found ${devices.length}, required ${minDevices}`,
197
+ tests: [{
198
+ name: testName,
199
+ passed: false,
200
+ skipped: true,
201
+ error: `No suitable devices found (found ${devices.length}, need ${minDevices})`,
202
+ duration: 0,
203
+ device: null
204
+ }]
205
+ };
206
+ }
207
+
208
+ // Run tests
209
+ // Only print initial info if in standalone mode (CLI handles this output)
210
+ if (options.verbose && options.standalone) {
211
+ console.log(`\nRunning ${testName} tests...`);
212
+ console.log(`Description: ${description}`);
213
+ console.log(`Devices: ${devices.length}`);
214
+ devices.forEach((device, idx) => {
215
+ console.log(` [${idx}] ${getDeviceName(device)}`);
216
+ });
217
+ console.log('');
218
+ }
219
+
220
+ const testResults = await testModule.runTests(context);
221
+
222
+ // Ensure testResults is an array
223
+ const resultsArray = Array.isArray(testResults) ? testResults : [testResults];
224
+
225
+ // Calculate summary
226
+ const passed = resultsArray.filter(r => r.passed && !r.skipped).length;
227
+ const failed = resultsArray.filter(r => !r.passed && !r.skipped).length;
228
+ const skipped = resultsArray.filter(r => r.skipped).length;
229
+ const duration = Date.now() - startTime;
230
+
231
+ // Only print results if verbose mode is enabled (for standalone execution)
232
+ // CLI will handle output formatting, so we skip it here to avoid duplicates
233
+ if (options.verbose && options.standalone) {
234
+ console.log('\n--- Test Results ---');
235
+ resultsArray.forEach((result, idx) => {
236
+ const status = result.skipped ? 'SKIPPED' : (result.passed ? 'PASSED' : 'FAILED');
237
+ const deviceInfo = result.device ? ` [${result.device}]` : '';
238
+ const durationInfo = result.duration ? ` (${result.duration}ms)` : '';
239
+ console.log(`${idx + 1}. ${result.name || 'Unknown test'}${deviceInfo}: ${status}${durationInfo}`);
240
+
241
+ if (result.error) {
242
+ console.log(` Error: ${result.error}`);
243
+ }
244
+ });
245
+
246
+ console.log(`\nSummary: ${passed} passed, ${failed} failed, ${skipped} skipped (${duration}ms)`);
247
+ }
248
+
249
+ return {
250
+ success: failed === 0,
251
+ passed: passed,
252
+ failed: failed,
253
+ skipped: skipped,
254
+ duration: duration,
255
+ tests: resultsArray
256
+ };
257
+ } catch (error) {
258
+ const duration = Date.now() - startTime;
259
+ const errorMessage = formatError(error, null, testType);
260
+
261
+ console.error(`\nTest execution error: ${errorMessage}`);
262
+
263
+ return {
264
+ success: false,
265
+ passed: 0,
266
+ failed: 1,
267
+ skipped: 0,
268
+ duration: duration,
269
+ error: errorMessage,
270
+ tests: [{
271
+ name: testType,
272
+ passed: false,
273
+ skipped: false,
274
+ error: errorMessage,
275
+ duration: duration,
276
+ device: null
277
+ }]
278
+ };
279
+ }
280
+ }
281
+
282
+ // Test metadata is now managed by test-registry.js
283
+
284
+ /**
285
+ * Gets available test types
286
+ * @returns {Array<string>} Array of test type names
287
+ */
288
+ function getAvailableTestTypes() {
289
+ return testRegistry.getAvailableTestTypes();
290
+ }
291
+
292
+ /**
293
+ * Gets description for a test type
294
+ * @param {string} testType - Test type name or alias
295
+ * @returns {string} Description of the test type
296
+ */
297
+ function getTestDescription(testType) {
298
+ return testRegistry.getTestDescription(testType);
299
+ }
300
+
301
+ /**
302
+ * Gets test file path for a given test type
303
+ * @param {string} testType - Test type name or alias
304
+ * @returns {string|null} Path to test file or null if not found
305
+ */
306
+ function getTestFile(testType) {
307
+ return testRegistry.getTestFile(testType, __dirname);
308
+ }
309
+
310
+ /**
311
+ * Finds devices for a given test type
312
+ * @param {string} testType - Test type name or alias
313
+ * @param {Object} manager - MerossManager instance
314
+ * @returns {Promise<Array>} Array of matching devices
315
+ */
316
+ async function findDevicesForTestType(testType, manager) {
317
+ const { OnlineStatus } = require('./test-helper');
318
+ const abilities = testRegistry.getRequiredAbilities(testType);
319
+
320
+ if (!abilities || abilities.length === 0) {
321
+ return [];
322
+ }
323
+
324
+ const allDevices = manager.getAllDevices();
325
+ if (!allDevices || allDevices.length === 0) {
326
+ const { waitForDevices } = require('./test-helper');
327
+ const deviceList = await waitForDevices(manager, 1000);
328
+ return deviceList.map(({ device }) => device).filter(d => d);
329
+ }
330
+
331
+ const matchingDevices = [];
332
+ const seenUuids = new Set();
333
+
334
+ // Import MerossHubDevice for filtering (lazy import to avoid circular dependency)
335
+ let MerossHubDevice = null;
336
+ try {
337
+ MerossHubDevice = require('meross-iot').MerossHubDevice;
338
+ } catch (e) {
339
+ // Fallback if import fails
340
+ }
341
+
342
+ for (const device of allDevices) {
343
+ const uuid = device.dev?.uuid || device.uuid;
344
+ if (seenUuids.has(uuid)) {
345
+ continue;
346
+ }
347
+
348
+ if (device.onlineStatus !== OnlineStatus.ONLINE &&
349
+ device.dev?.onlineStatus !== OnlineStatus.ONLINE) {
350
+ continue;
351
+ }
352
+
353
+ // Filter out subdevices when searching for hub abilities
354
+ // Subdevices inherit hub abilities but we want the actual hub device
355
+ const isSubdevice = device.constructor.name === 'MerossSubDevice' ||
356
+ device.constructor.name === 'HubTempHumSensor' ||
357
+ device.constructor.name === 'HubWaterLeakSensor' ||
358
+ device.constructor.name === 'HubSmokeDetector' ||
359
+ device.constructor.name === 'HubThermostatValve' ||
360
+ (device.subdeviceId || device._subdeviceId) ||
361
+ (device.hub || device._hub);
362
+
363
+ // Check if this is a hub ability search - if so, exclude subdevices
364
+ const isHubAbilitySearch = abilities.some(ability =>
365
+ ability.startsWith('Appliance.Hub.')
366
+ );
367
+
368
+ if (isHubAbilitySearch && isSubdevice) {
369
+ // Skip subdevices when searching for hub abilities
370
+ continue;
371
+ }
372
+
373
+ const hasAbility = abilities.some(ability =>
374
+ device._abilities && device._abilities[ability]
375
+ );
376
+
377
+ if (hasAbility) {
378
+ seenUuids.add(uuid);
379
+ matchingDevices.push(device);
380
+ }
381
+ }
382
+
383
+ // For garage, also try by device type if no matches found
384
+ if (testType.toLowerCase() === 'garage' && matchingDevices.length === 0) {
385
+ for (const device of allDevices) {
386
+ const uuid = device.dev?.uuid || device.uuid;
387
+ if (seenUuids.has(uuid)) {
388
+ continue;
389
+ }
390
+
391
+ const baseDeviceType = device.dev?.deviceType;
392
+ const subdeviceType = device.type || device._type;
393
+ const matchesType = baseDeviceType === 'msg100' || subdeviceType === 'msg100';
394
+
395
+ if (matchesType && (device.onlineStatus === OnlineStatus.ONLINE ||
396
+ device.dev?.onlineStatus === OnlineStatus.ONLINE)) {
397
+ seenUuids.add(uuid);
398
+ matchingDevices.push(device);
399
+ }
400
+ }
401
+ }
402
+
403
+ return matchingDevices;
404
+ }
405
+
406
+ module.exports = {
407
+ runTest,
408
+ runSingleTest,
409
+ getAvailableTestTypes,
410
+ getTestDescription,
411
+ getTestFile,
412
+ findDevicesForTestType,
413
+ getDeviceName,
414
+ formatError
415
+ };
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Runtime Device Tests
5
+ * Tests device runtime and system runtime information
6
+ */
7
+
8
+ const { waitForDeviceConnection, getDeviceName, OnlineStatus } = require('./test-helper');
9
+
10
+ const metadata = {
11
+ name: 'runtime',
12
+ description: 'Tests device runtime and system runtime information',
13
+ requiredAbilities: ['Appliance.System.Runtime'],
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 testDevices = devices || [];
24
+ if (testDevices.length === 0) {
25
+ // Runtime is typically available on most devices, so we'll test with any online device
26
+ const allDevices = manager.getAllDevices();
27
+ testDevices = allDevices.filter(device => {
28
+ const status = device.onlineStatus !== undefined ? device.onlineStatus : (device.dev?.onlineStatus);
29
+ if (status !== OnlineStatus.ONLINE) return false;
30
+ // Check if device has runtime ability or if it's a common device type
31
+ return device._abilities && (
32
+ device._abilities['Appliance.System.Runtime'] ||
33
+ // Most devices support runtime, so we'll test with any online device
34
+ true
35
+ );
36
+ }).slice(0, 3); // Test up to 3 devices
37
+ }
38
+
39
+ // Wait for devices to be connected
40
+ for (const device of testDevices) {
41
+ await waitForDeviceConnection(device, timeout);
42
+ await new Promise(resolve => setTimeout(resolve, 1000));
43
+ }
44
+
45
+ if (testDevices.length === 0) {
46
+ results.push({
47
+ name: 'should get runtime information',
48
+ passed: false,
49
+ skipped: true,
50
+ error: 'No device with runtime support has been found to run this test',
51
+ device: null
52
+ });
53
+ return results;
54
+ }
55
+
56
+ // Test 1: Get runtime information
57
+ let devicesWithRuntime = 0;
58
+ for (const device of testDevices) {
59
+ const deviceName = getDeviceName(device);
60
+
61
+ try {
62
+ // Check if device supports runtime
63
+ if (typeof device.updateRuntimeInfo !== 'function') {
64
+ continue; // Skip this device
65
+ }
66
+
67
+ const runtimeInfo = await device.updateRuntimeInfo();
68
+
69
+ if (!runtimeInfo) {
70
+ results.push({
71
+ name: 'should get runtime information',
72
+ passed: false,
73
+ skipped: false,
74
+ error: 'updateRuntimeInfo returned null or undefined',
75
+ device: deviceName
76
+ });
77
+ } else if (typeof runtimeInfo !== 'object') {
78
+ results.push({
79
+ name: 'should get runtime information',
80
+ passed: false,
81
+ skipped: false,
82
+ error: 'Runtime info is not an object',
83
+ device: deviceName
84
+ });
85
+ } else {
86
+ devicesWithRuntime++;
87
+ // Only add result for first device to avoid too many results
88
+ if (devicesWithRuntime === 1) {
89
+ results.push({
90
+ name: 'should get runtime information',
91
+ passed: true,
92
+ skipped: false,
93
+ error: null,
94
+ device: deviceName,
95
+ details: { runtimeInfo: runtimeInfo }
96
+ });
97
+ }
98
+ }
99
+ } catch (error) {
100
+ // Only report error for first device
101
+ if (devicesWithRuntime === 0) {
102
+ results.push({
103
+ name: 'should get runtime information',
104
+ passed: false,
105
+ skipped: false,
106
+ error: error.message,
107
+ device: deviceName
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ if (devicesWithRuntime === 0) {
114
+ results.push({
115
+ name: 'should get runtime information',
116
+ passed: false,
117
+ skipped: true,
118
+ error: 'No devices support updateRuntimeInfo',
119
+ device: null
120
+ });
121
+ }
122
+
123
+ // Test 2: Cache runtime information
124
+ const testDevice = testDevices[0];
125
+ const deviceName = getDeviceName(testDevice);
126
+
127
+ try {
128
+ if (typeof testDevice.updateRuntimeInfo !== 'function') {
129
+ results.push({
130
+ name: 'should cache runtime information',
131
+ passed: false,
132
+ skipped: true,
133
+ error: 'Device does not support runtime info',
134
+ device: deviceName
135
+ });
136
+ } else {
137
+ // Update runtime info
138
+ await testDevice.updateRuntimeInfo();
139
+ await new Promise(resolve => setTimeout(resolve, 1000));
140
+
141
+ // Get cached runtime info
142
+ const cachedInfo = testDevice.cachedSystemRuntimeInfo;
143
+
144
+ if (!cachedInfo) {
145
+ results.push({
146
+ name: 'should cache runtime information',
147
+ passed: false,
148
+ skipped: false,
149
+ error: 'Cached runtime info is null or undefined',
150
+ device: deviceName
151
+ });
152
+ } else if (typeof cachedInfo !== 'object') {
153
+ results.push({
154
+ name: 'should cache runtime information',
155
+ passed: false,
156
+ skipped: false,
157
+ error: 'Cached runtime info is not an object',
158
+ device: deviceName
159
+ });
160
+ } else {
161
+ results.push({
162
+ name: 'should cache runtime information',
163
+ passed: true,
164
+ skipped: false,
165
+ error: null,
166
+ device: deviceName,
167
+ details: { cachedInfo: cachedInfo }
168
+ });
169
+ }
170
+ }
171
+ } catch (error) {
172
+ results.push({
173
+ name: 'should cache runtime information',
174
+ passed: false,
175
+ skipped: false,
176
+ error: error.message,
177
+ device: deviceName
178
+ });
179
+ }
180
+
181
+ // Test 3: Update runtime info during refreshState
182
+ try {
183
+ if (typeof testDevice.refreshState !== 'function' || typeof testDevice.updateRuntimeInfo !== 'function') {
184
+ results.push({
185
+ name: 'should update runtime info during refreshState',
186
+ passed: false,
187
+ skipped: true,
188
+ error: 'Device does not support refreshState with runtime',
189
+ device: deviceName
190
+ });
191
+ } else {
192
+ // Call refreshState which should also update runtime info
193
+ await testDevice.refreshState();
194
+ await new Promise(resolve => setTimeout(resolve, 1000));
195
+
196
+ // Verify runtime info was updated
197
+ const cachedInfo = testDevice.cachedSystemRuntimeInfo;
198
+
199
+ if (!cachedInfo) {
200
+ results.push({
201
+ name: 'should update runtime info during refreshState',
202
+ passed: false,
203
+ skipped: false,
204
+ error: 'Runtime info was not cached after refreshState',
205
+ device: deviceName
206
+ });
207
+ } else {
208
+ results.push({
209
+ name: 'should update runtime info during refreshState',
210
+ passed: true,
211
+ skipped: false,
212
+ error: null,
213
+ device: deviceName,
214
+ details: { runtimeInfo: cachedInfo }
215
+ });
216
+ }
217
+ }
218
+ } catch (error) {
219
+ results.push({
220
+ name: 'should update runtime info during refreshState',
221
+ passed: false,
222
+ skipped: false,
223
+ error: error.message,
224
+ device: deviceName
225
+ });
226
+ }
227
+
228
+ return results;
229
+ }
230
+
231
+ module.exports = {
232
+ metadata,
233
+ runTests
234
+ };