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,587 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Provides automatic polling and data provisioning for Meross devices.
5
+ *
6
+ * Combines push notifications with polling to provide a unified update stream.
7
+ * This abstraction allows platforms to subscribe to devices and receive automatic
8
+ * updates without managing polling intervals, caching logic, or handling the
9
+ * complexity of coordinating push notifications with periodic polling.
10
+ *
11
+ * @class
12
+ */
13
+ class SubscriptionManager {
14
+ /**
15
+ * Creates a new SubscriptionManager instance
16
+ *
17
+ * @param {Object} manager - MerossManager instance
18
+ * @param {Object} [options={}] - Configuration options
19
+ * @param {Function} [options.logger] - Logger function for debug output
20
+ * @param {number} [options.deviceStateInterval=30000] - Device state polling interval in ms (30s)
21
+ * @param {number} [options.electricityInterval=30000] - Electricity polling interval in ms (30s)
22
+ * @param {number} [options.consumptionInterval=60000] - Consumption polling interval in ms (60s)
23
+ * @param {number} [options.httpDeviceListInterval=120000] - HTTP device list polling interval in ms (120s)
24
+ * @param {boolean} [options.smartCaching=true] - Enable smart caching to avoid unnecessary polls
25
+ * @param {number} [options.cacheMaxAge=10000] - Max cache age in ms before refreshing (10s)
26
+ */
27
+ constructor(manager, options = {}) {
28
+ if (!manager) {
29
+ throw new Error('Manager instance is required');
30
+ }
31
+
32
+ this.manager = manager;
33
+ this.logger = options.logger || (() => {});
34
+
35
+ this.defaultConfig = {
36
+ deviceStateInterval: options.deviceStateInterval || 30000,
37
+ electricityInterval: options.electricityInterval || 30000,
38
+ consumptionInterval: options.consumptionInterval || 60000,
39
+ httpDeviceListInterval: options.httpDeviceListInterval || 120000,
40
+ smartCaching: options.smartCaching !== false,
41
+ cacheMaxAge: options.cacheMaxAge || 10000
42
+ };
43
+
44
+ // Tracks subscription state for each device (deviceUuid -> SubscriptionState)
45
+ this.subscriptions = new Map();
46
+
47
+ // HTTP device list polling tracks device additions/removals from the cloud API
48
+ this.httpPollInterval = null;
49
+ this.httpSubscribers = new Map();
50
+ this._lastDeviceList = null;
51
+ }
52
+
53
+ /**
54
+ * Subscribe to device updates
55
+ *
56
+ * @param {MerossDevice} device - Device to subscribe to
57
+ * @param {Object} [config={}] - Subscription configuration (optional, uses defaults)
58
+ * @param {number} [config.deviceStateInterval] - Device state polling interval in ms
59
+ * @param {number} [config.electricityInterval] - Electricity polling interval in ms
60
+ * @param {number} [config.consumptionInterval] - Consumption polling interval in ms
61
+ * @param {boolean} [config.smartCaching] - Enable smart caching
62
+ * @param {number} [config.cacheMaxAge] - Max cache age in ms
63
+ * @param {Function} [onUpdate] - Callback for updates: (update) => {}
64
+ * @returns {string} Subscription ID
65
+ */
66
+ subscribe(device, config = {}, onUpdate = null) {
67
+ const deviceUuid = device.uuid;
68
+
69
+ if (!this.subscriptions.has(deviceUuid)) {
70
+ this._createSubscription(device);
71
+ }
72
+
73
+ const subscription = this.subscriptions.get(deviceUuid);
74
+ const subId = `${deviceUuid}-${Date.now()}-${Math.random()}`;
75
+
76
+ subscription.subscribers.set(subId, {
77
+ config: { ...this.defaultConfig, ...config },
78
+ onUpdate: onUpdate || (() => {}),
79
+ subscribedAt: Date.now()
80
+ });
81
+
82
+ // Start polling if this is the first subscriber
83
+ if (subscription.subscribers.size === 1) {
84
+ this._startPolling(device, subscription);
85
+ }
86
+
87
+ this.logger(`Subscribed to device ${device.name} (${deviceUuid}). Total subscribers: ${subscription.subscribers.size}`);
88
+
89
+ return subId;
90
+ }
91
+
92
+ /**
93
+ * Unsubscribe from device updates
94
+ *
95
+ * @param {string} deviceUuid - Device UUID
96
+ * @param {string} subscriptionId - Subscription ID returned from subscribe()
97
+ */
98
+ unsubscribe(deviceUuid, subscriptionId) {
99
+ const subscription = this.subscriptions.get(deviceUuid);
100
+ if (!subscription) {
101
+ return;
102
+ }
103
+
104
+ subscription.subscribers.delete(subscriptionId);
105
+
106
+ // Stop polling if no more subscribers
107
+ if (subscription.subscribers.size === 0) {
108
+ this._stopPolling(deviceUuid);
109
+ this.subscriptions.delete(deviceUuid);
110
+ this.logger(`Unsubscribed from device ${deviceUuid}. No more subscribers.`);
111
+ } else {
112
+ this.logger(`Unsubscribed from device ${deviceUuid}. Remaining subscribers: ${subscription.subscribers.size}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Subscribe to HTTP device list updates
118
+ *
119
+ * @param {Function} [onUpdate] - Callback: (devices) => {}
120
+ * @returns {string} Subscription ID
121
+ */
122
+ subscribeToDeviceList(onUpdate = null) {
123
+ const subId = `deviceList-${Date.now()}-${Math.random()}`;
124
+ this.httpSubscribers.set(subId, {
125
+ onUpdate: onUpdate || (() => {}),
126
+ subscribedAt: Date.now()
127
+ });
128
+
129
+ // Start HTTP polling if this is the first subscriber
130
+ if (this.httpSubscribers.size === 1) {
131
+ this._startHttpPolling();
132
+ }
133
+
134
+ // Immediately fetch current device list
135
+ this._pollHttpDeviceList();
136
+
137
+ return subId;
138
+ }
139
+
140
+ /**
141
+ * Unsubscribe from HTTP device list updates
142
+ *
143
+ * @param {string} subscriptionId - Subscription ID
144
+ */
145
+ unsubscribeFromDeviceList(subscriptionId) {
146
+ this.httpSubscribers.delete(subscriptionId);
147
+
148
+ if (this.httpSubscribers.size === 0) {
149
+ this._stopHttpPolling();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Cleanup all subscriptions and stop all polling
155
+ */
156
+ destroy() {
157
+ // Stop all device polling
158
+ this.subscriptions.forEach((subscription, deviceUuid) => {
159
+ this._stopPolling(deviceUuid);
160
+ });
161
+
162
+ this.subscriptions.clear();
163
+
164
+ // Stop HTTP polling
165
+ this._stopHttpPolling();
166
+ this.httpSubscribers.clear();
167
+ }
168
+
169
+ /**
170
+ * Create subscription state for a device
171
+ * @private
172
+ */
173
+ _createSubscription(device) {
174
+ const subscription = {
175
+ device,
176
+ subscribers: new Map(),
177
+ pollingIntervals: new Map(), // feature -> interval handle
178
+ lastPollTimes: new Map(), // feature -> timestamp
179
+ lastUpdate: null, // Last unified update
180
+ pushActive: false, // Whether push notifications are active
181
+ pushLastSeen: null // Last push notification timestamp
182
+ };
183
+
184
+ this.subscriptions.set(device.uuid, subscription);
185
+
186
+ // Listen to push notifications (only to mark push as active for smart polling)
187
+ device.on('pushNotification', () => {
188
+ this._markPushActive(device.uuid);
189
+ });
190
+
191
+ // Listen to stateChange events (unified handler for all state changes)
192
+ device.on('stateChange', (event) => {
193
+ this._handleStateChange(device.uuid, event);
194
+ });
195
+
196
+ // Listen to stateRefreshed events
197
+ device.on('stateRefreshed', (data) => {
198
+ this._handleStateRefreshed(device.uuid, data);
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Start polling for a device.
204
+ *
205
+ * @private
206
+ */
207
+ _startPolling(device, subscription) {
208
+ // Use the most aggressive polling config from all subscribers to ensure
209
+ // all subscribers receive updates at least as frequently as they require
210
+ const config = this._getAggressiveConfig(subscription);
211
+
212
+ // Poll device state (System.All)
213
+ if (config.deviceStateInterval > 0) {
214
+ const interval = setInterval(async () => {
215
+ await this._pollDeviceState(device, subscription);
216
+ }, config.deviceStateInterval);
217
+
218
+ subscription.pollingIntervals.set('deviceState', interval);
219
+
220
+ // Poll immediately
221
+ this._pollDeviceState(device, subscription);
222
+ }
223
+
224
+ // Poll electricity if device supports it
225
+ if (config.electricityInterval > 0 && typeof device.getElectricity === 'function') {
226
+ const interval = setInterval(async () => {
227
+ await this._pollElectricity(device, subscription, config);
228
+ }, config.electricityInterval);
229
+
230
+ subscription.pollingIntervals.set('electricity', interval);
231
+ }
232
+
233
+ // Poll consumption if device supports it
234
+ if (config.consumptionInterval > 0 && typeof device.getPowerConsumption === 'function') {
235
+ const interval = setInterval(async () => {
236
+ await this._pollConsumption(device, subscription, config);
237
+ }, config.consumptionInterval);
238
+
239
+ subscription.pollingIntervals.set('consumption', interval);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Stop polling for a device
245
+ * @private
246
+ */
247
+ _stopPolling(deviceUuid) {
248
+ const subscription = this.subscriptions.get(deviceUuid);
249
+ if (!subscription) {
250
+ return;
251
+ }
252
+
253
+ subscription.pollingIntervals.forEach((interval) => {
254
+ clearInterval(interval);
255
+ });
256
+
257
+ subscription.pollingIntervals.clear();
258
+ }
259
+
260
+ /**
261
+ * Poll device state (System.All)
262
+ * @private
263
+ */
264
+ async _pollDeviceState(device, subscription) {
265
+ // Skip polling if push notifications are active and recent to avoid
266
+ // redundant requests when the device is already sending updates via push
267
+ if (subscription.pushActive && subscription.pushLastSeen) {
268
+ const timeSincePush = Date.now() - subscription.pushLastSeen;
269
+ if (timeSincePush < 5000) {
270
+ return;
271
+ }
272
+ }
273
+
274
+ // Check cache age if smart caching is enabled to avoid unnecessary polls
275
+ // when cached data is still fresh, reducing network traffic and device load
276
+ const config = this._getAggressiveConfig(subscription);
277
+ if (config.smartCaching && device._lastFullUpdateTimestamp) {
278
+ const cacheAge = Date.now() - device._lastFullUpdateTimestamp;
279
+ if (cacheAge < config.cacheMaxAge) {
280
+ this._emitCachedState(device, subscription);
281
+ return;
282
+ }
283
+ }
284
+
285
+ try {
286
+ await device.refreshState();
287
+ subscription.lastPollTimes.set('deviceState', Date.now());
288
+ } catch (error) {
289
+ this.logger(`Error polling device state for ${device.uuid}: ${error.message}`);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Poll electricity metrics
295
+ * @private
296
+ */
297
+ async _pollElectricity(device, subscription, config) {
298
+ // Skip polling if push notifications are active to avoid redundant requests
299
+ if (subscription.pushActive) {
300
+ return;
301
+ }
302
+
303
+ try {
304
+ // Check cache first if smart caching enabled to avoid unnecessary polls
305
+ // when cached data is still fresh
306
+ if (config.smartCaching && typeof device.getCachedElectricity === 'function') {
307
+ const channels = device.channels || [{ index: 0 }];
308
+ let allCached = true;
309
+
310
+ for (const channel of channels) {
311
+ const cached = device.getCachedElectricity(channel.index);
312
+ if (!cached || !cached.sampleTimestamp) {
313
+ allCached = false;
314
+ break;
315
+ }
316
+ const age = Date.now() - cached.sampleTimestamp.getTime();
317
+ if (age >= config.cacheMaxAge) {
318
+ allCached = false;
319
+ break;
320
+ }
321
+ }
322
+
323
+ if (allCached) {
324
+ return;
325
+ }
326
+ }
327
+
328
+ // Poll all channels
329
+ const channels = device.channels || [{ index: 0 }];
330
+ for (const channel of channels) {
331
+ await device.getElectricity({ channel: channel.index });
332
+ }
333
+
334
+ subscription.lastPollTimes.set('electricity', Date.now());
335
+ } catch (error) {
336
+ this.logger(`Error polling electricity for ${device.uuid}: ${error.message}`);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Poll consumption data
342
+ * @private
343
+ */
344
+ async _pollConsumption(device, subscription, config) {
345
+ // Skip polling if push notifications are active to avoid redundant requests
346
+ if (subscription.pushActive) {
347
+ return;
348
+ }
349
+
350
+ try {
351
+ // Check cache first if smart caching enabled to avoid unnecessary polls
352
+ // when cached data is still fresh
353
+ if (config.smartCaching && typeof device.getCachedConsumption === 'function') {
354
+ const channels = device.channels || [{ index: 0 }];
355
+ let allCached = true;
356
+
357
+ for (const channel of channels) {
358
+ const cached = device.getCachedConsumption(channel.index);
359
+ if (!cached) {
360
+ allCached = false;
361
+ break;
362
+ }
363
+ }
364
+
365
+ if (allCached) {
366
+ return;
367
+ }
368
+ }
369
+
370
+ const channels = device.channels || [{ index: 0 }];
371
+ for (const channel of channels) {
372
+ await device.getPowerConsumption({ channel: channel.index });
373
+ }
374
+
375
+ subscription.lastPollTimes.set('consumption', Date.now());
376
+ } catch (error) {
377
+ this.logger(`Error polling consumption for ${device.uuid}: ${error.message}`);
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Mark push notifications as active (for smart polling)
383
+ * @private
384
+ */
385
+ _markPushActive(deviceUuid) {
386
+ const subscription = this.subscriptions.get(deviceUuid);
387
+ if (!subscription) {
388
+ return;
389
+ }
390
+
391
+ subscription.pushActive = true;
392
+ subscription.pushLastSeen = Date.now();
393
+
394
+ // Reset push activity timer (if no push for 60s, consider it inactive)
395
+ clearTimeout(subscription.pushInactivityTimer);
396
+ subscription.pushInactivityTimer = setTimeout(() => {
397
+ subscription.pushActive = false;
398
+ }, 60000);
399
+ }
400
+
401
+ /**
402
+ * Handle stateChange event (unified handler for all state changes from push/poll).
403
+ *
404
+ * @private
405
+ */
406
+ _handleStateChange(deviceUuid, event) {
407
+ const subscription = this.subscriptions.get(deviceUuid);
408
+ if (!subscription) {
409
+ return;
410
+ }
411
+
412
+ // Transform stateChange event to changes format expected by subscribers
413
+ const changes = {};
414
+ if (event.type && event.value !== undefined) {
415
+ changes[event.type] = { [event.channel]: event.value };
416
+ }
417
+
418
+ const unifiedUpdate = {
419
+ source: event.source || 'poll',
420
+ timestamp: event.timestamp || Date.now(),
421
+ event,
422
+ device: subscription.device,
423
+ state: subscription.device.getUnifiedState(),
424
+ changes
425
+ };
426
+
427
+ subscription.lastUpdate = unifiedUpdate;
428
+ this._distributeUpdate(deviceUuid, unifiedUpdate);
429
+ }
430
+
431
+
432
+ /**
433
+ * Handle stateRefreshed event
434
+ * @private
435
+ */
436
+ _handleStateRefreshed(deviceUuid, data) {
437
+ const subscription = this.subscriptions.get(deviceUuid);
438
+ if (!subscription) {
439
+ return;
440
+ }
441
+
442
+ const update = {
443
+ source: 'poll',
444
+ timestamp: data.timestamp || Date.now(),
445
+ device: subscription.device,
446
+ state: data.state || subscription.device.getUnifiedState(),
447
+ changes: {} // Full refresh means all state is new, so no specific changes to report
448
+ };
449
+
450
+ subscription.lastUpdate = update;
451
+ this._distributeUpdate(deviceUuid, update);
452
+ }
453
+
454
+ /**
455
+ * Distribute update to all subscribers
456
+ * @private
457
+ */
458
+ _distributeUpdate(deviceUuid, update) {
459
+ const subscription = this.subscriptions.get(deviceUuid);
460
+ if (!subscription) {
461
+ return;
462
+ }
463
+
464
+ subscription.subscribers.forEach((subscriber) => {
465
+ try {
466
+ subscriber.onUpdate(update);
467
+ } catch (error) {
468
+ this.logger(`Error in subscriber callback for ${deviceUuid}: ${error.message}`);
469
+ }
470
+ });
471
+ }
472
+
473
+ /**
474
+ * Get most aggressive (shortest interval) config from all subscribers.
475
+ *
476
+ * Uses the shortest polling intervals requested by any subscriber to ensure
477
+ * all subscribers receive updates at least as frequently as they require.
478
+ *
479
+ * @private
480
+ */
481
+ _getAggressiveConfig(subscription) {
482
+ const config = { ...this.defaultConfig };
483
+
484
+ subscription.subscribers.forEach((subscriber) => {
485
+ const subConfig = subscriber.config;
486
+ if (subConfig.deviceStateInterval < config.deviceStateInterval) {
487
+ config.deviceStateInterval = subConfig.deviceStateInterval;
488
+ }
489
+ if (subConfig.electricityInterval < config.electricityInterval) {
490
+ config.electricityInterval = subConfig.electricityInterval;
491
+ }
492
+ if (subConfig.consumptionInterval < config.consumptionInterval) {
493
+ config.consumptionInterval = subConfig.consumptionInterval;
494
+ }
495
+ if (subConfig.cacheMaxAge < config.cacheMaxAge) {
496
+ config.cacheMaxAge = subConfig.cacheMaxAge;
497
+ }
498
+ });
499
+
500
+ return config;
501
+ }
502
+
503
+ /**
504
+ * Start HTTP device list polling
505
+ * @private
506
+ */
507
+ _startHttpPolling() {
508
+ this.httpPollInterval = setInterval(() => {
509
+ this._pollHttpDeviceList();
510
+ }, this.defaultConfig.httpDeviceListInterval);
511
+
512
+ // Poll immediately
513
+ this._pollHttpDeviceList();
514
+ }
515
+
516
+ /**
517
+ * Stop HTTP device list polling
518
+ * @private
519
+ */
520
+ _stopHttpPolling() {
521
+ if (this.httpPollInterval) {
522
+ clearInterval(this.httpPollInterval);
523
+ this.httpPollInterval = null;
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Poll HTTP device list
529
+ * @private
530
+ */
531
+ async _pollHttpDeviceList() {
532
+ try {
533
+ const devices = await this.manager.httpClient.listDevices();
534
+
535
+ // Compare with previous list to detect changes
536
+ const previousUuids = new Set(this._lastDeviceList?.map(d => d.uuid) || []);
537
+ const currentUuids = new Set(devices.map(d => d.uuid));
538
+
539
+ const added = devices.filter(d => !previousUuids.has(d.uuid));
540
+ const removed = this._lastDeviceList?.filter(d => !currentUuids.has(d.uuid)) || [];
541
+ const changed = devices.filter(d => {
542
+ const prev = this._lastDeviceList?.find(p => p.uuid === d.uuid);
543
+ return prev && JSON.stringify(prev) !== JSON.stringify(d);
544
+ });
545
+
546
+ this._lastDeviceList = devices;
547
+
548
+ // Distribute to all subscribers
549
+ this.httpSubscribers.forEach((subscriber) => {
550
+ try {
551
+ subscriber.onUpdate({
552
+ devices,
553
+ added,
554
+ removed,
555
+ changed,
556
+ timestamp: Date.now()
557
+ });
558
+ } catch (error) {
559
+ this.logger(`Error in device list subscriber callback: ${error.message}`);
560
+ }
561
+ });
562
+ } catch (error) {
563
+ this.logger(`Error polling HTTP device list: ${error.message}`);
564
+ }
565
+ }
566
+
567
+
568
+ /**
569
+ * Emit cached state to subscribers
570
+ * @private
571
+ */
572
+ _emitCachedState(device, subscription) {
573
+ const update = {
574
+ source: 'cache',
575
+ timestamp: Date.now(),
576
+ device,
577
+ state: device.getUnifiedState()
578
+ };
579
+
580
+ subscription.lastUpdate = update;
581
+ this._distributeUpdate(device.uuid, update);
582
+ }
583
+
584
+ }
585
+
586
+ module.exports = SubscriptionManager;
587
+
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Converts RGB color to integer representation used by Meross devices.
5
+ *
6
+ * Meross devices expect RGB colors as a single integer value where each color component
7
+ * occupies specific bit ranges: red in bits 16-23, green in bits 8-15, and blue in bits 0-7.
8
+ * This function normalizes various input formats (array, object, or already-converted integer)
9
+ * to this device-compatible format.
10
+ *
11
+ * @param {number|Array<number>|Object} rgb - RGB color value
12
+ * @param {number} [rgb] - If number, treated as already converted integer
13
+ * @param {Array<number>} [rgb] - If array, expected format [r, g, b] where each is 0-255
14
+ * @param {Object} [rgb] - If object, expected format {r, g, b} or {red, green, blue}
15
+ * @returns {number} RGB color as integer
16
+ * @throws {CommandError} If RGB value is invalid
17
+ * @example
18
+ * const rgbInt = rgbToInt([255, 0, 0]); // Red
19
+ * const rgbInt2 = rgbToInt({r: 0, g: 255, b: 0}); // Green
20
+ * const rgbInt3 = rgbToInt(16711680); // Already an integer (red)
21
+ */
22
+ function rgbToInt(rgb) {
23
+ if (typeof rgb === 'number') {
24
+ return rgb;
25
+ } else if (Array.isArray(rgb) && rgb.length === 3) {
26
+ const [red, green, blue] = rgb;
27
+ return (red << 16) | (green << 8) | blue;
28
+ } else if (rgb && typeof rgb === 'object') {
29
+ const red = rgb.r || rgb.red || 0;
30
+ const green = rgb.g || rgb.green || 0;
31
+ const blue = rgb.b || rgb.blue || 0;
32
+ return (red << 16) | (green << 8) | blue;
33
+ } else {
34
+ const { CommandError } = require('../model/exception');
35
+ throw new CommandError('Invalid value for RGB! Must be integer, [r,g,b] tuple, or {r,g,b} object', { rgb });
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Converts RGB integer to [r, g, b] array.
41
+ *
42
+ * Extracts individual color components from the packed integer format used by Meross devices
43
+ * by masking and shifting the appropriate bit ranges.
44
+ *
45
+ * @param {number} rgbInt - RGB color as integer
46
+ * @returns {Array<number>} RGB tuple [r, g, b] where each value is 0-255
47
+ * @example
48
+ * const rgb = intToRgb(16711680); // Returns [255, 0, 0] (red)
49
+ * const [r, g, b] = intToRgb(65280); // Returns [0, 255, 0] (green)
50
+ */
51
+ function intToRgb(rgbInt) {
52
+ const red = (rgbInt & 16711680) >> 16;
53
+ const green = (rgbInt & 65280) >> 8;
54
+ const blue = (rgbInt & 255);
55
+ return [red, green, blue];
56
+ }
57
+
58
+ module.exports = {
59
+ rgbToInt,
60
+ intToRgb
61
+ };
62
+