homey-api 1.10.16 → 3.0.0-rc.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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/assets/specifications/HomeyAPIV3Local.json +19 -1
  3. package/assets/types/homey-api.d.ts +71 -589
  4. package/assets/types/homey-api.private.d.ts +71 -649
  5. package/index.js +1 -1
  6. package/lib/APIErrorNotFound.js +20 -0
  7. package/lib/AthomCloudAPI/Homey.js +3 -1
  8. package/lib/EventEmitter.js +0 -6
  9. package/lib/HomeyAPI/HomeyAPI.js +48 -5
  10. package/lib/HomeyAPI/HomeyAPIErrorNotFound.js +21 -0
  11. package/lib/HomeyAPI/HomeyAPIV2/Manager.js +2 -575
  12. package/lib/HomeyAPI/HomeyAPIV2/ManagerDevices/Capability.js +20 -0
  13. package/lib/HomeyAPI/HomeyAPIV2/ManagerDevices/Device.js +18 -0
  14. package/lib/HomeyAPI/HomeyAPIV2/ManagerDevices.js +20 -3
  15. package/lib/HomeyAPI/HomeyAPIV2/ManagerDrivers/Driver.js +25 -0
  16. package/lib/HomeyAPI/HomeyAPIV2/ManagerDrivers.js +29 -0
  17. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlow/AdvancedFlow.js +17 -0
  18. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlow/Flow.js +34 -0
  19. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlow/FlowCardAction.js +25 -0
  20. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlow/FlowCardCondition.js +25 -0
  21. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlow/FlowCardTrigger.js +25 -0
  22. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlow.js +104 -0
  23. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlowToken/FlowToken.js +24 -0
  24. package/lib/HomeyAPI/HomeyAPIV2/ManagerFlowToken.js +29 -0
  25. package/lib/HomeyAPI/HomeyAPIV2/ManagerInsights/Log.js +23 -0
  26. package/lib/HomeyAPI/HomeyAPIV2/ManagerInsights.js +29 -0
  27. package/lib/HomeyAPI/HomeyAPIV2.js +12 -716
  28. package/lib/HomeyAPI/HomeyAPIV3/Item.js +173 -2
  29. package/lib/HomeyAPI/HomeyAPIV3/Manager.js +531 -3
  30. package/lib/HomeyAPI/{HomeyAPIV2 → HomeyAPIV3/ManagerApps}/App.js +1 -1
  31. package/lib/HomeyAPI/{HomeyAPIV2 → HomeyAPIV3}/ManagerApps.js +4 -3
  32. package/lib/HomeyAPI/HomeyAPIV3/ManagerDevices/Capability.js +9 -0
  33. package/lib/HomeyAPI/{HomeyAPIV2 → HomeyAPIV3/ManagerDevices}/Device.js +78 -3
  34. package/lib/HomeyAPI/{HomeyAPIV2 → HomeyAPIV3/ManagerDevices}/DeviceCapability.js +3 -3
  35. package/lib/HomeyAPI/HomeyAPIV3/ManagerDevices.js +17 -0
  36. package/lib/HomeyAPI/HomeyAPIV3/ManagerDrivers/Driver.js +9 -0
  37. package/lib/HomeyAPI/HomeyAPIV3/ManagerDrivers.js +15 -0
  38. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlow/AdvancedFlow.js +9 -0
  39. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlow/Flow.js +9 -0
  40. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlow/FlowCard.js +9 -0
  41. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlow/FlowCardAction.js +9 -0
  42. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlow/FlowCardCondition.js +9 -0
  43. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlow/FlowCardTrigger.js +9 -0
  44. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlow.js +12 -23
  45. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlowToken/FlowToken.js +16 -0
  46. package/lib/HomeyAPI/HomeyAPIV3/ManagerFlowToken.js +15 -0
  47. package/lib/HomeyAPI/HomeyAPIV3/ManagerInsights/Log.js +9 -0
  48. package/lib/HomeyAPI/HomeyAPIV3/ManagerInsights.js +15 -0
  49. package/lib/HomeyAPI/HomeyAPIV3.js +728 -4
  50. package/lib/HomeyAPI/HomeyAPIV3Cloud.js +1 -1
  51. package/lib/HomeyAPI/HomeyAPIV3Local.js +1 -1
  52. package/package.json +1 -1
  53. package/lib/HomeyAPI/HomeyAPIApp.js +0 -127
  54. package/lib/HomeyAPI/HomeyAPIV2/Item.js +0 -177
