homey-api 3.0.10 → 3.0.12

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.
@@ -35,14 +35,26 @@ class Manager extends EventEmitter {
35
35
  });
36
36
 
37
37
  // Set Items
38
- Object.defineProperty(this, 'items', {
38
+ Object.defineProperty(this, 'itemClasses', {
39
39
  value: Object.entries(items).reduce((obj, [itemName, item]) => {
40
40
  const ItemClass = this.constructor.CRUD[itemName]
41
41
  ? this.constructor.CRUD[itemName]
42
- // eslint-disable-next-line no-eval
43
- : eval(`(class ${itemName} extends Item {})`);
42
+ : (() => {
43
+ return class extends Item {};
44
+ })();
45
+
44
46
  ItemClass.ID = item.id;
45
- obj[item.id] = ItemClass;
47
+ obj[itemName] = ItemClass;
48
+
49
+ return obj;
50
+ }, {}),
51
+ enumerable: false,
52
+ writable: false,
53
+ });
54
+
55
+ Object.defineProperty(this, 'itemNames', {
56
+ value: Object.entries(items).reduce((obj, [itemName, item]) => {
57
+ obj[item.id] = itemName;
46
58
 
47
59
  return obj;
48
60
  }, {}),
@@ -76,6 +88,12 @@ class Manager extends EventEmitter {
76
88
  writable: false,
77
89
  });
78
90
 
91
+ Object.defineProperty(this, '__pendingCalls', {
92
+ value: {},
93
+ enumerable: false,
94
+ writable: false,
95
+ });
96
+
79
97
  // Create methods
80
98
  for (const [operationId, operation] of Object.entries(operations)) {
81
99
  Object.defineProperty(this,
@@ -181,181 +199,219 @@ class Manager extends EventEmitter {
181
199
  path = `${path}?${queryString}`;
182
200
  }
183
201
 
184
- let result;
185
- const benchmark = Util.benchmark();
186
-
187
- // If connected to Socket.io,
188
- // try to get the CRUD Item from Cache.
189
- if (this.isConnected() && operation.crud && $cache === true) {
190
- const itemId = items[operation.crud.item].id;
191
-
192
- switch (operation.crud.type) {
193
- case 'getOne': {
194
- if (this.__cache[itemId][args.id]) {
195
- return this.__cache[itemId][args.id];
196
- }
202
+ if (
203
+ operation.method.toLowerCase() === 'get' &&
204
+ $cache === true &&
205
+ this.__pendingCalls[path] != null &&
206
+ Object.keys(body).length === 0
207
+ ) {
208
+ this.__debug(`Reusing pending call ${operationId}`);
209
+ const result = await this.__pendingCalls[path];
197
210
 
198
- break;
199
- }
200
- case 'getAll': {
201
- if (this.__cache[itemId]
202
- && this.__cacheAllComplete[itemId]) {
203
- return this.__cache[itemId];
204
- }
205
- break;
206
- }
207
- default:
208
- break;
209
- }
211
+ return result;
210
212
  }
211
213
 
212
- // If Homey is connected to Socket.io,
213
- // send the API request to socket.io.
214
- // This is about ~2x faster than HTTP
215
- if (this.homey.isConnected() && $socket === true) {
216
- result = await Util.timeout(new Promise((resolve, reject) => {
217
- this.__debug(`IO ${operationId}`);
218
- this.homey.__ioNamespace.emit('api', {
219
- args,
220
- operation: operationId,
221
- uri: this.uri,
222
- }, (err, result) => {
223
- // String Error
224
- if (typeof err === 'string') {
225
- err = new HomeyAPIError({
226
- error: err,
227
- }, 500);
228
- return reject(err);
229
- }
230
-
231
- // Object Error
232
- if (typeof err === 'object' && err !== null) {
233
- err = new HomeyAPIError({
234
- stack: err.stack,
235
- error: err.error,
236
- error_description: err.error_description,
237
- }, err.statusCode || err.code || 500);
238
- return reject(err);
239
- }
240
-
241
- return resolve(result);
242
- });
243
- }), $timeout);
244
- } else {
245
- // Get from HTTP
246
- result = await this.homey.call({
214
+ this.__pendingCalls[path] = (async () => {
215
+ const result = await this.__request({
216
+ $validate,
217
+ $cache,
247
218
  $timeout,
248
- headers,
219
+ $socket,
220
+ operationId,
221
+ operation,
222
+ path,
249
223
  body,
250
- path: `/api/manager/${this.constructor.ID}${path}`,
251
- method: operation.method,
224
+ query,
225
+ headers,
226
+ ...args
252
227
  });
253
- }
254
228
 
255
- // Transform and cache output if this is a CRUD call
256
- if (operation.crud) {
257
- const itemId = items[operation.crud.item].id;
258
- const Item = this.items[itemId];
259
-
260
- switch (operation.crud.type) {
261
- case 'getOne': {
262
- let item = { ...result };
263
- item = Item.transformGet(item);
264
- item = new Item({
265
- id: item.id,
266
- homey: this.homey,
267
- manager: this,
268
- properties: { ...item },
269
- });
229
+ return result;
230
+ })().finally(() => {
231
+ delete this.__pendingCalls[path];
232
+ });
270
233
 
271
- if (this.isConnected()) {
272
- this.__cache[itemId][item.id] = item;
273
- }
234
+ const result = await this.__pendingCalls[path];
274
235
 
275
- return item;
276
- }
277
- case 'getAll': {
278
- const items = {};
279
-
280
- // Add all to cache
281
- for (let item of Object.values(result)) {
282
- item = Item.transformGet(item);
283
-
284
- if (this.isConnected() && this.__cache[itemId][item.id]) {
285
- items[item.id] = this.__cache[itemId][item.id];
286
- items[item.id].__update(item);
287
- } else {
288
- items[item.id] = new Item({
289
- id: item.id,
290
- homey: this.homey,
291
- manager: this,
292
- properties: { ...item },
293
- });
294
-
295
- if (this.isConnected()) {
296
- this.__cache[itemId][item.id] = items[item.id];
297
- }
298
- }
299
- }
300
-
301
- // Find and delete deleted items from cache
302
- if (this.__cache[itemId]) {
303
- for (const cachedItem of Object.values(this.__cache[itemId])) {
304
- if (!items[cachedItem.id]) {
305
- delete this.__cache[itemId][cachedItem.id];
306
- }
307
- }
308
- }
309
-
310
- // Mark cache as complete
311
- if (this.isConnected()) {
312
- this.__cacheAllComplete[itemId] = true;
313
- }
236
+ return result;
237
+ },
238
+ });
239
+ }
240
+ }
314
241
 
315
- return items;
316
- }
317
- case 'createOne':
318
- case 'updateOne': {
319
- let item = { ...result };
320
- item = Item.transformGet(item);
321
-
322
- if (this.isConnected() && this.__cache[itemId][item.id]) {
323
- item = this.__cache[itemId][item.id];
324
- item.__update(item);
325
- } else {
326
- item = Item.transformGet(item);
327
- item = new Item({
328
- id: item.id,
329
- homey: this.homey,
330
- manager: this,
331
- properties: { ...item },
332
- });
242
+ async __request({ $cache, $timeout, $socket, operationId, operation, path, body, headers, ...args }) {
243
+ let result;
244
+ const benchmark = Util.benchmark();
245
+
246
+ // If connected to Socket.io,
247
+ // try to get the CRUD Item from Cache.
248
+ if (this.isConnected() && operation.crud && $cache === true) {
249
+ const itemId = this.itemClasses[operation.crud.item].ID;
250
+
251
+ switch (operation.crud.type) {
252
+ case 'getOne': {
253
+ if (this.__cache[itemId][args.id]) {
254
+ return this.__cache[itemId][args.id];
255
+ }
256
+
257
+ break;
258
+ }
259
+ case 'getAll': {
260
+ if (this.__cache[itemId] && this.__cacheAllComplete[itemId]) {
261
+ return this.__cache[itemId];
262
+ }
263
+ break;
264
+ }
265
+ default:
266
+ break;
267
+ }
268
+ }
333
269
 
334
- if (this.isConnected()) {
335
- this.__cache[itemId][item.id] = item;
336
- }
337
- }
270
+ // If Homey is connected to Socket.io,
271
+ // send the API request to socket.io.
272
+ // This is about ~2x faster than HTTP
273
+ if (this.homey.isConnected() && $socket === true) {
274
+ result = await Util.timeout(new Promise((resolve, reject) => {
275
+ this.__debug(`IO ${operationId}`);
276
+ this.homey.__ioNamespace.emit('api', {
277
+ args,
278
+ operation: operationId,
279
+ uri: this.uri,
280
+ }, (err, result) => {
281
+ // String Error
282
+ if (typeof err === 'string') {
283
+ err = new HomeyAPIError({
284
+ error: err,
285
+ }, 500);
286
+ return reject(err);
287
+ }
288
+
289
+ // Object Error
290
+ if (typeof err === 'object' && err !== null) {
291
+ err = new HomeyAPIError({
292
+ stack: err.stack,
293
+ error: err.error,
294
+ error_description: err.error_description,
295
+ }, err.statusCode || err.code || 500);
296
+ return reject(err);
297
+ }
298
+
299
+ return resolve(result);
300
+ });
301
+ }), $timeout);
302
+ } else {
303
+ // Get from HTTP
304
+ result = await this.homey.call({
305
+ $timeout,
306
+ headers,
307
+ body,
308
+ path: `/api/manager/${this.constructor.ID}${path}`,
309
+ method: operation.method,
310
+ });
311
+ }
338
312
 
339
- return item;
340
- }
341
- case 'deleteOne': {
342
- if (this.isConnected() && this.__cache[itemId][args.id]) {
343
- this.__cache[itemId][args.id].destroy();
344
- delete this.__cache[itemId][args.id];
345
- }
313
+ // Transform and cache output if this is a CRUD call
314
+ if (operation.crud) {
315
+ const ItemClass = this.itemClasses[operation.crud.item];
316
+
317
+ switch (operation.crud.type) {
318
+ case 'getOne': {
319
+ let props = { ...result };
320
+ props = ItemClass.transformGet(props);
321
+
322
+ const item = new ItemClass({
323
+ id: props.id,
324
+ homey: this.homey,
325
+ manager: this,
326
+ properties: props,
327
+ });
328
+
329
+ if (this.isConnected()) {
330
+ this.__cache[ItemClass.ID][item.id] = item;
331
+ }
332
+
333
+ return item;
334
+ }
335
+ case 'getAll': {
336
+ const items = {};
337
+
338
+ // Add all to cache
339
+ for (let props of Object.values(result)) {
340
+ props = ItemClass.transformGet(props);
341
+
342
+ if (this.isConnected() && this.__cache[ItemClass.ID][props.id]) {
343
+ items[props.id] = this.__cache[ItemClass.ID][props.id];
344
+ items[props.id].__update(props);
345
+ } else {
346
+ items[props.id] = new ItemClass({
347
+ id: props.id,
348
+ homey: this.homey,
349
+ manager: this,
350
+ properties: props,
351
+ });
346
352
 
347
- return undefined;
348
- }
349
- default:
350
- break;
353
+ if (this.isConnected()) {
354
+ this.__cache[ItemClass.ID][props.id] = items[props.id];
351
355
  }
352
356
  }
357
+ }
353
358
 
354
- this.__debug(`${operationId} took ${benchmark()}ms`);
355
- return result;
356
- },
357
- });
359
+ // Find and delete deleted items from cache
360
+ if (this.__cache[ItemClass.ID]) {
361
+ for (const cachedItem of Object.values(this.__cache[ItemClass.ID])) {
362
+ if (!items[cachedItem.id]) {
363
+ delete this.__cache[ItemClass.ID][cachedItem.id];
364
+ }
365
+ }
366
+ }
367
+
368
+ // Mark cache as complete
369
+ if (this.isConnected()) {
370
+ this.__cacheAllComplete[ItemClass.ID] = true;
371
+ }
372
+
373
+ return items;
374
+ }
375
+ case 'createOne':
376
+ case 'updateOne': {
377
+ let item = null;
378
+ let props = { ...result };
379
+
380
+ props = ItemClass.transformGet(props);
381
+
382
+ if (this.isConnected() && this.__cache[ItemClass.ID][props.id]) {
383
+ item = this.__cache[ItemClass.ID][props.id];
384
+ item.__update(props);
385
+ } else {
386
+ item = new ItemClass({
387
+ id: props.id,
388
+ homey: this.homey,
389
+ manager: this,
390
+ properties: { ...props },
391
+ });
392
+
393
+ if (this.isConnected()) {
394
+ this.__cache[ItemClass.ID][props.id] = item;
395
+ }
396
+ }
397
+
398
+ return item;
399
+ }
400
+ case 'deleteOne': {
401
+ if (this.isConnected() && this.__cache[ItemClass.ID][args.id]) {
402
+ this.__cache[ItemClass.ID][args.id].destroy();
403
+ delete this.__cache[ItemClass.ID][args.id];
404
+ }
405
+
406
+ return undefined;
407
+ }
408
+ default:
409
+ break;
410
+ }
358
411
  }
412
+
413
+ this.__debug(`${operationId} took ${benchmark()}ms`);
414
+ return result;
359
415
  }
360
416
 
361
417
  /**
@@ -426,40 +482,41 @@ class Manager extends EventEmitter {
426
482
  || event.endsWith('.update')
427
483
  || event.endsWith('.delete')) {
428
484
  const [itemId, operation] = event.split('.');
429
- const Item = this.items[itemId];
485
+ const itemName = this.itemNames[itemId];
486
+ const ItemClass = this.itemClasses[itemName];
430
487
 
431
488
  switch (operation) {
432
489
  case 'create': {
433
- data = Item.transformGet(data);
490
+ const props = ItemClass.transformGet(data);
434
491
 
435
- const item = new Item({
436
- id: data.id,
492
+ const item = new ItemClass({
493
+ id: props.id,
437
494
  homey: this.homey,
438
495
  manager: this,
439
- properties: { ...data },
496
+ properties: props,
440
497
  });
441
- this.__cache[itemId][data.id] = item;
498
+ this.__cache[ItemClass.ID][props.id] = item;
442
499
 
443
500
  return this.emit(event, item);
444
501
  }
445
502
  case 'update': {
446
- data = Item.transformGet(data);
503
+ const props = ItemClass.transformGet(data);
447
504
 
448
- if (this.__cache[itemId][data.id]) {
449
- const item = this.__cache[itemId][data.id];
450
- item.__update(data);
505
+ if (this.__cache[ItemClass.ID][props.id]) {
506
+ const item = this.__cache[ItemClass.ID][props.id];
507
+ item.__update(props);
451
508
  return this.emit(event, item);
452
509
  }
453
510
 
454
511
  break;
455
512
  }
456
513
  case 'delete': {
457
- data = Item.transformGet(data);
514
+ const props = ItemClass.transformGet(data);
458
515
 
459
- if (this.__cache[itemId][data.id]) {
460
- const item = this.__cache[itemId][data.id];
516
+ if (this.__cache[ItemClass.ID][props.id]) {
517
+ const item = this.__cache[ItemClass.ID][props.id];
461
518
  item.__delete();
462
- delete this.__cache[itemId][item.id];
519
+ delete this.__cache[ItemClass.ID][item.id];
463
520
  return this.emit(event, {
464
521
  id: item.id,
465
522
  });
@@ -10,6 +10,11 @@ const Item = require('../Item');
10
10
  */
11
11
  class Capability extends Item {
12
12
 
13
+ get uri() {
14
+ console.warn('Capability.uri is deprecated. Please use Capability.ownerUri instead.');
15
+ return undefined;
16
+ }
17
+
13
18
  }
14
19
 
15
20
  module.exports = Capability;
@@ -40,6 +40,8 @@ class Device extends Item {
40
40
  * onOffInstance.setValue(true).catch(console.error);
41
41
  */
42
42
  makeCapabilityInstance(capabilityId, listener) {
43
+ this.__debug('Creating capability instance for: ', capabilityId);
44
+
43
45
  this.connect().catch(err => {
44
46
  this.__debug(err);
45
47
  });
@@ -121,18 +123,20 @@ class Device extends Item {
121
123
  this.manager.getDevice({
122
124
  id: this.id,
123
125
  }).then(async device => {
124
- Object.entries(this.__capabilityInstances).forEach(([capabilityId, capabilityInstance]) => {
126
+ Object.entries(this.__capabilityInstances).forEach(([capabilityId, capabilityInstances]) => {
125
127
  const value = device.capabilitiesObj
126
128
  ? typeof device.capabilitiesObj[capabilityId] !== 'undefined'
127
129
  ? device.capabilitiesObj[capabilityId].value
128
130
  : null
129
131
  : null;
130
132
 
131
- capabilityInstance.__onCapabilityValue({
132
- capabilityId,
133
- value,
134
- transactionId: Util.uuid(),
135
- });
133
+ for (const capabilityInstance of capabilityInstances) {
134
+ capabilityInstance.__onCapabilityValue({
135
+ capabilityId,
136
+ value,
137
+ transactionId: Util.uuid(),
138
+ });
139
+ }
136
140
  });
137
141
  })
138
142
  // eslint-disable-next-line no-console
@@ -169,10 +173,10 @@ class Device extends Item {
169
173
 
170
174
  return Object.values(logs)
171
175
  .filter(log => log.ownerUri === this.uri)
172
- .reduce((result, log) => ({
173
- ...result,
174
- [log.id]: log,
175
- }), {});
176
+ .reduce((accumulator, log) => {
177
+ accumulator[log.id] = log;
178
+ return accumulator;
179
+ }, {});
176
180
  }
177
181
 
178
182
  /**
@@ -237,6 +241,16 @@ class Device extends Item {
237
241
  return item;
238
242
  }
239
243
 
244
+ get driverUri() {
245
+ console.warn('Device.driverUri is deprecated. Please use Device.driverId instead.');
246
+ return undefined;
247
+ }
248
+
249
+ get zoneName() {
250
+ console.warn('Device.zoneName is deprecated.');
251
+ return undefined;
252
+ }
253
+
240
254
  }
241
255
 
242
256
  module.exports = Device;
@@ -112,7 +112,7 @@ class DeviceCapability extends EventEmitter {
112
112
  capabilityReference.lastUpdated = this.__lastChanged;
113
113
  }
114
114
 
115
- this.__listener(value);
115
+ this.__listener(value, this);
116
116
  }
117
117
 
118
118
  __onDeviceDelete() {
@@ -160,7 +160,7 @@ class DeviceCapability extends EventEmitter {
160
160
  });
161
161
 
162
162
  this.__value = value;
163
- this.__lastChanged = new Date();
163
+ this.__lastChanged = transactionTime;
164
164
 
165
165
  // Mutate the current device capabilitiesObj so it always reflects the last value.
166
166
  const capabilityReference = this.device.capabilitiesObj && this.device.capabilitiesObj[this.id];
@@ -9,6 +9,16 @@ const Item = require('../Item');
9
9
  */
10
10
  class Driver extends Item {
11
11
 
12
+ get uri() {
13
+ console.warn('Driver.uri is deprecated. Please use Driver.ownerUri instead.');
14
+ return undefined;
15
+ }
16
+
17
+ get uriObj() {
18
+ console.warn('Driver.uriObj is deprecated.');
19
+ return undefined;
20
+ }
21
+
12
22
  }
13
23
 
14
24
  module.exports = Driver;
@@ -9,6 +9,14 @@ const Item = require('../Item');
9
9
  */
10
10
  class PairSession extends Item {
11
11
 
12
+ static transformGet(item) {
13
+ item = super.transformGet(item);
14
+
15
+ delete item.uri;
16
+
17
+ return item;
18
+ }
19
+
12
20
  }
13
21
 
14
22
  module.exports = PairSession;
@@ -10,6 +10,16 @@ const FlowCard = require('./FlowCard');
10
10
  */
11
11
  class FlowCardAction extends FlowCard {
12
12
 
13
+ get uri() {
14
+ console.warn('FlowCardAction.uri is deprecated. Use FlowCardAction.ownerUri instead.');
15
+ return undefined;
16
+ }
17
+
18
+ get uriObj() {
19
+ console.warn('FlowCardAction.uriObj is deprecated.');
20
+ return undefined;
21
+ }
22
+
13
23
  }
14
24
 
15
25
  module.exports = FlowCardAction;
@@ -10,6 +10,16 @@ const FlowCard = require('./FlowCard');
10
10
  */
11
11
  class FlowCardCondition extends FlowCard {
12
12
 
13
+ get uri() {
14
+ console.warn('FlowCardCondition.uri is deprecated. Use FlowCardCondition.ownerUri instead.');
15
+ return undefined;
16
+ }
17
+
18
+ get uriObj() {
19
+ console.warn('FlowCardCondition.uriObj is deprecated.');
20
+ return undefined;
21
+ }
22
+
13
23
  }
14
24
 
15
25
  module.exports = FlowCardCondition;
@@ -10,6 +10,16 @@ const FlowCard = require('./FlowCard');
10
10
  */
11
11
  class FlowCardTrigger extends FlowCard {
12
12
 
13
+ get uri() {
14
+ console.warn('FlowCardTrigger.uri is deprecated. Use FlowCardTrigger.ownerUri instead.');
15
+ return undefined;
16
+ }
17
+
18
+ get uriObj() {
19
+ console.warn('FlowCardTrigger.uriObj is deprecated.');
20
+ return undefined;
21
+ }
22
+
13
23
  }
14
24
 
15
25
  module.exports = FlowCardTrigger;
@@ -25,6 +25,21 @@ class FlowToken extends Item {
25
25
  return item;
26
26
  }
27
27
 
28
+ get uri() {
29
+ console.warn('FlowToken.uri is deprecated. Use FlowToken.ownerUri instead.');
30
+ return undefined;
31
+ }
32
+
33
+ get uriObj() {
34
+ console.warn('FlowToken.uriObj is deprecated.');
35
+ return undefined;
36
+ }
37
+
38
+ get ownerName() {
39
+ console.warn('FlowToken.ownerName is deprecated.');
40
+ return undefined;
41
+ }
42
+
28
43
  }
29
44
 
30
45
  module.exports = FlowToken;