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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/index.d.ts +2344 -0
- package/index.js +131 -0
- package/lib/controller/device.js +1317 -0
- package/lib/controller/features/alarm-feature.js +89 -0
- package/lib/controller/features/child-lock-feature.js +61 -0
- package/lib/controller/features/config-feature.js +54 -0
- package/lib/controller/features/consumption-feature.js +210 -0
- package/lib/controller/features/control-feature.js +62 -0
- package/lib/controller/features/diffuser-feature.js +411 -0
- package/lib/controller/features/digest-timer-feature.js +22 -0
- package/lib/controller/features/digest-trigger-feature.js +22 -0
- package/lib/controller/features/dnd-feature.js +79 -0
- package/lib/controller/features/electricity-feature.js +144 -0
- package/lib/controller/features/encryption-feature.js +259 -0
- package/lib/controller/features/garage-feature.js +337 -0
- package/lib/controller/features/hub-feature.js +687 -0
- package/lib/controller/features/light-feature.js +408 -0
- package/lib/controller/features/presence-sensor-feature.js +297 -0
- package/lib/controller/features/roller-shutter-feature.js +456 -0
- package/lib/controller/features/runtime-feature.js +74 -0
- package/lib/controller/features/screen-feature.js +67 -0
- package/lib/controller/features/sensor-history-feature.js +47 -0
- package/lib/controller/features/smoke-config-feature.js +50 -0
- package/lib/controller/features/spray-feature.js +166 -0
- package/lib/controller/features/system-feature.js +269 -0
- package/lib/controller/features/temp-unit-feature.js +55 -0
- package/lib/controller/features/thermostat-feature.js +804 -0
- package/lib/controller/features/timer-feature.js +507 -0
- package/lib/controller/features/toggle-feature.js +223 -0
- package/lib/controller/features/trigger-feature.js +333 -0
- package/lib/controller/hub-device.js +185 -0
- package/lib/controller/subdevice.js +1537 -0
- package/lib/device-factory.js +463 -0
- package/lib/error-budget.js +138 -0
- package/lib/http-api.js +766 -0
- package/lib/manager.js +1609 -0
- package/lib/model/channel-info.js +79 -0
- package/lib/model/constants.js +119 -0
- package/lib/model/enums.js +819 -0
- package/lib/model/exception.js +363 -0
- package/lib/model/http/device.js +215 -0
- package/lib/model/http/error-codes.js +121 -0
- package/lib/model/http/exception.js +151 -0
- package/lib/model/http/subdevice.js +133 -0
- package/lib/model/push/alarm.js +112 -0
- package/lib/model/push/bind.js +97 -0
- package/lib/model/push/common.js +282 -0
- package/lib/model/push/diffuser-light.js +100 -0
- package/lib/model/push/diffuser-spray.js +83 -0
- package/lib/model/push/factory.js +229 -0
- package/lib/model/push/generic.js +115 -0
- package/lib/model/push/hub-battery.js +59 -0
- package/lib/model/push/hub-mts100-all.js +64 -0
- package/lib/model/push/hub-mts100-mode.js +59 -0
- package/lib/model/push/hub-mts100-temperature.js +62 -0
- package/lib/model/push/hub-online.js +59 -0
- package/lib/model/push/hub-sensor-alert.js +61 -0
- package/lib/model/push/hub-sensor-all.js +59 -0
- package/lib/model/push/hub-sensor-smoke.js +110 -0
- package/lib/model/push/hub-sensor-temphum.js +62 -0
- package/lib/model/push/hub-subdevicelist.js +50 -0
- package/lib/model/push/hub-togglex.js +60 -0
- package/lib/model/push/index.js +81 -0
- package/lib/model/push/online.js +53 -0
- package/lib/model/push/presence-study.js +61 -0
- package/lib/model/push/sensor-latestx.js +106 -0
- package/lib/model/push/timerx.js +63 -0
- package/lib/model/push/togglex.js +78 -0
- package/lib/model/push/triggerx.js +62 -0
- package/lib/model/push/unbind.js +34 -0
- package/lib/model/push/water-leak.js +107 -0
- package/lib/model/states/diffuser-light-state.js +119 -0
- package/lib/model/states/diffuser-spray-state.js +58 -0
- package/lib/model/states/garage-door-state.js +71 -0
- package/lib/model/states/index.js +38 -0
- package/lib/model/states/light-state.js +134 -0
- package/lib/model/states/presence-sensor-state.js +239 -0
- package/lib/model/states/roller-shutter-state.js +82 -0
- package/lib/model/states/spray-state.js +58 -0
- package/lib/model/states/thermostat-state.js +297 -0
- package/lib/model/states/timer-state.js +192 -0
- package/lib/model/states/toggle-state.js +105 -0
- package/lib/model/states/trigger-state.js +155 -0
- package/lib/subscription.js +587 -0
- package/lib/utilities/conversion.js +62 -0
- package/lib/utilities/debug.js +165 -0
- package/lib/utilities/mqtt.js +152 -0
- package/lib/utilities/network.js +53 -0
- package/lib/utilities/options.js +64 -0
- package/lib/utilities/request-queue.js +161 -0
- package/lib/utilities/ssid.js +37 -0
- package/lib/utilities/state-changes.js +66 -0
- package/lib/utilities/stats.js +687 -0
- package/lib/utilities/timer.js +310 -0
- package/lib/utilities/trigger.js +286 -0
- 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
|
+
|