@homebridge-plugins/homebridge-ewelink 12.13.0 → 12.13.1-beta.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/lib/connection/ws.js +102 -52
- package/lib/platform.js +1 -1
- package/package.json +3 -3
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,54 @@ 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
|
+
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
|
+
}
|
|
29
80
|
}
|
|
30
81
|
|
|
31
82
|
async getHost(attempt = 0) {
|
|
@@ -81,17 +132,21 @@ export default class {
|
|
|
81
132
|
}
|
|
82
133
|
|
|
83
134
|
async login() {
|
|
84
|
-
// Used to create the web socket connection and authenticate
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
setTimeout(() => {
|
|
92
|
-
this.debounce = false
|
|
93
|
-
}, 4000)
|
|
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
|
|
94
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
|
+
|
|
149
|
+
try {
|
|
95
150
|
// Close any existing web socket connection
|
|
96
151
|
await this.closeConnection()
|
|
97
152
|
|
|
@@ -159,20 +214,19 @@ export default class {
|
|
|
159
214
|
}
|
|
160
215
|
}, (res.config.hbInterval + 7) * 1000)
|
|
161
216
|
|
|
162
|
-
// Login was successful and log if appropriate
|
|
163
|
-
this.
|
|
217
|
+
// Login was successful — reset the retry counter and log if appropriate
|
|
218
|
+
this.retryCount = 0
|
|
164
219
|
if (this.debug) {
|
|
165
220
|
this.log('%s.', this.lang.wsLoginSuccess)
|
|
166
221
|
}
|
|
167
222
|
} else if (res.error === 406) {
|
|
168
|
-
// Token invalidated (e.g. concurrent session
|
|
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.
|
|
169
226
|
this.log.warn(this.lang.wsLogin406)
|
|
170
|
-
|
|
171
|
-
this.
|
|
172
|
-
|
|
173
|
-
await sleep(delay)
|
|
174
|
-
await this.login()
|
|
175
|
-
}
|
|
227
|
+
await this.refreshHttpToken()
|
|
228
|
+
await this.closeConnection()
|
|
229
|
+
this.scheduleReconnect('406')
|
|
176
230
|
} else {
|
|
177
231
|
// There was a problem with the authentication response
|
|
178
232
|
const str = JSON.stringify(res, null, 2)
|
|
@@ -181,6 +235,8 @@ export default class {
|
|
|
181
235
|
} catch (err) {
|
|
182
236
|
// Catch any errors authenticating the WS connection
|
|
183
237
|
this.log.warn('%s %s.', this.lang.wsLoginError, parseError(err))
|
|
238
|
+
await this.closeConnection()
|
|
239
|
+
this.scheduleReconnect('auth-error')
|
|
184
240
|
}
|
|
185
241
|
})
|
|
186
242
|
|
|
@@ -267,14 +323,14 @@ export default class {
|
|
|
267
323
|
msg.params.updateSource = 'WS'
|
|
268
324
|
emitter.emit('update', { deviceid: msg.deviceid, params: msg.params })
|
|
269
325
|
}
|
|
270
|
-
} else {
|
|
326
|
+
} else if (msg.error !== 406) {
|
|
271
327
|
// WS message received has an unknown action
|
|
272
328
|
this.log.warn('%s.\n%s', this.lang.wsUnkCmd, JSON.stringify(msg, null, 2))
|
|
273
329
|
}
|
|
274
330
|
})
|
|
275
331
|
|
|
276
332
|
// Add a listener for when the web socket closes for any reason
|
|
277
|
-
this.wsClient.onClose.addListener(
|
|
333
|
+
this.wsClient.onClose.addListener((e) => {
|
|
278
334
|
// Don't continue further with logging/reconnection if it's a wanted closure
|
|
279
335
|
if (e === 1005) {
|
|
280
336
|
return
|
|
@@ -284,30 +340,23 @@ export default class {
|
|
|
284
340
|
this.log.warn('%s [%s].', this.lang.wsReconnectError, e)
|
|
285
341
|
}
|
|
286
342
|
|
|
287
|
-
|
|
288
|
-
this.
|
|
289
|
-
const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
|
|
290
|
-
await sleep(delay)
|
|
291
|
-
await this.login()
|
|
343
|
+
this.wsIsConnected = false
|
|
344
|
+
this.scheduleReconnect(`close-${e}`)
|
|
292
345
|
})
|
|
293
346
|
|
|
294
347
|
// Add a listener for when the web socket throws an error
|
|
295
|
-
this.wsClient.onError.addListener(
|
|
348
|
+
this.wsClient.onError.addListener((e) => {
|
|
296
349
|
if (this.debug) {
|
|
297
350
|
this.log.warn('%s [%s].', this.lang.wsReconnectClose, e)
|
|
298
351
|
}
|
|
299
352
|
|
|
300
|
-
this.
|
|
301
|
-
const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
|
|
302
|
-
await sleep(delay)
|
|
303
|
-
await this.login()
|
|
353
|
+
this.scheduleReconnect('error')
|
|
304
354
|
})
|
|
305
355
|
|
|
306
|
-
// Open the web socket connection
|
|
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.
|
|
307
359
|
await this.wsClient.open()
|
|
308
|
-
|
|
309
|
-
// Reset reconnect counter on successful connection
|
|
310
|
-
this.reconnectAttempts = 0
|
|
311
360
|
} catch (err) {
|
|
312
361
|
const errToShow = parseError(err)
|
|
313
362
|
|
|
@@ -315,10 +364,9 @@ export default class {
|
|
|
315
364
|
this.log('%s: %s.', this.lang.wsLoginErrRecon, errToShow)
|
|
316
365
|
}
|
|
317
366
|
|
|
318
|
-
this.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await this.login()
|
|
367
|
+
this.scheduleReconnect('open-failed')
|
|
368
|
+
} finally {
|
|
369
|
+
this.loginInProgress = false
|
|
322
370
|
}
|
|
323
371
|
}
|
|
324
372
|
|
|
@@ -419,7 +467,8 @@ export default class {
|
|
|
419
467
|
}
|
|
420
468
|
|
|
421
469
|
async closeConnection() {
|
|
422
|
-
// This is called when refreshing connection or if Homebridge shuts down
|
|
470
|
+
// This is called when refreshing connection or if Homebridge shuts down.
|
|
471
|
+
// Idempotent — safe to call regardless of current connection state.
|
|
423
472
|
|
|
424
473
|
// Clear any existing ping interval
|
|
425
474
|
if (this.hbInterval) {
|
|
@@ -429,21 +478,22 @@ export default class {
|
|
|
429
478
|
|
|
430
479
|
// Check the ws client is set up
|
|
431
480
|
if (this.wsClient) {
|
|
432
|
-
|
|
433
|
-
this.wsClient.removeAllListeners()
|
|
481
|
+
const wasConnected = this.wsIsConnected
|
|
434
482
|
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.wsIsConnected = false
|
|
483
|
+
// Remove any existing listeners so onClose/onError don't retrigger reconnection
|
|
484
|
+
this.wsClient.removeAllListeners()
|
|
485
|
+
this.wsIsConnected = false
|
|
439
486
|
|
|
440
|
-
|
|
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 {
|
|
441
490
|
await this.wsClient.close()
|
|
491
|
+
} catch {
|
|
492
|
+
// Already closed, never fully opened, or close threw — nothing actionable
|
|
493
|
+
}
|
|
442
494
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
this.log('%s.', this.lang.stoppedWS)
|
|
446
|
-
}
|
|
495
|
+
if (wasConnected && this.debug) {
|
|
496
|
+
this.log('%s.', this.lang.stoppedWS)
|
|
447
497
|
}
|
|
448
498
|
}
|
|
449
499
|
}
|
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.
|
|
6
|
+
"version": "12.13.1-beta.1",
|
|
7
7
|
"description": "Homebridge plugin to integrate eWeLink devices into HomeKit.",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "bwp91",
|
|
@@ -62,13 +62,13 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@homebridge/plugin-ui-utils": "^2.2.3",
|
|
65
|
-
"axios": "^1.15.
|
|
65
|
+
"axios": "^1.15.1",
|
|
66
66
|
"node-persist": "^4.0.4",
|
|
67
67
|
"p-queue": "^9.1.2",
|
|
68
68
|
"websocket-as-promised": "^3.0.1",
|
|
69
69
|
"ws": "^8.20.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@antfu/eslint-config": "^8.
|
|
72
|
+
"@antfu/eslint-config": "^8.2.0"
|
|
73
73
|
}
|
|
74
74
|
}
|