meross-iot 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 (99) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/index.d.ts +2344 -0
  5. package/index.js +131 -0
  6. package/lib/controller/device.js +1317 -0
  7. package/lib/controller/features/alarm-feature.js +89 -0
  8. package/lib/controller/features/child-lock-feature.js +61 -0
  9. package/lib/controller/features/config-feature.js +54 -0
  10. package/lib/controller/features/consumption-feature.js +210 -0
  11. package/lib/controller/features/control-feature.js +62 -0
  12. package/lib/controller/features/diffuser-feature.js +411 -0
  13. package/lib/controller/features/digest-timer-feature.js +22 -0
  14. package/lib/controller/features/digest-trigger-feature.js +22 -0
  15. package/lib/controller/features/dnd-feature.js +79 -0
  16. package/lib/controller/features/electricity-feature.js +144 -0
  17. package/lib/controller/features/encryption-feature.js +259 -0
  18. package/lib/controller/features/garage-feature.js +337 -0
  19. package/lib/controller/features/hub-feature.js +687 -0
  20. package/lib/controller/features/light-feature.js +408 -0
  21. package/lib/controller/features/presence-sensor-feature.js +297 -0
  22. package/lib/controller/features/roller-shutter-feature.js +456 -0
  23. package/lib/controller/features/runtime-feature.js +74 -0
  24. package/lib/controller/features/screen-feature.js +67 -0
  25. package/lib/controller/features/sensor-history-feature.js +47 -0
  26. package/lib/controller/features/smoke-config-feature.js +50 -0
  27. package/lib/controller/features/spray-feature.js +166 -0
  28. package/lib/controller/features/system-feature.js +269 -0
  29. package/lib/controller/features/temp-unit-feature.js +55 -0
  30. package/lib/controller/features/thermostat-feature.js +804 -0
  31. package/lib/controller/features/timer-feature.js +507 -0
  32. package/lib/controller/features/toggle-feature.js +223 -0
  33. package/lib/controller/features/trigger-feature.js +333 -0
  34. package/lib/controller/hub-device.js +185 -0
  35. package/lib/controller/subdevice.js +1537 -0
  36. package/lib/device-factory.js +463 -0
  37. package/lib/error-budget.js +138 -0
  38. package/lib/http-api.js +766 -0
  39. package/lib/manager.js +1609 -0
  40. package/lib/model/channel-info.js +79 -0
  41. package/lib/model/constants.js +119 -0
  42. package/lib/model/enums.js +819 -0
  43. package/lib/model/exception.js +363 -0
  44. package/lib/model/http/device.js +215 -0
  45. package/lib/model/http/error-codes.js +121 -0
  46. package/lib/model/http/exception.js +151 -0
  47. package/lib/model/http/subdevice.js +133 -0
  48. package/lib/model/push/alarm.js +112 -0
  49. package/lib/model/push/bind.js +97 -0
  50. package/lib/model/push/common.js +282 -0
  51. package/lib/model/push/diffuser-light.js +100 -0
  52. package/lib/model/push/diffuser-spray.js +83 -0
  53. package/lib/model/push/factory.js +229 -0
  54. package/lib/model/push/generic.js +115 -0
  55. package/lib/model/push/hub-battery.js +59 -0
  56. package/lib/model/push/hub-mts100-all.js +64 -0
  57. package/lib/model/push/hub-mts100-mode.js +59 -0
  58. package/lib/model/push/hub-mts100-temperature.js +62 -0
  59. package/lib/model/push/hub-online.js +59 -0
  60. package/lib/model/push/hub-sensor-alert.js +61 -0
  61. package/lib/model/push/hub-sensor-all.js +59 -0
  62. package/lib/model/push/hub-sensor-smoke.js +110 -0
  63. package/lib/model/push/hub-sensor-temphum.js +62 -0
  64. package/lib/model/push/hub-subdevicelist.js +50 -0
  65. package/lib/model/push/hub-togglex.js +60 -0
  66. package/lib/model/push/index.js +81 -0
  67. package/lib/model/push/online.js +53 -0
  68. package/lib/model/push/presence-study.js +61 -0
  69. package/lib/model/push/sensor-latestx.js +106 -0
  70. package/lib/model/push/timerx.js +63 -0
  71. package/lib/model/push/togglex.js +78 -0
  72. package/lib/model/push/triggerx.js +62 -0
  73. package/lib/model/push/unbind.js +34 -0
  74. package/lib/model/push/water-leak.js +107 -0
  75. package/lib/model/states/diffuser-light-state.js +119 -0
  76. package/lib/model/states/diffuser-spray-state.js +58 -0
  77. package/lib/model/states/garage-door-state.js +71 -0
  78. package/lib/model/states/index.js +38 -0
  79. package/lib/model/states/light-state.js +134 -0
  80. package/lib/model/states/presence-sensor-state.js +239 -0
  81. package/lib/model/states/roller-shutter-state.js +82 -0
  82. package/lib/model/states/spray-state.js +58 -0
  83. package/lib/model/states/thermostat-state.js +297 -0
  84. package/lib/model/states/timer-state.js +192 -0
  85. package/lib/model/states/toggle-state.js +105 -0
  86. package/lib/model/states/trigger-state.js +155 -0
  87. package/lib/subscription.js +587 -0
  88. package/lib/utilities/conversion.js +62 -0
  89. package/lib/utilities/debug.js +165 -0
  90. package/lib/utilities/mqtt.js +152 -0
  91. package/lib/utilities/network.js +53 -0
  92. package/lib/utilities/options.js +64 -0
  93. package/lib/utilities/request-queue.js +161 -0
  94. package/lib/utilities/ssid.js +37 -0
  95. package/lib/utilities/state-changes.js +66 -0
  96. package/lib/utilities/stats.js +687 -0
  97. package/lib/utilities/timer.js +310 -0
  98. package/lib/utilities/trigger.js +286 -0
  99. package/package.json +73 -0
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const { MqttStatsCounter, HttpStatsCounter } = require('./stats');
4
+
5
+ /**
6
+ * Debugging utilities for Meross manager development and troubleshooting.
7
+ *
8
+ * Provides access to internal debugging methods that expose implementation details
9
+ * useful for development and troubleshooting. These methods are not part of the
10
+ * stable public API and may change between versions.
11
+ *
12
+ * @module utilities/debug
13
+ */
14
+
15
+ /**
16
+ * Creates debugging utilities for a MerossManager instance
17
+ *
18
+ * @param {MerossManager} manager - MerossManager instance
19
+ * @returns {Object} Debug utilities object
20
+ */
21
+ function createDebugUtils(manager) {
22
+ return {
23
+ /**
24
+ * Gets the current error budget for a device.
25
+ *
26
+ * Error budgets prevent repeated failed communication attempts from consuming
27
+ * excessive resources. When the budget is exhausted, LAN HTTP communication is
28
+ * temporarily disabled for that device to avoid flooding the network with failed requests.
29
+ *
30
+ * @param {string} deviceUuid - Device UUID
31
+ * @returns {number} Current error budget (number of errors remaining)
32
+ */
33
+ getErrorBudget(deviceUuid) {
34
+ return manager._errorBudgetManager.getBudget(deviceUuid);
35
+ },
36
+
37
+ /**
38
+ * Resets the error budget for a device.
39
+ *
40
+ * Manually resets a device's error budget, immediately re-enabling LAN HTTP
41
+ * communication. Useful when a device's budget was exhausted due to temporary
42
+ * network issues that have since been resolved.
43
+ *
44
+ * @param {string} deviceUuid - Device UUID
45
+ */
46
+ resetErrorBudget(deviceUuid) {
47
+ manager._errorBudgetManager.resetBudget(deviceUuid);
48
+ },
49
+
50
+ /**
51
+ * Gets MQTT statistics for a given time window.
52
+ *
53
+ * Returns aggregated statistics about MQTT API calls if statistics tracking is enabled.
54
+ * Statistics include total calls, calls grouped by method/namespace combination, and
55
+ * calls grouped by device. Useful for monitoring API usage patterns and identifying
56
+ * potential bottlenecks.
57
+ *
58
+ * @param {number} [timeWindowMs=60000] - Time window in milliseconds (default: 1 minute)
59
+ * @returns {ApiStatsResult|null} Statistics result object or null if statistics not enabled
60
+ */
61
+ getMqttStats(timeWindowMs = 60000) {
62
+ if (!manager._mqttStatsCounter) {
63
+ return null;
64
+ }
65
+ return manager._mqttStatsCounter.getApiStats(timeWindowMs);
66
+ },
67
+
68
+ /**
69
+ * Gets HTTP statistics for a given time window.
70
+ *
71
+ * Returns aggregated statistics about HTTP API calls if statistics tracking is enabled.
72
+ * Statistics include total calls, calls grouped by HTTP status code, calls grouped by
73
+ * Meross API status code, and calls grouped by URL. Useful for monitoring API health
74
+ * and identifying error patterns.
75
+ *
76
+ * @param {number} [timeWindowMs=60000] - Time window in milliseconds (default: 1 minute)
77
+ * @returns {HttpStatsResult|null} Statistics result object or null if statistics not enabled
78
+ */
79
+ getHttpStats(timeWindowMs = 60000) {
80
+ if (!manager.httpClient || !manager.httpClient.stats) {
81
+ return null;
82
+ }
83
+ return manager.httpClient.stats.getStats(timeWindowMs);
84
+ },
85
+
86
+ /**
87
+ * Gets delayed MQTT statistics for a given time window.
88
+ *
89
+ * Returns statistics about MQTT API calls that were delayed (e.g., due to rate limiting
90
+ * or queue management) if statistics tracking is enabled. High numbers of delayed calls
91
+ * may indicate that request rates exceed device capabilities or queue capacity limits.
92
+ *
93
+ * @param {number} [timeWindowMs=60000] - Time window in milliseconds (default: 1 minute)
94
+ * @returns {ApiStatsResult|null} Statistics result object or null if statistics not enabled
95
+ */
96
+ getDelayedMqttStats(timeWindowMs = 60000) {
97
+ if (!manager._mqttStatsCounter) {
98
+ return null;
99
+ }
100
+ return manager._mqttStatsCounter.getDelayedApiStats(timeWindowMs);
101
+ },
102
+
103
+ /**
104
+ * Gets dropped MQTT statistics for a given time window.
105
+ *
106
+ * Returns statistics about MQTT API calls that were dropped (e.g., due to queue overflow
107
+ * or resource constraints) if statistics tracking is enabled. Dropped calls indicate that
108
+ * the system could not process all requests, potentially requiring queue size adjustments
109
+ * or reduced request rates.
110
+ *
111
+ * @param {number} [timeWindowMs=60000] - Time window in milliseconds (default: 1 minute)
112
+ * @returns {ApiStatsResult|null} Statistics result object or null if statistics not enabled
113
+ */
114
+ getDroppedMqttStats(timeWindowMs = 60000) {
115
+ if (!manager._mqttStatsCounter) {
116
+ return null;
117
+ }
118
+ return manager._mqttStatsCounter.getDroppedApiStats(timeWindowMs);
119
+ },
120
+
121
+ /**
122
+ * Enables statistics tracking for HTTP and MQTT requests.
123
+ *
124
+ * Initializes statistics collection for both HTTP and MQTT communication channels.
125
+ * Statistics are stored in memory with a configurable limit to prevent unbounded growth.
126
+ *
127
+ * @param {number} [maxStatsSamples=1000] - Maximum number of samples to keep in statistics
128
+ */
129
+ enableStats(maxStatsSamples = 1000) {
130
+ if (!manager._mqttStatsCounter) {
131
+ manager._mqttStatsCounter = new MqttStatsCounter(maxStatsSamples);
132
+ }
133
+ if (!manager.httpClient.stats) {
134
+ manager.httpClient._httpStatsCounter = new HttpStatsCounter(maxStatsSamples);
135
+ }
136
+ },
137
+
138
+ /**
139
+ * Disables statistics tracking for HTTP and MQTT requests.
140
+ *
141
+ * Stops collecting statistics and releases associated memory. Existing statistics
142
+ * are discarded when tracking is disabled.
143
+ */
144
+ disableStats() {
145
+ manager._mqttStatsCounter = null;
146
+ if (manager.httpClient) {
147
+ manager.httpClient._httpStatsCounter = null;
148
+ }
149
+ },
150
+
151
+ /**
152
+ * Checks if statistics tracking is enabled.
153
+ *
154
+ * @returns {boolean} True if statistics tracking is enabled, false otherwise
155
+ */
156
+ isStatsEnabled() {
157
+ return manager._mqttStatsCounter !== null && manager.httpClient && manager.httpClient.stats !== null;
158
+ }
159
+ };
160
+ }
161
+
162
+ module.exports = {
163
+ createDebugUtils
164
+ };
165
+
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { v4: uuidv4 } = require('uuid');
5
+
6
+ /**
7
+ * Builds the MQTT topic where commands should be sent to specific devices.
8
+ *
9
+ * Meross devices subscribe to this topic pattern to receive commands. The topic structure
10
+ * follows the Meross protocol convention: /appliance/{deviceUuid}/subscribe
11
+ *
12
+ * @param {string} clientUuid - Device UUID
13
+ * @returns {string} MQTT topic string
14
+ * @example
15
+ * const topic = buildDeviceRequestTopic('device-uuid-123');
16
+ * // Returns: '/appliance/device-uuid-123/subscribe'
17
+ */
18
+ function buildDeviceRequestTopic(clientUuid) {
19
+ return `/appliance/${clientUuid}/subscribe`;
20
+ }
21
+
22
+ /**
23
+ * Builds the MQTT topic where devices send acknowledgment responses to commands.
24
+ *
25
+ * The client application subscribes to this topic to receive responses from devices.
26
+ * The topic combines userId and appId to create a unique subscription channel per application instance.
27
+ *
28
+ * @param {string} userId - User ID
29
+ * @param {string} appId - Application ID
30
+ * @returns {string} MQTT topic string
31
+ * @example
32
+ * const topic = buildClientResponseTopic('user123', 'app456');
33
+ * // Returns: '/app/user123-app456/subscribe'
34
+ */
35
+ function buildClientResponseTopic(userId, appId) {
36
+ return `/app/${userId}-${appId}/subscribe`;
37
+ }
38
+
39
+ /**
40
+ * Builds the MQTT topic where user push notifications are received.
41
+ *
42
+ * This topic is used for device-initiated push notifications (e.g., state changes, alerts)
43
+ * that are broadcast to all of a user's applications, as opposed to command responses
44
+ * which are sent to a specific app instance.
45
+ *
46
+ * @param {string} userId - User ID
47
+ * @returns {string} MQTT topic string
48
+ * @example
49
+ * const topic = buildClientUserTopic('user123');
50
+ * // Returns: '/app/user123/subscribe'
51
+ */
52
+ function buildClientUserTopic(userId) {
53
+ return `/app/${userId}/subscribe`;
54
+ }
55
+
56
+ /**
57
+ * Extracts the device UUID from the "from" header of received messages.
58
+ *
59
+ * Meross MQTT messages include a "from" field containing the topic path. The device UUID
60
+ * is located at a fixed position in this path, allowing extraction without full topic parsing.
61
+ *
62
+ * @param {string} fromTopic - The "from" topic string from message header
63
+ * @returns {string} Device UUID
64
+ * @example
65
+ * const uuid = deviceUuidFromPushNotification('/appliance/device-uuid-123/subscribe');
66
+ * // Returns: 'device-uuid-123'
67
+ */
68
+ function deviceUuidFromPushNotification(fromTopic) {
69
+ return fromTopic.split('/')[2];
70
+ }
71
+
72
+ /**
73
+ * Generates a new app ID and client ID for MQTT connection.
74
+ *
75
+ * Meross MQTT protocol requires unique identifiers for each application instance. This function
76
+ * generates a random UUID, hashes it with MD5 (as required by the protocol), and formats the
77
+ * client ID with the 'app:' prefix that Meross servers expect.
78
+ *
79
+ * @returns {Object} Object with appId and clientId properties
80
+ * @returns {string} returns.appId - Application ID (MD5 hash)
81
+ * @returns {string} returns.clientId - Client ID in format 'app:{appId}'
82
+ * @example
83
+ * const { appId, clientId } = generateClientAndAppId();
84
+ * // appId: 'a1b2c3d4e5f6...'
85
+ * // clientId: 'app:a1b2c3d4e5f6...'
86
+ */
87
+ function generateClientAndAppId() {
88
+ const md5Hash = crypto.createHash('md5');
89
+ const rndUuid = uuidv4();
90
+ md5Hash.update(`API${rndUuid}`);
91
+ const appId = md5Hash.digest('hex');
92
+ const clientId = `app:${appId}`;
93
+ return { appId, clientId };
94
+ }
95
+
96
+ /**
97
+ * Generates the MQTT password for connecting to Meross MQTT servers.
98
+ *
99
+ * Meross requires passwords to be MD5 hashes of the concatenated userId and cloud key.
100
+ * This matches the authentication mechanism used by the official Meross mobile app.
101
+ *
102
+ * @param {string} userId - User ID
103
+ * @param {string} key - Meross cloud key
104
+ * @returns {string} MD5 hash of userId + key
105
+ * @example
106
+ * const password = generateMqttPassword('user123', 'secret-key');
107
+ * // Returns: 'a1b2c3d4e5f6...' (MD5 hash)
108
+ */
109
+ function generateMqttPassword(userId, key) {
110
+ const md5Hash = crypto.createHash('md5');
111
+ const clearPwd = `${userId}${key}`;
112
+ md5Hash.update(clearPwd);
113
+ return md5Hash.digest('hex');
114
+ }
115
+
116
+ /**
117
+ * Verifies if a message header has a valid signature.
118
+ *
119
+ * Meross messages include signatures to prevent tampering. The signature is computed as
120
+ * MD5(messageId + key + timestamp) and must match the sign field in the header. Comparison
121
+ * is case-insensitive to handle variations in how signatures are encoded.
122
+ *
123
+ * @param {Object} header - Message header object
124
+ * @param {string} header.messageId - Message ID
125
+ * @param {string} header.sign - Signature to verify
126
+ * @param {number} header.timestamp - Timestamp
127
+ * @param {string} key - Meross cloud key
128
+ * @returns {boolean} True if signature is valid, false otherwise
129
+ * @example
130
+ * const isValid = verifyMessageSignature(
131
+ * { messageId: 'abc123', sign: 'def456', timestamp: 1234567890 },
132
+ * 'secret-key'
133
+ * );
134
+ */
135
+ function verifyMessageSignature(header, key) {
136
+ const messageHash = crypto.createHash('md5');
137
+ const strToHash = `${header.messageId}${key}${header.timestamp}`;
138
+ messageHash.update(strToHash);
139
+ const expectedSignature = messageHash.digest('hex').toLowerCase();
140
+ return expectedSignature === header.sign.toLowerCase();
141
+ }
142
+
143
+ module.exports = {
144
+ buildDeviceRequestTopic,
145
+ buildClientResponseTopic,
146
+ buildClientUserTopic,
147
+ deviceUuidFromPushNotification,
148
+ generateClientAndAppId,
149
+ generateMqttPassword,
150
+ verifyMessageSignature
151
+ };
152
+
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extracts the domain/hostname from an address string.
5
+ *
6
+ * Parses addresses that may include port numbers, returning only the hostname portion.
7
+ * Used when separating hostname and port information from Meross device addresses.
8
+ *
9
+ * @param {string} address - Address string in format "hostname:port" or just "hostname"
10
+ * @returns {string|null} Domain/hostname portion, or null if address is empty
11
+ * @example
12
+ * const host = extractDomain('iot.meross.com:2001'); // Returns 'iot.meross.com'
13
+ * const host2 = extractDomain('iot.meross.com'); // Returns 'iot.meross.com'
14
+ */
15
+ function extractDomain(address) {
16
+ if (!address) {
17
+ return null;
18
+ }
19
+ const tokens = address.split(':');
20
+ return tokens[0];
21
+ }
22
+
23
+ /**
24
+ * Extracts the port from an address string.
25
+ *
26
+ * Parses addresses that may include port numbers, returning the port if present or
27
+ * falling back to the provided default. Handles invalid port values gracefully by
28
+ * returning the default rather than throwing errors.
29
+ *
30
+ * @param {string} address - Address string in format "hostname:port" or just "hostname"
31
+ * @param {number} defaultPort - Default port to return if no port is specified
32
+ * @returns {number} Port number
33
+ * @example
34
+ * const port = extractPort('iot.meross.com:2001', 2001); // Returns 2001
35
+ * const port2 = extractPort('iot.meross.com', 2001); // Returns 2001 (default)
36
+ */
37
+ function extractPort(address, defaultPort) {
38
+ if (!address) {
39
+ return defaultPort;
40
+ }
41
+ const tokens = address.split(':');
42
+ if (tokens.length > 1) {
43
+ const port = parseInt(tokens[1], 10);
44
+ return isNaN(port) ? defaultPort : port;
45
+ }
46
+ return defaultPort;
47
+ }
48
+
49
+ module.exports = {
50
+ extractDomain,
51
+ extractPort
52
+ };
53
+
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Options normalization utilities.
5
+ *
6
+ * Provides helper functions for normalizing and validating options objects used across
7
+ * feature methods. These utilities ensure consistent handling of optional parameters,
8
+ * default values, and required field validation.
9
+ *
10
+ * @module utilities/options
11
+ */
12
+
13
+ /**
14
+ * Normalizes channel parameter from options object.
15
+ *
16
+ * Many Meross devices support multiple channels (e.g., multi-outlet power strips).
17
+ * This function extracts the channel number from options, defaulting to 0 (first channel)
18
+ * if not specified. Handles null or non-object inputs gracefully.
19
+ *
20
+ * @param {Object} options - Options object
21
+ * @param {number} [defaultChannel=0] - Default channel value if not specified
22
+ * @returns {number} Channel number
23
+ * @example
24
+ * normalizeChannel({channel: 1}); // Returns 1
25
+ * normalizeChannel({}); // Returns 0
26
+ * normalizeChannel({}, 1); // Returns 1
27
+ */
28
+ function normalizeChannel(options = {}, defaultChannel = 0) {
29
+ if (options === null || typeof options !== 'object') {
30
+ return defaultChannel;
31
+ }
32
+ return options.channel !== undefined ? options.channel : defaultChannel;
33
+ }
34
+
35
+ /**
36
+ * Validates that required fields are present in options object.
37
+ *
38
+ * Ensures that all specified required fields exist in the options object before
39
+ * proceeding with operations that depend on them. Provides clear error messages
40
+ * listing all missing fields when validation fails.
41
+ *
42
+ * @param {Object} options - Options object to validate
43
+ * @param {Array<string>} requiredFields - Array of required field names
44
+ * @throws {Error} If any required field is missing
45
+ * @example
46
+ * validateRequired({onoff: true}, ['onoff']); // OK
47
+ * validateRequired({}, ['onoff']); // Throws error
48
+ */
49
+ function validateRequired(options = {}, requiredFields = []) {
50
+ if (!options || typeof options !== 'object') {
51
+ throw new Error('Options must be an object');
52
+ }
53
+
54
+ const missing = requiredFields.filter(field => options[field] === undefined);
55
+ if (missing.length > 0) {
56
+ throw new Error(`Missing required fields: ${missing.join(', ')}`);
57
+ }
58
+ }
59
+
60
+ module.exports = {
61
+ normalizeChannel,
62
+ validateRequired
63
+ };
64
+
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Manages per-device request queues with batch processing and inter-batch delays.
5
+ *
6
+ * Meross devices enforce rate limits on API requests. This class throttles requests
7
+ * per device by processing them in configurable batches with delays between batches,
8
+ * preventing rate limit violations while maintaining reasonable throughput. Each device
9
+ * has an isolated queue to ensure fair resource allocation.
10
+ *
11
+ * @module utilities/request-queue
12
+ */
13
+ class RequestQueue {
14
+ /**
15
+ * Creates a new RequestQueue instance.
16
+ *
17
+ * @param {Object} options - Configuration options
18
+ * @param {number} [options.batchSize=3] - Maximum concurrent requests per device per batch.
19
+ * Higher values increase throughput but risk hitting rate limits.
20
+ * @param {number} [options.batchDelay=100] - Delay in milliseconds between batches.
21
+ * Prevents overwhelming devices with rapid successive batches.
22
+ * @param {Function} [options.logger] - Optional logger function for debugging
23
+ */
24
+ constructor(options = {}) {
25
+ this.batchSize = options.batchSize || 3;
26
+ this.batchDelay = options.batchDelay || 100;
27
+ this.logger = options.logger;
28
+
29
+ this._queues = new Map();
30
+ this._processing = new Map();
31
+ }
32
+
33
+ /**
34
+ * Enqueues a request for asynchronous processing.
35
+ *
36
+ * Adds a request to the device's queue and returns a promise that resolves when
37
+ * the request completes. The queue handles throttling and ordering automatically,
38
+ * allowing callers to await results without managing rate limits directly.
39
+ *
40
+ * @param {string} deviceUuid - Device UUID
41
+ * @param {Function} requestFn - Function that returns a Promise
42
+ * @returns {Promise} Promise that resolves when the request completes
43
+ */
44
+ enqueue(deviceUuid, requestFn) {
45
+ return new Promise((resolve, reject) => {
46
+ const queueEntry = {
47
+ requestFn,
48
+ resolve,
49
+ reject
50
+ };
51
+
52
+ if (!this._queues.has(deviceUuid)) {
53
+ this._queues.set(deviceUuid, []);
54
+ }
55
+ const queue = this._queues.get(deviceUuid);
56
+ queue.push(queueEntry);
57
+
58
+ if (!this._processing.get(deviceUuid)) {
59
+ this._processQueue(deviceUuid);
60
+ }
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Processes queued requests for a device in batches with delays.
66
+ *
67
+ * Uses a processing flag to prevent concurrent execution, ensuring only one
68
+ * batch processor runs per device at a time. This prevents race conditions
69
+ * and ensures requests are processed in order.
70
+ *
71
+ * @private
72
+ * @param {string} deviceUuid - Device UUID
73
+ */
74
+ async _processQueue(deviceUuid) {
75
+ this._processing.set(deviceUuid, true);
76
+
77
+ try {
78
+ const queue = this._queues.get(deviceUuid);
79
+ if (!queue || queue.length === 0) {
80
+ return;
81
+ }
82
+
83
+ while (queue.length > 0) {
84
+ const batch = queue.splice(0, this.batchSize);
85
+
86
+ await this._processBatch(deviceUuid, batch);
87
+
88
+ if (queue.length > 0) {
89
+ await new Promise(resolve => setTimeout(resolve, this.batchDelay));
90
+ }
91
+ }
92
+ } finally {
93
+ this._processing.set(deviceUuid, false);
94
+
95
+ const queue = this._queues.get(deviceUuid);
96
+ if (!queue || queue.length === 0) {
97
+ this._queues.delete(deviceUuid);
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Executes a batch of requests concurrently and forwards results to callers.
104
+ *
105
+ * Each request's promise is resolved or rejected individually, allowing partial
106
+ * success within a batch without affecting other requests. This ensures that
107
+ * one failed request doesn't prevent others in the same batch from completing.
108
+ *
109
+ * @private
110
+ * @param {string} deviceUuid - Device UUID
111
+ * @param {Array} batch - Array of queue entries
112
+ */
113
+ async _processBatch(deviceUuid, batch) {
114
+ const promises = batch.map(entry => {
115
+ return Promise.resolve(entry.requestFn())
116
+ .then(result => {
117
+ entry.resolve(result);
118
+ return { success: true, result };
119
+ })
120
+ .catch(error => {
121
+ entry.reject(error);
122
+ return { success: false, error };
123
+ });
124
+ });
125
+
126
+ await Promise.all(promises);
127
+ }
128
+
129
+ /**
130
+ * Clears all pending requests for a device and rejects their promises.
131
+ *
132
+ * Useful when a device disconnects or needs to be reset, preventing
133
+ * stale requests from executing after the device is no longer available.
134
+ *
135
+ * @param {string} deviceUuid - Device UUID
136
+ */
137
+ clearQueue(deviceUuid) {
138
+ const queue = this._queues.get(deviceUuid);
139
+ if (queue) {
140
+ queue.forEach(entry => {
141
+ entry.reject(new Error(`Queue cleared for device ${deviceUuid}`));
142
+ });
143
+ this._queues.delete(deviceUuid);
144
+ }
145
+ this._processing.delete(deviceUuid);
146
+ }
147
+
148
+ /**
149
+ * Returns the number of pending requests for a device.
150
+ *
151
+ * @param {string} deviceUuid - Device UUID
152
+ * @returns {number} Number of queued requests, or 0 if no queue exists
153
+ */
154
+ getQueueLength(deviceUuid) {
155
+ const queue = this._queues.get(deviceUuid);
156
+ return queue ? queue.length : 0;
157
+ }
158
+ }
159
+
160
+ module.exports = RequestQueue;
161
+
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Decodes a base64-encoded SSID string.
5
+ *
6
+ * Meross devices store WiFi SSIDs as base64-encoded strings in their configuration.
7
+ * This function decodes them to their original UTF-8 representation for display or
8
+ * comparison purposes. Returns the original string if decoding fails, allowing the
9
+ * function to handle both encoded and already-decoded SSIDs gracefully.
10
+ *
11
+ * @param {string} encodedSSID - Base64-encoded SSID string
12
+ * @returns {string} Decoded SSID string, or original string if decoding fails
13
+ * @example
14
+ * const decoded = decodeSSID('SG9tZQ=='); // Returns "Home"
15
+ * const decoded2 = decodeSSID('not-base64'); // Returns "not-base64" (not valid base64)
16
+ */
17
+ function decodeSSID(encodedSSID) {
18
+ if (!encodedSSID || typeof encodedSSID !== 'string') {
19
+ return encodedSSID || '';
20
+ }
21
+
22
+ try {
23
+ const decoded = Buffer.from(encodedSSID, 'base64').toString('utf-8');
24
+ if (decoded && decoded.length > 0) {
25
+ return decoded;
26
+ }
27
+ } catch (error) {
28
+ // Decoding failed, return original string
29
+ }
30
+
31
+ return encodedSSID;
32
+ }
33
+
34
+ module.exports = {
35
+ decodeSSID
36
+ };
37
+