@@ -1,21 +1,745 @@
1
1
  'use strict';
2
2
 
3
- const HomeyAPIV2 = require('./HomeyAPIV2');
4
-
3
+ const SocketIOClient = require('socket.io-client');
4
+ const APIErrorHomeyOffline = require('../APIErrorHomeyOffline');
5
+ const Util = require('../Util');
6
+ const HomeyAPI = require('./HomeyAPI');
7
+ const HomeyAPIError = require('./HomeyAPIError');
8
+ const ManagerApps = require('./HomeyAPIV3/ManagerApps');
9
+ const ManagerDrivers = require('./HomeyAPIV3/ManagerDrivers');
10
+ const ManagerDevices = require('./HomeyAPIV3/ManagerDevices');
5
11
  const ManagerFlow = require('./HomeyAPIV3/ManagerFlow');
12
+ const ManagerFlowToken = require('./HomeyAPIV3/ManagerFlowToken');
13
+ const ManagerInsights = require('./HomeyAPIV3/ManagerInsights');
14
+
15
+ // eslint-disable-next-line no-unused-vars
16
+ const Manager = require('./HomeyAPIV3/Manager');
6
17
 
7
18
  /**
8
19
  * @class
9
20
  * @hideconstructor
10
21
  * @extends HomeyAPIV2
11
22
  */
