@homebridge-plugins/homebridge-ewelink 12.13.1-beta.0 → 12.13.1-beta.2

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.
@@ -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) {
14
+ constructor(platform, authData, httpClient) {
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,6 +19,9 @@ 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
+
22
25
  // Set up variables from the authData (from HTTP)
23
26
  this.httpHost = authData.httpHost
24
27
  this.aToken = authData.aToken
@@ -26,6 +29,51 @@ export default class {
26
29
 
27
30
  // Flag used to determine ws connection status
28
31
  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
+ // Log recovery at info level — users need to see that retries are happening
51
+ this.log('WS reconnect scheduled [%s] in %s ms (attempt %s).', reason, delay, this.retryCount)
52
+ this.reconnectTimer = setTimeout(() => {
53
+ this.reconnectTimer = null
54
+ this.login().catch(() => {
55
+ // login() schedules its own retry on internal failure — nothing to do here
56
+ })
57
+ }, delay)
58
+ }
59
+
60
+ async refreshHttpToken() {
61
+ // Re-run HTTP login to get a fresh aToken; a 406 means the old one is invalidated.
62
+ // This does NOT re-fetch homes/devices — only the bearer token changes.
63
+ if (!this.httpClient) {
64
+ return false
65
+ }
66
+ try {
67
+ const authData = await this.httpClient.login()
68
+ this.aToken = authData.aToken
69
+ this.apiKey = authData.apiKey
70
+ this.httpHost = authData.httpHost
71
+ this.log('HTTP token refreshed after 406.')
72
+ return true
73
+ } catch (err) {
74
+ this.log.warn('%s %s.', 'HTTP token refresh failed:', parseError(err))
75
+ return false
76
+ }
29
77
  }
30
78
 
31
79
  async getHost(attempt = 0) {
@@ -81,17 +129,21 @@ export default class {
81
129
  }
82
130
 
83
131
  async login() {
84
- // Used to create the web socket connection and authenticate
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)
132
+ // Used to create the web socket connection and authenticate.
133
+ // All reconnection paths (onClose, onError, 406, open-fail) funnel through
134
+ // scheduleReconnect(), which guards against concurrent logins via loginInProgress.
135
+ if (this.loginInProgress) {
136
+ return
137
+ }
138
+ this.loginInProgress = true
139
+
140
+ // Cancel any pending reconnect — we're attempting a login right now
141
+ if (this.reconnectTimer) {
142
+ clearTimeout(this.reconnectTimer)
143
+ this.reconnectTimer = null
144
+ }
94
145
 
146
+ try {
95
147
  // Close any existing web socket connection
96
148
  await this.closeConnection()
97
149
 
@@ -159,22 +211,19 @@ export default class {
159
211
  }
160
212
  }, (res.config.hbInterval + 7) * 1000)
161
213
 
162
- // Login was successful and log if appropriate
163
- this.authRetries = 0
214
+ // Login was successful — reset the retry counter and log if appropriate
215
+ this.retryCount = 0
164
216
  if (this.debug) {
165
217
  this.log('%s.', this.lang.wsLoginSuccess)
166
218
  }
167
219
  } else if (res.error === 406) {
168
- // Token invalidated (e.g. concurrent session), re-authenticate with backoff
220
+ // Token invalidated (e.g. concurrent session from the mobile app).
221
+ // Re-sending userOnline with the same aToken will keep returning 406,
222
+ // so refresh the HTTP token first, then reconnect with backoff.
169
223
  this.log.warn(this.lang.wsLogin406)
170
- const delay = Math.min(5000 * 2 ** (this.authRetries || 0), 300000)
171
- this.authRetries = (this.authRetries || 0) + 1
172
- this.log.error('delay: %s ', delay, 'authRetries: %s', this.authRetries)
173
- if (this.authRetries <= 10) {
174
- await sleep(delay)
175
- this.log.error('retrying login')
176
- await this.login()
177
- }
224
+ await this.refreshHttpToken()
225
+ await this.closeConnection()
226
+ this.scheduleReconnect('406')
178
227
  } else {
179
228
  // There was a problem with the authentication response
180
229
  const str = JSON.stringify(res, null, 2)
@@ -183,6 +232,8 @@ export default class {
183
232
  } catch (err) {
184
233
  // Catch any errors authenticating the WS connection
185
234
  this.log.warn('%s %s.', this.lang.wsLoginError, parseError(err))
235
+ await this.closeConnection()
236
+ this.scheduleReconnect('auth-error')
186
237
  }
187
238
  })
