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,408 @@
1
+ 'use strict';
2
+
3
+ const LightState = require('../../model/states/light-state');
4
+ const { LightMode } = require('../../model/enums');
5
+ const { rgbToInt } = require('../../utilities/conversion');
6
+ const { normalizeChannel } = require('../../utilities/options');
7
+ const { buildStateChanges } = require('../../utilities/state-changes');
8
+
9
+ /**
10
+ * Light feature module.
11
+ * Provides control over light settings including color, brightness, temperature, and on/off state.
12
+ */
13
+ module.exports = {
14
+ /**
15
+ * Controls the light settings.
16
+ *
17
+ * @param {Object} options - Light options
18
+ * @param {Object} options.light - Light configuration object
19
+ * @returns {Promise<Object>} Response from the device
20
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
21
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
22
+ */
23
+ async setLight(options = {}) {
24
+ if (!options.light) {
25
+ throw new Error('light configuration object is required');
26
+ }
27
+ const payload = { light: options.light };
28
+ const response = await this.publishMessage('SET', 'Appliance.Control.Light', payload, null);
29
+
30
+ if (response && response.light) {
31
+ this._updateLightState(response.light, 'response');
32
+ this._lastFullUpdateTimestamp = Date.now();
33
+ } else if (options.light) {
34
+ this._updateLightState(options.light, 'response');
35
+ this._lastFullUpdateTimestamp = Date.now();
36
+ }
37
+
38
+ return response;
39
+ },
40
+
41
+ /**
42
+ * Gets the current light state from the device.
43
+ *
44
+ * @param {Object} [options={}] - Get options
45
+ * @returns {Promise<Object>} Response containing light state
46
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
47
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
48
+ */
49
+ async getLightState(_options = {}) {
50
+ const response = await this.publishMessage('GET', 'Appliance.Control.Light', {}, null);
51
+ if (response && response.light) {
52
+ this._updateLightState(response.light, 'response');
53
+ this._lastFullUpdateTimestamp = Date.now();
54
+ }
55
+ return response;
56
+ },
57
+
58
+ /**
59
+ * Gets the cached light state for the specified channel.
60
+ *
61
+ * @param {number} [channel=0] - Channel to get state for (default: 0)
62
+ * @returns {Object|undefined} Cached light state or undefined if not available
63
+ * @throws {Error} If state has not been initialized (call refreshState() first)
64
+ */
65
+ getCachedLightState(channel = 0) {
66
+ this.validateState();
67
+ return this._lightStateByChannel.get(channel);
68
+ },
69
+
70
+ /**
71
+ * Checks if the light is on for the specified channel.
72
+ *
73
+ * For devices that support ToggleX and Toggle abilities, the onoff state is not exposed
74
+ * in the light status. In that case, we return the toggle state.
75
+ *
76
+ * @param {number} [channel=0] - Channel to check (default: 0)
77
+ * @returns {boolean|undefined} True if on, false if off, undefined if not available
78
+ * @throws {Error} If state has not been initialized (call refreshState() first)
79
+ */
80
+ getLightIsOn(channel = 0) {
81
+ this.validateState();
82
+ if (this._abilities) {
83
+ const hasToggleX = this._abilities['Appliance.Control.ToggleX'];
84
+ const hasToggle = this._abilities['Appliance.Control.Toggle'];
85
+ if (hasToggleX || hasToggle) {
86
+ if (typeof this.isOn === 'function') {
87
+ return this.isOn(channel);
88
+ }
89
+ }
90
+ }
91
+ const lightState = this._lightStateByChannel.get(channel);
92
+ if (lightState) {
93
+ return lightState.isOn;
94
+ }
95
+ return undefined;
96
+ },
97
+
98
+ /**
99
+ * Gets the light RGB color for the specified channel.
100
+ *
101
+ * @param {number} [channel=0] - Channel to get color for (default: 0)
102
+ * @returns {Array<number>|undefined} RGB tuple [r, g, b] or undefined if not available
103
+ * @throws {Error} If state has not been initialized (call refreshState() first)
104
+ */
105
+ getLightRgbColor(channel = 0) {
106
+ this.validateState();
107
+ const lightState = this._lightStateByChannel.get(channel);
108
+ if (lightState) {
109
+ return lightState.rgbTuple;
110
+ }
111
+ return undefined;
112
+ },
113
+
114
+ /**
115
+ * Gets the light brightness for the specified channel.
116
+ *
117
+ * @param {number} [channel=0] - Channel to get brightness for (default: 0)
118
+ * @returns {number|undefined} Brightness value or undefined if not available
119
+ * @throws {Error} If state has not been initialized (call refreshState() first)
120
+ */
121
+ getLightBrightness(channel = 0) {
122
+ this.validateState();
123
+ const lightState = this._lightStateByChannel.get(channel);
124
+ if (lightState) {
125
+ return lightState.luminance;
126
+ }
127
+ return undefined;
128
+ },
129
+
130
+ /**
131
+ * Gets the light temperature for the specified channel.
132
+ *
133
+ * @param {number} [channel=0] - Channel to get temperature for (default: 0)
134
+ * @returns {number|undefined} Temperature value or undefined if not available
135
+ * @throws {Error} If state has not been initialized (call refreshState() first)
136
+ */
137
+ getLightTemperature(channel = 0) {
138
+ this.validateState();
139
+ const lightState = this._lightStateByChannel.get(channel);
140
+ if (lightState) {
141
+ return lightState.temperature;
142
+ }
143
+ return undefined;
144
+ },
145
+
146
+ /**
147
+ * Gets the light mode (capacity) for the specified channel.
148
+ *
149
+ * @param {number} [channel=0] - Channel to get mode for (default: 0)
150
+ * @returns {number|undefined} Light mode/capacity or undefined if not available
151
+ * @throws {Error} If state has not been initialized (call refreshState() first)
152
+ */
153
+ getLightMode(channel = 0) {
154
+ this.validateState();
155
+ const lightState = this._lightStateByChannel.get(channel);
156
+ if (lightState) {
157
+ return lightState.capacity;
158
+ }
159
+ return undefined;
160
+ },
161
+
162
+ /**
163
+ * Checks if the light supports RGB mode for the specified channel.
164
+ *
165
+ * @param {number} [channel=0] - Channel to check (default: 0)
166
+ * @returns {boolean} True if RGB is supported
167
+ */
168
+ getSupportsRgb(channel = 0) {
169
+ return this._supportsLightMode(LightMode.MODE_RGB, channel);
170
+ },
171
+
172
+ /**
173
+ * Checks if the light supports luminance mode for the specified channel.
174
+ *
175
+ * @param {number} [channel=0] - Channel to check (default: 0)
176
+ * @returns {boolean} True if luminance is supported
177
+ */
178
+ getSupportsLuminance(channel = 0) {
179
+ return this._supportsLightMode(LightMode.MODE_LUMINANCE, channel);
180
+ },
181
+
182
+ /**
183
+ * Checks if the light supports temperature mode for the specified channel.
184
+ *
185
+ * @param {number} [channel=0] - Channel to check (default: 0)
186
+ * @returns {boolean} True if temperature is supported
187
+ */
188
+ getSupportsTemperature(channel = 0) {
189
+ return this._supportsLightMode(LightMode.MODE_TEMPERATURE, channel);
190
+ },
191
+
192
+ /**
193
+ * Turns on the light for the specified channel.
194
+ *
195
+ * Automatically selects the appropriate control method (ToggleX/Toggle or Light) based on
196
+ * device capabilities.
197
+ *
198
+ * @param {Object} [options={}] - Turn on options
199
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
200
+ * @returns {Promise<Object>} Response from the device
201
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
202
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
203
+ */
204
+ async turnOn(options = {}) {
205
+ if (this._abilities) {
206
+ const hasToggleX = this._abilities['Appliance.Control.ToggleX'];
207
+ const hasToggle = this._abilities['Appliance.Control.Toggle'];
208
+
209
+ if (hasToggleX && typeof this.setToggleX === 'function') {
210
+ return await this.setToggleX({ ...options, onoff: true });
211
+ } else if (hasToggle && typeof this.setToggle === 'function') {
212
+ return await this.setToggle({ onoff: true });
213
+ }
214
+ }
215
+
216
+ return await this.setLightColor({ ...options, onoff: true });
217
+ },
218
+
219
+ /**
220
+ * Turns off the light for the specified channel.
221
+ *
222
+ * Automatically selects the appropriate control method (ToggleX/Toggle or Light) based on
223
+ * device capabilities.
224
+ *
225
+ * @param {Object} [options={}] - Turn off options
226
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
227
+ * @returns {Promise<Object>} Response from the device
228
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
229
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
230
+ */
231
+ async turnOff(options = {}) {
232
+ if (this._abilities) {
233
+ const hasToggleX = this._abilities['Appliance.Control.ToggleX'];
234
+ const hasToggle = this._abilities['Appliance.Control.Toggle'];
235
+
236
+ if (hasToggleX && typeof this.setToggleX === 'function') {
237
+ return await this.setToggleX({ ...options, onoff: false });
238
+ } else if (hasToggle && typeof this.setToggle === 'function') {
239
+ return await this.setToggle({ onoff: false });
240
+ }
241
+ }
242
+
243
+ return await this.setLightColor({ ...options, onoff: false });
244
+ },
245
+
246
+ /**
247
+ * Controls the light color, brightness, and temperature.
248
+ *
249
+ * Allows setting multiple light properties in a single call. If ToggleX or Toggle is
250
+ * supported, uses those for on/off control; otherwise includes onoff in the light payload.
251
+ *
252
+ * @param {Object} [options={}] - Light control options
253
+ * @param {number} [options.channel] - Channel to control (default: 0)
254
+ * @param {boolean} [options.onoff] - Turn on/off
255
+ * @param {Array<number>|number} [options.rgb] - RGB color [r, g, b] or integer
256
+ * @param {number} [options.luminance] - Brightness value
257
+ * @param {number} [options.temperature] - Temperature value
258
+ * @param {boolean|number} [options.gradual] - Enable gradual transition (default: true for RGB, false otherwise)
259
+ * @returns {Promise<Object|null>} Response from the device or null if no changes needed
260
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
261
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
262
+ */
263
+ async setLightColor(options = {}) {
264
+ const channel = normalizeChannel(options);
265
+ const { onoff, rgb, luminance, temperature, gradual } = options;
266
+
267
+ const currentIsOn = this.getLightIsOn(channel);
268
+ const hasToggleX = this._abilities && this._abilities['Appliance.Control.ToggleX'];
269
+ const hasToggle = this._abilities && this._abilities['Appliance.Control.Toggle'];
270
+ const hasToggleSupport = hasToggleX || hasToggle;
271
+
272
+ if (hasToggleSupport && onoff === false && currentIsOn !== false) {
273
+ if (hasToggleX && typeof this.setToggleX === 'function') {
274
+ await this.setToggleX({ channel, onoff: false });
275
+ } else if (hasToggle && typeof this.setToggle === 'function') {
276
+ await this.setToggle({ onoff: false });
277
+ }
278
+ }
279
+
280
+ const lightPayload = {};
281
+
282
+ if (!hasToggleSupport) {
283
+ if (onoff !== undefined) {
284
+ lightPayload.onoff = onoff ? 1 : 0;
285
+ } else if (currentIsOn !== undefined) {
286
+ lightPayload.onoff = currentIsOn ? 1 : 0;
287
+ }
288
+ }
289
+
290
+ if (rgb !== undefined && this._supportsLightMode(LightMode.MODE_RGB, channel)) {
291
+ lightPayload.rgb = rgbToInt(rgb);
292
+ lightPayload.capacity = (lightPayload.capacity || 0) | LightMode.MODE_RGB;
293
+ }
294
+
295
+ if (luminance !== undefined && this._supportsLightMode(LightMode.MODE_LUMINANCE, channel)) {
296
+ lightPayload.luminance = luminance;
297
+ lightPayload.capacity = (lightPayload.capacity || 0) | LightMode.MODE_LUMINANCE;
298
+ }
299
+
300
+ if (temperature !== undefined && this._supportsLightMode(LightMode.MODE_TEMPERATURE, channel)) {
301
+ lightPayload.temperature = temperature;
302
+ lightPayload.capacity = (lightPayload.capacity || 0) | LightMode.MODE_TEMPERATURE;
303
+ }
304
+
305
+ if (Object.keys(lightPayload).length === 0) {
306
+ return null;
307
+ }
308
+
309
+ lightPayload.channel = channel;
310
+
311
+ // Handle gradual transition parameter
312
+ if (gradual !== undefined) {
313
+ // Allow both boolean and number (0/1) for convenience
314
+ lightPayload.gradual = typeof gradual === 'boolean' ? (gradual ? 1 : 0) : gradual;
315
+ } else if (rgb !== undefined) {
316
+ // Default to gradual for RGB changes (matches app behavior)
317
+ lightPayload.gradual = 1;
318
+ } else {
319
+ // Default to instant for non-RGB changes
320
+ lightPayload.gradual = 0;
321
+ }
322
+
323
+ const payload = { light: lightPayload };
324
+ const response = await this.publishMessage('SET', 'Appliance.Control.Light', payload, null);
325
+
326
+ if (response && response.light) {
327
+ this._updateLightState(response.light, 'response');
328
+ } else {
329
+ this._updateLightState(lightPayload, 'response');
330
+ }
331
+
332
+ return response;
333
+ },
334
+
335
+ /**
336
+ * Checks if the device supports a specific light mode for the given channel.
337
+ *
338
+ * @param {number} mode - Light mode to check (from LightMode enum)
339
+ * @param {number} [channel=0] - Channel to check (default: 0)
340
+ * @returns {boolean} True if the mode is supported
341
+ * @private
342
+ */
343
+ _supportsLightMode(mode) {
344
+ if (!this._abilities) {return false;}
345
+
346
+ const lightAbility = this._abilities['Appliance.Control.Light'];
347
+ if (!lightAbility || !lightAbility.capacity) {return false;}
348
+
349
+ const { capacity } = lightAbility;
350
+ return (capacity & mode) === mode;
351
+ },
352
+
353
+ /**
354
+ * Updates the cached light state from light data.
355
+ *
356
+ * Called automatically when light push notifications are received or commands complete.
357
+ * Handles both single objects and arrays of light data.
358
+ *
359
+ * @param {Object|Array} lightData - Light data (single object or array)
360
+ * @param {string} [source='response'] - Source of the update ('push' | 'poll' | 'response')
361
+ * @private
362
+ */
363
+ _updateLightState(lightData, source = 'response') {
364
+ if (!lightData) {return;}
365
+
366
+ const lightArray = Array.isArray(lightData) ? lightData : [lightData];
367
+
368
+ for (const lightItem of lightArray) {
369
+ const channelIndex = lightItem.channel;
370
+ if (channelIndex === undefined || channelIndex === null) {continue;}
371
+
372
+ const oldState = this._lightStateByChannel.get(channelIndex);
373
+ const oldValue = oldState ? {
374
+ isOn: oldState.isOn,
375
+ brightness: oldState.luminance,
376
+ rgb: oldState.rgbTuple,
377
+ temperature: oldState.temperature
378
+ } : undefined;
379
+
380
+ let state = this._lightStateByChannel.get(channelIndex);
381
+ if (!state) {
382
+ state = new LightState(lightItem);
383
+ this._lightStateByChannel.set(channelIndex, state);
384
+ } else {
385
+ state.update(lightItem);
386
+ }
387
+
388
+ const newValue = buildStateChanges(oldValue, {
389
+ isOn: state.isOn,
390
+ brightness: state.luminance,
391
+ rgb: state.rgbTuple,
392
+ temperature: state.temperature
393
+ }, ['rgb']);
394
+
395
+ if (Object.keys(newValue).length > 0) {
396
+ this.emit('stateChange', {
397
+ type: 'light',
398
+ channel: channelIndex,
399
+ value: newValue,
400
+ oldValue,
401
+ source,
402
+ timestamp: Date.now()
403
+ });
404
+ }
405
+ }
406
+ }
407
+ };
408
+