meross-iot 0.9.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 +5 -0
- package/README.md +9 -4
- package/lib/controller/device.js +0 -9
- package/lib/utilities/heartbeat.js +29 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ 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
|
+
|
|
8
13
|
## [0.9.0] - 2026-01-22
|
|
9
14
|
|
|
10
15
|
### 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,14 @@ 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
|
+
|
|
180
188
|
### [0.9.0] - 2026-01-22
|
|
181
189
|
|
|
182
190
|
#### Added
|
|
@@ -190,9 +198,6 @@ Please create an issue on GitHub and include:
|
|
|
190
198
|
- Respects smart caching configuration to reduce network traffic
|
|
191
199
|
- Polling automatically skips when device is offline
|
|
192
200
|
|
|
193
|
-
<details>
|
|
194
|
-
<summary>Older</summary>
|
|
195
|
-
|
|
196
201
|
### [0.8.0] - 2026-01-22
|
|
197
202
|
|
|
198
203
|
#### Added
|
package/lib/controller/device.js
CHANGED
|
@@ -1068,9 +1068,6 @@ class MerossDevice extends EventEmitter {
|
|
|
1068
1068
|
|
|
1069
1069
|
if (message.header.method === 'ERROR') {
|
|
1070
1070
|
const errorPayload = message.payload || {};
|
|
1071
|
-
if (this._heartbeat) {
|
|
1072
|
-
this._heartbeat.recordFailure();
|
|
1073
|
-
}
|
|
1074
1071
|
pending.reject(new MerossErrorCommand(
|
|
1075
1072
|
`Device returned error: ${JSON.stringify(errorPayload)}`,
|
|
1076
1073
|
errorPayload,
|
|
@@ -1320,9 +1317,6 @@ class MerossDevice extends EventEmitter {
|
|
|
1320
1317
|
namespace,
|
|
1321
1318
|
messageId
|
|
1322
1319
|
};
|
|
1323
|
-
if (this._heartbeat) {
|
|
1324
|
-
this._heartbeat.recordFailure();
|
|
1325
|
-
}
|
|
1326
1320
|
this.waitingMessageIds[messageId].reject(
|
|
1327
1321
|
new MerossErrorCommandTimeout(
|
|
1328
1322
|
`Command timed out after ${timeoutDuration}ms`,
|
|
@@ -1337,9 +1331,6 @@ class MerossDevice extends EventEmitter {
|
|
|
1337
1331
|
};
|
|
1338
1332
|
|
|
1339
1333
|
} catch (error) {
|
|
1340
|
-
if (this._heartbeat) {
|
|
1341
|
-
this._heartbeat.recordFailure();
|
|
1342
|
-
}
|
|
1343
1334
|
reject(error);
|
|
1344
1335
|
}
|
|
1345
1336
|
});
|
|
@@ -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,14 +64,13 @@ 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);
|
|
@@ -82,22 +79,11 @@ class Heartbeat {
|
|
|
82
79
|
this._evaluateStatus();
|
|
83
80
|
}
|
|
84
81
|
|
|
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
|
-
this._evaluateStatus();
|
|
94
|
-
}
|
|
95
|
-
|
|
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
|
*/
|
|
@@ -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,
|