@homebridge-plugins/homebridge-ewelink 12.13.1-beta.1 → 12.14.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-ewelink` will be documented in this file.
4
4
 
5
+ ## v12.14.0 (2026-05-05)
6
+
7
+ ### Changed
8
+
9
+ - dependency updates
10
+
5
11
  ## v12.13.0 (2026-04-12)
6
12
 
7
13
  ### Changed
@@ -11,7 +11,7 @@ import { hasProperty, parseError, sleep } from '../utils/functions.js'
11
11
  const emitter = new events()
12
12
 
13
13
  export default class {
14
- constructor(platform, authData, httpClient) {
14
+ constructor(platform, authData) {
15
15
  // Set up variables from the platform
16
16
  this.appId = platform.config.appId || platformConsts.appId
17
17
  this.appSecret = platform.config.appSecret || platformConsts.appSecret
@@ -19,9 +19,6 @@ export default class {
19
19
  this.lang = platform.lang
20
20
  this.log = platform.log
21
21
 
22
- // HTTP client used to refresh the bearer token when a 406 invalidates the session
23
- this.httpClient = httpClient
24
-
25
22
  // Set up variables from the authData (from HTTP)
26
23
  this.httpHost = authData.httpHost
27
24
  this.aToken = authData.aToken
@@ -29,54 +26,6 @@ export default class {
29
26
 
30
27
  // Flag used to determine ws connection status
31
28
  this.wsIsConnected = false
32
-
33
- // Retry state — a single counter/timer serialises all reconnection paths
34
- this.retryCount = 0
35
- this.loginInProgress = false
36
- this.reconnectTimer = null
37
- }
38
-
39
- scheduleReconnect(reason) {
40
- // Another retry is already queued or a login is actively running — nothing to do
41
- if (this.reconnectTimer || this.loginInProgress) {
42
- return
43
- }
44
- if (this.retryCount >= 10) {
45
- this.log.warn('%s [giving up after %s attempts, reason: %s].', this.lang.wsLoginError, this.retryCount, reason)
46
- return
47
- }
48
- const delay = Math.min(5000 * 2 ** this.retryCount, 300000)
49
- this.retryCount += 1
50
- if (this.debug) {
51
- this.log('%s [%s] in %s ms (attempt %s).', this.lang.wsLoginErrRecon, reason, delay, this.retryCount)
52
- }
53
- this.reconnectTimer = setTimeout(() => {
54
- this.reconnectTimer = null
55
- this.login().catch(() => {
56
- // login() schedules its own retry on internal failure — nothing to do here
57
- })
58
- }, delay)
59
- }
60
-
61
- async refreshHttpToken() {
62
- // Re-run HTTP login to get a fresh aToken; a 406 means the old one is invalidated.
63
- // This does NOT re-fetch homes/devices — only the bearer token changes.
64
- if (!this.httpClient) {
65
- return false
66
- }
67
- try {
68
- const authData = await this.httpClient.login()
69
- this.aToken = authData.aToken
70
- this.apiKey = authData.apiKey
71
- this.httpHost = authData.httpHost
72
- if (this.debug) {
73
- this.log('%s.', 'HTTP token refreshed after 406')
74
- }
75
- return true
76
- } catch (err) {
77
- this.log.warn('%s %s.', 'HTTP token refresh failed:', parseError(err))
78
- return false
79
- }
80
29
  }
81
30
 
82
31
  async getHost(attempt = 0) {
@@ -132,21 +81,17 @@ export default class {
132
81
  }
133
82
 
134
83
  async login() {
135
- // Used to create the web socket connection and authenticate.
136
- // All reconnection paths (onClose, onError, 406, open-fail) funnel through
137
- // scheduleReconnect(), which guards against concurrent logins via loginInProgress.
138
- if (this.loginInProgress) {
139
- return
140
- }
141
- this.loginInProgress = true
142
-
143
- // Cancel any pending reconnect — we're attempting a login right now
144
- if (this.reconnectTimer) {
145
- clearTimeout(this.reconnectTimer)
146
- this.reconnectTimer = null
147
- }
148
-
84
+ // Used to create the web socket connection and authenticate
149
85
  try {
86
+ // This may be called onClose and onError, we just want it to run once
87
+ if (this.debounce) {
88
+ return
89
+ }
90
+ this.debounce = true
91
+ setTimeout(() => {
92
+ this.debounce = false
93
+ }, 4000)
94
+
150
95
  // Close any existing web socket connection
151
96
  await this.closeConnection()
152
97
 
@@ -214,19 +159,20 @@ export default class {
214
159
  }
215
160
  }, (res.config.hbInterval + 7) * 1000)
216
161
 
217
- // Login was successful — reset the retry counter and log if appropriate
218
- this.retryCount = 0
162
+ // Login was successful and log if appropriate
163
+ this.authRetries = 0
219
164
  if (this.debug) {
220
165
  this.log('%s.', this.lang.wsLoginSuccess)
221
166
  }
222
167
  } else if (res.error === 406) {
223
- // Token invalidated (e.g. concurrent session from the mobile app).
224
- // Re-sending userOnline with the same aToken will keep returning 406,
225
- // so refresh the HTTP token first, then reconnect with backoff.
168
+ // Token invalidated (e.g. concurrent session), re-authenticate with backoff
226
169
  this.log.warn(this.lang.wsLogin406)
227
- await this.refreshHttpToken()
228
- await this.closeConnection()
229
- this.scheduleReconnect('406')
170
+ const delay = Math.min(5000 * 2 ** (this.authRetries || 0), 300000)
171
+ this.authRetries = (this.authRetries || 0) + 1
172
+ if (this.authRetries <= 10) {
173
+ await sleep(delay)
174
+ await this.login()
175
+ }
230
176
  } else {
231
177
  // There was a problem with the authentication response
232
178
  const str = JSON.stringify(res, null, 2)
@@ -235,8 +181,6 @@ export default class {
235
181
  } catch (err) {
236
182
  // Catch any errors authenticating the WS connection
237
183
  this.log.warn('%s %s.', this.lang.wsLoginError, parseError(err))
238
- await this.closeConnection()
239
- this.scheduleReconnect('auth-error')
240
184
  }
241
185
  })
242
186
 
@@ -323,14 +267,14 @@ export default class {
323
267
  msg.params.updateSource = 'WS'
324
268
  emitter.emit('update', { deviceid: msg.deviceid, params: msg.params })
325
269
  }
326
- } else if (msg.error !== 406) {
270
+ } else {
327
271
  // WS message received has an unknown action
328
272
  this.log.warn('%s.\n%s', this.lang.wsUnkCmd, JSON.stringify(msg, null, 2))
329
273
  }
330
274
  })
331
275
 
332
276
  // Add a listener for when the web socket closes for any reason
333
- this.wsClient.onClose.addListener((e) => {
277
+ this.wsClient.onClose.addListener(async (e) => {
334
278
  // Don't continue further with logging/reconnection if it's a wanted closure
335
279
  if (e === 1005) {
336
280
  return
@@ -340,23 +284,30 @@ export default class {
340
284
  this.log.warn('%s [%s].', this.lang.wsReconnectError, e)
341
285
  }
342
286
 
343
- this.wsIsConnected = false
344
- this.scheduleReconnect(`close-${e}`)
287
+ // Reconnect with exponential backoff
288
+ this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
289
+ const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
290
+ await sleep(delay)
291
+ await this.login()
345
292
  })
346
293
 
347
294
  // Add a listener for when the web socket throws an error
348
- this.wsClient.onError.addListener((e) => {
295
+ this.wsClient.onError.addListener(async (e) => {
349
296
  if (this.debug) {
350
297
  this.log.warn('%s [%s].', this.lang.wsReconnectClose, e)
351
298
  }
352
299
 
353
- this.scheduleReconnect('error')
300
+ this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
301
+ const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
302
+ await sleep(delay)
303
+ await this.login()
354
304
  })
355
305
 
356
- // Open the web socket connection.
357
- // NB: retryCount is reset only on successful auth, not on TCP handshake —
358
- // otherwise a repeated auth-failure loop would reset backoff every cycle.
306
+ // Open the web socket connection
359
307
  await this.wsClient.open()
308
+
309
+ // Reset reconnect counter on successful connection
310
+ this.reconnectAttempts = 0
360
311
  } catch (err) {
361
312
  const errToShow = parseError(err)
362
313
 
@@ -364,9 +315,10 @@ export default class {
364
315
  this.log('%s: %s.', this.lang.wsLoginErrRecon, errToShow)
365
316
  }
366
317
 
367
- this.scheduleReconnect('open-failed')
368
- } finally {
369
- this.loginInProgress = false
318
+ this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
319
+ const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
320
+ await sleep(delay)
321
+ await this.login()
370
322
  }
371
323
  }
372
324
 
@@ -467,8 +419,7 @@ export default class {
467
419
  }
468
420
 
469
421
  async closeConnection() {
470
- // This is called when refreshing connection or if Homebridge shuts down.
471
- // Idempotent — safe to call regardless of current connection state.
422
+ // This is called when refreshing connection or if Homebridge shuts down
472
423
 
473
424
  // Clear any existing ping interval
474
425
  if (this.hbInterval) {
@@ -478,22 +429,21 @@ export default class {
478
429
 
479
430
  // Check the ws client is set up
480
431
  if (this.wsClient) {
481
- const wasConnected = this.wsIsConnected
482
-
483
- // Remove any existing listeners so onClose/onError don't retrigger reconnection
432
+ // Remove any existing listeners
484
433
  this.wsClient.removeAllListeners()
485
- this.wsIsConnected = false
486
434
 
487
- // Always attempt to close the underlying socket — auth-failure paths (e.g. 406)
488
- // leave wsIsConnected=false but the TCP/WS socket is still open.
489
- try {
435
+ // Close the web socket connection
436
+ if (this.wsIsConnected) {
437
+ // Set the connection flag to false
438
+ this.wsIsConnected = false
439
+
440
+ // Close the connection
490
441
  await this.wsClient.close()
491
- } catch {
492
- // Already closed, never fully opened, or close threw — nothing actionable
493
- }
494
442
 
495
- if (wasConnected && this.debug) {
496
- this.log('%s.', this.lang.stoppedWS)
443
+ // Log if appropriate
444
+ if (this.debug) {
445
+ this.log('%s.', this.lang.stoppedWS)
446
+ }
497
447
  }
498
448
  }
499
449
  }
package/lib/platform.js CHANGED
@@ -513,7 +513,7 @@ export default class {
513
513
 
514
514
  // Set up the WS client, get the user WS host and login
515
515
  if (this.config.mode !== 'lan') {
516
- this.wsClient = new wsClient(this, authData, this.httpClient)
516
+ this.wsClient = new wsClient(this, authData)
517
517
  await this.wsClient.login()
518
518
 
519
519
  // Refresh the WS connection every 60 minutes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge eWeLink",
4
4
  "alias": "eWeLink",
5
5
  "type": "module",
6
- "version": "12.13.1-beta.1",
6
+ "version": "12.14.0",
7
7
  "description": "Homebridge plugin to integrate eWeLink devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",
@@ -53,8 +53,8 @@
53
53
  ],
54
54
  "main": "lib/index.js",
55
55
  "engines": {
56
- "homebridge": "^1.6.0 || ^2.0.0-beta.0",
57
- "node": "^22 || ^24"
56
+ "homebridge": "^1.6.0 || ^2.0.0",
57
+ "node": "^22.12.0 || ^24.0.0"
58
58
  },
59
59
  "scripts": {
60
60
  "lint": "eslint . --max-warnings=0",
@@ -62,9 +62,9 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@homebridge/plugin-ui-utils": "^2.2.3",
65
- "axios": "^1.15.1",
65
+ "axios": "^1.16.0",
66
66
  "node-persist": "^4.0.4",
67
- "p-queue": "^9.1.2",
67
+ "p-queue": "^9.2.0",
68
68
  "websocket-as-promised": "^3.0.1",
69
69
  "ws": "^8.20.0"
70
70
  },