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,363 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Base error class for all Meross errors.
5
+ *
6
+ * Provides a common interface for error handling with errorCode property to
7
+ * identify specific API error conditions. All library-specific errors extend
8
+ * this class to enable instanceof checks and consistent error handling
9
+ * throughout the library.
10
+ *
11
+ * @class
12
+ * @extends Error
13
+ * @property {number|null} errorCode - API error code (if available)
14
+ */
15
+ class MerossError extends Error {
16
+ constructor(message, errorCode = null) {
17
+ super(message);
18
+ this.name = this.constructor.name;
19
+ this.errorCode = errorCode;
20
+ Error.captureStackTrace(this, this.constructor);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Authentication related errors (error codes 1000-1008).
26
+ *
27
+ * Thrown when authentication fails due to invalid credentials, account issues,
28
+ * or authentication service problems. Indicates the login request cannot be
29
+ * completed with the provided credentials.
30
+ *
31
+ * Base class for authentication errors. HTTP-specific auth errors are in
32
+ * lib/model/http/exception.js.
33
+ *
34
+ * @class
35
+ * @extends MerossError
36
+ * @property {number} errorCode - API error code (1000-1008)
37
+ */
38
+ class AuthenticationError extends MerossError {
39
+ constructor(message, errorCode) {
40
+ super(message || 'Authentication failed', errorCode);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * API limit reached error (error code 1042).
46
+ *
47
+ * Thrown when the API rate limit has been exceeded. Meross enforces rate limits
48
+ * to prevent abuse and ensure service stability. Requests should be throttled
49
+ * to stay within the allowed rate. Retry after a delay or reduce request frequency.
50
+ *
51
+ * @class
52
+ * @extends MerossError
53
+ * @property {number} errorCode - Always 1042
54
+ */
55
+ class ApiLimitReachedError extends MerossError {
56
+ constructor(message) {
57
+ super(message || 'API top limit reached', 1042);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Resource access denied error (error code 1043).
63
+ *
64
+ * Thrown when access to a resource is denied due to insufficient permissions.
65
+ * The account may not have access to the requested device or resource, or the
66
+ * resource may not exist. Verify the resource exists and the account has
67
+ * appropriate permissions.
68
+ *
69
+ * @class
70
+ * @extends MerossError
71
+ * @property {number} errorCode - Always 1043
72
+ */
73
+ class ResourceAccessDeniedError extends MerossError {
74
+ constructor(message) {
75
+ super(message || 'Resource access deny', 1043);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Device command timeout error.
81
+ *
82
+ * Thrown when a device command doesn't receive a response within the timeout period.
83
+ * Indicates the device may be offline, unreachable, or experiencing network issues
84
+ * that prevent timely communication. Check device connectivity and network status.
85
+ *
86
+ * @class
87
+ * @extends MerossError
88
+ * @property {string|null} deviceUuid - UUID of the device that timed out
89
+ * @property {number|null} timeout - Timeout duration in milliseconds
90
+ * @property {Object|null} command - Command information (method, namespace, etc.)
91
+ */
92
+ class CommandTimeoutError extends MerossError {
93
+ constructor(message, deviceUuid = null, timeout = null, command = null) {
94
+ super(message || 'Command timeout occurred');
95
+ this.deviceUuid = deviceUuid;
96
+ this.timeout = timeout;
97
+ this.command = command;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Device command error (device returned an error response).
103
+ *
104
+ * Thrown when a device command fails and the device returns an error response.
105
+ * The command was received and processed by the device, but the device rejected
106
+ * it or encountered an error during execution. Check the errorPayload for
107
+ * device-specific error details.
108
+ *
109
+ * @class
110
+ * @extends MerossError
111
+ * @property {Object|null} errorPayload - Error payload from device response
112
+ * @property {string|null} deviceUuid - UUID of the device that returned the error
113
+ */
114
+ class CommandError extends MerossError {
115
+ constructor(message, errorPayload = null, deviceUuid = null) {
116
+ super(message || 'Device command failed');
117
+ this.errorPayload = errorPayload;
118
+ this.deviceUuid = deviceUuid;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * MQTT connection/communication error.
124
+ *
125
+ * Thrown when MQTT connection or communication fails. Can occur due to network
126
+ * issues, MQTT broker unavailability, connection timeouts, or protocol errors
127
+ * during message transmission. Check network connectivity and MQTT broker status.
128
+ *
129
+ * @class
130
+ * @extends MerossError
131
+ * @property {string|null} topic - MQTT topic related to the error
132
+ * @property {Object|null} mqttMessage - MQTT message related to the error
133
+ */
134
+ class MqttError extends MerossError {
135
+ constructor(message, topic = null, mqttMessage = null) {
136
+ super(message || 'MQTT error occurred');
137
+ this.topic = topic;
138
+ this.mqttMessage = mqttMessage;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Device not connected error.
144
+ *
145
+ * Thrown when attempting to send a command to a device that is not connected.
146
+ * Commands can only be sent after the device has established a connection and
147
+ * emitted the 'connected' event. Wait for the device to connect before sending commands.
148
+ *
149
+ * @class
150
+ * @extends MerossError
151
+ * @property {string|null} deviceUuid - UUID of the device that is not connected
152
+ */
153
+ class UnconnectedError extends MerossError {
154
+ constructor(message, deviceUuid = null) {
155
+ super(message || 'Device is not connected');
156
+ this.deviceUuid = deviceUuid;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Unknown or unsupported device type error.
162
+ *
163
+ * Thrown when a device operation is attempted on a device type that doesn't
164
+ * support the requested feature or when the device type is not recognized.
165
+ * Indicates a mismatch between the requested operation and device capabilities.
166
+ * Verify the device type supports the requested operation.
167
+ *
168
+ * @class
169
+ * @extends MerossError
170
+ * @property {string|null} deviceType - Device type that is unsupported
171
+ */
172
+ class UnknownDeviceTypeError extends MerossError {
173
+ constructor(message, deviceType = null) {
174
+ super(message || 'Unknown or unsupported device type');
175
+ this.deviceType = deviceType;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Handles token-related error codes.
181
+ *
182
+ * Maps token expiration and token limit error codes to appropriate error
183
+ * classes. Token errors indicate authentication token issues that require
184
+ * re-authentication or token management.
185
+ *
186
+ * @private
187
+ * @param {number} errorCode - The error code
188
+ * @param {string} message - Error message
189
+ * @returns {MerossError|null} Error instance or null if not a token error
190
+ */
191
+ function _handleTokenErrors(errorCode, message) {
192
+ const { TokenExpiredError, TooManyTokensError } = require('./http/exception');
193
+
194
+ if (errorCode === 1019 || errorCode === 1022 || errorCode === 1200) {
195
+ return new TokenExpiredError(message, errorCode);
196
+ }
197
+ if (errorCode === 1301) {
198
+ return new TooManyTokensError(message);
199
+ }
200
+ return null;
201
+ }
202
+
203
+ /**
204
+ * Handles authentication error codes (1000-1008).
205
+ *
206
+ * Maps authentication error codes to AuthenticationError instances. These
207
+ * errors indicate login failures due to invalid credentials or account issues.
208
+ *
209
+ * @private
210
+ * @param {number} errorCode - The error code
211
+ * @param {string} message - Error message
212
+ * @returns {MerossError|null} Error instance or null if not an auth error
213
+ */
214
+ function _handleAuthenticationErrors(errorCode, message) {
215
+ if (errorCode >= 1000 && errorCode <= 1008) {
216
+ return new AuthenticationError(message, errorCode);
217
+ }
218
+ return null;
219
+ }
220
+
221
+ /**
222
+ * Handles MFA-related error codes.
223
+ *
224
+ * Maps multi-factor authentication error codes to appropriate error classes.
225
+ * MFA errors indicate issues with two-factor authentication during login.
226
+ *
227
+ * @private
228
+ * @param {number} errorCode - The error code
229
+ * @param {string} message - Error message
230
+ * @returns {MerossError|null} Error instance or null if not an MFA error
231
+ */
232
+ function _handleMFAErrors(errorCode, message) {
233
+ const { MFARequiredError, WrongMFAError } = require('./http/exception');
234
+
235
+ if (errorCode === 1032) {
236
+ return new WrongMFAError(message);
237
+ }
238
+ if (errorCode === 1033) {
239
+ return new MFARequiredError(message);
240
+ }
241
+ return null;
242
+ }
243
+
244
+ /**
245
+ * Handles domain and API limit/access error codes.
246
+ *
247
+ * Maps domain configuration errors and API limit/access errors to appropriate
248
+ * error classes. Domain errors indicate incorrect API/MQTT domain configuration,
249
+ * while limit errors indicate rate limiting or permission issues.
250
+ *
251
+ * @private
252
+ * @param {number} errorCode - The error code
253
+ * @param {string} message - Error message
254
+ * @param {Object} context - Additional context
255
+ * @returns {MerossError|null} Error instance or null if not a domain/limit error
256
+ */
257
+ function _handleDomainAndLimitErrors(errorCode, message, context) {
258
+ const { BadDomainError } = require('./http/exception');
259
+
260
+ if (errorCode === 1030) {
261
+ return new BadDomainError(message, context.apiDomain, context.mqttDomain);
262
+ }
263
+ if (errorCode === 1042) {
264
+ return new ApiLimitReachedError(message);
265
+ }
266
+ if (errorCode === 1043) {
267
+ return new ResourceAccessDeniedError(message);
268
+ }
269
+ return null;
270
+ }
271
+
272
+ /**
273
+ * Handles HTTP status code-based errors.
274
+ *
275
+ * Maps HTTP status codes to appropriate error classes. HTTP errors indicate
276
+ * server-side issues or authentication problems at the HTTP protocol level.
277
+ *
278
+ * @private
279
+ * @param {number} httpStatusCode - HTTP status code
280
+ * @param {number} errorCode - The API error code
281
+ * @param {string} message - Error message
282
+ * @returns {MerossError|null} Error instance or null if not an HTTP error
283
+ */
284
+ function _handleHttpStatusErrors(httpStatusCode, errorCode, message) {
285
+ const { HttpApiError, UnauthorizedError } = require('./http/exception');
286
+
287
+ if (httpStatusCode === 401) {
288
+ return new UnauthorizedError(message, errorCode, httpStatusCode);
289
+ }
290
+ if (httpStatusCode && httpStatusCode >= 400) {
291
+ return new HttpApiError(message, errorCode, httpStatusCode);
292
+ }
293
+ return null;
294
+ }
295
+
296
+ /**
297
+ * Maps error codes to appropriate error classes.
298
+ *
299
+ * Converts API error codes into specific error class instances based on the
300
+ * error code value and context. Provides consistent error handling by mapping
301
+ * numeric codes to typed error objects with appropriate properties. This
302
+ * centralizes error handling logic and ensures all errors are properly typed.
303
+ *
304
+ * @param {number} errorCode - The error code from API response
305
+ * @param {Object} [context={}] - Additional context
306
+ * @param {string} [context.info] - Error message from API
307
+ * @param {string} [context.deviceUuid] - Device UUID (for device-related errors)
308
+ * @param {number} [context.httpStatusCode] - HTTP status code
309
+ * @param {string} [context.apiDomain] - API domain (for BadDomainError)
310
+ * @param {string} [context.mqttDomain] - MQTT domain (for BadDomainError)
311
+ * @returns {MerossError} Appropriate error instance
312
+ */
313
+ function mapErrorCodeToError(errorCode, context = {}) {
314
+ const { info, httpStatusCode } = context;
315
+ const { getErrorMessage } = require('./http/error-codes');
316
+ const message = info || getErrorMessage(errorCode);
317
+
318
+ // Check token-related errors
319
+ const tokenError = _handleTokenErrors(errorCode, message);
320
+ if (tokenError) {
321
+ return tokenError;
322
+ }
323
+
324
+ // Check authentication errors
325
+ const authError = _handleAuthenticationErrors(errorCode, message);
326
+ if (authError) {
327
+ return authError;
328
+ }
329
+
330
+ // Check MFA errors
331
+ const mfaError = _handleMFAErrors(errorCode, message);
332
+ if (mfaError) {
333
+ return mfaError;
334
+ }
335
+
336
+ // Check domain and API limit errors
337
+ const domainError = _handleDomainAndLimitErrors(errorCode, message, context);
338
+ if (domainError) {
339
+ return domainError;
340
+ }
341
+
342
+ // Check HTTP status code errors
343
+ const httpError = _handleHttpStatusErrors(httpStatusCode, errorCode, message);
344
+ if (httpError) {
345
+ return httpError;
346
+ }
347
+
348
+ // Default: return generic MerossError
349
+ return new MerossError(`${errorCode} (${getErrorMessage(errorCode)})${info ? ` - ${info}` : ''}`, errorCode);
350
+ }
351
+
352
+ module.exports = {
353
+ MerossError,
354
+ AuthenticationError,
355
+ ApiLimitReachedError,
356
+ ResourceAccessDeniedError,
357
+ CommandTimeoutError,
358
+ CommandError,
359
+ MqttError,
360
+ UnconnectedError,
361
+ UnknownDeviceTypeError,
362
+ mapErrorCodeToError
363
+ };
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ const { OnlineStatus } = require('../enums');
4
+ const { DEFAULT_MQTT_HOST, DEFAULT_MQTT_PORT } = require('../constants');
5
+ const { extractDomain, extractPort } = require('../../utilities/network');
6
+
7
+ /**
8
+ * Represents device information from the HTTP API
9
+ *
10
+ * This class encapsulates device metadata retrieved from the Meross HTTP API,
11
+ * including device identification, versions, channels, and network configuration.
12
+ * It provides helper methods for extracting MQTT connection information.
13
+ *
14
+ * @class
15
+ * @example
16
+ * // Use fromDict() to create instances
17
+ * const deviceInfo = HttpDeviceInfo.fromDict({
18
+ * uuid: '12345',
19
+ * dev_name: 'My Device',
20
+ * device_type: 'mss310',
21
+ * online_status: 1,
22
+ * channels: []
23
+ * });
24
+ */
25
+ class HttpDeviceInfo {
26
+ /**
27
+ * Creates an HttpDeviceInfo instance from a dictionary object.
28
+ * Normalizes incoming keys (handles camelCase and snake_case) to camelCase.
29
+ * This is the only way to create instances.
30
+ *
31
+ * @static
32
+ * @param {Object} jsonDict - Raw API response object with any key format
33
+ * @param {string} [jsonDict.uuid] - Device UUID
34
+ * @param {string} [jsonDict.devName] - Device name (camelCase)
35
+ * @param {string} [jsonDict.dev_name] - Device name (snake_case)
36
+ * @param {string} [jsonDict.deviceType] - Device type (camelCase)
37
+ * @param {string} [jsonDict.device_type] - Device type (snake_case)
38
+ * @param {Array<Object>} [jsonDict.channels] - Raw channels array from API
39
+ * @param {string} [jsonDict.fmwareVersion] - Firmware version (camelCase)
40
+ * @param {string} [jsonDict.fmware_version] - Firmware version (snake_case)
41
+ * @param {string} [jsonDict.hdwareVersion] - Hardware version (camelCase)
42
+ * @param {string} [jsonDict.hdware_version] - Hardware version (snake_case)
43
+ * @param {string} [jsonDict.domain] - MQTT domain
44
+ * @param {string} [jsonDict.reservedDomain] - Reserved MQTT domain (camelCase)
45
+ * @param {string} [jsonDict.reserved_domain] - Reserved MQTT domain (snake_case)
46
+ * @param {string} [jsonDict.subType] - Device sub-type (camelCase)
47
+ * @param {string} [jsonDict.sub_type] - Device sub-type (snake_case)
48
+ * @param {number|Date|string} [jsonDict.bindTime] - Device bind timestamp (camelCase)
49
+ * @param {number|Date|string} [jsonDict.bind_time] - Device bind timestamp (snake_case)
50
+ * @param {string} [jsonDict.skillNumber] - Skill number (camelCase)
51
+ * @param {string} [jsonDict.skill_number] - Skill number (snake_case)
52
+ * @param {string} [jsonDict.userDevIcon] - User device icon (camelCase)
53
+ * @param {string} [jsonDict.user_dev_icon] - User device icon (snake_case)
54
+ * @param {number} [jsonDict.iconType] - Icon type (camelCase)
55
+ * @param {number} [jsonDict.icon_type] - Icon type (snake_case)
56
+ * @param {string} [jsonDict.region] - Device region
57
+ * @param {string} [jsonDict.devIconId] - Device icon ID (camelCase)
58
+ * @param {string} [jsonDict.dev_icon_id] - Device icon ID (snake_case)
59
+ * @param {number} [jsonDict.onlineStatus] - Online status (camelCase)
60
+ * @param {number} [jsonDict.online_status] - Online status (snake_case)
61
+ * @returns {HttpDeviceInfo} New HttpDeviceInfo instance
62
+ */
63
+ static fromDict(jsonDict) {
64
+ if (!jsonDict || typeof jsonDict !== 'object') {
65
+ throw new Error('Device info dictionary is required');
66
+ }
67
+
68
+ // The Meross API returns properties in inconsistent formats (camelCase vs snake_case).
69
+ // This mapping allows us to accept either format and normalize to camelCase for internal use.
70
+ const propertyMappings = {
71
+ uuid: ['uuid'],
72
+ devName: ['devName', 'dev_name'],
73
+ deviceType: ['deviceType', 'device_type'],
74
+ channels: ['channels'],
75
+ fmwareVersion: ['fmwareVersion', 'fmware_version'],
76
+ hdwareVersion: ['hdwareVersion', 'hdware_version'],
77
+ domain: ['domain'],
78
+ reservedDomain: ['reservedDomain', 'reserved_domain'],
79
+ subType: ['subType', 'sub_type'],
80
+ bindTime: ['bindTime', 'bind_time'],
81
+ skillNumber: ['skillNumber', 'skill_number'],
82
+ userDevIcon: ['userDevIcon', 'user_dev_icon'],
83
+ iconType: ['iconType', 'icon_type'],
84
+ region: ['region'],
85
+ devIconId: ['devIconId', 'dev_icon_id'],
86
+ onlineStatus: ['onlineStatus', 'online_status']
87
+ };
88
+
89
+ // Normalize properties by checking alternatives in priority order (camelCase first, then snake_case).
90
+ // This ensures consistent property names regardless of API response format.
91
+ const normalized = {};
92
+ for (const [camelKey, alternatives] of Object.entries(propertyMappings)) {
93
+ for (const key of alternatives) {
94
+ if (jsonDict[key] !== null && jsonDict[key] !== undefined) {
95
+ normalized[camelKey] = jsonDict[key];
96
+ break;
97
+ }
98
+ }
99
+ }
100
+
101
+ // Use Object.create() instead of a constructor to allow static factory pattern.
102
+ // This prevents accidental instantiation via 'new' and enforces use of fromDict().
103
+ const instance = Object.create(HttpDeviceInfo.prototype);
104
+ instance.uuid = normalized.uuid;
105
+ instance.devName = normalized.devName;
106
+ instance.deviceType = normalized.deviceType;
107
+ instance.channels = normalized.channels || [];
108
+ instance.fmwareVersion = normalized.fmwareVersion;
109
+ instance.hdwareVersion = normalized.hdwareVersion;
110
+ instance.domain = normalized.domain;
111
+ instance.reservedDomain = normalized.reservedDomain;
112
+ instance.subType = normalized.subType;
113
+ instance.skillNumber = normalized.skillNumber;
114
+ instance.userDevIcon = normalized.userDevIcon;
115
+ instance.iconType = normalized.iconType;
116
+ instance.region = normalized.region;
117
+ instance.devIconId = normalized.devIconId;
118
+
119
+ // Convert onlineStatus to a number, defaulting to UNKNOWN if invalid or missing.
120
+ // The API may return non-numeric values or omit the field entirely.
121
+ if (normalized.onlineStatus !== undefined && normalized.onlineStatus !== null) {
122
+ if (typeof normalized.onlineStatus === 'number') {
123
+ instance.onlineStatus = normalized.onlineStatus;
124
+ } else {
125
+ instance.onlineStatus = OnlineStatus.UNKNOWN;
126
+ }
127
+ } else {
128
+ instance.onlineStatus = OnlineStatus.UNKNOWN;
129
+ }
130
+
131
+ // Convert bindTime from Unix timestamp (seconds) to Date object.
132
+ // The API returns timestamps as numbers, but we store them as Date instances for easier manipulation.
133
+ if (normalized.bindTime !== undefined && normalized.bindTime !== null) {
134
+ if (typeof normalized.bindTime === 'number') {
135
+ instance.bindTime = new Date(normalized.bindTime * 1000);
136
+ } else if (normalized.bindTime instanceof Date) {
137
+ instance.bindTime = normalized.bindTime;
138
+ } else if (typeof normalized.bindTime === 'string') {
139
+ instance.bindTime = new Date(normalized.bindTime);
140
+ } else {
141
+ instance.bindTime = null;
142
+ }
143
+ } else {
144
+ instance.bindTime = null;
145
+ }
146
+
147
+ return instance;
148
+ }
149
+
150
+ /**
151
+ * Converts the instance to a plain object dictionary with camelCase keys
152
+ *
153
+ * @returns {Object} Plain object with camelCase property keys
154
+ */
155
+ toDict() {
156
+ return {
157
+ uuid: this.uuid,
158
+ devName: this.devName,
159
+ deviceType: this.deviceType,
160
+ channels: this.channels,
161
+ fmwareVersion: this.fmwareVersion,
162
+ hdwareVersion: this.hdwareVersion,
163
+ domain: this.domain,
164
+ reservedDomain: this.reservedDomain,
165
+ subType: this.subType,
166
+ bindTime: this.bindTime,
167
+ skillNumber: this.skillNumber,
168
+ userDevIcon: this.userDevIcon,
169
+ iconType: this.iconType,
170
+ region: this.region,
171
+ devIconId: this.devIconId,
172
+ onlineStatus: this.onlineStatus
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Gets the MQTT server hostname for this device
178
+ *
179
+ * Extracts the hostname from the domain or reservedDomain field.
180
+ * The domain field is preferred as it represents the active MQTT endpoint,
181
+ * while reservedDomain is a fallback. Defaults to a standard hostname if neither is available.
182
+ *
183
+ * @returns {string} MQTT hostname
184
+ */
185
+ getMqttHost() {
186
+ if (this.domain) {
187
+ return extractDomain(this.domain);
188
+ }
189
+ if (this.reservedDomain) {
190
+ return extractDomain(this.reservedDomain);
191
+ }
192
+ return DEFAULT_MQTT_HOST;
193
+ }
194
+
195
+ /**
196
+ * Gets the MQTT server port for this device
197
+ *
198
+ * Extracts the port from the domain or reservedDomain field.
199
+ * The domain field is preferred as it represents the active MQTT endpoint,
200
+ * while reservedDomain is a fallback. Defaults to a standard port if neither is available.
201
+ *
202
+ * @returns {number} MQTT port number
203
+ */
204
+ getMqttPort() {
205
+ if (this.domain) {
206
+ return extractPort(this.domain, DEFAULT_MQTT_PORT);
207
+ }
208
+ if (this.reservedDomain) {
209
+ return extractPort(this.reservedDomain, DEFAULT_MQTT_PORT);
210
+ }
211
+ return DEFAULT_MQTT_PORT;
212
+ }
213
+ }
214
+
215
+ module.exports = HttpDeviceInfo;
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Error code to message mapping
5
+ *
6
+ * Maps Meross API error codes to human-readable error messages. These messages
7
+ * are provided by the Meross API and used as default error messages when the
8
+ * API doesn't provide a custom message.
9
+ *
10
+ * @module errorcodes
11
+ */
12
+
13
+ const errorMessages = {
14
+ 500: 'The selected timezone is not supported',
15
+ 1000: 'Wrong or missing user',
16
+ 1001: 'Wrong or missing password',
17
+ 1002: 'Account does not exist',
18
+ 1003: 'This account has been disabled or deleted',
19
+ 1004: 'Wrong email or password',
20
+ 1005: 'Invalid email address',
21
+ 1006: 'Bad password format',
22
+ 1007: 'User already exists',
23
+ 1008: 'This email is not registered',
24
+ 1009: 'Email send failed',
25
+ 1011: 'Wrong Ticket',
26
+ 1012: 'Code Overdue',
27
+ 1013: 'Wrong Code',
28
+ 1014: 'Duplicate password',
29
+ 1015: 'Same email when changing account email',
30
+ 1019: 'Token expired',
31
+ 1021: 'Unknown error',
32
+ 1022: 'Token error',
33
+ 1023: 'Unknown error',
34
+ 1024: 'Unknown error',
35
+ 1025: 'Unknown error',
36
+ 1026: 'Unknown error',
37
+ 1027: 'Unknown error',
38
+ 1028: 'Requested too frequently',
39
+ 1030: 'Redirect app to login other than this region',
40
+ 1031: 'Username does not match',
41
+ 1032: 'Invalid MFA code. Please use a current MFA code.',
42
+ 1033: 'MFA is activated for the account but MFA code not provided. Please provide a current MFA code.',
43
+ 1035: 'Operation is locked',
44
+ 1041: 'Repeat checkin',
45
+ 1042: 'API Top limit reached',
46
+ 1043: 'Resource access deny',
47
+ 1200: 'Token has expired',
48
+ 1201: 'Server was unable to generate token',
49
+ 1202: 'Unknown error',
50
+ 1203: 'Unknown error',
51
+ 1204: 'Unknown error',
52
+ 1210: 'Unknown error',
53
+ 1211: 'Unknown error',
54
+ 1212: 'Unknown error',
55
+ 1213: 'Unknown error',
56
+ 1214: 'Unknown error',
57
+ 1215: 'Unknown error',
58
+ 1226: 'Unknown error',
59
+ 1227: 'Unknown error',
60
+ 1228: 'Unknown error',
61
+ 1229: 'Unknown error',
62
+ 1230: 'Unknown error',
63
+ 1231: 'Unknown error',
64
+ 1232: 'Unknown error',
65
+ 1233: 'Unknown error',
66
+ 1255: 'The number of remote control boards exceeded the limit',
67
+ 1256: 'Compatible mode having',
68
+ 1257: 'Compatible mode not having',
69
+ 1301: 'You have issued too many tokens without logging out and your account might have been temporarily disabled.',
70
+ 1400: 'Unknown error',
71
+ 1401: 'Unknown error',
72
+ 1402: 'Unknown error',
73
+ 1403: 'Unknown error',
74
+ 1500: 'Unknown error',
75
+ 1501: 'Unknown error',
76
+ 1502: 'Unknown error',
77
+ 1503: 'Unknown error',
78
+ 1504: 'Unknown error',
79
+ 1601: 'Unknown error',
80
+ 1602: 'Unknown error',
81
+ 1603: 'Unknown error',
82
+ 1604: 'Unknown error',
83
+ 1605: 'Unknown error',
84
+ 1700: 'Unknown error',
85
+ 5000: 'Unknown or generic error',
86
+ 5001: 'Unknown or generic error',
87
+ 5002: 'Unknown or generic error',
88
+ 5003: 'Unknown or generic error',
89
+ 5004: 'Unknown or generic error',
90
+ 5020: 'Infrared Remote device is busy',
91
+ 5021: 'Infrared record timeout',
92
+ 5022: 'Infrared record invalid',
93
+ 10001: 'System error',
94
+ 10002: 'Unknown error',
95
+ 10003: 'Serialize error',
96
+ 10006: 'Http common error',
97
+ 20101: 'Invalid parameter',
98
+ 20106: 'Not existing resource',
99
+ 20112: 'Unsupported',
100
+ 20115: 'Send email limit'
101
+ };
102
+
103
+ /**
104
+ * Gets the error message for a given error code
105
+ *
106
+ * Maps Meross API error codes to human-readable messages. These messages are
107
+ * standardized by the Meross API documentation and provide context when the API
108
+ * response doesn't include a custom error message. Returns a generic message for
109
+ * unmapped codes to ensure callers always receive a meaningful error description.
110
+ *
111
+ * @param {number} code - The error code to look up
112
+ * @returns {string} The error message for the code, or 'Unknown error' if not found
113
+ */
114
+ function getErrorMessage(code) {
115
+ return errorMessages[code] || 'Unknown error';
116
+ }
117
+
118
+ module.exports = {
119
+ getErrorMessage
120
+ };
121
+