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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/index.d.ts +2344 -0
- package/index.js +131 -0
- package/lib/controller/device.js +1317 -0
- package/lib/controller/features/alarm-feature.js +89 -0
- package/lib/controller/features/child-lock-feature.js +61 -0
- package/lib/controller/features/config-feature.js +54 -0
- package/lib/controller/features/consumption-feature.js +210 -0
- package/lib/controller/features/control-feature.js +62 -0
- package/lib/controller/features/diffuser-feature.js +411 -0
- package/lib/controller/features/digest-timer-feature.js +22 -0
- package/lib/controller/features/digest-trigger-feature.js +22 -0
- package/lib/controller/features/dnd-feature.js +79 -0
- package/lib/controller/features/electricity-feature.js +144 -0
- package/lib/controller/features/encryption-feature.js +259 -0
- package/lib/controller/features/garage-feature.js +337 -0
- package/lib/controller/features/hub-feature.js +687 -0
- package/lib/controller/features/light-feature.js +408 -0
- package/lib/controller/features/presence-sensor-feature.js +297 -0
- package/lib/controller/features/roller-shutter-feature.js +456 -0
- package/lib/controller/features/runtime-feature.js +74 -0
- package/lib/controller/features/screen-feature.js +67 -0
- package/lib/controller/features/sensor-history-feature.js +47 -0
- package/lib/controller/features/smoke-config-feature.js +50 -0
- package/lib/controller/features/spray-feature.js +166 -0
- package/lib/controller/features/system-feature.js +269 -0
- package/lib/controller/features/temp-unit-feature.js +55 -0
- package/lib/controller/features/thermostat-feature.js +804 -0
- package/lib/controller/features/timer-feature.js +507 -0
- package/lib/controller/features/toggle-feature.js +223 -0
- package/lib/controller/features/trigger-feature.js +333 -0
- package/lib/controller/hub-device.js +185 -0
- package/lib/controller/subdevice.js +1537 -0
- package/lib/device-factory.js +463 -0
- package/lib/error-budget.js +138 -0
- package/lib/http-api.js +766 -0
- package/lib/manager.js +1609 -0
- package/lib/model/channel-info.js +79 -0
- package/lib/model/constants.js +119 -0
- package/lib/model/enums.js +819 -0
- package/lib/model/exception.js +363 -0
- package/lib/model/http/device.js +215 -0
- package/lib/model/http/error-codes.js +121 -0
- package/lib/model/http/exception.js +151 -0
- package/lib/model/http/subdevice.js +133 -0
- package/lib/model/push/alarm.js +112 -0
- package/lib/model/push/bind.js +97 -0
- package/lib/model/push/common.js +282 -0
- package/lib/model/push/diffuser-light.js +100 -0
- package/lib/model/push/diffuser-spray.js +83 -0
- package/lib/model/push/factory.js +229 -0
- package/lib/model/push/generic.js +115 -0
- package/lib/model/push/hub-battery.js +59 -0
- package/lib/model/push/hub-mts100-all.js +64 -0
- package/lib/model/push/hub-mts100-mode.js +59 -0
- package/lib/model/push/hub-mts100-temperature.js +62 -0
- package/lib/model/push/hub-online.js +59 -0
- package/lib/model/push/hub-sensor-alert.js +61 -0
- package/lib/model/push/hub-sensor-all.js +59 -0
- package/lib/model/push/hub-sensor-smoke.js +110 -0
- package/lib/model/push/hub-sensor-temphum.js +62 -0
- package/lib/model/push/hub-subdevicelist.js +50 -0
- package/lib/model/push/hub-togglex.js +60 -0
- package/lib/model/push/index.js +81 -0
- package/lib/model/push/online.js +53 -0
- package/lib/model/push/presence-study.js +61 -0
- package/lib/model/push/sensor-latestx.js +106 -0
- package/lib/model/push/timerx.js +63 -0
- package/lib/model/push/togglex.js +78 -0
- package/lib/model/push/triggerx.js +62 -0
- package/lib/model/push/unbind.js +34 -0
- package/lib/model/push/water-leak.js +107 -0
- package/lib/model/states/diffuser-light-state.js +119 -0
- package/lib/model/states/diffuser-spray-state.js +58 -0
- package/lib/model/states/garage-door-state.js +71 -0
- package/lib/model/states/index.js +38 -0
- package/lib/model/states/light-state.js +134 -0
- package/lib/model/states/presence-sensor-state.js +239 -0
- package/lib/model/states/roller-shutter-state.js +82 -0
- package/lib/model/states/spray-state.js +58 -0
- package/lib/model/states/thermostat-state.js +297 -0
- package/lib/model/states/timer-state.js +192 -0
- package/lib/model/states/toggle-state.js +105 -0
- package/lib/model/states/trigger-state.js +155 -0
- package/lib/subscription.js +587 -0
- package/lib/utilities/conversion.js +62 -0
- package/lib/utilities/debug.js +165 -0
- package/lib/utilities/mqtt.js +152 -0
- package/lib/utilities/network.js +53 -0
- package/lib/utilities/options.js +64 -0
- package/lib/utilities/request-queue.js +161 -0
- package/lib/utilities/ssid.js +37 -0
- package/lib/utilities/state-changes.js +66 -0
- package/lib/utilities/stats.js +687 -0
- package/lib/utilities/timer.js +310 -0
- package/lib/utilities/trigger.js +286 -0
- 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
|
+
|