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 +16 -0
- package/README.md +1 -0
- package/index.d.ts +27 -8
- package/lib/subscription.js +201 -172
- package/package.json +1 -2
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
|
|
1352
|
+
* Device update event data
|
|
1353
1353
|
*/
|
|
1354
1354
|
export interface DeviceUpdate {
|
|
1355
|
-
|
|
1355
|
+
source: string
|
|
1356
1356
|
timestamp: number
|
|
1357
|
-
|
|
1357
|
+
event?: any
|
|
1358
|
+
device: MerossDevice
|
|
1359
|
+
state: any
|
|
1360
|
+
changes: Record<string, any>
|
|
1358
1361
|
[key: string]: any
|
|
1359
1362
|
}
|
|
1360
1363
|
|
|
1361
|
-
|
|
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
|
|
1364
|
-
unsubscribe(deviceUuid: string
|
|
1365
|
-
subscribeToDeviceList(
|
|
1366
|
-
unsubscribeFromDeviceList(
|
|
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
|
/**
|
package/lib/subscription.js
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* Manages automatic polling and unified update streams for Meross devices.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
21
|
-
* @param {number} [options.electricityInterval=30000] - Electricity polling interval in
|
|
22
|
-
* @param {number} [options.consumptionInterval=60000] -
|
|
23
|
-
* @param {number} [options.httpDeviceListInterval=120000] - HTTP device list polling interval in
|
|
24
|
-
* @param {boolean} [options.smartCaching=true] -
|
|
25
|
-
* @param {number} [options.cacheMaxAge=10000] -
|
|
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
|
|
58
|
-
* @param {number} [config.deviceStateInterval] - Device state polling interval in
|
|
59
|
-
* @param {number} [config.electricityInterval] - Electricity polling interval in
|
|
60
|
-
* @param {number} [config.consumptionInterval] -
|
|
61
|
-
* @param {boolean} [config.smartCaching] - Enable
|
|
62
|
-
* @param {number} [config.cacheMaxAge] -
|
|
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 = {}
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
if (subscription.subscribers.size === 1) {
|
|
102
|
+
if (isNewSubscription) {
|
|
84
103
|
this._startPolling(device, subscription);
|
|
85
104
|
}
|
|
86
105
|
|
|
87
|
-
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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
|
|
118
|
+
unsubscribe(deviceUuid) {
|
|
99
119
|
const subscription = this.subscriptions.get(deviceUuid);
|
|
100
120
|
if (!subscription) {
|
|
101
121
|
return;
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
const eventName = `deviceUpdate:${deviceUuid}`;
|
|
125
|
+
const listenerCount = this.listenerCount(eventName);
|
|
105
126
|
|
|
106
|
-
|
|
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
|
|
130
|
+
this.logger(`Unsubscribed from device ${deviceUuid}. No more listeners.`);
|
|
111
131
|
} else {
|
|
112
|
-
this.logger(`Unsubscribed from device ${deviceUuid}. Remaining
|
|
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
|
-
*
|
|
120
|
-
*
|
|
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(
|
|
123
|
-
|
|
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
|
-
|
|
135
|
-
this.
|
|
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
|
-
*
|
|
155
|
+
* Stops HTTP polling when no listeners remain. Call this after removing
|
|
156
|
+
* all listeners with `removeAllListeners('deviceListUpdate')` or `off()`.
|
|
144
157
|
*/
|
|
145
|
-
unsubscribeFromDeviceList(
|
|
146
|
-
this.
|
|
158
|
+
unsubscribeFromDeviceList() {
|
|
159
|
+
const listenerCount = this.listenerCount('deviceListUpdate');
|
|
147
160
|
|
|
148
|
-
if (
|
|
161
|
+
if (listenerCount === 0) {
|
|
149
162
|
this._stopHttpPolling();
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
/**
|
|
154
|
-
* Cleanup all subscriptions and
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
177
|
-
pollingIntervals: new Map(),
|
|
178
|
-
lastPollTimes: new Map(),
|
|
179
|
-
lastUpdate: null,
|
|
180
|
-
pushActive: false,
|
|
181
|
-
pushLastSeen: null
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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: {}
|
|
487
|
+
changes: {}
|
|
448
488
|
};
|
|
449
489
|
|
|
450
490
|
subscription.lastUpdate = update;
|
|
@@ -452,8 +492,14 @@ class SubscriptionManager {
|
|
|
452
492
|
}
|
|
453
493
|
|
|
454
494
|
/**
|
|
455
|
-
*
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
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
|
-
*
|
|
520
|
+
* Start periodic polling of HTTP API device list.
|
|
475
521
|
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
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 = {
|