@webex/plugin-device-manager 2.59.3-next.1 → 2.59.4-next.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.
@@ -1,643 +1,643 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
- import {cloneDeep} from 'lodash';
5
- import '@webex/internal-plugin-lyra';
6
- import '@webex/internal-plugin-search';
7
- import {WebexPlugin} from '@webex/webex-core';
8
- import uuid from 'uuid';
9
-
10
- import {LYRA_SPACE, UC_CLOUD, DEFAULT_PRODUCT_NAME} from './constants';
11
- import DeviceCollection from './collection';
12
-
13
- const DeviceManager = WebexPlugin.extend({
14
- namespace: 'DeviceManager',
15
- _devicePendingPinChallenge: null,
16
- _pairedDevice: null,
17
- _boundSpace: null,
18
-
19
- initialize() {
20
- // Lyra mercury events listener
21
- this.webex.internal.mercury.on('event:lyra.space_updated', ({data}) => {
22
- this._receiveDeviceUpdates(data);
23
- });
24
- },
25
-
26
- /**
27
- * Gets a list of all recent devices associated with the user
28
- * the device list gets populated from Redis
29
- * @returns {Promise<Device>}
30
- */
31
- getAll() {
32
- return DeviceCollection.getAll();
33
- },
34
-
35
- /**
36
- * Gets a list of all recent devices associated with the user
37
- * the device list gets populated from Redis
38
- * @returns {Promise<Device>}
39
- */
40
- refresh() {
41
- DeviceCollection.reset();
42
-
43
- return this.webex
44
- .request({
45
- api: 'wdm',
46
- resource: 'devices/auxiliary',
47
- })
48
- .then((res) => {
49
- if (!res.body) {
50
- return Promise.reject();
51
- }
52
-
53
- return this._updateDeviceMetadata(res.body.items).then((devices) => {
54
- /* eslint-disable consistent-return */
55
- devices.forEach((device) => {
56
- if (device.deviceInfo && device.deviceInfo.machineType === LYRA_SPACE) {
57
- return this.webex.internal.lyra.space.get(device.deviceInfo).catch((err) => {
58
- this.logger.error('DeviceManager#refresh: failed to receive device info', err);
59
- });
60
- }
61
- });
62
- /* eslint-enable consistent-return */
63
- res.body.items.forEach((device) => {
64
- DeviceCollection.set(device);
65
- });
66
-
67
- return this.getAll();
68
- });
69
- })
70
- .catch((err) => {
71
- this.logger.error('DeviceManager#refresh: failed to fetch recent devices', err);
72
- });
73
- },
74
-
75
- /**
76
- * Search for a device by name
77
- * @param {Object} options
78
- * @param {string} options.searchQuery
79
- * @returns {Promise<Device>}
80
- */
81
- search(options) {
82
- if (!options || !options.searchQuery) {
83
- this.logger.error('DeviceManager#search: options.searchQuery is required');
84
-
85
- return Promise.reject(new Error('DeviceManager#search: options.searchQuery is required'));
86
- }
87
-
88
- return this.webex.internal.search
89
- .people({
90
- searchId: uuid.v4(),
91
- searchType: 'DEVICE_SEARCH',
92
- searchEntity: 'device',
93
- includePeople: false,
94
- includeRooms: true,
95
- queryString: options.searchQuery,
96
- })
97
- .catch((err) => {
98
- this.logger.error('DeviceManager#search: failed to search a device', err);
99
-
100
- throw err;
101
- });
102
- },
103
-
104
- /**
105
- * Caches the device info and also registers to Redis for subsequent fetches
106
- * @param {Object} device
107
- * @param {string} device.id
108
- * @returns {deviceInfo}
109
- */
110
- upsert(device) {
111
- const deviceId = device.id || (device.identity && device.identity.id);
112
-
113
- if (!deviceId) {
114
- this.logger.error('DeviceManager#upsert: device.id is required');
115
-
116
- return Promise.reject(new Error('DeviceManager#upsert: device.id is required'));
117
- }
118
- this._pairedDevice = this._devicePendingPinChallenge;
119
- this._devicePendingPinChallenge = null;
120
- // check if the device is already existing, if so then merge else add
121
- const existingDevice = DeviceCollection.get(deviceId);
122
-
123
- if (existingDevice) {
124
- DeviceCollection.set(device);
125
-
126
- return Promise.resolve(DeviceCollection.get(deviceId));
127
- }
128
-
129
- // new device requested, add to wdm for subsequent retreivals
130
- return (
131
- this.webex
132
- .request({
133
- api: 'wdm',
134
- method: 'PUT',
135
- resource: `devices/auxiliary/Room/${deviceId}`,
136
- })
137
- .then((res) => {
138
- const auxDevice = res.body;
139
-
140
- return this._decryptDeviceName(auxDevice);
141
- })
142
- // eslint-disable-next-line no-shadow
143
- .then((device) => {
144
- DeviceCollection.set(device);
145
-
146
- return Promise.resolve(DeviceCollection.get(deviceId));
147
- })
148
- .catch((err) => {
149
- this.logger.error('DeviceManager#upsert: failed to add/update a device', err);
150
-
151
- return Promise.reject(err);
152
- })
153
- );
154
- },
155
-
156
- /**
157
- * Retreives device info of a particular device
158
- * @param {string} token
159
- * @returns {Promise<deviceInfo>}
160
- */
161
- get(token) {
162
- if (!token) {
163
- this.logger.error('DeviceManager#get: token is required');
164
-
165
- return Promise.reject(new Error('DeviceManager#get: token is required'));
166
- }
167
- let deviceInfo;
168
-
169
- return this.webex.internal.lyra
170
- .getAdvertisedEndpoint(token)
171
- .then((res) => {
172
- deviceInfo = res;
173
-
174
- return this.webex.internal.lyra.space.get({id: res.advertiser.id});
175
- })
176
- .then((result) => {
177
- // the deviceInfo doesn't have proper displayName, hence update
178
- // displayName on deviceInfo for search to work
179
- if (result && result.identity && result.identity.displayName) {
180
- deviceInfo.advertiser.displayName = result.identity.displayName;
181
- }
182
-
183
- return deviceInfo;
184
- })
185
- .catch((err) => {
186
- this.logger.error('DeviceManager#get: failed to get device info', err);
187
-
188
- return Promise.reject(err);
189
- });
190
- },
191
-
192
- /**
193
- * Unregisters the device from Redis, will not fetch in subsequent loads,
194
- * similar to space.deleteBinding()
195
- * @param {string} deviceId
196
- * @returns {Promise<deviceInfo>}
197
- */
198
- remove(deviceId) {
199
- if (!deviceId) {
200
- this.logger.error('DeviceManager#remove: deviceId is required');
201
-
202
- return Promise.reject(new Error('DeviceManager#remove: deviceId is required'));
203
- }
204
-
205
- return this.webex
206
- .request({
207
- method: 'delete',
208
- api: 'wdm',
209
- resource: `devices/auxiliary/${deviceId}`,
210
- })
211
- .catch((error) => {
212
- this.logger.error('DeviceManager#remove: failed to remove the device', error);
213
-
214
- return Promise.reject(error);
215
- });
216
- },
217
-
218
- /**
219
- * Requests to display PIN on the device
220
- * @param {object} device
221
- * @param {object} options
222
- * @param {object} options.data
223
- * @returns {Promise<deviceInfo>}
224
- */
225
- requestPin(device, options = {}) {
226
- const deviceId = device.id || (device.identity && device.identity.id);
227
-
228
- if (!deviceId) {
229
- this.logger.error('DeviceManager#requestPin: device.id is required');
230
-
231
- return Promise.reject(new Error('DeviceManager#requestPin: device.id is required'));
232
- }
233
- const space = {id: deviceId, url: `/spaces/${deviceId}`};
234
-
235
- return this.webex.internal.lyra.space
236
- .get(space)
237
- .then((dev) => {
238
- // check if the space is pinChallenge capable
239
- if (dev && dev.occupants && dev.occupants.pinChallenge) {
240
- this.logger.info('DeviceManager#requestPin: space is PIN challenge capable');
241
-
242
- return this.webex.internal.lyra.space
243
- .join(space, {
244
- passType: 'MANUAL',
245
- verificationInitiation: 'PIN',
246
- data: options.data,
247
- })
248
- .then(() => {
249
- this._devicePendingPinChallenge = dev;
250
-
251
- // return the actual device so that it can be upserted on successful
252
- // PIN exchange
253
- return dev;
254
- });
255
- }
256
- // pairs with the space if it's not PIN challenge capable
257
- this.logger.info(
258
- 'DeviceManager#requestPin: space is not PIN challenge capable, probably already occupied, will still return device info'
259
- );
260
-
261
- return this.webex.internal.lyra.space.get(space).then(() => Promise.resolve(dev));
262
- })
263
- .catch((err) => {
264
- this.logger.error('DeviceManager#requestPin: device failed PIN challenge request', err);
265
-
266
- return Promise.reject(err);
267
- });
268
- },
269
-
270
- /**
271
- * pairs the device with the user (manual pairing), also adds it to
272
- * user's recents list for subsequent fetches.
273
- * similar to space.join()
274
- * @param {object} options
275
- * @param {number} options.pin
276
- * @returns {Promise<deviceInfo>}
277
- */
278
- pair(options = {}) {
279
- if (!options.pin) {
280
- this.logger.error('DeviceManager#pair: options.pin is required');
281
-
282
- return Promise.reject(new Error('DeviceManager#pair: options.pin is required'));
283
- }
284
- if (this._devicePendingPinChallenge) {
285
- const space = {
286
- id: this._devicePendingPinChallenge.identity.id,
287
- url: `/spaces/${this._devicePendingPinChallenge.identity.id}`,
288
- };
289
-
290
- return this.webex.internal.lyra.space
291
- .join(space, {
292
- passType: 'PIN_ANSWER',
293
- data: options.pin,
294
- })
295
- .catch((err) => {
296
- this.logger.error('DeviceManager#pair: incorrect PIN, unable to pair ', err);
297
-
298
- return Promise.reject(err);
299
- })
300
- .then(() => this.upsert(this._devicePendingPinChallenge));
301
- }
302
- this.logger.error('DeviceManager#pair: no device to pair');
303
-
304
- return Promise.reject(new Error('DeviceManager#pair: no device to pair'));
305
- },
306
-
307
- /**
308
- * unpairs the device with the user (manual/ultrasonic pairing), but still
309
- * keeps in the recents list/does not remove from Redis
310
- * options.removeAllDevices will remove all associated devices to user
311
- * similar to space.leave()
312
- * @param {object} options
313
- * @param {boolean} options.removeAllDevices
314
- * @returns {Promise<deviceInfo>}
315
- */
316
- unpair(options = {}) {
317
- if (!this._pairedDevice) {
318
- this.logger.error('DeviceManager#unpair: no device to unpair');
319
-
320
- return Promise.reject(new Error('DeviceManager#unpair: no device to unpair'));
321
- }
322
- const space = {url: `/spaces/${this._pairedDevice.identity.id}`};
323
-
324
- return this.webex.internal.lyra.space.leave(space, options).catch((err) => {
325
- this.logger.error('DeviceManager#unpair: failed to remove device from Lyra', err);
326
-
327
- return Promise.reject(err);
328
- });
329
- },
330
-
331
- /**
332
- * binds the space to the paired device (if supported)
333
- * similar to space.bindConversation()
334
- * @param {object} options
335
- * @param {boolean} options.url, conversation url
336
- * @param {boolean} options.kmsResourceObjectUrl of the convo
337
- * @returns {Promise<deviceInfo>}
338
- */
339
- bindSpace(options = {}) {
340
- if (!options.url) {
341
- this.logger.error('DeviceManager#pair: options.url is required');
342
-
343
- return Promise.reject(new Error('DeviceManager#bindSpace: options.url is required'));
344
- }
345
- if (!options.kmsResourceObjectUrl) {
346
- this.logger.error('DeviceManager#bindSpace: options.kmsResourceObjectUrl is required');
347
-
348
- return Promise.reject(
349
- new Error('DeviceManager#bindSpace: options.kmsResourceObjectUrl is required')
350
- );
351
- }
352
- if (!this._pairedDevice) {
353
- this.logger.error('DeviceManager#bindSpace: No device paired currently');
354
-
355
- return Promise.reject(new Error('DeviceManager#bindSpace: No device paired currently'));
356
- }
357
- const space = {
358
- url: `/spaces/${this._pairedDevice.identity.id}`,
359
- id: this._pairedDevice.identity.id,
360
- };
361
-
362
- this._boundSpace = {
363
- kmsResourceObjectUrl: options.kmsResourceObjectUrl,
364
- url: options.url,
365
- };
366
-
367
- return this.webex.internal.lyra.space.bindConversation(space, this._boundSpace).catch((err) => {
368
- this.logger.error('DeviceManager#bindSpace: failed to bind device to Space');
369
-
370
- return Promise.reject(err);
371
- });
372
- },
373
-
374
- /**
375
- * unbinds the space to the paired device (if supported)
376
- * similar to space.unbindConversation()
377
- * @returns {Promise<deviceInfo>}
378
- */
379
- unbindSpace() {
380
- if (!this._pairedDevice || !this._boundSpace) {
381
- this.logger.error('DeviceManager#unbindSpace: No space currently bound to the device');
382
-
383
- return Promise.reject(
384
- new Error('DeviceManager#unbindSpace: No space currently bound to the device')
385
- );
386
- }
387
- const space = {
388
- url: `/spaces/${this._pairedDevice.identity.id}`,
389
- id: this._pairedDevice.identity.id,
390
- };
391
-
392
- return this.webex.internal.lyra.space
393
- .unbindConversation(space, this._boundSpace)
394
- .then((res) => {
395
- this._boundSpace = null;
396
-
397
- return Promise.resolve(res);
398
- })
399
- .catch((err) => {
400
- this.logger.error('DeviceManager#unbindSpace: failed to unbind Space to device');
401
-
402
- return Promise.reject(err);
403
- });
404
- },
405
-
406
- /**
407
- * Gets the audio state of the paired device
408
- * similar to device.getAudioState()
409
- * @returns {Promise<audioState>}
410
- */
411
- getAudioState() {
412
- if (!this._pairedDevice) {
413
- this.logger.error('DeviceManager#getAudioState: Currently no device is paired');
414
-
415
- return Promise.reject(
416
- new Error('DeviceManager#getAudioState: Currently no device is paired')
417
- );
418
- }
419
-
420
- return this.webex.internal.lyra.device.getAudioState(this._pairedDevice);
421
- },
422
-
423
- /**
424
- * Updates audio state of the paired device, should be called every 10 minutes
425
- * or when mic or volume state is changed
426
- * similar to device.putAudioState()
427
- * @param {object} space
428
- * @param {object} audioState
429
- * @returns {Promise<audioState>}
430
- */
431
- putAudioState(space, audioState = {}) {
432
- return this.webex.internal.lyra.device.putAudioState(space, audioState);
433
- },
434
-
435
- /**
436
- * Mutes paired device
437
- * similar to device.mute()
438
- * @returns {Promise<audioState>}
439
- */
440
- mute() {
441
- if (!this._pairedDevice) {
442
- this.logger.error('DeviceManager#mute: Currently no device is paired');
443
-
444
- return Promise.reject(new Error('DeviceManager#mute: Currently no device is paired'));
445
- }
446
-
447
- return this.webex.internal.lyra.device.mute(this._pairedDevice);
448
- },
449
-
450
- /**
451
- * Unmutes paired device
452
- * similar to device.unmute()
453
- * @returns {Promise<audioState>}
454
- */
455
- unmute() {
456
- if (!this._pairedDevice) {
457
- this.logger.error('DeviceManager#unmute: Currently no device is paired');
458
-
459
- return Promise.reject(new Error('DeviceManager#unmute: Currently no device is paired'));
460
- }
461
-
462
- return this.webex.internal.lyra.device.unmute(this._pairedDevice);
463
- },
464
-
465
- /**
466
- * Increases paired device's volume
467
- * similar to device.increaseVolume()
468
- * @returns {Promise<audioState>}
469
- */
470
- increaseVolume() {
471
- if (!this._pairedDevice) {
472
- this.logger.error('DeviceManager#increaseVolume: Currently no device is paired');
473
-
474
- return Promise.reject(
475
- new Error('DeviceManager#increaseVolume: Currently no device is paired')
476
- );
477
- }
478
-
479
- return this.webex.internal.lyra.device.increaseVolume(this._pairedDevice);
480
- },
481
-
482
- /**
483
- * Decreases paired device's volume
484
- * similar to device.decreaseVolume()
485
- * @returns {Promise<audioState>}
486
- */
487
- decreaseVolume() {
488
- if (!this._pairedDevice) {
489
- this.logger.error('DeviceManager#decreaseVolume: Currently no device is paired');
490
-
491
- return Promise.reject(
492
- new Error('DeviceManager#decreaseVolume: Currently no device is paired')
493
- );
494
- }
495
-
496
- return this.webex.internal.lyra.device.decreaseVolume(this._pairedDevice);
497
- },
498
-
499
- /**
500
- * Sets paired device's volume but should use increase and decrease api instead
501
- * similar to device.setVolume()
502
- * @param {number} level
503
- * @returns {Promise<audioState>}
504
- */
505
- setVolume(level = 0) {
506
- if (!this._pairedDevice) {
507
- this.logger.error('DeviceManager#setVolume: Currently no device is paired');
508
-
509
- return Promise.reject(new Error('DeviceManager#setVolume: Currently no device is paired'));
510
- }
511
-
512
- return this.webex.internal.lyra.device.setVolume(this._pairedDevice, level);
513
- },
514
-
515
- /**
516
- * Utility function to update decrypted device name on device object
517
- * @param {Array} deviceArray
518
- * @returns {device}
519
- */
520
- _updateDeviceMetadata(deviceArray = []) {
521
- if (!deviceArray.length) {
522
- return Promise.resolve(deviceArray);
523
- }
524
- const devices = cloneDeep(deviceArray);
525
-
526
- return Promise.all(
527
- devices.map((device, index) =>
528
- this.webex.internal.services
529
- .waitForCatalog('postauth')
530
- .then(() => {
531
- if (device.deviceClass === UC_CLOUD) {
532
- device.id = `${this.webex.internal.services.get('wdm')}/${device.id}`;
533
- }
534
-
535
- return this._decryptDeviceName(device);
536
- })
537
- .then((updatedDevice) => {
538
- devices[index] = updatedDevice;
539
-
540
- return Promise.resolve();
541
- })
542
- )
543
- ).then(() => Promise.resolve(devices));
544
- },
545
-
546
- /**
547
- * Utility function to update decrypted device name on device object
548
- * @param {object} inDevice
549
- * @returns {device}
550
- */
551
- _decryptDeviceName(inDevice = {}) {
552
- const device = cloneDeep(inDevice);
553
-
554
- if (
555
- device.metadata &&
556
- device.metadata.encryptedUserAssignedName &&
557
- device.metadata.encryptionKeyUrl
558
- ) {
559
- return this.webex.internal.encryption
560
- .decryptText(device.metadata.encryptionKeyUrl, device.metadata.encryptedUserAssignedName)
561
- .then((decryptedDeviceName) => {
562
- // set userAssignedName as the decypted value, unset encryptedUserAssignedName since it's not needed
563
- device.metadata.encryptedUserAssignedName = undefined;
564
- device.metadata.userAssignedName = decryptedDeviceName;
565
-
566
- return Promise.resolve(device);
567
- })
568
- .catch((err) => {
569
- // unset encryptedUserAssignedName if failed to decrypt
570
- device.metadata.encryptedUserAssignedName = undefined;
571
- this.logger.error(
572
- 'DeviceCollection#_decryptDeviceName: failed to decrypt device name',
573
- err
574
- );
575
- });
576
- }
577
-
578
- return Promise.resolve(device);
579
- },
580
-
581
- /**
582
- * Utility function to update device info on mercury updates
583
- * @param {object} device
584
- * @returns {device}
585
- */
586
- _receiveDeviceUpdates(device) {
587
- // we care only the updates are for the registered devices
588
- if (device && device.spaceUrl) {
589
- const deviceId = device.spaceUrl.substring(device.spaceUrl.lastIndexOf('/') + 1);
590
- const existingDevice = DeviceCollection.get(deviceId);
591
-
592
- if (existingDevice) {
593
- return this.webex.internal.lyra.space
594
- .get({id: deviceId})
595
- .then((space) => {
596
- // eslint-disable-next-line no-shadow
597
- const device = DeviceCollection.get(deviceId);
598
-
599
- if (
600
- device &&
601
- space.occupants &&
602
- (!space.occupants.self || !space.occupants.self.verified)
603
- ) {
604
- device.productName =
605
- (space.devices && space.devices[0] && space.devices[0].productName) ||
606
- DEFAULT_PRODUCT_NAME;
607
- // pin challenge is not verified reset _pairedDevice if ids
608
- // match
609
- const pairedDeviceId =
610
- this._pairedDevice && (this._pairedDevice.id || this._pairedDevice.identity.id);
611
-
612
- if (pairedDeviceId === deviceId) {
613
- this._pairedDevice = null;
614
- this.logger.info(
615
- `DeviceManager#_receiveDeviceUpdates: device ${deviceId} lost pairing`
616
- );
617
-
618
- return Promise.resolve();
619
- }
620
- // we do not want to reset the device pending PIN challenge
621
- if (this._devicePendingPinChallenge.identity.id !== deviceId) {
622
- return this.upsert(device);
623
- }
624
-
625
- return Promise.resolve();
626
- }
627
-
628
- return Promise.resolve();
629
- })
630
- .catch((err) => {
631
- this.logger.error(
632
- 'DeviceManager#_receiveDeviceUpdates: failed to receive updates for Lyra space',
633
- err
634
- );
635
- });
636
- }
637
- }
638
-
639
- return Promise.resolve();
640
- },
641
- });
642
-
643
- export default DeviceManager;
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+ import {cloneDeep} from 'lodash';
5
+ import '@webex/internal-plugin-lyra';
6
+ import '@webex/internal-plugin-search';
7
+ import {WebexPlugin} from '@webex/webex-core';
8
+ import uuid from 'uuid';
9
+
10
+ import {LYRA_SPACE, UC_CLOUD, DEFAULT_PRODUCT_NAME} from './constants';
11
+ import DeviceCollection from './collection';
12
+
13
+ const DeviceManager = WebexPlugin.extend({
14
+ namespace: 'DeviceManager',
15
+ _devicePendingPinChallenge: null,
16
+ _pairedDevice: null,
17
+ _boundSpace: null,
18
+
19
+ initialize() {
20
+ // Lyra mercury events listener
21
+ this.webex.internal.mercury.on('event:lyra.space_updated', ({data}) => {
22
+ this._receiveDeviceUpdates(data);
23
+ });
24
+ },
25
+
26
+ /**
27
+ * Gets a list of all recent devices associated with the user
28
+ * the device list gets populated from Redis
29
+ * @returns {Promise<Device>}
30
+ */
31
+ getAll() {
32
+ return DeviceCollection.getAll();
33
+ },
34
+
35
+ /**
36
+ * Gets a list of all recent devices associated with the user
37
+ * the device list gets populated from Redis
38
+ * @returns {Promise<Device>}
39
+ */
40
+ refresh() {
41
+ DeviceCollection.reset();
42
+
43
+ return this.webex
44
+ .request({
45
+ api: 'wdm',
46
+ resource: 'devices/auxiliary',
47
+ })
48
+ .then((res) => {
49
+ if (!res.body) {
50
+ return Promise.reject();
51
+ }
52
+
53
+ return this._updateDeviceMetadata(res.body.items).then((devices) => {
54
+ /* eslint-disable consistent-return */
55
+ devices.forEach((device) => {
56
+ if (device.deviceInfo && device.deviceInfo.machineType === LYRA_SPACE) {
57
+ return this.webex.internal.lyra.space.get(device.deviceInfo).catch((err) => {
58
+ this.logger.error('DeviceManager#refresh: failed to receive device info', err);
59
+ });
60
+ }
61
+ });
62
+ /* eslint-enable consistent-return */
63
+ res.body.items.forEach((device) => {
64
+ DeviceCollection.set(device);
65
+ });
66
+
67
+ return this.getAll();
68
+ });
69
+ })
70
+ .catch((err) => {
71
+ this.logger.error('DeviceManager#refresh: failed to fetch recent devices', err);
72
+ });
73
+ },
74
+
75
+ /**
76
+ * Search for a device by name
77
+ * @param {Object} options
78
+ * @param {string} options.searchQuery
79
+ * @returns {Promise<Device>}
80
+ */
81
+ search(options) {
82
+ if (!options || !options.searchQuery) {
83
+ this.logger.error('DeviceManager#search: options.searchQuery is required');
84
+
85
+ return Promise.reject(new Error('DeviceManager#search: options.searchQuery is required'));
86
+ }
87
+
88
+ return this.webex.internal.search
89
+ .people({
90
+ searchId: uuid.v4(),
91
+ searchType: 'DEVICE_SEARCH',
92
+ searchEntity: 'device',
93
+ includePeople: false,
94
+ includeRooms: true,
95
+ queryString: options.searchQuery,
96
+ })
97
+ .catch((err) => {
98
+ this.logger.error('DeviceManager#search: failed to search a device', err);
99
+
100
+ throw err;
101
+ });
102
+ },
103
+
104
+ /**
105
+ * Caches the device info and also registers to Redis for subsequent fetches
106
+ * @param {Object} device
107
+ * @param {string} device.id
108
+ * @returns {deviceInfo}
109
+ */
110
+ upsert(device) {
111
+ const deviceId = device.id || (device.identity && device.identity.id);
112
+
113
+ if (!deviceId) {
114
+ this.logger.error('DeviceManager#upsert: device.id is required');
115
+
116
+ return Promise.reject(new Error('DeviceManager#upsert: device.id is required'));
117
+ }
118
+ this._pairedDevice = this._devicePendingPinChallenge;
119
+ this._devicePendingPinChallenge = null;
120
+ // check if the device is already existing, if so then merge else add
121
+ const existingDevice = DeviceCollection.get(deviceId);
122
+
123
+ if (existingDevice) {
124
+ DeviceCollection.set(device);
125
+
126
+ return Promise.resolve(DeviceCollection.get(deviceId));
127
+ }
128
+
129
+ // new device requested, add to wdm for subsequent retreivals
130
+ return (
131
+ this.webex
132
+ .request({
133
+ api: 'wdm',
134
+ method: 'PUT',
135
+ resource: `devices/auxiliary/Room/${deviceId}`,
136
+ })
137
+ .then((res) => {
138
+ const auxDevice = res.body;
139
+
140
+ return this._decryptDeviceName(auxDevice);
141
+ })
142
+ // eslint-disable-next-line no-shadow
143
+ .then((device) => {
144
+ DeviceCollection.set(device);
145
+
146
+ return Promise.resolve(DeviceCollection.get(deviceId));
147
+ })
148
+ .catch((err) => {
149
+ this.logger.error('DeviceManager#upsert: failed to add/update a device', err);
150
+
151
+ return Promise.reject(err);
152
+ })
153
+ );
154
+ },
155
+
156
+ /**
157
+ * Retreives device info of a particular device
158
+ * @param {string} token
159
+ * @returns {Promise<deviceInfo>}
160
+ */
161
+ get(token) {
162
+ if (!token) {
163
+ this.logger.error('DeviceManager#get: token is required');
164
+
165
+ return Promise.reject(new Error('DeviceManager#get: token is required'));
166
+ }
167
+ let deviceInfo;
168
+
169
+ return this.webex.internal.lyra
170
+ .getAdvertisedEndpoint(token)
171
+ .then((res) => {
172
+ deviceInfo = res;
173
+
174
+ return this.webex.internal.lyra.space.get({id: res.advertiser.id});
175
+ })
176
+ .then((result) => {
177
+ // the deviceInfo doesn't have proper displayName, hence update
178
+ // displayName on deviceInfo for search to work
179
+ if (result && result.identity && result.identity.displayName) {
180
+ deviceInfo.advertiser.displayName = result.identity.displayName;
181
+ }
182
+
183
+ return deviceInfo;
184
+ })
185
+ .catch((err) => {
186
+ this.logger.error('DeviceManager#get: failed to get device info', err);
187
+
188
+ return Promise.reject(err);
189
+ });
190
+ },
191
+
192
+ /**
193
+ * Unregisters the device from Redis, will not fetch in subsequent loads,
194
+ * similar to space.deleteBinding()
195
+ * @param {string} deviceId
196
+ * @returns {Promise<deviceInfo>}
197
+ */
198
+ remove(deviceId) {
199
+ if (!deviceId) {
200
+ this.logger.error('DeviceManager#remove: deviceId is required');
201
+
202
+ return Promise.reject(new Error('DeviceManager#remove: deviceId is required'));
203
+ }
204
+
205
+ return this.webex
206
+ .request({
207
+ method: 'delete',
208
+ api: 'wdm',
209
+ resource: `devices/auxiliary/${deviceId}`,
210
+ })
211
+ .catch((error) => {
212
+ this.logger.error('DeviceManager#remove: failed to remove the device', error);
213
+
214
+ return Promise.reject(error);
215
+ });
216
+ },
217
+
218
+ /**
219
+ * Requests to display PIN on the device
220
+ * @param {object} device
221
+ * @param {object} options
222
+ * @param {object} options.data
223
+ * @returns {Promise<deviceInfo>}
224
+ */
225
+ requestPin(device, options = {}) {
226
+ const deviceId = device.id || (device.identity && device.identity.id);
227
+
228
+ if (!deviceId) {
229
+ this.logger.error('DeviceManager#requestPin: device.id is required');
230
+
231
+ return Promise.reject(new Error('DeviceManager#requestPin: device.id is required'));
232
+ }
233
+ const space = {id: deviceId, url: `/spaces/${deviceId}`};
234
+
235
+ return this.webex.internal.lyra.space
236
+ .get(space)
237
+ .then((dev) => {
238
+ // check if the space is pinChallenge capable
239
+ if (dev && dev.occupants && dev.occupants.pinChallenge) {
240
+ this.logger.info('DeviceManager#requestPin: space is PIN challenge capable');
241
+
242
+ return this.webex.internal.lyra.space
243
+ .join(space, {
244
+ passType: 'MANUAL',
245
+ verificationInitiation: 'PIN',
246
+ data: options.data,
247
+ })
248
+ .then(() => {
249
+ this._devicePendingPinChallenge = dev;
250
+
251
+ // return the actual device so that it can be upserted on successful
252
+ // PIN exchange
253
+ return dev;
254
+ });
255
+ }
256
+ // pairs with the space if it's not PIN challenge capable
257
+ this.logger.info(
258
+ 'DeviceManager#requestPin: space is not PIN challenge capable, probably already occupied, will still return device info'
259
+ );
260
+
261
+ return this.webex.internal.lyra.space.get(space).then(() => Promise.resolve(dev));
262
+ })
263
+ .catch((err) => {
264
+ this.logger.error('DeviceManager#requestPin: device failed PIN challenge request', err);
265
+
266
+ return Promise.reject(err);
267
+ });
268
+ },
269
+
270
+ /**
271
+ * pairs the device with the user (manual pairing), also adds it to
272
+ * user's recents list for subsequent fetches.
273
+ * similar to space.join()
274
+ * @param {object} options
275
+ * @param {number} options.pin
276
+ * @returns {Promise<deviceInfo>}
277
+ */
278
+ pair(options = {}) {
279
+ if (!options.pin) {
280
+ this.logger.error('DeviceManager#pair: options.pin is required');
281
+
282
+ return Promise.reject(new Error('DeviceManager#pair: options.pin is required'));
283
+ }
284
+ if (this._devicePendingPinChallenge) {
285
+ const space = {
286
+ id: this._devicePendingPinChallenge.identity.id,
287
+ url: `/spaces/${this._devicePendingPinChallenge.identity.id}`,
288
+ };
289
+
290
+ return this.webex.internal.lyra.space
291
+ .join(space, {
292
+ passType: 'PIN_ANSWER',
293
+ data: options.pin,
294
+ })
295
+ .catch((err) => {
296
+ this.logger.error('DeviceManager#pair: incorrect PIN, unable to pair ', err);
297
+
298
+ return Promise.reject(err);
299
+ })
300
+ .then(() => this.upsert(this._devicePendingPinChallenge));
301
+ }
302
+ this.logger.error('DeviceManager#pair: no device to pair');
303
+
304
+ return Promise.reject(new Error('DeviceManager#pair: no device to pair'));
305
+ },
306
+
307
+ /**
308
+ * unpairs the device with the user (manual/ultrasonic pairing), but still
309
+ * keeps in the recents list/does not remove from Redis
310
+ * options.removeAllDevices will remove all associated devices to user
311
+ * similar to space.leave()
312
+ * @param {object} options
313
+ * @param {boolean} options.removeAllDevices
314
+ * @returns {Promise<deviceInfo>}
315
+ */
316
+ unpair(options = {}) {
317
+ if (!this._pairedDevice) {
318
+ this.logger.error('DeviceManager#unpair: no device to unpair');
319
+
320
+ return Promise.reject(new Error('DeviceManager#unpair: no device to unpair'));
321
+ }
322
+ const space = {url: `/spaces/${this._pairedDevice.identity.id}`};
323
+
324
+ return this.webex.internal.lyra.space.leave(space, options).catch((err) => {
325
+ this.logger.error('DeviceManager#unpair: failed to remove device from Lyra', err);
326
+
327
+ return Promise.reject(err);
328
+ });
329
+ },
330
+
331
+ /**
332
+ * binds the space to the paired device (if supported)
333
+ * similar to space.bindConversation()
334
+ * @param {object} options
335
+ * @param {boolean} options.url, conversation url
336
+ * @param {boolean} options.kmsResourceObjectUrl of the convo
337
+ * @returns {Promise<deviceInfo>}
338
+ */
339
+ bindSpace(options = {}) {
340
+ if (!options.url) {
341
+ this.logger.error('DeviceManager#pair: options.url is required');
342
+
343
+ return Promise.reject(new Error('DeviceManager#bindSpace: options.url is required'));
344
+ }
345
+ if (!options.kmsResourceObjectUrl) {
346
+ this.logger.error('DeviceManager#bindSpace: options.kmsResourceObjectUrl is required');
347
+
348
+ return Promise.reject(
349
+ new Error('DeviceManager#bindSpace: options.kmsResourceObjectUrl is required')
350
+ );
351
+ }
352
+ if (!this._pairedDevice) {
353
+ this.logger.error('DeviceManager#bindSpace: No device paired currently');
354
+
355
+ return Promise.reject(new Error('DeviceManager#bindSpace: No device paired currently'));
356
+ }
357
+ const space = {
358
+ url: `/spaces/${this._pairedDevice.identity.id}`,
359
+ id: this._pairedDevice.identity.id,
360
+ };
361
+
362
+ this._boundSpace = {
363
+ kmsResourceObjectUrl: options.kmsResourceObjectUrl,
364
+ url: options.url,
365
+ };
366
+
367
+ return this.webex.internal.lyra.space.bindConversation(space, this._boundSpace).catch((err) => {
368
+ this.logger.error('DeviceManager#bindSpace: failed to bind device to Space');
369
+
370
+ return Promise.reject(err);
371
+ });
372
+ },
373
+
374
+ /**
375
+ * unbinds the space to the paired device (if supported)
376
+ * similar to space.unbindConversation()
377
+ * @returns {Promise<deviceInfo>}
378
+ */
379
+ unbindSpace() {
380
+ if (!this._pairedDevice || !this._boundSpace) {
381
+ this.logger.error('DeviceManager#unbindSpace: No space currently bound to the device');
382
+
383
+ return Promise.reject(
384
+ new Error('DeviceManager#unbindSpace: No space currently bound to the device')
385
+ );
386
+ }
387
+ const space = {
388
+ url: `/spaces/${this._pairedDevice.identity.id}`,
389
+ id: this._pairedDevice.identity.id,
390
+ };
391
+
392
+ return this.webex.internal.lyra.space
393
+ .unbindConversation(space, this._boundSpace)
394
+ .then((res) => {
395
+ this._boundSpace = null;
396
+
397
+ return Promise.resolve(res);
398
+ })
399
+ .catch((err) => {
400
+ this.logger.error('DeviceManager#unbindSpace: failed to unbind Space to device');
401
+
402
+ return Promise.reject(err);
403
+ });
404
+ },
405
+
406
+ /**
407
+ * Gets the audio state of the paired device
408
+ * similar to device.getAudioState()
409
+ * @returns {Promise<audioState>}
410
+ */
411
+ getAudioState() {
412
+ if (!this._pairedDevice) {
413
+ this.logger.error('DeviceManager#getAudioState: Currently no device is paired');
414
+
415
+ return Promise.reject(
416
+ new Error('DeviceManager#getAudioState: Currently no device is paired')
417
+ );
418
+ }
419
+
420
+ return this.webex.internal.lyra.device.getAudioState(this._pairedDevice);
421
+ },
422
+
423
+ /**
424
+ * Updates audio state of the paired device, should be called every 10 minutes
425
+ * or when mic or volume state is changed
426
+ * similar to device.putAudioState()
427
+ * @param {object} space
428
+ * @param {object} audioState
429
+ * @returns {Promise<audioState>}
430
+ */
431
+ putAudioState(space, audioState = {}) {
432
+ return this.webex.internal.lyra.device.putAudioState(space, audioState);
433
+ },
434
+
435
+ /**
436
+ * Mutes paired device
437
+ * similar to device.mute()
438
+ * @returns {Promise<audioState>}
439
+ */
440
+ mute() {
441
+ if (!this._pairedDevice) {
442
+ this.logger.error('DeviceManager#mute: Currently no device is paired');
443
+
444
+ return Promise.reject(new Error('DeviceManager#mute: Currently no device is paired'));
445
+ }
446
+
447
+ return this.webex.internal.lyra.device.mute(this._pairedDevice);
448
+ },
449
+
450
+ /**
451
+ * Unmutes paired device
452
+ * similar to device.unmute()
453
+ * @returns {Promise<audioState>}
454
+ */
455
+ unmute() {
456
+ if (!this._pairedDevice) {
457
+ this.logger.error('DeviceManager#unmute: Currently no device is paired');
458
+
459
+ return Promise.reject(new Error('DeviceManager#unmute: Currently no device is paired'));
460
+ }
461
+
462
+ return this.webex.internal.lyra.device.unmute(this._pairedDevice);
463
+ },
464
+
465
+ /**
466
+ * Increases paired device's volume
467
+ * similar to device.increaseVolume()
468
+ * @returns {Promise<audioState>}
469
+ */
470
+ increaseVolume() {
471
+ if (!this._pairedDevice) {
472
+ this.logger.error('DeviceManager#increaseVolume: Currently no device is paired');
473
+
474
+ return Promise.reject(
475
+ new Error('DeviceManager#increaseVolume: Currently no device is paired')
476
+ );
477
+ }
478
+
479
+ return this.webex.internal.lyra.device.increaseVolume(this._pairedDevice);
480
+ },
481
+
482
+ /**
483
+ * Decreases paired device's volume
484
+ * similar to device.decreaseVolume()
485
+ * @returns {Promise<audioState>}
486
+ */
487
+ decreaseVolume() {
488
+ if (!this._pairedDevice) {
489
+ this.logger.error('DeviceManager#decreaseVolume: Currently no device is paired');
490
+
491
+ return Promise.reject(
492
+ new Error('DeviceManager#decreaseVolume: Currently no device is paired')
493
+ );
494
+ }
495
+
496
+ return this.webex.internal.lyra.device.decreaseVolume(this._pairedDevice);
497
+ },
498
+
499
+ /**
500
+ * Sets paired device's volume but should use increase and decrease api instead
501
+ * similar to device.setVolume()
502
+ * @param {number} level
503
+ * @returns {Promise<audioState>}
504
+ */
505
+ setVolume(level = 0) {
506
+ if (!this._pairedDevice) {
507
+ this.logger.error('DeviceManager#setVolume: Currently no device is paired');
508
+
509
+ return Promise.reject(new Error('DeviceManager#setVolume: Currently no device is paired'));
510
+ }
511
+
512
+ return this.webex.internal.lyra.device.setVolume(this._pairedDevice, level);
513
+ },
514
+
515
+ /**
516
+ * Utility function to update decrypted device name on device object
517
+ * @param {Array} deviceArray
518
+ * @returns {device}
519
+ */
520
+ _updateDeviceMetadata(deviceArray = []) {
521
+ if (!deviceArray.length) {
522
+ return Promise.resolve(deviceArray);
523
+ }
524
+ const devices = cloneDeep(deviceArray);
525
+
526
+ return Promise.all(
527
+ devices.map((device, index) =>
528
+ this.webex.internal.services
529
+ .waitForCatalog('postauth')
530
+ .then(() => {
531
+ if (device.deviceClass === UC_CLOUD) {
532
+ device.id = `${this.webex.internal.services.get('wdm')}/${device.id}`;
533
+ }
534
+
535
+ return this._decryptDeviceName(device);
536
+ })
537
+ .then((updatedDevice) => {
538
+ devices[index] = updatedDevice;
539
+
540
+ return Promise.resolve();
541
+ })
542
+ )
543
+ ).then(() => Promise.resolve(devices));
544
+ },
545
+
546
+ /**
547
+ * Utility function to update decrypted device name on device object
548
+ * @param {object} inDevice
549
+ * @returns {device}
550
+ */
551
+ _decryptDeviceName(inDevice = {}) {
552
+ const device = cloneDeep(inDevice);
553
+
554
+ if (
555
+ device.metadata &&
556
+ device.metadata.encryptedUserAssignedName &&
557
+ device.metadata.encryptionKeyUrl
558
+ ) {
559
+ return this.webex.internal.encryption
560
+ .decryptText(device.metadata.encryptionKeyUrl, device.metadata.encryptedUserAssignedName)
561
+ .then((decryptedDeviceName) => {
562
+ // set userAssignedName as the decypted value, unset encryptedUserAssignedName since it's not needed
563
+ device.metadata.encryptedUserAssignedName = undefined;
564
+ device.metadata.userAssignedName = decryptedDeviceName;
565
+
566
+ return Promise.resolve(device);
567
+ })
568
+ .catch((err) => {
569
+ // unset encryptedUserAssignedName if failed to decrypt
570
+ device.metadata.encryptedUserAssignedName = undefined;
571
+ this.logger.error(
572
+ 'DeviceCollection#_decryptDeviceName: failed to decrypt device name',
573
+ err
574
+ );
575
+ });
576
+ }
577
+
578
+ return Promise.resolve(device);
579
+ },
580
+
581
+ /**
582
+ * Utility function to update device info on mercury updates
583
+ * @param {object} device
584
+ * @returns {device}
585
+ */
586
+ _receiveDeviceUpdates(device) {
587
+ // we care only the updates are for the registered devices
588
+ if (device && device.spaceUrl) {
589
+ const deviceId = device.spaceUrl.substring(device.spaceUrl.lastIndexOf('/') + 1);
590
+ const existingDevice = DeviceCollection.get(deviceId);
591
+
592
+ if (existingDevice) {
593
+ return this.webex.internal.lyra.space
594
+ .get({id: deviceId})
595
+ .then((space) => {
596
+ // eslint-disable-next-line no-shadow
597
+ const device = DeviceCollection.get(deviceId);
598
+
599
+ if (
600
+ device &&
601
+ space.occupants &&
602
+ (!space.occupants.self || !space.occupants.self.verified)
603
+ ) {
604
+ device.productName =
605
+ (space.devices && space.devices[0] && space.devices[0].productName) ||
606
+ DEFAULT_PRODUCT_NAME;
607
+ // pin challenge is not verified reset _pairedDevice if ids
608
+ // match
609
+ const pairedDeviceId =
610
+ this._pairedDevice && (this._pairedDevice.id || this._pairedDevice.identity.id);
611
+
612
+ if (pairedDeviceId === deviceId) {
613
+ this._pairedDevice = null;
614
+ this.logger.info(
615
+ `DeviceManager#_receiveDeviceUpdates: device ${deviceId} lost pairing`
616
+ );
617
+
618
+ return Promise.resolve();
619
+ }
620
+ // we do not want to reset the device pending PIN challenge
621
+ if (this._devicePendingPinChallenge.identity.id !== deviceId) {
622
+ return this.upsert(device);
623
+ }
624
+
625
+ return Promise.resolve();
626
+ }
627
+
628
+ return Promise.resolve();
629
+ })
630
+ .catch((err) => {
631
+ this.logger.error(
632
+ 'DeviceManager#_receiveDeviceUpdates: failed to receive updates for Lyra space',
633
+ err
634
+ );
635
+ });
636
+ }
637
+ }
638
+
639
+ return Promise.resolve();
640
+ },
641
+ });
642
+
643
+ export default DeviceManager;