meross-iot 0.8.0 → 0.9.1
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 +18 -0
- package/README.md +22 -4
- package/index.d.ts +2 -0
- package/lib/controller/device.js +2 -9
- package/lib/controller/features/runtime-feature.js +6 -0
- package/lib/device-factory.js +17 -17
- package/lib/managers/subscription.js +72 -30
- package/lib/model/exception.js +1 -1
- package/lib/utilities/heartbeat.js +31 -49
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.1] - 2026-01-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Improve heartbeat offline detection by using response silence (≥ heartbeat interval) instead of treating individual command errors/timeouts as offline signals
|
|
12
|
+
|
|
13
|
+
## [0.9.0] - 2026-01-22
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Signal strength property (`device.signalStrength`) from Appliance.System.Runtime
|
|
17
|
+
- Provides signal strength percentage (1-100) on production firmware
|
|
18
|
+
- Automatically updated when runtime data is fetched
|
|
19
|
+
- Available in TypeScript definitions
|
|
20
|
+
- Runtime polling support in ManagerSubscription
|
|
21
|
+
- Added `runtimeInterval` option (default: 60000ms) for periodic runtime data polling
|
|
22
|
+
- Runtime data (signal strength, network type, IoT status) requires polling as it doesn't support push notifications
|
|
23
|
+
- Respects smart caching configuration to reduce network traffic
|
|
24
|
+
- Polling automatically skips when device is offline
|
|
25
|
+
|
|
8
26
|
## [0.8.0] - 2026-01-22
|
|
9
27
|
|
|
10
28
|
### Added
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ The library can control devices locally via HTTP or via cloud MQTT server.
|
|
|
26
26
|
npm install meross-iot@alpha
|
|
27
27
|
|
|
28
28
|
# Or install specific version
|
|
29
|
-
npm install meross-iot@0.
|
|
29
|
+
npm install meross-iot@0.9.1
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Usage & Documentation
|
|
@@ -177,6 +177,27 @@ Please create an issue on GitHub and include:
|
|
|
177
177
|
|
|
178
178
|
## Changelog
|
|
179
179
|
|
|
180
|
+
### [0.9.1] - 2026-01-22
|
|
181
|
+
|
|
182
|
+
#### Fixed
|
|
183
|
+
- Improve heartbeat offline detection by using response silence (≥ heartbeat interval) instead of treating individual command errors/timeouts as offline signals
|
|
184
|
+
|
|
185
|
+
<details>
|
|
186
|
+
<summary>Older</summary>
|
|
187
|
+
|
|
188
|
+
### [0.9.0] - 2026-01-22
|
|
189
|
+
|
|
190
|
+
#### Added
|
|
191
|
+
- Signal strength property (`device.signalStrength`) from Appliance.System.Runtime
|
|
192
|
+
- Provides signal strength percentage (1-100) on production firmware
|
|
193
|
+
- Automatically updated when runtime data is fetched
|
|
194
|
+
- Available in TypeScript definitions
|
|
195
|
+
- Runtime polling support in ManagerSubscription
|
|
196
|
+
- Added `runtimeInterval` option (default: 60000ms) for periodic runtime data polling
|
|
197
|
+
- Runtime data (signal strength, network type, IoT status) requires polling as it doesn't support push notifications
|
|
198
|
+
- Respects smart caching configuration to reduce network traffic
|
|
199
|
+
- Polling automatically skips when device is offline
|
|
200
|
+
|
|
180
201
|
### [0.8.0] - 2026-01-22
|
|
181
202
|
|
|
182
203
|
#### Added
|
|
@@ -212,9 +233,6 @@ Please create an issue on GitHub and include:
|
|
|
212
233
|
- Prevent redundant ability updates when abilities haven't changed
|
|
213
234
|
- Device initialization event now properly emitted by device itself after System.All is received
|
|
214
235
|
|
|
215
|
-
<details>
|
|
216
|
-
<summary>Older</summary>
|
|
217
|
-
|
|
218
236
|
### [0.7.2] - 2026-01-21
|
|
219
237
|
|
|
220
238
|
#### Changed
|
package/index.d.ts
CHANGED
|
@@ -2854,6 +2854,8 @@ declare module 'meross-iot' {
|
|
|
2854
2854
|
readonly lanIp: string | null
|
|
2855
2855
|
readonly mqttHost: string | null
|
|
2856
2856
|
readonly mqttPort: number | null
|
|
2857
|
+
/** Signal strength percentage (1-100) from Appliance.System.Runtime. Available on production firmware. */
|
|
2858
|
+
readonly signalStrength: number | null
|
|
2857
2859
|
/** Device capabilities object containing supported features and namespaces */
|
|
2858
2860
|
readonly abilities: Record<string, any> | null
|
|
2859
2861
|
/** Normalized device capabilities map for easy feature discovery without namespace knowledge */
|
package/lib/controller/device.js
CHANGED
|
@@ -204,6 +204,7 @@ class MerossDevice extends EventEmitter {
|
|
|
204
204
|
this.mqttPort = port;
|
|
205
205
|
this.rssi = null;
|
|
206
206
|
this.wifiSignal = null;
|
|
207
|
+
this.signalStrength = null;
|
|
207
208
|
this.wifiSsid = null;
|
|
208
209
|
this.wifiChannel = null;
|
|
209
210
|
this.wifiSnr = null;
|
|
@@ -905,6 +906,7 @@ class MerossDevice extends EventEmitter {
|
|
|
905
906
|
firmwareVersion: this.firmwareVersion,
|
|
906
907
|
rssi: this.rssi,
|
|
907
908
|
wifiSignal: this.wifiSignal,
|
|
909
|
+
signalStrength: this.signalStrength,
|
|
908
910
|
wifiSsid: this.wifiSsid,
|
|
909
911
|
wifiChannel: this.wifiChannel,
|
|
910
912
|
wifiSnr: this.wifiSnr,
|
|
@@ -1066,9 +1068,6 @@ class MerossDevice extends EventEmitter {
|
|
|
1066
1068
|
|
|
1067
1069
|
if (message.header.method === 'ERROR') {
|
|
1068
1070
|
const errorPayload = message.payload || {};
|
|
1069
|
-
if (this._heartbeat) {
|
|
1070
|
-
this._heartbeat.recordFailure();
|
|
1071
|
-
}
|
|
1072
1071
|
pending.reject(new MerossErrorCommand(
|
|
1073
1072
|
`Device returned error: ${JSON.stringify(errorPayload)}`,
|
|
1074
1073
|
errorPayload,
|
|
@@ -1318,9 +1317,6 @@ class MerossDevice extends EventEmitter {
|
|
|
1318
1317
|
namespace,
|
|
1319
1318
|
messageId
|
|
1320
1319
|
};
|
|
1321
|
-
if (this._heartbeat) {
|
|
1322
|
-
this._heartbeat.recordFailure();
|
|
1323
|
-
}
|
|
1324
1320
|
this.waitingMessageIds[messageId].reject(
|
|
1325
1321
|
new MerossErrorCommandTimeout(
|
|
1326
1322
|
`Command timed out after ${timeoutDuration}ms`,
|
|
@@ -1335,9 +1331,6 @@ class MerossDevice extends EventEmitter {
|
|
|
1335
1331
|
};
|
|
1336
1332
|
|
|
1337
1333
|
} catch (error) {
|
|
1338
|
-
if (this._heartbeat) {
|
|
1339
|
-
this._heartbeat.recordFailure();
|
|
1340
|
-
}
|
|
1341
1334
|
reject(error);
|
|
1342
1335
|
}
|
|
1343
1336
|
});
|
|
@@ -31,6 +31,12 @@ function createRuntimeFeature(device) {
|
|
|
31
31
|
const result = await device.publishMessage('GET', 'Appliance.System.Runtime', {});
|
|
32
32
|
const data = result && result.runtime ? result.runtime : {};
|
|
33
33
|
device._runtimeInfo = data;
|
|
34
|
+
|
|
35
|
+
// Update signalStrength property from runtime data (production firmware)
|
|
36
|
+
if (data.signal !== undefined) {
|
|
37
|
+
device.signalStrength = data.signal;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
return data;
|
|
35
41
|
},
|
|
36
42
|
|
package/lib/device-factory.js
CHANGED
|
@@ -210,30 +210,30 @@ function getCachedDeviceClass(deviceType, hardwareVersion, firmwareVersion) {
|
|
|
210
210
|
function _buildDynamicClass(typeKey, abilities, BaseClass) {
|
|
211
211
|
const features = new Set();
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
213
|
+
features.add(require('./controller/features/system-feature'));
|
|
214
|
+
|
|
215
|
+
if (abilities && typeof abilities === 'object') {
|
|
216
|
+
const hasXVersion = new Set();
|
|
217
|
+
for (const namespace of Object.keys(abilities)) {
|
|
218
|
+
if (namespace.endsWith('X')) {
|
|
219
|
+
const baseNamespace = namespace.slice(0, -1);
|
|
220
|
+
hasXVersion.add(baseNamespace);
|
|
222
221
|
}
|
|
222
|
+
}
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
for (const [namespace] of Object.entries(abilities)) {
|
|
225
|
+
const feature = ABILITY_MATRIX[namespace];
|
|
226
|
+
if (feature) {
|
|
227
|
+
if (namespace.endsWith('X')) {
|
|
228
|
+
features.add(feature);
|
|
229
|
+
} else {
|
|
230
|
+
if (!hasXVersion.has(namespace)) {
|
|
228
231
|
features.add(feature);
|
|
229
|
-
} else {
|
|
230
|
-
if (!hasXVersion.has(namespace)) {
|
|
231
|
-
features.add(feature);
|
|
232
|
-
}
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
}
|
|
236
235
|
}
|
|
236
|
+
}
|
|
237
237
|
|
|
238
238
|
class DynamicDevice extends BaseClass {}
|
|
239
239
|
|
|
@@ -10,11 +10,11 @@ const { OnlineStatus } = require('../model/enums');
|
|
|
10
10
|
* Coordinates push notifications and targeted polling to provide a single event stream for
|
|
11
11
|
* device state changes. Device state is polled once on initial subscription to establish a
|
|
12
12
|
* baseline for listeners, then typically relies on push notifications for ongoing updates.
|
|
13
|
-
* Some features (electricity, consumption) require periodic polling because they do not
|
|
13
|
+
* Some features (electricity, consumption, runtime) require periodic polling because they do not
|
|
14
14
|
* emit push notifications.
|
|
15
15
|
*
|
|
16
16
|
* Push notifications reduce latency and network traffic compared to frequent polling.
|
|
17
|
-
* Periodic polling is intended for features without push support (electricity, consumption)
|
|
17
|
+
* Periodic polling is intended for features without push support (electricity, consumption, runtime)
|
|
18
18
|
* or as an explicit fallback when a device is not producing push updates.
|
|
19
19
|
*
|
|
20
20
|
* @class
|
|
@@ -30,6 +30,7 @@ class ManagerSubscription extends EventEmitter {
|
|
|
30
30
|
* @param {number} [options.deviceStateInterval=0] - Device state polling interval in milliseconds (0 to disable periodic polling, rely on push only after initial state)
|
|
31
31
|
* @param {number} [options.electricityInterval=30000] - Electricity metrics polling interval in milliseconds (0 to disable)
|
|
32
32
|
* @param {number} [options.consumptionInterval=60000] - Power consumption polling interval in milliseconds (0 to disable)
|
|
33
|
+
* @param {number} [options.runtimeInterval=60000] - Runtime information polling interval in milliseconds (0 to disable)
|
|
33
34
|
* @param {number} [options.httpDeviceListInterval=120000] - HTTP device list polling interval in milliseconds
|
|
34
35
|
* @param {boolean} [options.smartCaching=true] - Skip polling when cached data is fresh to reduce network traffic
|
|
35
36
|
* @param {number} [options.cacheMaxAge=10000] - Maximum cache age in milliseconds before considering data stale
|
|
@@ -49,6 +50,7 @@ class ManagerSubscription extends EventEmitter {
|
|
|
49
50
|
deviceStateInterval: options.deviceStateInterval !== undefined ? options.deviceStateInterval : 0,
|
|
50
51
|
electricityInterval: options.electricityInterval !== undefined ? options.electricityInterval : 30000,
|
|
51
52
|
consumptionInterval: options.consumptionInterval !== undefined ? options.consumptionInterval : 60000,
|
|
53
|
+
runtimeInterval: options.runtimeInterval !== undefined ? options.runtimeInterval : 60000,
|
|
52
54
|
httpDeviceListInterval: options.httpDeviceListInterval || 120000,
|
|
53
55
|
smartCaching: options.smartCaching !== false,
|
|
54
56
|
cacheMaxAge: options.cacheMaxAge || 10000,
|
|
@@ -64,16 +66,17 @@ class ManagerSubscription extends EventEmitter {
|
|
|
64
66
|
* Subscribe to device updates.
|
|
65
67
|
*
|
|
66
68
|
* Registers event listeners for push notifications and starts periodic polling
|
|
67
|
-
* based on configuration.
|
|
68
|
-
*
|
|
69
|
+
* based on configuration. Configuration is only applied on the first subscription
|
|
70
|
+
* call for a device; subsequent calls reuse the existing configuration.
|
|
69
71
|
* Device state polling is configured explicitly via `deviceStateInterval` (with a baseline
|
|
70
72
|
* state poll on initial subscription).
|
|
71
73
|
*
|
|
72
74
|
* @param {MerossDevice} device - Device to subscribe to
|
|
73
|
-
* @param {Object} [config={}] - Subscription configuration
|
|
75
|
+
* @param {Object} [config={}] - Subscription configuration (only applied on first subscription)
|
|
74
76
|
* @param {number} [config.deviceStateInterval] - Device state polling interval in milliseconds (0 to disable periodic polling, rely on push only after initial state)
|
|
75
77
|
* @param {number} [config.electricityInterval] - Electricity metrics polling interval in milliseconds (0 to disable)
|
|
76
78
|
* @param {number} [config.consumptionInterval] - Power consumption polling interval in milliseconds (0 to disable)
|
|
79
|
+
* @param {number} [config.runtimeInterval] - Runtime information polling interval in milliseconds (0 to disable)
|
|
77
80
|
* @param {boolean} [config.smartCaching] - Skip polling when cached data is fresh
|
|
78
81
|
* @param {number} [config.cacheMaxAge] - Maximum cache age in milliseconds before refresh
|
|
79
82
|
* @param {boolean} [config.pushOnly=false] - Prefer push-driven updates and emit cached state immediately when available
|
|
@@ -95,33 +98,24 @@ class ManagerSubscription extends EventEmitter {
|
|
|
95
98
|
const subscription = this.subscriptions.get(deviceUuid);
|
|
96
99
|
const isNewSubscription = !subscription.pollingIntervals || subscription.pollingIntervals.size === 0;
|
|
97
100
|
|
|
98
|
-
const existingConfig = subscription.config || { ...this.defaultConfig };
|
|
99
|
-
const pushOnly = config.pushOnly !== undefined ? config.pushOnly : (existingConfig.pushOnly || this.defaultConfig.pushOnly);
|
|
100
|
-
|
|
101
|
-
subscription.config = {
|
|
102
|
-
deviceStateInterval: config.deviceStateInterval !== undefined ? config.deviceStateInterval : (existingConfig.deviceStateInterval !== undefined ? existingConfig.deviceStateInterval : this.defaultConfig.deviceStateInterval),
|
|
103
|
-
electricityInterval: Math.min(
|
|
104
|
-
existingConfig.electricityInterval || this.defaultConfig.electricityInterval,
|
|
105
|
-
config.electricityInterval !== undefined ? config.electricityInterval : (existingConfig.electricityInterval || this.defaultConfig.electricityInterval)
|
|
106
|
-
),
|
|
107
|
-
consumptionInterval: Math.min(
|
|
108
|
-
existingConfig.consumptionInterval || this.defaultConfig.consumptionInterval,
|
|
109
|
-
config.consumptionInterval !== undefined ? config.consumptionInterval : (existingConfig.consumptionInterval || this.defaultConfig.consumptionInterval)
|
|
110
|
-
),
|
|
111
|
-
smartCaching: config.smartCaching !== undefined ? config.smartCaching : existingConfig.smartCaching,
|
|
112
|
-
cacheMaxAge: Math.min(
|
|
113
|
-
existingConfig.cacheMaxAge || this.defaultConfig.cacheMaxAge,
|
|
114
|
-
config.cacheMaxAge !== undefined ? config.cacheMaxAge : (existingConfig.cacheMaxAge || this.defaultConfig.cacheMaxAge)
|
|
115
|
-
),
|
|
116
|
-
pushOnly
|
|
117
|
-
};
|
|
118
|
-
|
|
119
101
|
if (isNewSubscription) {
|
|
102
|
+
const getConfigValue = (key) => config[key] !== undefined ? config[key] : this.defaultConfig[key];
|
|
103
|
+
|
|
104
|
+
subscription.config = {
|
|
105
|
+
deviceStateInterval: getConfigValue('deviceStateInterval'),
|
|
106
|
+
electricityInterval: getConfigValue('electricityInterval'),
|
|
107
|
+
consumptionInterval: getConfigValue('consumptionInterval'),
|
|
108
|
+
runtimeInterval: getConfigValue('runtimeInterval'),
|
|
109
|
+
smartCaching: getConfigValue('smartCaching'),
|
|
110
|
+
cacheMaxAge: getConfigValue('cacheMaxAge'),
|
|
111
|
+
pushOnly: getConfigValue('pushOnly')
|
|
112
|
+
};
|
|
113
|
+
|
|
120
114
|
this._startPolling(device, subscription);
|
|
121
|
-
}
|
|
122
115
|
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
if (subscription.config.pushOnly) {
|
|
117
|
+
this._emitCachedState(device, subscription);
|
|
118
|
+
}
|
|
125
119
|
}
|
|
126
120
|
|
|
127
121
|
const listenerCount = this.listenerCount(eventName);
|
|
@@ -233,7 +227,7 @@ class ManagerSubscription extends EventEmitter {
|
|
|
233
227
|
}
|
|
234
228
|
|
|
235
229
|
/**
|
|
236
|
-
* Start polling for device state, electricity, and
|
|
230
|
+
* Start polling for device state, electricity, consumption, and runtime data.
|
|
237
231
|
*
|
|
238
232
|
* Performs a one-time device state poll on initial subscription so consumers
|
|
239
233
|
* can receive a current baseline state without waiting for the next push event.
|
|
@@ -272,6 +266,14 @@ class ManagerSubscription extends EventEmitter {
|
|
|
272
266
|
|
|
273
267
|
subscription.pollingIntervals.set('consumption', interval);
|
|
274
268
|
}
|
|
269
|
+
|
|
270
|
+
if (config.runtimeInterval > 0 && device.runtime && typeof device.runtime.get === 'function') {
|
|
271
|
+
const interval = setInterval(async () => {
|
|
272
|
+
await this._pollRuntime(device, subscription, config);
|
|
273
|
+
}, config.runtimeInterval);
|
|
274
|
+
|
|
275
|
+
subscription.pollingIntervals.set('runtime', interval);
|
|
276
|
+
}
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
/**
|
|
@@ -429,6 +431,46 @@ class ManagerSubscription extends EventEmitter {
|
|
|
429
431
|
}
|
|
430
432
|
}
|
|
431
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Poll runtime information from the device.
|
|
436
|
+
*
|
|
437
|
+
* Runtime data (signal strength, network type, IoT status) requires polling as it
|
|
438
|
+
* doesn't support push notifications. Skips polling when cached runtime data is fresh
|
|
439
|
+
* (if smartCaching enabled) to reduce network traffic.
|
|
440
|
+
*
|
|
441
|
+
* @private
|
|
442
|
+
* @param {MerossDevice} device - Device to poll
|
|
443
|
+
* @param {Object} subscription - Subscription state object
|
|
444
|
+
* @param {Object} config - Configuration object with caching settings
|
|
445
|
+
*/
|
|
446
|
+
async _pollRuntime(device, subscription, config) {
|
|
447
|
+
if (device.onlineStatus !== OnlineStatus.ONLINE) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const namespace = 'Appliance.System.Runtime';
|
|
452
|
+
if (this._hasRecentPush(subscription, namespace, 5000)) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
if (config.smartCaching && device._runtimeInfo) {
|
|
458
|
+
const cached = device.runtime.getCached();
|
|
459
|
+
if (cached && Object.keys(cached).length > 0) {
|
|
460
|
+
const cacheAge = Date.now() - (subscription.lastPollTimes.get('runtime') || 0);
|
|
461
|
+
if (cacheAge < config.cacheMaxAge) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
await device.runtime.get();
|
|
468
|
+
subscription.lastPollTimes.set('runtime', Date.now());
|
|
469
|
+
} catch (error) {
|
|
470
|
+
this.logger(`Error polling runtime for ${device.uuid}: ${error.message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
432
474
|
/**
|
|
433
475
|
* Mark push notifications as active for a specific namespace.
|
|
434
476
|
*
|
package/lib/model/exception.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
const { OnlineStatus } = require('../model/enums');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Manages device online/offline detection using
|
|
6
|
+
* Manages device online/offline detection using time-based silence detection.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* response
|
|
10
|
-
*
|
|
8
|
+
* Tracks the last response time from the device and marks it offline when no
|
|
9
|
+
* response is received for the configured timeout period. Uses time-based detection
|
|
10
|
+
* rather than failure counting to avoid false offline states from transient network
|
|
11
|
+
* issues or temporary HTTP unavailability when MQTT is still functional.
|
|
11
12
|
*
|
|
12
13
|
* @class Heartbeat
|
|
13
14
|
*/
|
|
@@ -17,21 +18,18 @@ class Heartbeat {
|
|
|
17
18
|
*
|
|
18
19
|
* @param {Object} device - MerossDevice instance to monitor
|
|
19
20
|
* @param {Object} [options={}] - Configuration options
|
|
20
|
-
* @param {number} [options.heartbeatInterval=
|
|
21
|
-
* @param {number} [options.consecutiveFailureThreshold=1] - Number of consecutive failures before marking offline
|
|
21
|
+
* @param {number} [options.heartbeatInterval=295000] - Heartbeat interval in milliseconds (295 seconds)
|
|
22
22
|
* @param {boolean} [options.enabled=true] - Whether heartbeat monitoring is enabled
|
|
23
23
|
*/
|
|
24
24
|
constructor(device, options = {}) {
|
|
25
25
|
this.device = device;
|
|
26
|
-
this.heartbeatInterval = options.heartbeatInterval ||
|
|
27
|
-
this.consecutiveFailureThreshold = options.consecutiveFailureThreshold || 1;
|
|
26
|
+
this.heartbeatInterval = options.heartbeatInterval || 295000;
|
|
28
27
|
this.enabled = options.enabled !== false;
|
|
29
28
|
|
|
30
29
|
this._lastResponseTime = null;
|
|
31
|
-
this._consecutiveFailures = 0;
|
|
32
30
|
this._heartbeatTimer = null;
|
|
33
31
|
this._isRunning = false;
|
|
34
|
-
//
|
|
32
|
+
// Start with shorter delay to quickly detect when device comes back online
|
|
35
33
|
this._pollingDelay = Math.floor(this.heartbeatInterval / 2);
|
|
36
34
|
}
|
|
37
35
|
|
|
@@ -53,7 +51,7 @@ class Heartbeat {
|
|
|
53
51
|
/**
|
|
54
52
|
* Stops heartbeat monitoring and cleans up timers.
|
|
55
53
|
*
|
|
56
|
-
*
|
|
54
|
+
* Prevents memory leaks by clearing scheduled timers when device disconnects.
|
|
57
55
|
*/
|
|
58
56
|
stop() {
|
|
59
57
|
this._isRunning = false;
|
|
@@ -66,38 +64,26 @@ class Heartbeat {
|
|
|
66
64
|
/**
|
|
67
65
|
* Records a successful response from the device.
|
|
68
66
|
*
|
|
69
|
-
* Updates last response
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
* Updates the last response timestamp to reset the silence timer. Resets polling
|
|
68
|
+
* delay when device transitions from offline to online to quickly detect if it
|
|
69
|
+
* goes offline again.
|
|
72
70
|
*/
|
|
73
71
|
recordResponse() {
|
|
74
72
|
const wasOffline = this.device.onlineStatus === OnlineStatus.OFFLINE;
|
|
75
73
|
this._lastResponseTime = Date.now();
|
|
76
|
-
this._consecutiveFailures = 0;
|
|
77
74
|
|
|
78
75
|
if (wasOffline) {
|
|
79
76
|
this._pollingDelay = Math.floor(this.heartbeatInterval / 2);
|
|
80
77
|
}
|
|
81
|
-
|
|
82
|
-
this._evaluateStatus();
|
|
83
|
-
}
|
|
84
78
|
|
|
85
|
-
/**
|
|
86
|
-
* Records a command failure or timeout.
|
|
87
|
-
*
|
|
88
|
-
* Increments consecutive failure counter and evaluates if device should
|
|
89
|
-
* be marked offline. Called when commands fail or timeout.
|
|
90
|
-
*/
|
|
91
|
-
recordFailure() {
|
|
92
|
-
this._consecutiveFailures++;
|
|
93
79
|
this._evaluateStatus();
|
|
94
80
|
}
|
|
95
81
|
|
|
96
82
|
/**
|
|
97
83
|
* Records a System.All response.
|
|
98
84
|
*
|
|
99
|
-
* System.All responses
|
|
100
|
-
*
|
|
85
|
+
* System.All responses are comprehensive state updates that confirm device
|
|
86
|
+
* connectivity, so they reset the silence timer.
|
|
101
87
|
*/
|
|
102
88
|
recordSystemAll() {
|
|
103
89
|
this.recordResponse();
|
|
@@ -106,9 +92,8 @@ class Heartbeat {
|
|
|
106
92
|
/**
|
|
107
93
|
* Evaluates device status and updates if necessary.
|
|
108
94
|
*
|
|
109
|
-
*
|
|
110
|
-
* -
|
|
111
|
-
* - No response for > 2x heartbeat interval (240s) when device was online
|
|
95
|
+
* Only marks devices offline that are currently online to avoid redundant
|
|
96
|
+
* status updates and ensure we don't mark already-offline devices offline again.
|
|
112
97
|
*
|
|
113
98
|
* @private
|
|
114
99
|
*/
|
|
@@ -126,20 +111,18 @@ class Heartbeat {
|
|
|
126
111
|
}
|
|
127
112
|
|
|
128
113
|
/**
|
|
129
|
-
* Determines if device should be marked offline based on
|
|
114
|
+
* Determines if device should be marked offline based on silence timeout.
|
|
115
|
+
*
|
|
116
|
+
* Requires device to be currently online to avoid marking already-offline devices.
|
|
117
|
+
* Requires at least one previous response to have a baseline for silence detection.
|
|
130
118
|
*
|
|
131
119
|
* @private
|
|
132
120
|
* @returns {boolean} True if device should be marked offline
|
|
133
121
|
*/
|
|
134
122
|
_shouldBeOffline() {
|
|
135
|
-
if (this._consecutiveFailures >= this.consecutiveFailureThreshold) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
123
|
if (this._lastResponseTime && this.device.onlineStatus === OnlineStatus.ONLINE) {
|
|
140
124
|
const timeSinceLastResponse = Date.now() - this._lastResponseTime;
|
|
141
|
-
|
|
142
|
-
if (timeSinceLastResponse > maxSilenceInterval) {
|
|
125
|
+
if (timeSinceLastResponse >= this.heartbeatInterval) {
|
|
143
126
|
return true;
|
|
144
127
|
}
|
|
145
128
|
}
|
|
@@ -150,8 +133,9 @@ class Heartbeat {
|
|
|
150
133
|
/**
|
|
151
134
|
* Schedules the next heartbeat check.
|
|
152
135
|
*
|
|
153
|
-
* Uses
|
|
154
|
-
*
|
|
136
|
+
* Uses shorter polling delay when offline to quickly detect when device comes
|
|
137
|
+
* back online. Uses standard heartbeat interval when online to avoid unnecessary
|
|
138
|
+
* network traffic when device is actively responding.
|
|
155
139
|
*
|
|
156
140
|
* @private
|
|
157
141
|
*/
|
|
@@ -160,8 +144,8 @@ class Heartbeat {
|
|
|
160
144
|
return;
|
|
161
145
|
}
|
|
162
146
|
|
|
163
|
-
const delay = this.device.onlineStatus === OnlineStatus.OFFLINE
|
|
164
|
-
? this._pollingDelay
|
|
147
|
+
const delay = this.device.onlineStatus === OnlineStatus.OFFLINE
|
|
148
|
+
? this._pollingDelay
|
|
165
149
|
: this.heartbeatInterval;
|
|
166
150
|
|
|
167
151
|
this._heartbeatTimer = setTimeout(() => {
|
|
@@ -172,10 +156,9 @@ class Heartbeat {
|
|
|
172
156
|
/**
|
|
173
157
|
* Performs a heartbeat check by querying System.Online.
|
|
174
158
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* records a failure and applies exponential backoff when offline.
|
|
159
|
+
* Skips heartbeat if device responded recently to avoid redundant requests when
|
|
160
|
+
* device is actively communicating. On failure, increases polling delay when
|
|
161
|
+
* already offline to reduce network load while waiting for device recovery.
|
|
179
162
|
*
|
|
180
163
|
* @private
|
|
181
164
|
*/
|
|
@@ -184,7 +167,6 @@ class Heartbeat {
|
|
|
184
167
|
return;
|
|
185
168
|
}
|
|
186
169
|
|
|
187
|
-
// Only perform heartbeat if no response received for >= heartbeat interval
|
|
188
170
|
if (this._lastResponseTime) {
|
|
189
171
|
const timeSinceLastResponse = Date.now() - this._lastResponseTime;
|
|
190
172
|
if (timeSinceLastResponse < this.heartbeatInterval) {
|
|
@@ -196,8 +178,8 @@ class Heartbeat {
|
|
|
196
178
|
try {
|
|
197
179
|
await this.device.system.getOnlineStatus();
|
|
198
180
|
} catch (error) {
|
|
199
|
-
|
|
200
|
-
|
|
181
|
+
// Single heartbeat failure doesn't mark device offline; only extended
|
|
182
|
+
// silence triggers offline detection to handle transient network issues
|
|
201
183
|
if (this.device.onlineStatus === OnlineStatus.OFFLINE) {
|
|
202
184
|
this._pollingDelay = Math.min(
|
|
203
185
|
this._pollingDelay * 2,
|