188
239
 
@@ -276,7 +327,7 @@ export default class {
276
327
  })
277
328
 
278
329
  // Add a listener for when the web socket closes for any reason
279
- this.wsClient.onClose.addListener(async (e) => {
330
+ this.wsClient.onClose.addListener((e) => {
280
331
  // Don't continue further with logging/reconnection if it's a wanted closure
281
332
  if (e === 1005) {
282
333
  return
@@ -286,30 +337,23 @@ export default class {
286
337
  this.log.warn('%s [%s].', this.lang.wsReconnectError, e)
287
338
  }
288
339
 
289
- // Reconnect with exponential backoff
290
- this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
291
- const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
292
- await sleep(delay)
293
- await this.login()
340
+ this.wsIsConnected = false
341
+ this.scheduleReconnect(`close-${e}`)
294
342
  })
295
343
 
296
344
  // Add a listener for when the web socket throws an error
297
- this.wsClient.onError.addListener(async (e) => {
345
+ this.wsClient.onError.addListener((e) => {
298
346
  if (this.debug) {
299
347
  this.log.warn('%s [%s].', this.lang.wsReconnectClose, e)
300
348
  }
301
349
 
302
- this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
303
- const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
304
- await sleep(delay)
305
- await this.login()
350
+ this.scheduleReconnect('error')
306
351
  })
307
352
 
308
- // Open the web socket connection
353
+ // Open the web socket connection.
354
+ // NB: retryCount is reset only on successful auth, not on TCP handshake —
355
+ // otherwise a repeated auth-failure loop would reset backoff every cycle.
309
356
  await this.wsClient.open()
310
-
311
- // Reset reconnect counter on successful connection
312
- this.reconnectAttempts = 0
313
357
  } catch (err) {
314
358
  const errToShow = parseError(err)
315
359
 
@@ -317,10 +361,9 @@ export default class {
317
361
  this.log('%s: %s.', this.lang.wsLoginErrRecon, errToShow)
318
362
  }
319
363
 
320
- this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
321
- const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
322
- await sleep(delay)
323
- await this.login()
364
+ this.scheduleReconnect('open-failed')
365
+ } finally {
366
+ this.loginInProgress = false
324
367
  }
325
368
  }
326
369
 
@@ -421,7 +464,8 @@ export default class {
421
464
  }
422
465
 
423
466
  async closeConnection() {
424
- // This is called when refreshing connection or if Homebridge shuts down
467
+ // This is called when refreshing connection or if Homebridge shuts down.
468
+ // Idempotent — safe to call regardless of current connection state.
425
469
 
426
470
  // Clear any existing ping interval
427
471
  if (this.hbInterval) {
@@ -431,21 +475,22 @@ export default class {
431
475
 
432
476
  // Check the ws client is set up
433
477
  if (this.wsClient) {
434
- // Remove any existing listeners
435
- this.wsClient.removeAllListeners()
478
+ const wasConnected = this.wsIsConnected
436
479
 
437
- // Close the web socket connection
438
- if (this.wsIsConnected) {
439
- // Set the connection flag to false
440
- this.wsIsConnected = false
480
+ // Remove any existing listeners so onClose/onError don't retrigger reconnection
481
+ this.wsClient.removeAllListeners()
482
+ this.wsIsConnected = false
441
483
 
442
- // Close the connection
484
+ // Always attempt to close the underlying socket — auth-failure paths (e.g. 406)
485
+ // leave wsIsConnected=false but the TCP/WS socket is still open.
486
+ try {
443
487
  await this.wsClient.close()
488
+ } catch {
489
+ // Already closed, never fully opened, or close threw — nothing actionable
490
+ }
444
491
 
445
- // Log if appropriate
446
- if (this.debug) {
447
- this.log('%s.', this.lang.stoppedWS)
448
- }
492
+ if (wasConnected && this.debug) {
493
+ this.log('%s.', this.lang.stoppedWS)
449
494
  }
450
495
  }
451
496
  }
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)
516
+ this.wsClient = new wsClient(this, authData, this.httpClient)
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.0",
6
+ "version": "12.13.1-beta.2",
7
7
  "description": "Homebridge plugin to integrate eWeLink devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",