@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 +6 -0
- package/lib/connection/ws.js +52 -102
- package/lib/platform.js +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
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) {
|
|
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
|
|
218
|
-
this.
|
|
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
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
this.
|
|
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
|
|
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
|
-
|
|
344
|
-
this.
|
|
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.
|
|
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.
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
65
|
+
"axios": "^1.16.0",
|
|
66
66
|
"node-persist": "^4.0.4",
|
|
67
|
-
"p-queue": "^9.
|
|
67
|
+
"p-queue": "^9.2.0",
|
|
68
68
|
"websocket-as-promised": "^3.0.1",
|
|
69
69
|
"ws": "^8.20.0"
|
|
70
70
|
},
|