@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.
- package/lib/connection/ws.js +98 -53
- package/lib/platform.js +1 -1
- package/package.json +1 -1
package/lib/connection/ws.js
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) {
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
171
|
-
this.
|
|
172
|
-
this.
|
|
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(
|
|
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
|
-
|
|
290
|
-
this.
|
|
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(
|
|
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.
|
|
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.
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
435
|
-
this.wsClient.removeAllListeners()
|
|
478
|
+
const wasConnected = this.wsIsConnected
|
|
436
479
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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