meross-iot 0.1.0 → 0.2.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 CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-01-14
11
+
12
+ ### Changed
13
+ - **BREAKING**: `SubscriptionManager` now uses EventEmitter pattern instead of callbacks
14
+ - `subscribe(device, config, onUpdate)` → `subscribe(device, config)` (no callback, no return value)
15
+ - `unsubscribe(deviceUuid, subscriptionId)` → `unsubscribe(deviceUuid)` (no subscription ID needed)
16
+ - `subscribeToDeviceList(onUpdate)` → `subscribeToDeviceList()` (no callback, no return value)
17
+ - `unsubscribeFromDeviceList(subscriptionId)` → `unsubscribeFromDeviceList()` (no subscription ID needed)
18
+ - Listen for updates using: `on('deviceUpdate:${deviceUuid}', handler)` and `on('deviceListUpdate', handler)`
19
+ - Use standard EventEmitter methods: `on()`, `once()`, `off()`, `removeAllListeners()`
20
+ - Configuration is now per-device subscription (merged aggressively) rather than per-listener
21
+
22
+ ### Added
23
+ - `subscription-manager.js` example demonstrating EventEmitter-based SubscriptionManager usage
24
+ - Enhanced documentation for SubscriptionManager with JSDoc comments explaining implementation rationale
25
+
10
26
  ## [0.1.0] - 2026-01-10
11
27
 
12
28
  ### Added
package/README.md CHANGED
@@ -75,6 +75,7 @@ The `example/` directory contains focused examples for different use cases:
75
75
  - **`basic-usage.js`** - Simple connection and device discovery
76
76
  - **`device-control.js`** - Controlling switches, lights, and monitoring devices
77
77
  - **`event-handling.js`** - Handling events from devices and the manager
78
+ - **`subscription-manager.js`** - Automatic polling and unified update streams with SubscriptionManager
78
79
  - **`token-reuse.js`** - Saving and reusing authentication tokens
79
80
  - **`statistics.js`** - Enabling and viewing API call statistics
80
81
  - **`error-handling.js`** - Comprehensive error handling and MFA
package/index.d.ts CHANGED
@@ -1349,22 +1349,41 @@ declare module 'meross-iot' {
1349
1349
  }
1350
1350
 
1351
1351
  /**
1352
- * Device update callback data
1352
+ * Device update event data
1353
1353
  */
1354
1354
  export interface DeviceUpdate {
1355
- deviceUuid: string
1355
+ source: string
1356
1356
  timestamp: number
1357
- data?: Record<string, any>
1357
+ event?: any
1358
+ device: MerossDevice
1359
+ state: any
1360
+ changes: Record<string, any>
1358
1361
  [key: string]: any
1359
1362
  }
1360
1363
 
