homey-api 3.17.3 → 3.17.5
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/assets/specifications/AthomCloudAPI.json +6 -1
- package/assets/types/homey-api.private.d.ts +4 -4
- package/lib/HomeyAPI/HomeyAPIV3/Item.js +38 -81
- package/lib/HomeyAPI/HomeyAPIV3/Manager.js +91 -181
- package/lib/HomeyAPI/HomeyAPIV3/RealtimeConsumer.js +140 -0
- package/lib/HomeyAPI/HomeyAPIV3/SocketSession.js +617 -0
- package/lib/HomeyAPI/HomeyAPIV3/SubscriptionRegistry.js +341 -0
- package/lib/HomeyAPI/HomeyAPIV3.js +55 -332
- package/package.json +2 -2
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SocketIOClient = require('socket.io-client');
|
|
4
|
+
const EventEmitter = require('../../EventEmitter');
|
|
5
|
+
const Util = require('../../Util');
|
|
6
|
+
const HomeyAPIError = require('../HomeyAPIError');
|
|
7
|
+
|
|
8
|
+
const SOCKET_SESSION_NOT_READY_CODE = 'ERR_SOCKET_SESSION_NOT_READY';
|
|
9
|
+
|
|
10
|
+
class SocketSession extends EventEmitter {
|
|
11
|
+
static NOT_READY_CODE = SOCKET_SESSION_NOT_READY_CODE;
|
|
12
|
+
|
|
13
|
+
constructor(homey) {
|
|
14
|
+
super();
|
|
15
|
+
|
|
16
|
+
this.homey = homey;
|
|
17
|
+
this.__phase = 'idle';
|
|
18
|
+
this.__socket = null;
|
|
19
|
+
this.__homeySocket = null;
|
|
20
|
+
this.__boundHomeySocket = null;
|
|
21
|
+
this.__homeySocketHandlers = null;
|
|
22
|
+
this.__readyPromise = null;
|
|
23
|
+
this.__readyAbortController = null;
|
|
24
|
+
this.__readyRevision = 0;
|
|
25
|
+
this.__destroyed = false;
|
|
26
|
+
this.__closing = false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get socket() {
|
|
30
|
+
return this.__socket;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get homeySocket() {
|
|
34
|
+
return this.__homeySocket;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get phase() {
|
|
38
|
+
return this.__phase;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get revision() {
|
|
42
|
+
return this.__readyRevision;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isConnected() {
|
|
46
|
+
return Boolean(this.__homeySocket && this.__homeySocket.connected);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isConnecting() {
|
|
50
|
+
return this.__readyPromise != null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async ensureConnected() {
|
|
54
|
+
if (this.__destroyed) {
|
|
55
|
+
throw new Error('Socket session destroyed.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (this.isConnected()) {
|
|
59
|
+
return this.__getReadyState();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (this.__readyPromise) {
|
|
63
|
+
return this.__readyPromise;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isReconnect = this.__readyRevision > 0 || this.__homeySocket != null;
|
|
67
|
+
const abortController = new AbortController();
|
|
68
|
+
this.__readyAbortController = abortController;
|
|
69
|
+
|
|
70
|
+
const readyPromise = Promise.resolve().then(async () => {
|
|
71
|
+
if (this.__destroyed) {
|
|
72
|
+
throw this.__createAbortError('Socket session destroyed.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (abortController.signal.aborted) {
|
|
76
|
+
throw this.__createAbortError(abortController.signal.reason);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.__setPhase('connecting');
|
|
80
|
+
|
|
81
|
+
const baseUrl = await this.homey.baseUrl;
|
|
82
|
+
|
|
83
|
+
if (!this.homey.__token) {
|
|
84
|
+
await this.homey.login();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!this.__socket) {
|
|
88
|
+
this.__createSocket(baseUrl);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await this.__waitForSocketConnect(abortController.signal);
|
|
92
|
+
|
|
93
|
+
this.__setPhase('handshaking');
|
|
94
|
+
const namespace = await this.__handshakeClient(abortController.signal);
|
|
95
|
+
await this.__connectHomeySocket(namespace, abortController.signal);
|
|
96
|
+
|
|
97
|
+
this.__readyRevision += 1;
|
|
98
|
+
this.__setPhase('ready');
|
|
99
|
+
|
|
100
|
+
const readyState = {
|
|
101
|
+
...this.__getReadyState(),
|
|
102
|
+
isReconnect,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this.emit('ready', readyState);
|
|
106
|
+
|
|
107
|
+
return readyState;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.__readyPromise = readyPromise;
|
|
111
|
+
|
|
112
|
+
readyPromise
|
|
113
|
+
.catch((err) => {
|
|
114
|
+
this.homey.__debug('SocketSession Error', err.message);
|
|
115
|
+
|
|
116
|
+
if (!this.__destroyed && !this.__closing && isReconnect) {
|
|
117
|
+
this.emit('ready_error', err);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!this.isConnected() && !this.__destroyed && !this.__closing) {
|
|
121
|
+
this.__setPhase('idle');
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
.finally(() => {
|
|
125
|
+
if (this.__readyPromise === readyPromise) {
|
|
126
|
+
this.__readyPromise = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.__readyAbortController === abortController) {
|
|
130
|
+
this.__readyAbortController = null;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return readyPromise;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async requestApi({
|
|
138
|
+
uri,
|
|
139
|
+
operation,
|
|
140
|
+
args,
|
|
141
|
+
timeout = this.homey.constructor.DEFAULT_TIMEOUT,
|
|
142
|
+
}) {
|
|
143
|
+
if (!this.isConnected()) {
|
|
144
|
+
throw this.__createNotReadyError();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const homeySocket = this.__homeySocket;
|
|
148
|
+
|
|
149
|
+
return Util.timeout(
|
|
150
|
+
new Promise((resolve, reject) => {
|
|
151
|
+
const onDisconnect = (reason) => {
|
|
152
|
+
cleanup();
|
|
153
|
+
reject(this.__createNotReadyError(reason));
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const cleanup = () => {
|
|
157
|
+
homeySocket.removeListener('disconnect', onDisconnect);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
homeySocket.once('disconnect', onDisconnect);
|
|
161
|
+
homeySocket.emit(
|
|
162
|
+
'api',
|
|
163
|
+
{
|
|
164
|
+
args,
|
|
165
|
+
operation,
|
|
166
|
+
uri,
|
|
167
|
+
},
|
|
168
|
+
(err, result) => {
|
|
169
|
+
cleanup();
|
|
170
|
+
|
|
171
|
+
if (err != null) {
|
|
172
|
+
reject(this.__normalizeSocketError(err, `Unknown API error for ${operation}.`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
resolve(result);
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}),
|
|
180
|
+
timeout
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async disconnect() {
|
|
185
|
+
if (this.__destroyed) return;
|
|
186
|
+
|
|
187
|
+
this.__closing = true;
|
|
188
|
+
this.__cancelReadyCycle('Socket session disconnected.');
|
|
189
|
+
const shouldEmitDisconnect = this.isConnected();
|
|
190
|
+
this.__setPhase('disconnecting');
|
|
191
|
+
|
|
192
|
+
const homeySocket = this.__homeySocket;
|
|
193
|
+
if (homeySocket) {
|
|
194
|
+
homeySocket.removeAllListeners();
|
|
195
|
+
homeySocket.close();
|
|
196
|
+
this.__homeySocket = null;
|
|
197
|
+
this.__unbindHomeySocket();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (this.__socket) {
|
|
201
|
+
const socket = this.__socket;
|
|
202
|
+
|
|
203
|
+
await new Promise((resolve) => {
|
|
204
|
+
if (socket.connected) {
|
|
205
|
+
socket.once('disconnect', () => {
|
|
206
|
+
socket.removeAllListeners();
|
|
207
|
+
resolve();
|
|
208
|
+
});
|
|
209
|
+
socket.disconnect();
|
|
210
|
+
} else {
|
|
211
|
+
socket.close();
|
|
212
|
+
socket.removeAllListeners();
|
|
213
|
+
resolve();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this.__socket = null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (shouldEmitDisconnect) {
|
|
221
|
+
this.emit('disconnect', 'io client disconnect');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.__closing = false;
|
|
225
|
+
this.__setPhase('idle');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
destroy() {
|
|
229
|
+
if (this.__destroyed) return;
|
|
230
|
+
|
|
231
|
+
this.__destroyed = true;
|
|
232
|
+
this.__closing = true;
|
|
233
|
+
this.__cancelReadyCycle('Socket session destroyed.');
|
|
234
|
+
|
|
235
|
+
if (this.__homeySocket) {
|
|
236
|
+
this.__homeySocket.removeAllListeners();
|
|
237
|
+
this.__homeySocket.close();
|
|
238
|
+
this.__homeySocket = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.__unbindHomeySocket();
|
|
242
|
+
|
|
243
|
+
if (this.__socket) {
|
|
244
|
+
this.__socket.removeAllListeners();
|
|
245
|
+
this.__socket.close();
|
|
246
|
+
this.__socket = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.__setPhase('destroyed');
|
|
250
|
+
this.removeAllListeners();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
__getReadyState() {
|
|
254
|
+
return {
|
|
255
|
+
homeySocket: this.__homeySocket,
|
|
256
|
+
socket: this.__socket,
|
|
257
|
+
revision: this.__readyRevision,
|
|
258
|
+
isReconnect: this.__readyRevision > 1,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
__setPhase(phase) {
|
|
263
|
+
this.__phase = phase;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
__createSocket(baseUrl) {
|
|
267
|
+
this.homey.__debug(`SocketIOClient ${baseUrl}`);
|
|
268
|
+
|
|
269
|
+
this.__socket = SocketIOClient(baseUrl, {
|
|
270
|
+
autoConnect: false,
|
|
271
|
+
transports: ['websocket'],
|
|
272
|
+
reconnection: this.homey.__reconnect,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
this.__socket.on('disconnect', (reason) => {
|
|
276
|
+
this.homey.__debug('SocketIOClient.onDisconnect', reason);
|
|
277
|
+
this.__handleSessionDisconnect(reason);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.__socket.on('error', (err) => {
|
|
281
|
+
this.homey.__debug('SocketIOClient.onError', err.message);
|
|
282
|
+
this.emit('error', err);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
this.__socket.on('reconnect', () => {
|
|
286
|
+
this.homey.__debug('SocketIOClient.onReconnect');
|
|
287
|
+
this.emit('reconnect');
|
|
288
|
+
this.__scheduleEnsureConnected('reconnect');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
this.__socket.on('reconnect_attempt', () => {
|
|
292
|
+
this.homey.__debug('SocketIOClient.onReconnectAttempt');
|
|
293
|
+
this.emit('reconnect_attempt');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
this.__socket.on('reconnecting', (attempt) => {
|
|
297
|
+
this.homey.__debug(`SocketIOClient.onReconnecting (Attempt #${attempt})`);
|
|
298
|
+
this.emit('reconnecting');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
this.__socket.on('reconnect_error', (err) => {
|
|
302
|
+
this.homey.__debug('SocketIOClient.onReconnectError', err.message, err);
|
|
303
|
+
this.emit('reconnect_error', err);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
this.__socket.on('connect_error', (err) => {
|
|
307
|
+
this.homey.__debug('SocketIOClient.onConnectError', err.message);
|
|
308
|
+
this.emit('connect_error', err);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
this.__socket.on('connect', () => {
|
|
312
|
+
this.homey.__debug('SocketIOClient.onConnect');
|
|
313
|
+
this.emit('connect');
|
|
314
|
+
this.__scheduleEnsureConnected('connect');
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
__scheduleEnsureConnected(origin) {
|
|
319
|
+
if (this.__destroyed || this.__closing || this.isConnected() || this.__readyPromise) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.ensureConnected().catch((err) => {
|
|
324
|
+
this.homey.__debug(`SocketIOClient.${origin}.ensureConnectedError`, err.message);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
__handleSessionDisconnect(reason) {
|
|
329
|
+
const shouldEmitDisconnect = this.__phase === 'ready' || this.isConnected();
|
|
330
|
+
|
|
331
|
+
this.__cancelReadyCycle('Socket disconnected.');
|
|
332
|
+
this.__setPhase(this.__closing ? 'disconnecting' : 'idle');
|
|
333
|
+
|
|
334
|
+
if (shouldEmitDisconnect) {
|
|
335
|
+
this.emit('disconnect', reason);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async __waitForSocketConnect(signal) {
|
|
340
|
+
if (this.__socket.connected) return;
|
|
341
|
+
|
|
342
|
+
await this.__waitForConnect({
|
|
343
|
+
socket: this.__socket,
|
|
344
|
+
signal,
|
|
345
|
+
errorEvents: ['connect_error'],
|
|
346
|
+
fallbackMessage: 'Unknown error connecting to Socket.io.',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async __handshakeClient(signal) {
|
|
351
|
+
this.homey.__debug('__handshakeClient');
|
|
352
|
+
|
|
353
|
+
const handshakeClient = (token) => {
|
|
354
|
+
return new Promise((resolve, reject) => {
|
|
355
|
+
this.__socket.emit(
|
|
356
|
+
'handshakeClient',
|
|
357
|
+
{
|
|
358
|
+
token,
|
|
359
|
+
homeyId: this.homey.id,
|
|
360
|
+
},
|
|
361
|
+
(err, result) => {
|
|
362
|
+
if (err != null) {
|
|
363
|
+
reject(this.__normalizeSocketError(err, 'Unknown handshake error.'));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
resolve(result);
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const token = this.homey.__token;
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const result = await this.__timeoutWithAbort(
|
|
377
|
+
handshakeClient(token),
|
|
378
|
+
this.homey.constructor.HANDSHAKE_TIMEOUT,
|
|
379
|
+
`Failed to handshake client (Timeout after ${this.homey.constructor.HANDSHAKE_TIMEOUT}ms).`,
|
|
380
|
+
signal
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
this.homey.__debug('SocketIOClient.onHandshakeClientSuccess', `Namespace: ${result.namespace}`);
|
|
384
|
+
|
|
385
|
+
return result.namespace;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if ((err.statusCode === 401 || err.code === 401) && this.homey.__api != null) {
|
|
388
|
+
this.homey.__debug('Token expired, refreshing...');
|
|
389
|
+
await this.homey.refreshForToken(token, false);
|
|
390
|
+
|
|
391
|
+
const result = await this.__timeoutWithAbort(
|
|
392
|
+
handshakeClient(this.homey.__token),
|
|
393
|
+
this.homey.constructor.HANDSHAKE_TIMEOUT,
|
|
394
|
+
`Failed to handshake client (Timeout after ${this.homey.constructor.HANDSHAKE_TIMEOUT}ms).`,
|
|
395
|
+
signal
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
this.homey.__debug('SocketIOClient.onHandshakeClientSuccess', `Namespace: ${result.namespace}`);
|
|
399
|
+
|
|
400
|
+
return result.namespace;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async __connectHomeySocket(namespace, signal) {
|
|
408
|
+
this.__homeySocket = this.__socket.io.socket(namespace);
|
|
409
|
+
this.__bindHomeySocket(namespace, this.__homeySocket);
|
|
410
|
+
|
|
411
|
+
if (this.__homeySocket.connected) {
|
|
412
|
+
return this.__homeySocket;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
await this.__waitForConnect({
|
|
416
|
+
socket: this.__homeySocket,
|
|
417
|
+
signal,
|
|
418
|
+
errorEvents: ['connect_error', 'error'],
|
|
419
|
+
fallbackMessage: `Unknown error connecting to namespace ${namespace}.`,
|
|
420
|
+
onConnect: () => {
|
|
421
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onConnect`);
|
|
422
|
+
},
|
|
423
|
+
onConnectError: (err) => {
|
|
424
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onConnectError`, err.message);
|
|
425
|
+
},
|
|
426
|
+
open: () => {
|
|
427
|
+
this.__homeySocket.open();
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return this.__homeySocket;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
__bindHomeySocket(namespace, homeySocket) {
|
|
435
|
+
if (this.__boundHomeySocket === homeySocket) return;
|
|
436
|
+
|
|
437
|
+
this.__unbindHomeySocket();
|
|
438
|
+
|
|
439
|
+
this.__homeySocketHandlers = {
|
|
440
|
+
disconnect: (reason) => {
|
|
441
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onDisconnect`, reason);
|
|
442
|
+
this.__handleSessionDisconnect(reason);
|
|
443
|
+
},
|
|
444
|
+
reconnecting: (attempt) => {
|
|
445
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onReconnecting (Attempt #${attempt})`);
|
|
446
|
+
},
|
|
447
|
+
reconnect: () => {
|
|
448
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onReconnect`);
|
|
449
|
+
},
|
|
450
|
+
reconnectError: (err) => {
|
|
451
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onReconnectError`, err.message);
|
|
452
|
+
},
|
|
453
|
+
connectError: (err) => {
|
|
454
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onConnectError`, err.message);
|
|
455
|
+
},
|
|
456
|
+
error: (err) => {
|
|
457
|
+
this.homey.__debug(`SocketIOClient.Namespace[${namespace}].onError`, err.message || err);
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
homeySocket.on('disconnect', this.__homeySocketHandlers.disconnect);
|
|
462
|
+
homeySocket.on('reconnecting', this.__homeySocketHandlers.reconnecting);
|
|
463
|
+
homeySocket.on('reconnect', this.__homeySocketHandlers.reconnect);
|
|
464
|
+
homeySocket.on('reconnect_error', this.__homeySocketHandlers.reconnectError);
|
|
465
|
+
homeySocket.on('connect_error', this.__homeySocketHandlers.connectError);
|
|
466
|
+
homeySocket.on('error', this.__homeySocketHandlers.error);
|
|
467
|
+
|
|
468
|
+
this.__boundHomeySocket = homeySocket;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
__unbindHomeySocket() {
|
|
472
|
+
if (!this.__boundHomeySocket || !this.__homeySocketHandlers) {
|
|
473
|
+
this.__boundHomeySocket = null;
|
|
474
|
+
this.__homeySocketHandlers = null;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.__boundHomeySocket.removeListener('disconnect', this.__homeySocketHandlers.disconnect);
|
|
479
|
+
this.__boundHomeySocket.removeListener('reconnecting', this.__homeySocketHandlers.reconnecting);
|
|
480
|
+
this.__boundHomeySocket.removeListener('reconnect', this.__homeySocketHandlers.reconnect);
|
|
481
|
+
this.__boundHomeySocket.removeListener('reconnect_error', this.__homeySocketHandlers.reconnectError);
|
|
482
|
+
this.__boundHomeySocket.removeListener('connect_error', this.__homeySocketHandlers.connectError);
|
|
483
|
+
this.__boundHomeySocket.removeListener('error', this.__homeySocketHandlers.error);
|
|
484
|
+
|
|
485
|
+
this.__boundHomeySocket = null;
|
|
486
|
+
this.__homeySocketHandlers = null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async __waitForConnect({
|
|
490
|
+
socket,
|
|
491
|
+
signal,
|
|
492
|
+
errorEvents = [],
|
|
493
|
+
fallbackMessage,
|
|
494
|
+
onConnect = () => {},
|
|
495
|
+
onConnectError = () => {},
|
|
496
|
+
open = () => socket.open(),
|
|
497
|
+
}) {
|
|
498
|
+
if (socket.connected) return;
|
|
499
|
+
|
|
500
|
+
await new Promise((resolve, reject) => {
|
|
501
|
+
const onAbort = () => {
|
|
502
|
+
cleanup();
|
|
503
|
+
reject(this.__createAbortError(signal.reason));
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const handleConnect = () => {
|
|
507
|
+
cleanup();
|
|
508
|
+
onConnect();
|
|
509
|
+
resolve();
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const handleConnectError = (err) => {
|
|
513
|
+
cleanup();
|
|
514
|
+
|
|
515
|
+
const normalizedError = this.__normalizeSocketError(err, fallbackMessage);
|
|
516
|
+
onConnectError(normalizedError);
|
|
517
|
+
reject(normalizedError);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const cleanup = () => {
|
|
521
|
+
socket.removeListener('connect', handleConnect);
|
|
522
|
+
for (const errorEvent of errorEvents) {
|
|
523
|
+
socket.removeListener(errorEvent, handleConnectError);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (signal) {
|
|
527
|
+
signal.removeEventListener('abort', onAbort);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
socket.once('connect', handleConnect);
|
|
532
|
+
for (const errorEvent of errorEvents) {
|
|
533
|
+
socket.once(errorEvent, handleConnectError);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (signal) {
|
|
537
|
+
if (signal.aborted) {
|
|
538
|
+
cleanup();
|
|
539
|
+
reject(this.__createAbortError(signal.reason));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
open();
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async __timeoutWithAbort(promise, timeout, message, signal) {
|
|
551
|
+
const abortPromise = signal
|
|
552
|
+
? new Promise((_, reject) => {
|
|
553
|
+
if (signal.aborted) {
|
|
554
|
+
reject(this.__createAbortError(signal.reason));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
signal.addEventListener(
|
|
559
|
+
'abort',
|
|
560
|
+
() => {
|
|
561
|
+
reject(this.__createAbortError(signal.reason));
|
|
562
|
+
},
|
|
563
|
+
{ once: true }
|
|
564
|
+
);
|
|
565
|
+
})
|
|
566
|
+
: null;
|
|
567
|
+
|
|
568
|
+
if (abortPromise) {
|
|
569
|
+
return Promise.race([Util.timeout(promise, timeout, message), abortPromise]);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return Util.timeout(promise, timeout, message);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
__cancelReadyCycle(message) {
|
|
576
|
+
if (this.__readyAbortController) {
|
|
577
|
+
this.__readyAbortController.abort(message);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
__createAbortError(message) {
|
|
582
|
+
const err = new Error(message || 'Socket session aborted.');
|
|
583
|
+
err.name = 'AbortError';
|
|
584
|
+
return err;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
__createNotReadyError(reason) {
|
|
588
|
+
const err = new Error(reason || 'Socket session not ready.');
|
|
589
|
+
err.code = SOCKET_SESSION_NOT_READY_CODE;
|
|
590
|
+
return err;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
__normalizeSocketError(err, fallbackMessage) {
|
|
594
|
+
if (err instanceof Error) {
|
|
595
|
+
return err;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (typeof err === 'object' && err != null) {
|
|
599
|
+
return new HomeyAPIError(
|
|
600
|
+
{
|
|
601
|
+
stack: err.stack,
|
|
602
|
+
error: err.error,
|
|
603
|
+
error_description: err.error_description || err.message,
|
|
604
|
+
},
|
|
605
|
+
err.statusCode || err.code
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (typeof err === 'string') {
|
|
610
|
+
return new Error(err);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return new Error(fallbackMessage);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
module.exports = SocketSession;
|