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 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.7.1
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
@@ -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 multiple strategies.
6
+ * Manages device online/offline detection using time-based silence detection.
7
7
  *
8
- * Provides comprehensive connectivity monitoring through periodic heartbeat checks,
9
- * response tracking, failure counting, and System.All monitoring. Automatically
10
- * updates device online status when connectivity issues are detected.
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=120000] - Heartbeat interval in milliseconds (120 seconds)
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 || 120000;
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
- // Exponential backoff: starts at base delay, doubles when offline, capped at heartbeat interval
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
- * Should be called when device disconnects.
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 time and resets consecutive failure counter.
70
- * Resets polling delay to base interval when device comes back online.
71
- * Called whenever any successful response is received.
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 indicate device is active and responding.
100
- * Updates last response time and resets failure counter.
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
- * Checks multiple conditions to determine if device should be marked offline:
110
- * - 1 consecutive failure
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 tracking data.
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
- const maxSilenceInterval = this.heartbeatInterval * 2;
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 exponential backoff delay when device is offline, otherwise uses
154
- * heartbeat interval. Checks if heartbeat is needed (conditional triggering).
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
- * Only performs heartbeat if device has been silent for >= heartbeat interval
176
- * (conditional triggering, like meross_lan). On success, the response will be
177
- * handled by recordResponse() via the normal message handling flow. On failure,
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
- this.recordFailure();
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meross-iot",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Control Meross cloud devices using nodejs",
5
5
  "author": "Abe Haverkamp",
6
6
  "contributors": [