12
- class HomeyAPIV3 extends HomeyAPIV2 {
23
+ class HomeyAPIV3 extends HomeyAPI {
13
24
 
14
25
  static MANAGERS = {
15
- ...HomeyAPIV2.MANAGERS,
26
+ ManagerApps,
27
+ ManagerDrivers,
28
+ ManagerDevices,
16
29
  ManagerFlow,
30
+ ManagerFlowToken,
31
+ ManagerInsights,
17
32
  };
18
33
 
34
+ constructor({
35
+ properties,
36
+ strategy = [
37
+ HomeyAPI.DISCOVERY_STRATEGIES.MDNS,
38
+ HomeyAPI.DISCOVERY_STRATEGIES.CLOUD,
39
+ HomeyAPI.DISCOVERY_STRATEGIES.LOCAL,
40
+ HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE,
41
+ HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED,
42
+ ],
43
+ baseUrl = null,
44
+ token = null,
45
+ ...props
46
+ }) {
47
+ super({ properties, ...props });
48
+
49
+ Object.defineProperty(this, '__baseUrl', {
50
+ value: null,
51
+ enumerable: false,
52
+ writable: true,
53
+ });
54
+
55
+ Object.defineProperty(this, '__strategyId', {
56
+ value: null,
57
+ enumerable: false,
58
+ writable: true,
59
+ });
60
+
61
+ Object.defineProperty(this, '__token', {
62
+ value: token,
63
+ enumerable: false,
64
+ writable: true,
65
+ });
66
+
67
+ Object.defineProperty(this, '__strategies', {
68
+ value: Array.isArray(strategy)
69
+ ? strategy
70
+ : [strategy],
71
+ enumerable: false,
72
+ writable: false,
73
+ });
74
+
75
+ Object.defineProperty(this, '__managers', {
76
+ value: {},
77
+ enumerable: false,
78
+ writable: false,
79
+ });
80
+
81
+ Object.defineProperty(this, '__baseUrlPromise', {
82
+ value: typeof baseUrl === 'string'
83
+ ? Promise.resolve(baseUrl)
84
+ : null,
85
+ enumerable: false,
86
+ writable: true,
87
+ });
88
+
89
+ Object.defineProperty(this, '__loginPromise', {
90
+ value: null,
91
+ enumerable: false,
92
+ writable: true,
93
+ });
94
+
95
+ Object.defineProperty(this, '__connected', {
96
+ value: false,
97
+ enumerable: false,
98
+ writable: true,
99
+ });
100
+
101
+ this.generateManagersFromSpecification();
102
+ }
103
+
104
+ /*
105
+ * Get the Homey's base URL promise
106
+ */
107
+ get baseUrl() {
108
+ return (async () => {
109
+ if (!this.__baseUrlPromise) {
110
+ this.__baseUrlPromise = this.discoverBaseUrl().then(({ baseUrl }) => baseUrl);
111
+ this.__baseUrlPromise.catch(() => { });
112
+ }
113
+
114
+ return this.__baseUrlPromise;
115
+ })();
116
+ }
117
+
118
+ get strategyId() {
119
+ return this.__strategyId;
120
+ }
121
+
122
+ /*
123
+ * Generate Managers from JSON specification
124
+ * A manager instance is created when it's first accessed
125
+ */
126
+
127
+ getSpecification() {
128
+ // eslint-disable-next-line global-require
129
+ return require('../../assets/specifications/HomeyAPIV2.json');
130
+ }
131
+
132
+ generateManagersFromSpecification() {
133
+ const { managers } = this.getSpecification();
134
+ Object.entries(managers).forEach(([managerName, manager]) => {
135
+ this.generateManagerFromSpecification(managerName, manager);
136
+ });
137
+ }
138
+
139
+ generateManagerFromSpecification(managerName, manager) {
140
+ Object.defineProperty(this, manager.idCamelCase, {
141
+ get: () => {
142
+ if (!this.__managers[managerName]) {
143
+ const ManagerClass = this.constructor.MANAGERS[managerName]
144
+ ? this.constructor.MANAGERS[managerName]
145
+ // eslint-disable-next-line no-eval
146
+ : eval(`(class ${managerName} extends Manager {})`);
147
+
148
+ ManagerClass.ID = manager.id;
149
+
150
+ this.__managers[managerName] = new ManagerClass({
151
+ homey: this,
152
+ items: manager.items || {},
153
+ operations: manager.operations || {},
154
+ });
155
+ }
156
+
157
+ return this.__managers[managerName];
158
+ },
159
+ enumerable: false,
160
+ });
161
+ }
162
+
163
+ /*
164
+ * Discover the URL to talk to Homey
165
+ * We prefer localSecure, because it's fastest and most secure
166
+ * If that doesn't work, we prefer local OR mdns, whichever is fastest
167
+ * Finally, we fallback to cloud
168
+ */
169
+
170
+ async discoverBaseUrl() {
171
+ const urls = {};
172
+
173
+ if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.MDNS)) {
174
+ if (Util.isHTTPUnsecureSupported()) {
175
+ urls[HomeyAPI.DISCOVERY_STRATEGIES.MDNS] = `http://homey-${this.id}.local`;
176
+ }
177
+ }
178
+
179
+ if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL)) {
180
+ if (Util.isHTTPUnsecureSupported() && this.__properties.localUrl) {
181
+ urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL] = `${this.__properties.localUrl}`;
182
+ }
183
+ }
184
+
185
+ if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE)) {
186
+ if (this.__properties.localUrlSecure) {
187
+ urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE] = `${this.__properties.localUrlSecure}`;
188
+ }
189
+ }
190
+
191
+ if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.CLOUD)) {
192
+ if (this.__properties.remoteUrl) {
193
+ urls[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD] = `${this.__properties.remoteUrl}`;
194
+ }
195
+ }
196
+
197
+ if (this.__strategies.includes(HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED)) {
198
+ if (this.__properties.remoteUrlForwarded) {
199
+ urls[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = `${this.__properties.remoteUrlForwarded}`;
200
+ }
201
+ }
202
+
203
+ if (!Object.keys(urls).length) {
204
+ throw new Error('No Discovery Strategies Available');
205
+ }
206
+
207
+ // Don't discover, just set the only strategy
208
+ if (Object.keys(urls).length === 1) {
209
+ this.__baseUrl = Object.values(urls)[0];
210
+ this.__strategyId = Object.keys(urls)[0];
211
+
212
+ return {
213
+ baseUrl: this.__baseUrl,
214
+ strategyId: this.__strategyId,
215
+ };
216
+ }
217
+
218
+ this.__debug(`Discovery Strategies: ${Object.keys(urls).join(',')}`);
219
+
220
+ // Create the returned Promise
221
+ let resolve;
222
+ let reject;
223
+ const promise = new Promise((resolve_, reject_) => {
224
+ resolve = resolve_;
225
+ reject = reject_;
226
+ });
227
+ promise
228
+ .then(({ baseUrl, strategyId }) => {
229
+ this.__baseUrl = baseUrl;
230
+ this.__strategyId = strategyId;
231
+ })
232
+ .catch(() => { });
233
+
234
+ // Ping method
235
+ const ping = async (strategyId, timeout) => {
236
+ let pingTimeout;
237
+ const baseUrl = urls[strategyId];
238
+ return Promise.race([
239
+ Util.fetch(`${baseUrl}/api/manager/system/ping?id=${this.id}`, {
240
+ headers: {
241
+ 'X-Homey-ID': this.id,
242
+ },
243
+ }).then(async res => {
244
+ const text = await res.text();
245
+ if (!res.ok) throw new Error(text || res.statusText);
246
+ if (text === 'false') throw new Error('Invalid Homey ID');
247
+
248
+ const homeyId = res.headers.get('X-Homey-ID');
249
+ if (homeyId) {
250
+ if (homeyId !== this.id) throw new Error('Invalid Homey ID'); // TODO: Add to Homey Connect
251
+ }
252
+
253
+ // Set the version that Homey told us.
254
+ // It's the absolute truth, because the Cloud API may be behind.
255
+ const homeyVersion = res.headers.get('X-Homey-Version');
256
+ if (homeyVersion !== this.version) {
257
+ this.version = homeyVersion;
258
+ }
259
+
260
+ return {
261
+ baseUrl,
262
+ strategyId,
263
+ };
264
+ }),
265
+ new Promise((_, reject) => {
266
+ pingTimeout = setTimeout(() => reject(new Error('PingTimeout')), timeout);
267
+ }),
268
+ ]).finally(() => clearTimeout(pingTimeout));
269
+ };
270
+
271
+ const pings = {};
272
+
273
+ // Ping localSecure (https://xxx-xxx-xxx-xx.homey.homeylocal.com)
274
+ if (urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
275
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE] = ping(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE, 1200);
276
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE].catch(err => this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE} Error:`, err && err.message));
277
+ }
278
+
279
+ // Ping local (http://xxx-xxx-xxx-xxx)
280
+ if (urls[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
281
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL] = ping(HomeyAPI.DISCOVERY_STRATEGIES.LOCAL, 1000);
282
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL].catch(err => this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.LOCAL} Error:`, err && err.message));
283
+ }
284
+
285
+ // Ping mdns (http://homey-<homeyId>.local)
286
+ if (urls[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
287
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS] = ping(HomeyAPI.DISCOVERY_STRATEGIES.MDNS, 3000);
288
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS].catch(err => this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.MDNS} Error:`, err && err.message));
289
+ }
290
+
291
+ // Ping cloud (https://<homeyId>.connect.athom.com)
292
+ if (urls[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
293
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD] = ping(HomeyAPI.DISCOVERY_STRATEGIES.CLOUD, 5000);
294
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD].catch(err => this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.CLOUD} Error:`, err && err.message));
295
+ }
296
+
297
+ // Ping Direct (https://xxx-xxx-xxx-xx.homey.homeylocal.com:12345)
298
+ if (urls[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
299
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED] = ping(HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED, 2000);
300
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED].catch(err => this.__debug(`Ping ${HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED} Error:`, err && err.message));
301
+ }
302
+
303
+ // Select the best route
304
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]) {
305
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL_SECURE]
306
+ .then(result => resolve(result))
307
+ .catch(() => {
308
+ const promises = [];
309
+
310
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
311
+ promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]);
312
+ }
313
+
314
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
315
+ promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]);
316
+ }
317
+
318
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
319
+ promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]);
320
+ }
321
+
322
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
323
+ promises.push(pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]);
324
+ }
325
+
326
+ if (!promises.length) {
327
+ throw new APIErrorHomeyOffline();
328
+ }
329
+
330
+ return Util.promiseAny(promises);
331
+ })
332
+ .then(result => resolve(result))
333
+ .catch(() => reject(new APIErrorHomeyOffline()));
334
+ } else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
335
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]
336
+ .then(result => resolve(result))
337
+ .catch(() => {
338
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
339
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
340
+ .then(result => resolve(result))
341
+ .catch(err => reject(new APIErrorHomeyOffline(err)));
342
+ }
343
+ });
344
+ } else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
345
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]
346
+ .then(result => resolve(result))
347
+ .catch(() => {
348
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
349
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
350
+ .then(result => resolve(result))
351
+ .catch(err => reject(new APIErrorHomeyOffline(err)));
352
+ }
353
+ });
354
+ } else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]) {
355
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.REMOTE_FORWARDED]
356
+ .then(result => resolve(result))
357
+ .catch(() => {
358
+ if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
359
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
360
+ .then(result => resolve(result))
361
+ .catch(err => reject(new APIErrorHomeyOffline(err)));
362
+ }
363
+ });
364
+ } else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
365
+ pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
366
+ .then(result => resolve(result))
367
+ .catch(err => reject(new APIErrorHomeyOffline(err)));
368
+ } else {
369
+ reject(new APIErrorHomeyOffline());
370
+ }
371
+
372
+ return promise;
373
+ }
374
+
375
+ async call({
376
+ $timeout = 10000,
377
+ method,
378
+ headers,
379
+ path,
380
+ body,
381
+ retryAfterRefresh = false,
382
+ }) {
383
+ const baseUrl = await this.baseUrl;
384
+
385
+ method = method.toUpperCase();
386
+
387
+ headers = {
388
+ ...headers,
389
+ 'X-Homey-ID': this.id,
390
+ };
391
+
392
+ if (body) {
393
+ headers['Content-Type'] = 'application/json';
394
+ }
395
+
396
+ if (this.__token) {
397
+ headers['Authorization'] = `Bearer ${this.__token}`;
398
+ }
399
+
400
+ this.__debug(method, `${baseUrl}${path}`);
401
+ const res = await Util.timeout(Util.fetch(`${baseUrl}${path}`, {
402
+ method,
403
+ headers,
404
+ body: ['PUT', 'POST'].includes(method) && typeof body !== 'undefined'
405
+ ? JSON.stringify(body)
406
+ : undefined,
407
+ }), $timeout);
408
+
409
+ const resStatusCode = res.status;
410
+ if (resStatusCode === 204) return undefined;
411
+
412
+ const resStatusText = res.status;
413
+ const resHeadersContentType = res.headers.get('Content-Type');
414
+ const resBodyText = await res.text();
415
+ let resBodyJson;
416
+ if (resHeadersContentType && resHeadersContentType.startsWith('application/json')) {
417
+ try {
418
+ resBodyJson = JSON.parse(resBodyText);
419
+ } catch (err) { }
420
+ }
421
+
422
+ if (!res.ok) {
423
+ if (resBodyJson) {
424
+ // If Session Expired, clear the stored token
425
+ if (resStatusCode === 401) {
426
+ this.__debug('Session expired, invalidating token...');
427
+ await this.logout();
428
+ }
429
+
430
+ // If Session Expired, try to refresh the Token
431
+ if (resStatusCode === 401 && !retryAfterRefresh) {
432
+ this.__debug('Session expired, refreshing...');
433
+ await this.login();
434
+ return this.call({
435
+ method,
436
+ headers,
437
+ path,
438
+ body,
439
+ retryAfterRefresh: true,
440
+ });
441
+ }
442
+
443
+ throw new HomeyAPIError({
444
+ error: resBodyJson.error,
445
+ error_description: resBodyJson.error_description,
446
+ stack: resBodyJson.stack,
447
+ }, resStatusCode);
448
+ }
449
+
450
+ if (resBodyText) {
451
+ throw new HomeyAPIError({
452
+ error: resBodyText,
453
+ }, resStatusCode);
454
+ }
455
+
456
+ throw new HomeyAPIError({
457
+ error: resStatusText,
458
+ }, resStatusCode);
459
+ }
460
+
461
+ if (typeof resBodyJson !== 'undefined') {
462
+ return resBodyJson;
463
+ }
464
+
465
+ return resBodyText;
466
+ }
467
+
468
+ async login() {
469
+ if (!this.__loginPromise) {
470
+ this.__loginPromise = Promise.resolve().then(async () => {
471
+ // Check store for a valid Homey.Session
472
+ const store = await this.__getStore();
473
+ if (store && store.token) {
474
+ this.__debug('Got token from store');
475
+ return store.token;
476
+ }
477
+
478
+ // Create a Session by generating a JWT token on AthomCloudAPI,
479
+ // and then sending the JWT token to Homey.
480
+ this.__debug('Retrieving token...');
481
+ const jwtToken = await this.__api.createDelegationToken({ audience: 'homey' });
482
+ const token = await this.users.login({
483
+ $socket: false,
484
+ token: jwtToken,
485
+ });
486
+ await this.__setStore({ token });
487
+ this.__debug('Got token');
488
+
489
+ return token;
490
+ });
491
+
492
+ this.__loginPromise
493
+ .then(token => {
494
+ this.__token = token;
495
+ })
496
+ .catch(err => {
497
+ this.__debug('Error Logging In:', err);
498
+ })
499
+ .finally(() => {
500
+ this.__loginPromise = null;
501
+ });
502
+ }
503
+
504
+ return this.__loginPromise;
505
+ }
506
+
507
+ async logout() {
508
+ await this.__setStore({
509
+ token: null,
510
+ });
511
+
512
+ this.__token = null;
513
+ }
514
+
515
+ /**
516
+ * If Homey is connected to Socket.io.
517
+ * @returns {Boolean}
518
+ */
519
+ isConnected() {
520
+ return this.__connected === true;
521
+ }
522
+
523
+ async subscribe(uri, {
524
+ onConnect = () => { },
525
+ onReconnect = () => { },
526
+ onReconnectError = () => { },
527
+ onDisconnect = () => { },
528
+ onEvent = () => { },
529
+ }) {
530
+ this.__debug('subscribe', uri);
531
+
532
+ await this.connect();
533
+ await new Promise((resolve, reject) => {
534
+ this.__ioNamespace.once('disconnect', reject);
535
+ this.__ioNamespace.emit('subscribe', uri, err => {
536
+ if (err) return reject(err);
537
+ return resolve();
538
+ });
539
+ });
540
+
541
+ // On Connect
542
+ const __onEvent = (event, data) => {
543
+ onEvent(event, data);
544
+ };
545
+ this.__ioNamespace.on(uri, __onEvent);
546
+
547
+ onConnect();
548
+
549
+ // On Disconnect
550
+ const __onDisconnect = reason => {
551
+ onDisconnect(reason);
552
+ };
553
+ this.__io.on('disconnect', __onDisconnect);
554
+
555
+ // On Reconnect
556
+ const __onReconnect = () => {
557
+ Promise.resolve().then(async () => {
558
+ await this.connect();
559
+ await new Promise((resolve, reject) => {
560
+ this.__ioNamespace.emit('subscribe', uri, err => {
561
+ if (err) return reject(err);
562
+ return resolve();
563
+ });
564
+ });
565
+
566
+ this.__ioNamespace.on(uri, __onEvent);
567
+
568
+ onReconnect();
569
+ }).catch(err => onReconnectError(err));
570
+ };
571
+ this.__io.on('reconnect', __onReconnect);
572
+
573
+ return {
574
+ unsubscribe: () => {
575
+ this.__ioNamespace.emit('unsubscribe', uri);
576
+ this.__ioNamespace.removeListener(uri, __onEvent);
577
+ this.__io.removeListener('disconnect', __onDisconnect);
578
+ this.__io.removeListener('reconnect', __onReconnect);
579
+ },
580
+ };
581
+ }
582
+
583
+ async connect() {
584
+ if (!this.io) {
585
+ this.io = Promise.resolve().then(async () => {
586
+ // Ensure Base URL
587
+ const baseUrl = await this.baseUrl;
588
+
589
+ // Ensure Token
590
+ if (!this.__token) await this.login();
591
+
592
+ return new Promise((resolve, reject) => {
593
+ this.__debug(`SocketIOClient ${baseUrl}`);
594
+ this.__io = SocketIOClient(baseUrl, {
595
+ autoConnect: false,
596
+ transports: ['websocket'],
597
+ transportOptions: {
598
+ pingTimeout: 8000,
599
+ pingInterval: 5000,
600
+ },
601
+ });
602
+ this.__io.on('disconnect', reason => {
603
+ this.__debug('SocketIOClient.onDisconnect', reason);
604
+ this.__connected = false;
605
+
606
+ if (this.__ioNamespace) {
607
+ this.__ioNamespace.disconnect();
608
+ this.__ioNamespace.destroy();
609
+ this.__ioNamespace.removeAllListeners();
610
+ }
611
+
612
+ reject(new Error('Disconnected'));
613
+ });
614
+ this.__io.on('error', err => {
615
+ this.__debug('SocketIOClient.onError', err.message);
616
+ });
617
+ this.__io.on('reconnect', () => {
618
+ this.__debug('SocketIOClient.onReconnect');
619
+ this.__handshakeClient()
620
+ .then(() => {
621
+ this.__debug('SocketIOClient.onReconnect.onHandshakeClientSuccess');
622
+ this.__connected = true;
623
+ resolve();
624
+ })
625
+ .catch(err => {
626
+ this.__debug('SocketIOClient.onReconnect.onHandshakeClientError', err.message);
627
+ reject(err);
628
+ });
629
+ });
630
+ this.__io.on('reconnecting', attempt => {
631
+ this.__debug(`SocketIOClient.onReconnecting (Attempt #${attempt})`);
632
+ });
633
+ this.__io.on('reconnect_error', err => {
634
+ this.__debug('SocketIOClient.onReconnectError', err.message);
635
+ });
636
+ this.__io.once('connect_error', err => {
637
+ this.__debug('SocketIOClient.onConnectError', err.message);
638
+ reject(err);
639
+ });
640
+ this.__io.once('connect', () => {
641
+ this.__debug('SocketIOClient.onConnect');
642
+ this.__handshakeClient()
643
+ .then(() => {
644
+ this.__debug('SocketIOClient.onConnect.onHandshakeClientSuccess');
645
+ this.__connected = true;
646
+ resolve();
647
+ })
648
+ .catch(err => {
649
+ this.__debug('SocketIOClient.onConnect.onHandshakeClientError', err.message);
650
+ reject(err);
651
+ });
652
+ });
653
+ this.__io.connect();
654
+ });
655
+ });
656
+ this.io.catch(err => {
657
+ this.__debug('SocketIOClient Error', err.message);
658
+ });
659
+ }
660
+
661
+ return this.io;
662
+ }
663
+
664
+ async disconnect() {
665
+ if (this.__io) {
666
+ await new Promise(resolve => {
667
+ this.__io.once('disconnect', resolve());
668
+ this.__io.disconnect();
669
+ this.__io.removeAllListeners();
670
+ this.__io = null;
671
+ });
672
+ }
673
+ // TODO todo what?
674
+ }
675
+
676
+ destroy() {
677
+ if (this.__io) {
678
+ this.__io.removeAllListeners();
679
+ this.__io.close();
680
+ }
681
+ }
682
+
683
+ async __handshakeClient() {
684
+ return new Promise((resolve, reject) => {
685
+ this.__io.emit('handshakeClient', {
686
+ token: this.__token,
687
+ homeyId: this.id,
688
+ }, (err, result) => {
689
+ if (err) return reject(err);
690
+ return resolve(result);
691
+ });
692
+ })
693
+ .catch(async err => {
694
+ // If token is expired, try to refresh
695
+ if (err.statusCode === 401) {
696
+ this.__debug('Token expired, refreshing...');
697
+ await this.logout();
698
+ await this.login();
699
+
700
+ return new Promise((resolve, reject) => {
701
+ this.__io.emit('handshakeClient', {
702
+ token: this.__token,
703
+ homeyId: this.id,
704
+ }, (err, result) => {
705
+ if (err) return reject(err);
706
+ return resolve(result);
707
+ });
708
+ });
709
+ }
710
+
711
+ throw err;
712
+ })
713
+ .then(({ namespace }) => {
714
+ this.__debug('SocketIOClient.onHandshakeClientSuccess', `Namespace: ${namespace}`);
715
+
716
+ return new Promise((resolve, reject) => {
717
+ this.__ioNamespace = this.__io.io.socket(namespace);
718
+ this.__ioNamespace.once('connect', () => {
719
+ this.__debug(`SocketIOClient.Namespace[${namespace}].onConnect`);
720
+ resolve();
721
+ });
722
+ this.__ioNamespace.once('connect_error', err => {
723
+ this.__debug(`SocketIOClient.Namespace[${namespace}].onConnectError`, err.message);
724
+ reject(err);
725
+ });
726
+ this.__ioNamespace.on('reconnecting', attempt => {
727
+ this.__debug(`SocketIOClient.Namespace[${namespace}].onReconnecting (Attempt #${attempt})`);
728
+ });
729
+ this.__ioNamespace.on('reconnect', () => {
730
+ this.__debug(`SocketIOClient.Namespace[${namespace}].onReconnect`);
731
+ });
732
+ this.__ioNamespace.on('reconnect_error', err => {
733
+ this.__debug(`SocketIOClient.Namespace[${namespace}].onReconnectError`, err.message);
734
+ });
735
+ this.__ioNamespace.on('disconnect', reason => {
736
+ this.__debug(`SocketIOClient.Namespace[${namespace}].onDisconnect`, reason);
737
+ });
738
+ this.__ioNamespace.connect();
739
+ });
740
+ });
741
+ }
742
+
19
743
  }
20
744
 
21
745
  module.exports = HomeyAPIV3;