homey-api 1.5.20 → 1.5.23

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.
@@ -9,20 +9,6 @@ class Device extends Item {
9
9
  constructor(...props) {
10
10
  super(...props);
11
11
 
12
- // Set URI
13
- Object.defineProperty(this, 'uri', {
14
- value: `homey:device:${this.id}`,
15
- enumerable: false,
16
- writable: true,
17
- });
18
-
19
- // Set Connected
20
- Object.defineProperty(this, '__connected', {
21
- value: false,
22
- enumerable: false,
23
- writable: true,
24
- });
25
-
26
12
  // Set Capability Instances
27
13
  Object.defineProperty(this, '__capabilityInstances', {
28
14
  value: {},
@@ -110,108 +96,31 @@ class Device extends Item {
110
96
  });
111
97
  }
112
98
 
113
- async connect() {
114
- this.__debug('connect');
115
-
116
- // If disconnecting, await that first
117
- try {
118
- await this.__disconnectPromise;
119
- } catch (err) { }
120
-
121
- this.__connectPromise = Promise.resolve().then(async () => {
122
- if (!this.io) {
123
- this.io = this.homey.subscribe(this.uri, {
124
- onConnect: () => {
125
- this.__debug('onConnect');
126
- this.__connected = true;
127
- },
128
- onDisconnect: () => {
129
- this.__debug('onDisconnect');
130
- this.__connected = false;
131
- },
132
- onReconnect: () => {
133
- this.__debug('onDisconnect');
134
-
135
- const capabilityInstances = this.__capabilityInstances;
136
- if (Object.keys(capabilityInstances).length > 0) {
137
- // Get the device's latest values
138
- // TODO: Optimize this with `getDevices()` when >1 device has >0 capability instances.
139
- this.manager.getDevice({
140
- id: this.id,
141
- }).then(async device => {
142
- Object.entries(capabilityInstances).forEach(([capabilityId, capabilityInstance]) => {
143
- const value = device.capabilitiesObj
144
- ? typeof device.capabilitiesObj[capabilityId] !== 'undefined'
145
- ? device.capabilitiesObj[capabilityId].value
146
- : null
147
- : null;
148
-
149
- capabilityInstance.__onCapabilityValue({
150
- capabilityId,
151
- value,
152
- transactionId: Util.uuid(),
153
- });
154
- });
155
- })
156
- // eslint-disable-next-line no-console
157
- .catch(err => console.error(`Device[${this.id}].onReconnectError:`, err));
158
- }
159
- },
160
- onEvent: (event, data) => {
161
- // // Fire event listeners
162
- if (Array.isArray(this.__listeners[event])) {
163
- this.__listeners[event].forEach(listener => listener(data));
164
- }
165
- },
99
+ onReconnect() {
100
+ const capabilityInstances = this.__capabilityInstances;
101
+ if (Object.keys(capabilityInstances).length > 0) {
102
+ // Get the device's latest values
103
+ // TODO: Optimize this with `getDevices()` when >1 device has >0 capability instances.
104
+ this.manager.getDevice({
105
+ id: this.id,
106
+ }).then(async device => {
107
+ Object.entries(capabilityInstances).forEach(([capabilityId, capabilityInstance]) => {
108
+ const value = device.capabilitiesObj
109
+ ? typeof device.capabilitiesObj[capabilityId] !== 'undefined'
110
+ ? device.capabilitiesObj[capabilityId].value
111
+ : null
112
+ : null;
113
+
114
+ capabilityInstance.__onCapabilityValue({
115
+ capabilityId,
116
+ value,
117
+ transactionId: Util.uuid(),
118
+ });
166
119
  });
167
- }
168
-
169
- await this.io;
170
- });
171
-
172
- // Delete the connecting Promise
173
- this.__connectPromise
174
- .catch(() => { })
175
- .finally(() => {
176
- delete this.__connectPromise;
177
- });
178
-
179
- await this.__connectPromise;
180
- }
181
-
182
- async disconnect() {
183
- this.__debug('disconnect');
184
-
185
- // If connecting, await that first
186
- try {
187
- await this.__connectPromise;
188
- } catch (err) { }
189
-
190
- this.__disconnectPromise = Promise.resolve().then(async () => {
191
- this.__connected = false;
192
-
193
- if (this.io) {
194
- this.io
195
- .then(io => io.unsubscribe())
196
- .catch(err => this.__debug('Error Disconnecting:', err));
197
- }
198
- });
199
-
200
- // Delete the disconnecting Promise
201
- this.__disconnectPromise
202
- .catch(() => { })
203
- .finally(() => {
204
- delete this.__disconnectPromise;
205
- });
206
-
207
- await this.__disconnectPromise;
208
- }
209
-
210
- destroy() {
211
- super.destroy();
212
-
213
- // Disconnect from Socket.io
214
- this.disconnect().catch(() => { });
120
+ })
121
+ // eslint-disable-next-line no-console
122
+ .catch(err => console.error(`Device[${this.id}].onReconnectError:`, err));
123
+ }
215
124
  }
216
125
 
217
126
  }
@@ -5,6 +5,7 @@ const EventEmitter = require('../../EventEmitter');
5
5
  class Item extends EventEmitter {
6
6
 
7
7
  constructor({
8
+ uri,
8
9
  key,
9
10
  homey,
10
11
  manager,
@@ -33,6 +34,20 @@ class Item extends EventEmitter {
33
34
  writable: false,
34
35
  });
35
36
 
37
+ // Set URI
38
+ Object.defineProperty(this, '__uri', {
39
+ value: uri,
40
+ enumerable: false,
41
+ writable: true,
42
+ });
43
+
44
+ // Set Connected
45
+ Object.defineProperty(this, '__connected', {
46
+ value: false,
47
+ enumerable: false,
48
+ writable: true,
49
+ });
50
+
36
51
  // Set Properties
37
52
  for (const [key, value] of Object.entries(properties)) {
38
53
  Object.defineProperty(this, key, {
@@ -54,9 +69,102 @@ class Item extends EventEmitter {
54
69
  return this;
55
70
  }
56
71
 
72
+ async connect() {
73
+ this.__debug('connect');
74
+
75
+ // If disconnecting, await that first
76
+ try {
77
+ await this.__disconnectPromise;
78
+ } catch (err) { }
79
+
80
+ this.__connectPromise = Promise.resolve().then(async () => {
81
+ if (!this.io) {
82
+ this.io = this.homey.subscribe(this.__uri, {
83
+ onConnect: () => {
84
+ this.__debug('onConnect');
85
+ this.__connected = true;
86
+
87
+ this.onConnect();
88
+ },
89
+ onDisconnect: () => {
90
+ this.__debug('onDisconnect');
91
+ this.__connected = false;
92
+
93
+ this.onDisconnect();
94
+ },
95
+ onReconnect: () => {
96
+ this.__debug('onDisconnect');
97
+
98
+ this.onReconnect();
99
+ },
100
+ onEvent: (event, data) => {
101
+ // // Fire event listeners
102
+ if (Array.isArray(this.__listeners[event])) {
103
+ this.__listeners[event].forEach(listener => listener(data));
104
+ }
105
+ },
106
+ });
107
+ }
108
+
109
+ await this.io;
110
+ });
111
+
112
+ // Delete the connecting Promise
113
+ this.__connectPromise
114
+ .catch(() => { })
115
+ .finally(() => {
116
+ delete this.__connectPromise;
117
+ });
118
+
119
+ await this.__connectPromise;
120
+ }
121
+
122
+ async disconnect() {
123
+ this.__debug('disconnect');
124
+
125
+ // If connecting, await that first
126
+ try {
127
+ await this.__connectPromise;
128
+ } catch (err) { }
129
+
130
+ this.__disconnectPromise = Promise.resolve().then(async () => {
131
+ this.__connected = false;
132
+
133
+ if (this.io) {
134
+ this.io
135
+ .then(io => io.unsubscribe())
136
+ .catch(err => this.__debug('Error Disconnecting:', err));
137
+ }
138
+ });
139
+
140
+ // Delete the disconnecting Promise
141
+ this.__disconnectPromise
142
+ .catch(() => { })
143
+ .finally(() => {
144
+ delete this.__disconnectPromise;
145
+ });
146
+
147
+ await this.__disconnectPromise;
148
+ }
149
+
150
+ onConnect() {
151
+ // Overload Me
152
+ }
153
+
154
+ onReconnect() {
155
+ // Overload Me
156
+ }
157
+
158
+ onDisconnect() {
159
+ // Overload Me
160
+ }
161
+
57
162
  destroy() {
58
163
  // Remove all event listeners
59
164
  this.removeAllListeners();
165
+
166
+ // Disconnect from Socket.io
167
+ this.disconnect().catch(() => { });
60
168
  }
61
169
 
62
170
  }
@@ -109,6 +109,7 @@ class Manager extends EventEmitter {
109
109
  $validate = true,
110
110
  $cache = true,
111
111
  $timeout = 5000,
112
+ $socket = true,
112
113
  $body = {},
113
114
  $query = {},
114
115
  $headers = {},
@@ -189,6 +190,14 @@ class Manager extends EventEmitter {
189
190
  }
190
191
  }
191
192
 
193
+ // Append query to path
194
+ if (Object.keys(query).length > 0) {
195
+ const queryString = Object.entries(query).map(([key, value]) => {
196
+ return `${key}=${encodeURIComponent(value)}`;
197
+ }).join('&');
198
+ path = `${path}?${queryString}`;
199
+ }
200
+
192
201
  let result;
193
202
  const benchmark = Util.benchmark();
194
203
 
@@ -236,7 +245,7 @@ class Manager extends EventEmitter {
236
245
  // If Homey is connected to Socket.io,
237
246
  // send the API request to socket.io.
238
247
  // This is about ~2x faster than HTTP
239
- if (this.homey.isConnected()) {
248
+ if (this.homey.isConnected() && $socket === true) {
240
249
  result = await Util.timeout(new Promise((resolve, reject) => {
241
250
  this.homey.__ioNamespace.emit('api', {
242
251
  args,
@@ -268,13 +277,20 @@ class Manager extends EventEmitter {
268
277
  if (itemType === 'id') return props.id;
269
278
  throw new Error('Invalid Item Type');
270
279
  };
280
+ const getItemUri = props => {
281
+ if (itemType === 'filter') return null;
282
+ if (itemType === 'id') return `homey:${itemId}:${props.id}`;
283
+ throw new Error('Invalid Item Type');
284
+ };
271
285
 
272
286
  switch (operation.crud.type) {
273
287
  case 'getOne': {
274
288
  const key = getItemKey(result);
289
+ const uri = getItemUri(result);
275
290
 
276
291
  result = new ItemClass({
277
292
  key,
293
+ uri,
278
294
  homey: this.homey,
279
295
  manager: this,
280
296
  properties: { ...result },
@@ -290,12 +306,14 @@ class Manager extends EventEmitter {
290
306
  // Add all to cache
291
307
  for (const [resultKey, item] of Object.entries(result)) {
292
308
  const key = getItemKey(item);
309
+ const uri = getItemUri(item);
293
310
 
294
311
  if (this.__cache[itemId][key]) {
295
312
  result[resultKey] = this.__cache[itemId][key].__update(item);
296
313
  } else {
297
314
  result[resultKey] = new ItemClass({
298
315
  key,
316
+ uri,
299
317
  homey: this.homey,
300
318
  manager: this,
301
319
  properties: { ...item },
@@ -326,11 +344,14 @@ class Manager extends EventEmitter {
326
344
  case 'createOne':
327
345
  case 'updateOne': {
328
346
  const key = getItemKey(result);
347
+ const uri = getItemUri(result);
348
+
329
349
  if (this.__cache[itemId][key]) {
330
350
  result = this.__cache[itemId][key].__update(result);
331
351
  } else {
332
352
  result = new ItemClass({
333
353
  key,
354
+ uri,
334
355
  manager: this,
335
356
  properties: result,
336
357
  });
@@ -343,6 +364,7 @@ class Manager extends EventEmitter {
343
364
  }
344
365
  case 'deleteOne': {
345
366
  const key = getItemKey(args);
367
+
346
368
  if (this.__cache[itemId][key]) {
347
369
  this.__cache[itemId][key].destroy();
348
370
  delete this.__cache[itemId][key];
@@ -416,11 +438,15 @@ class Manager extends EventEmitter {
416
438
  const key = itemType === 'filter'
417
439
  ? `${data.uri}:${data.id}`
418
440
  : `${data.id}`;
441
+ const uri = itemType === 'filter'
442
+ ? null
443
+ : `homey:${itemId}:${data.id}`;
419
444
 
420
445
  switch (operation) {
421
446
  case 'create': {
422
447
  this.__cache[itemId][key] = new ItemClass({
423
448
  key,
449
+ uri,
424
450
  manager: this,
425
451
  properties: { ...data },
426
452
  });
@@ -437,6 +463,7 @@ class Manager extends EventEmitter {
437
463
  } else {
438
464
  this.__cache[itemId][key] = new ItemClass({
439
465
  key,
466
+ uri,
440
467
  manager: this,
441
468
  properties: { ...data },
442
469
  });
@@ -7,7 +7,7 @@ const ManagerApps = require('./HomeyAPIV2/ManagerApps');
7
7
  const ManagerDevices = require('./HomeyAPIV2/ManagerDevices');
8
8
  const HomeyAPI = require('./HomeyAPI');
9
9
  const HomeyAPIError = require('./HomeyAPIError');
10
- const HomeyOfflineError = require('./HomeyOfflineError');
10
+ const APIErrorHomeyOffline = require('../APIErrorHomeyOffline');
11
11
  const Util = require('../Util');
12
12
 
13
13
  /**
@@ -211,40 +211,39 @@ class HomeyAPIV2 extends HomeyAPI {
211
211
 
212
212
  // Ping method
213
213
  const ping = async (strategyId, timeout) => {
214
+ let pingTimeout;
214
215
  const baseUrl = urls[strategyId];
215
- const res = await Promise.race([
216
+ return Promise.race([
216
217
  Util.fetch(`${baseUrl}/api/manager/system/ping?id=${this.id}`, {
217
218
  headers: {
218
219
  'X-Homey-ID': this.id,
219
220
  },
221
+ }).then(async res => {
222
+ const text = await res.text();
223
+ if (!res.ok) throw new Error(text || res.statusText);
224
+ if (text === 'false') throw new Error('Invalid Homey ID');
225
+
226
+ const homeyId = res.headers.get('X-Homey-ID');
227
+ if (homeyId) {
228
+ if (homeyId !== this.id) throw new Error('Invalid Homey ID'); // TODO: Add to Homey Connect
229
+ }
230
+
231
+ // Set the version that Homey told us.
232
+ // It's the absolute truth, because the Cloud API may be behind.
233
+ const homeyVersion = res.headers.get('X-Homey-Version');
234
+ if (homeyVersion !== this.version) {
235
+ this.version = homeyVersion;
236
+ }
237
+
238
+ return {
239
+ baseUrl,
240
+ strategyId,
241
+ };
220
242
  }),
221
243
  new Promise((_, reject) => {
222
- const pingTimeout = setTimeout(() => reject(new Error('PingTimeout')), timeout);
223
- promise
224
- .catch(() => { })
225
- .finally(() => clearTimeout(pingTimeout));
244
+ pingTimeout = setTimeout(() => reject(new Error('PingTimeout')), timeout);
226
245
  }),
227
- ]);
228
- const text = await res.text();
229
- if (!res.ok) throw new Error(text || res.statusText);
230
- if (text === 'false') throw new Error('Invalid Homey ID');
231
-
232
- const homeyId = res.headers.get('X-Homey-ID');
233
- if (homeyId) {
234
- if (homeyId !== this.id) throw new Error('Invalid Homey ID'); // TODO: Add to Homey Connect
235
- }
236
-
237
- // Set the version that Homey told us.
238
- // It's the absolute truth, because the Cloud API may be behind.
239
- const homeyVersion = res.headers.get('X-Homey-Version');
240
- if (homeyVersion !== this.version) {
241
- this.version = homeyVersion;
242
- }
243
-
244
- return {
245
- baseUrl,
246
- strategyId,
247
- };
246
+ ]).finally(() => clearTimeout(pingTimeout));
248
247
  };
249
248
 
250
249
  const pings = {};
@@ -293,22 +292,13 @@ class HomeyAPIV2 extends HomeyAPI {
293
292
  }
294
293
 
295
294
  if (!promises.length) {
296
- throw new HomeyOfflineError();
295
+ throw new APIErrorHomeyOffline();
297
296
  }
298
297
 
299
- return Promise.race(promises);
298
+ return Util.promiseAny(promises);
300
299
  })
301
300
  .then(result => resolve(result))
302
- .catch(err => {
303
- // Last resort: try cloud
304
- if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
305
- return pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD].catch(err => {
306
- throw new HomeyOfflineError(err);
307
- });
308
- }
309
-
310
- reject(new HomeyOfflineError(err));
311
- });
301
+ .catch(() => reject(new APIErrorHomeyOffline()));
312
302
  } else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]) {
313
303
  pings[HomeyAPI.DISCOVERY_STRATEGIES.LOCAL]
314
304
  .then(result => resolve(result))
@@ -316,7 +306,7 @@ class HomeyAPIV2 extends HomeyAPI {
316
306
  if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
317
307
  pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
318
308
  .then(result => resolve(result))
319
- .catch(err => reject(new HomeyOfflineError(err)));
309
+ .catch(err => reject(new APIErrorHomeyOffline(err)));
320
310
  }
321
311
  });
322
312
  } else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.MDNS]) {
@@ -326,15 +316,15 @@ class HomeyAPIV2 extends HomeyAPI {
326
316
  if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
327
317
  pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
328
318
  .then(result => resolve(result))
329
- .catch(err => reject(new HomeyOfflineError(err)));
319
+ .catch(err => reject(new APIErrorHomeyOffline(err)));
330
320
  }
331
321
  });
332
322
  } else if (pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]) {
333
323
  pings[HomeyAPI.DISCOVERY_STRATEGIES.CLOUD]
334
324
  .then(result => resolve(result))
335
- .catch(err => reject(new HomeyOfflineError(err)));
325
+ .catch(err => reject(new APIErrorHomeyOffline(err)));
336
326
  } else {
337
- reject(new HomeyOfflineError());
327
+ reject(new APIErrorHomeyOffline());
338
328
  }
339
329
 
340
330
  return promise;
package/lib/Util.js CHANGED
@@ -172,6 +172,29 @@ class Util {
172
172
  .toUpperCase();
173
173
  }
174
174
 
175
+ /**
176
+ * Polyfill for Promise.any, which is only supported on Node.js >=15
177
+ * @param {Array<Promise>} promises
178
+ */
179
+ static async promiseAny(promises) {
180
+ if (promises.length === 0) return;
181
+ const rejections = [];
182
+
183
+ return new Promise((resolve, reject) => {
184
+ promises.forEach((promise, i) => {
185
+ promise
186
+ .then(result => resolve(result))
187
+ .catch(err => {
188
+ rejections[i] = err;
189
+
190
+ if (rejections.length === promises.length) {
191
+ reject(rejections);
192
+ }
193
+ });
194
+ });
195
+ });
196
+ }
197
+
175
198
  }
176
199
 
177
200
  module.exports = Util;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homey-api",
3
- "version": "1.5.20",
3
+ "version": "1.5.23",
4
4
  "description": "Homey API",
5
5
  "main": "index.js",
6
6
  "types": "assets/types/homey-api.d.ts",