1361
- export class SubscriptionManager {
1364
+ /**
1365
+ * Device list update event data
1366
+ */
1367
+ export interface DeviceListUpdate {
1368
+ devices: DeviceDefinition[]
1369
+ added: DeviceDefinition[]
1370
+ removed: DeviceDefinition[]
1371
+ changed: DeviceDefinition[]
1372
+ timestamp: number
1373
+ }
1374
+
1375
+ export class SubscriptionManager extends EventEmitter {
1362
1376
  constructor(manager: MerossManager, options?: SubscriptionManagerOptions);
1363
- subscribe(device: MerossDevice, config?: SubscriptionManagerOptions, onUpdate?: (update: DeviceUpdate) => void): string;
1364
- unsubscribe(deviceUuid: string, subscriptionId: string): void;
1365
- subscribeToDeviceList(onUpdate?: (devices: DeviceDefinition[]) => void): string;
1366
- unsubscribeFromDeviceList(subscriptionId: string): void;
1377
+ subscribe(device: MerossDevice, config?: SubscriptionManagerOptions): void;
1378
+ unsubscribe(deviceUuid: string): void;
1379
+ subscribeToDeviceList(): void;
1380
+ unsubscribeFromDeviceList(): void;
1367
1381
  destroy(): void;
1382
+
1383
+ // EventEmitter events
1384
+ on(event: `deviceUpdate:${string}`, listener: (update: DeviceUpdate) => void): this;
1385
+ on(event: 'deviceListUpdate', listener: (update: DeviceListUpdate) => void): this;
1386
+ on(event: 'error', listener: (error: Error, context?: string) => void): this;
1368
1387
  }
1369
1388
 
1370
1389
  /**
@@ -1,28 +1,30 @@
1
1
  'use strict';
2
2
 
3
+ const EventEmitter = require('events');
4
+
3
5
  /**
4
- * Provides automatic polling and data provisioning for Meross devices.
6
+ * Manages automatic polling and unified update streams for Meross devices.
5
7
  *
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.
8
+ * Coordinates push notifications and periodic polling to provide a single event stream
9
+ * for device state changes. Handles polling lifecycle, cache management, and prevents
10
+ * redundant network requests when push notifications are active or cached data is fresh.
10
11
  *
11
12
  * @class
13
+ * @extends EventEmitter
12
14
  */
13
- class SubscriptionManager {
15
+ class SubscriptionManager extends EventEmitter {
14
16
  /**
15
- * Creates a new SubscriptionManager instance
17
+ * Creates a new SubscriptionManager instance.
16
18
  *
17
- * @param {Object} manager - MerossManager instance
19
+ * @param {Object} manager - MerossManager instance that provides device access
18
20
  * @param {Object} [options={}] - Configuration options
19
21
  * @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)
22
+ * @param {number} [options.deviceStateInterval=30000] - Device state polling interval in milliseconds
23
+ * @param {number} [options.electricityInterval=30000] - Electricity metrics polling interval in milliseconds
24
+ * @param {number} [options.consumptionInterval=60000] - Power consumption polling interval in milliseconds
25
+ * @param {number} [options.httpDeviceListInterval=120000] - HTTP device list polling interval in milliseconds
26
+ * @param {boolean} [options.smartCaching=true] - Skip polling when cached data is fresh to reduce network traffic
27
+ * @param {number} [options.cacheMaxAge=10000] - Maximum cache age in milliseconds before considering data stale
26
28
  */
27
29
  constructor(manager, options = {}) {
28
30
  if (!manager) {
@@ -41,187 +43,200 @@ class SubscriptionManager {
41
43
  cacheMaxAge: options.cacheMaxAge || 10000
42
44
  };
43
45
 
44
- // Tracks subscription state for each device (deviceUuid -> SubscriptionState)
45
46
  this.subscriptions = new Map();
46
-
47
- // HTTP device list polling tracks device additions/removals from the cloud API
48
47
  this.httpPollInterval = null;
49
- this.httpSubscribers = new Map();
50
48
  this._lastDeviceList = null;
51
49
  }
52
50
 
53
51
  /**
54
- * Subscribe to device updates
52
+ * Subscribe to device updates and start polling if needed.
53
+ *
54
+ * Configures polling intervals for the device and merges with existing configuration
55
+ * using the most aggressive (shortest) intervals to ensure all listeners receive
56
+ * updates at least as frequently as required. Polling starts automatically when
57
+ * the first subscription is created.
58
+ *
59
+ * Listen for updates using: `on('deviceUpdate:${deviceUuid}', handler)`
55
60
  *
56
61
  * @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
62
+ * @param {Object} [config={}] - Subscription configuration overrides
63
+ * @param {number} [config.deviceStateInterval] - Device state polling interval in milliseconds
64
+ * @param {number} [config.electricityInterval] - Electricity metrics polling interval in milliseconds
65
+ * @param {number} [config.consumptionInterval] - Power consumption polling interval in milliseconds
66
+ * @param {boolean} [config.smartCaching] - Enable cache-based polling optimization
67
+ * @param {number} [config.cacheMaxAge] - Maximum cache age in milliseconds before refresh
65
68
  */
66
- subscribe(device, config = {}, onUpdate = null) {
69
+ subscribe(device, config = {}) {
67
70
  const deviceUuid = device.uuid;
71
+ const eventName = `deviceUpdate:${deviceUuid}`;
68
72
 
69
73
  if (!this.subscriptions.has(deviceUuid)) {
70
74
  this._createSubscription(device);
71
75
  }
72
76
 
73
77
  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
- });
78
+ const isNewSubscription = !subscription.pollingIntervals || subscription.pollingIntervals.size === 0;
79
+
80
+ // Merge configuration using shortest intervals to satisfy all listeners
81
+ const existingConfig = subscription.config || { ...this.defaultConfig };
82
+ subscription.config = {
83
+ deviceStateInterval: Math.min(
84
+ existingConfig.deviceStateInterval || this.defaultConfig.deviceStateInterval,
85
+ config.deviceStateInterval || this.defaultConfig.deviceStateInterval
86
+ ),
87
+ electricityInterval: Math.min(
88
+ existingConfig.electricityInterval || this.defaultConfig.electricityInterval,
89
+ config.electricityInterval || this.defaultConfig.electricityInterval
90
+ ),
91
+ consumptionInterval: Math.min(
92
+ existingConfig.consumptionInterval || this.defaultConfig.consumptionInterval,
93
+ config.consumptionInterval || this.defaultConfig.consumptionInterval
94
+ ),
95
+ smartCaching: config.smartCaching !== undefined ? config.smartCaching : existingConfig.smartCaching,
96
+ cacheMaxAge: Math.min(
97
+ existingConfig.cacheMaxAge || this.defaultConfig.cacheMaxAge,
98
+ config.cacheMaxAge || this.defaultConfig.cacheMaxAge
99
+ )
100
+ };
81
101
 
82
- // Start polling if this is the first subscriber
83
- if (subscription.subscribers.size === 1) {
102
+ if (isNewSubscription) {
84
103
  this._startPolling(device, subscription);
85
104
  }
86
105
 
87
- this.logger(`Subscribed to device ${device.name} (${deviceUuid}). Total subscribers: ${subscription.subscribers.size}`);
88
-
89
- return subId;
106
+ const listenerCount = this.listenerCount(eventName);
107
+ this.logger(`Subscribed to device ${device.name} (${deviceUuid}). Total listeners: ${listenerCount}`);
90
108
  }
91
109
 
92
110
  /**
93
- * Unsubscribe from device updates
111
+ * Unsubscribe from device updates and stop polling when no listeners remain.
94
112
  *
95
- * @param {string} deviceUuid - Device UUID
96
- * @param {string} subscriptionId - Subscription ID returned from subscribe()
113
+ * Stops polling and cleans up resources when the last event listener is removed.
114
+ * Call this after removing all listeners with `removeAllListeners()` or `off()`.
115
+ *
116
+ * @param {string} deviceUuid - Device UUID to unsubscribe from
97
117
  */
98
- unsubscribe(deviceUuid, subscriptionId) {
118
+ unsubscribe(deviceUuid) {
99
119
  const subscription = this.subscriptions.get(deviceUuid);
100
120
  if (!subscription) {
101
121
  return;
102
122
  }
103
123
 
104
- subscription.subscribers.delete(subscriptionId);
124
+ const eventName = `deviceUpdate:${deviceUuid}`;
125
+ const listenerCount = this.listenerCount(eventName);
105
126
 
106
- // Stop polling if no more subscribers
107
- if (subscription.subscribers.size === 0) {
127
+ if (listenerCount === 0) {
108
128
  this._stopPolling(deviceUuid);
109
129
  this.subscriptions.delete(deviceUuid);
110
- this.logger(`Unsubscribed from device ${deviceUuid}. No more subscribers.`);
130
+ this.logger(`Unsubscribed from device ${deviceUuid}. No more listeners.`);
111
131
  } else {
112
- this.logger(`Unsubscribed from device ${deviceUuid}. Remaining subscribers: ${subscription.subscribers.size}`);
132
+ this.logger(`Unsubscribed from device ${deviceUuid}. Remaining listeners: ${listenerCount}`);
113
133
  }
114
134
  }
115
135
 
116
136
  /**
117
- * Subscribe to HTTP device list updates
137
+ * Subscribe to HTTP device list updates.
118
138
  *
119
- * @param {Function} [onUpdate] - Callback: (devices) => {}
120
- * @returns {string} Subscription ID
139
+ * Starts periodic polling of the HTTP API device list to detect additions,
140
+ * removals, and metadata changes. Listen for updates using:
141
+ * `on('deviceListUpdate', handler)`
121
142
  */
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) {
143
+ subscribeToDeviceList() {
144
+ if (!this.httpPollInterval) {
131
145
  this._startHttpPolling();
132
146
  }
133
147
 
134
- // Immediately fetch current device list
135
- this._pollHttpDeviceList();
136
-
137
- return subId;
148
+ const listenerCount = this.listenerCount('deviceListUpdate');
149
+ this.logger(`Subscribed to device list. Total listeners: ${listenerCount}`);
138
150
  }
139
151
 
140
152
  /**
141
- * Unsubscribe from HTTP device list updates
153
+ * Unsubscribe from HTTP device list updates.
142
154
  *
143
- * @param {string} subscriptionId - Subscription ID
155
+ * Stops HTTP polling when no listeners remain. Call this after removing
156
+ * all listeners with `removeAllListeners('deviceListUpdate')` or `off()`.
144
157
  */
145
- unsubscribeFromDeviceList(subscriptionId) {
146
- this.httpSubscribers.delete(subscriptionId);
158
+ unsubscribeFromDeviceList() {
159
+ const listenerCount = this.listenerCount('deviceListUpdate');
147
160
 
148
- if (this.httpSubscribers.size === 0) {
161
+ if (listenerCount === 0) {
149
162
  this._stopHttpPolling();
150
163
  }
151
164
  }
152
165
 
153
166
  /**
154
- * Cleanup all subscriptions and stop all polling
167
+ * Cleanup all subscriptions, stop all polling, and remove all event listeners.
168
+ *
169
+ * Call this method when shutting down to prevent memory leaks and ensure
170
+ * all intervals are cleared.
155
171
  */
156
172
  destroy() {
157
- // Stop all device polling
173
+ this.removeAllListeners();
174
+
158
175
  this.subscriptions.forEach((subscription, deviceUuid) => {
159
176
  this._stopPolling(deviceUuid);
160
177
  });
161
178
 
162
179
  this.subscriptions.clear();
163
-
164
- // Stop HTTP polling
165
180
  this._stopHttpPolling();
166
- this.httpSubscribers.clear();
167
181
  }
168
182
 
169
183
  /**
170
- * Create subscription state for a device
184
+ * Initialize subscription state and register device event handlers.
185
+ *
186
+ * Sets up event listeners on the device to capture push notifications and state
187
+ * changes, which are then distributed to SubscriptionManager listeners via events.
188
+ *
171
189
  * @private
190
+ * @param {MerossDevice} device - Device to create subscription for
172
191
  */
173
192
  _createSubscription(device) {
174
193
  const subscription = {
175
194
  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
195
+ config: null,
196
+ pollingIntervals: new Map(),
197
+ lastPollTimes: new Map(),
198
+ lastUpdate: null,
199
+ pushActive: false,
200
+ pushLastSeen: null
182
201
  };
183
202
 
184
203
  this.subscriptions.set(device.uuid, subscription);
185
204
 
186
- // Listen to push notifications (only to mark push as active for smart polling)
187
205
  device.on('pushNotification', () => {
188
206
  this._markPushActive(device.uuid);
189
207
  });
190
208
 
191
- // Listen to stateChange events (unified handler for all state changes)
192
209
  device.on('stateChange', (event) => {
193
210
  this._handleStateChange(device.uuid, event);
194
211
  });
195
212
 
196
- // Listen to stateRefreshed events
197
213
  device.on('stateRefreshed', (data) => {
198
214
  this._handleStateRefreshed(device.uuid, data);
199
215
  });
200
216
  }
201
217
 
202
218
  /**
203
- * Start polling for a device.
219
+ * Start periodic polling for device state, electricity, and consumption data.
220
+ *
221
+ * Creates intervals for each supported feature based on configuration. Performs
222
+ * an immediate poll on startup to provide initial state to listeners.
204
223
  *
205
224
  * @private
225
+ * @param {MerossDevice} device - Device to poll
226
+ * @param {Object} subscription - Subscription state object
206
227
  */
207
228
  _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);
229
+ const config = subscription.config || this.defaultConfig;
211
230
 
212
- // Poll device state (System.All)
213
231
  if (config.deviceStateInterval > 0) {
214
232
  const interval = setInterval(async () => {
215
233
  await this._pollDeviceState(device, subscription);
216
234
  }, config.deviceStateInterval);
217
235
 
218
236
  subscription.pollingIntervals.set('deviceState', interval);
219
-
220
- // Poll immediately
221
237
  this._pollDeviceState(device, subscription);
222
238
  }
223
239
 
224
- // Poll electricity if device supports it
225
240
  if (config.electricityInterval > 0 && typeof device.getElectricity === 'function') {
226
241
  const interval = setInterval(async () => {
227
242
  await this._pollElectricity(device, subscription, config);
@@ -230,7 +245,6 @@ class SubscriptionManager {
230
245
  subscription.pollingIntervals.set('electricity', interval);
231
246
  }
232
247
 
233
- // Poll consumption if device supports it
234
248
  if (config.consumptionInterval > 0 && typeof device.getPowerConsumption === 'function') {
235
249
  const interval = setInterval(async () => {
236
250
  await this._pollConsumption(device, subscription, config);
@@ -241,8 +255,10 @@ class SubscriptionManager {
241
255
  }
242
256
 
243
257
  /**
244
- * Stop polling for a device
258
+ * Stop all polling intervals for a device.
259
+ *
245
260
  * @private
261
+ * @param {string} deviceUuid - Device UUID to stop polling for
246
262
  */
247
263
  _stopPolling(deviceUuid) {
248
264
  const subscription = this.subscriptions.get(deviceUuid);
@@ -258,12 +274,17 @@ class SubscriptionManager {
258
274
  }
259
275
 
260
276
  /**
261
- * Poll device state (System.All)
277
+ * Poll device state via System.All namespace.
278
+ *
279
+ * Skips polling if push notifications were received recently (within 5 seconds)
280
+ * or if cached data is fresh (when smart caching is enabled). This prevents
281
+ * redundant network requests when the device is already providing updates.
282
+ *
262
283
  * @private
284
+ * @param {MerossDevice} device - Device to poll
285
+ * @param {Object} subscription - Subscription state object
263
286
  */
264
287
  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
288
  if (subscription.pushActive && subscription.pushLastSeen) {
268
289
  const timeSincePush = Date.now() - subscription.pushLastSeen;
269
290
  if (timeSincePush < 5000) {
@@ -271,9 +292,7 @@ class SubscriptionManager {
271
292
  }
272
293
  }
273
294
 
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);
295
+ const config = subscription.config || this.defaultConfig;
277
296
  if (config.smartCaching && device._lastFullUpdateTimestamp) {
278
297
  const cacheAge = Date.now() - device._lastFullUpdateTimestamp;
279
298
  if (cacheAge < config.cacheMaxAge) {
@@ -291,18 +310,22 @@ class SubscriptionManager {
291
310
  }
292
311
 
293
312
  /**
294
- * Poll electricity metrics
313
+ * Poll electricity metrics (power, voltage, current) for all device channels.
314
+ *
315
+ * Skips polling when push notifications are active or when cached data for all
316
+ * channels is fresh, reducing unnecessary network requests.
317
+ *
295
318
  * @private
319
+ * @param {MerossDevice} device - Device to poll
320
+ * @param {Object} subscription - Subscription state object
321
+ * @param {Object} config - Configuration object with caching settings
296
322
  */
297
323
  async _pollElectricity(device, subscription, config) {
298
- // Skip polling if push notifications are active to avoid redundant requests
299
324
  if (subscription.pushActive) {
300
325
  return;
301
326
  }
302
327
 
303
328
  try {
304
- // Check cache first if smart caching enabled to avoid unnecessary polls
305
- // when cached data is still fresh
306
329
  if (config.smartCaching && typeof device.getCachedElectricity === 'function') {
307
330
  const channels = device.channels || [{ index: 0 }];
308
331
  let allCached = true;
@@ -325,7 +348,6 @@ class SubscriptionManager {
325
348
  }
326
349
  }
327
350
 
328
- // Poll all channels
329
351
  const channels = device.channels || [{ index: 0 }];
330
352
  for (const channel of channels) {
331
353
  await device.getElectricity({ channel: channel.index });
@@ -338,18 +360,22 @@ class SubscriptionManager {
338
360
  }
339
361
 
340
362
  /**
341
- * Poll consumption data
363
+ * Poll power consumption data for all device channels.
364
+ *
365
+ * Skips polling when push notifications are active or when cached consumption
366
+ * data exists for all channels, reducing network traffic.
367
+ *
342
368
  * @private
369
+ * @param {MerossDevice} device - Device to poll
370
+ * @param {Object} subscription - Subscription state object
371
+ * @param {Object} config - Configuration object with caching settings
343
372
  */
344
373
  async _pollConsumption(device, subscription, config) {
345
- // Skip polling if push notifications are active to avoid redundant requests
346
374
  if (subscription.pushActive) {
347
375
  return;
348
376
  }
349
377
 
350
378
  try {
351
- // Check cache first if smart caching enabled to avoid unnecessary polls
352
- // when cached data is still fresh
353
379
  if (config.smartCaching && typeof device.getCachedConsumption === 'function') {
354
380
  const channels = device.channels || [{ index: 0 }];
355
381
  let allCached = true;
@@ -379,8 +405,13 @@ class SubscriptionManager {
379
405
  }
380
406
 
381
407
  /**
382
- * Mark push notifications as active (for smart polling)
408
+ * Mark push notifications as active and reset inactivity timer.
409
+ *
410
+ * When push notifications are active, polling is reduced to avoid redundant
411
+ * requests. Push activity expires after 60 seconds of inactivity.
412
+ *
383
413
  * @private
414
+ * @param {string} deviceUuid - Device UUID that received push notification
384
415
  */
385
416
  _markPushActive(deviceUuid) {
386
417
  const subscription = this.subscriptions.get(deviceUuid);
@@ -391,7 +422,6 @@ class SubscriptionManager {
391
422
  subscription.pushActive = true;
392
423
  subscription.pushLastSeen = Date.now();
393
424
 
394
- // Reset push activity timer (if no push for 60s, consider it inactive)
395
425
  clearTimeout(subscription.pushInactivityTimer);
396
426
  subscription.pushInactivityTimer = setTimeout(() => {
397
427
  subscription.pushActive = false;
@@ -399,9 +429,14 @@ class SubscriptionManager {
399
429
  }
400
430
 
401
431
  /**
402
- * Handle stateChange event (unified handler for all state changes from push/poll).
432
+ * Handle incremental state change events from push notifications or polling.
433
+ *
434
+ * Transforms device stateChange events into unified update objects containing
435
+ * both the full state and only the changed values for efficient processing.
403
436
  *
404
437
  * @private
438
+ * @param {string} deviceUuid - Device UUID that changed state
439
+ * @param {Object} event - State change event from device
405
440
  */
406
441
  _handleStateChange(deviceUuid, event) {
407
442
  const subscription = this.subscriptions.get(deviceUuid);
@@ -409,7 +444,6 @@ class SubscriptionManager {
409
444
  return;
410
445
  }
411
446
 
412
- // Transform stateChange event to changes format expected by subscribers
413
447
  const changes = {};
414
448
  if (event.type && event.value !== undefined) {
415
449
  changes[event.type] = { [event.channel]: event.value };
@@ -430,8 +464,14 @@ class SubscriptionManager {
430
464
 
431
465
 
432
466
  /**
433
- * Handle stateRefreshed event
467
+ * Handle full state refresh events from polling.
468
+ *
469
+ * Creates an update object with empty changes since all state is refreshed,
470
+ * allowing listeners to process the complete state snapshot.
471
+ *
434
472
  * @private
473
+ * @param {string} deviceUuid - Device UUID that was refreshed
474
+ * @param {Object} data - Refresh data containing state and timestamp
435
475
  */
436
476
  _handleStateRefreshed(deviceUuid, data) {
437
477
  const subscription = this.subscriptions.get(deviceUuid);
@@ -444,7 +484,7 @@ class SubscriptionManager {
444
484
  timestamp: data.timestamp || Date.now(),
445
485
  device: subscription.device,
446
486
  state: data.state || subscription.device.getUnifiedState(),
447
- changes: {} // Full refresh means all state is new, so no specific changes to report
487
+ changes: {}
448
488
  };
449
489
 
450
490
  subscription.lastUpdate = update;
@@ -452,8 +492,14 @@ class SubscriptionManager {
452
492
  }
453
493
 
454
494
  /**
455
- * Distribute update to all subscribers
495
+ * Emit device update event to all listeners.
496
+ *
497
+ * Emits errors as separate events to prevent one failing listener from
498
+ * blocking others.
499
+ *
456
500
  * @private
501
+ * @param {string} deviceUuid - Device UUID to emit update for
502
+ * @param {Object} update - Update object containing state and changes
457
503
  */
458
504
  _distributeUpdate(deviceUuid, update) {
459
505
  const subscription = this.subscriptions.get(deviceUuid);
@@ -461,60 +507,34 @@ class SubscriptionManager {
461
507
  return;
462
508
  }
463
509
 
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
- });
510
+ try {
511
+ this.emit(`deviceUpdate:${deviceUuid}`, update);
512
+ } catch (error) {
513
+ this.logger(`Error emitting deviceUpdate event for ${deviceUuid}: ${error.message}`);
514
+ this.emit('error', error, deviceUuid);
515
+ }
471
516
  }
472
517
 
518
+
473
519
  /**
474
- * Get most aggressive (shortest interval) config from all subscribers.
520
+ * Start periodic polling of HTTP API device list.
475
521
  *
476
- * Uses the shortest polling intervals requested by any subscriber to ensure
477
- * all subscribers receive updates at least as frequently as they require.
522
+ * Performs an immediate poll on startup to provide current device list
523
+ * to listeners without waiting for the first interval.
478
524
  *
479
525
  * @private
480
526
  */
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
527
  _startHttpPolling() {
508
528
  this.httpPollInterval = setInterval(() => {
509
529
  this._pollHttpDeviceList();
510
530
  }, this.defaultConfig.httpDeviceListInterval);
511
531
 
512
- // Poll immediately
513
532
  this._pollHttpDeviceList();
514
533
  }
515
534
 
516
535
  /**
517
- * Stop HTTP device list polling
536
+ * Stop HTTP device list polling.
537
+ *
518
538
  * @private
519
539
  */
520
540
  _stopHttpPolling() {
@@ -525,14 +545,18 @@ class SubscriptionManager {
525
545
  }
526
546
 
527
547
  /**
528
- * Poll HTTP device list
548
+ * Poll HTTP API for device list and detect changes.
549
+ *
550
+ * Compares the current device list with the previous one to identify additions,
551
+ * removals, and metadata changes. Emits a single event with all changes to
552
+ * minimize listener processing overhead.
553
+ *
529
554
  * @private
530
555
  */
531
556
  async _pollHttpDeviceList() {
532
557
  try {
533
558
  const devices = await this.manager.httpClient.listDevices();
534
559
 
535
- // Compare with previous list to detect changes
536
560
  const previousUuids = new Set(this._lastDeviceList?.map(d => d.uuid) || []);
537
561
  const currentUuids = new Set(devices.map(d => d.uuid));
538
562
 
@@ -545,29 +569,34 @@ class SubscriptionManager {
545
569
 
546
570
  this._lastDeviceList = devices;
547
571
 
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
- });
572
+ try {
573
+ this.emit('deviceListUpdate', {
574
+ devices,
575
+ added,
576
+ removed,
577
+ changed,
578
+ timestamp: Date.now()
579
+ });
580
+ } catch (error) {
581
+ this.logger(`Error emitting deviceListUpdate event: ${error.message}`);
582
+ this.emit('error', error);
583
+ }
562
584
  } catch (error) {
563
585
  this.logger(`Error polling HTTP device list: ${error.message}`);
586
+ this.emit('error', error);
564
587
  }
565
588
  }
566
589
 
567
590
 
568
591
  /**
569
- * Emit cached state to subscribers
592
+ * Emit cached device state without performing a network request.
593
+ *
594
+ * Used when cached data is fresh and polling would be redundant. Provides
595
+ * listeners with current state while avoiding unnecessary network traffic.
596
+ *
570
597
  * @private
598
+ * @param {MerossDevice} device - Device to emit cached state for
599
+ * @param {Object} subscription - Subscription state object
571
600
  */
572
601
  _emitCachedState(device, subscription) {
573
602
  const update = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meross-iot",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Control Meross cloud devices using nodejs",
5
5
  "author": "Abe Haverkamp",
6
6
  "contributors": [
@@ -70,4 +70,3 @@
70
70
  }
71
71
  }
72
72
  }
73
-