@capturebridge/sdk 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/IFace.js ADDED
@@ -0,0 +1,274 @@
1
+ import ICAO from './ICAO.js'
2
+ import { BASE_URL, blobToBase64 } from './Common.js'
3
+
4
+ // This is not exported as we may eventually detect this from the plugin
5
+ // See: Iface.matchModes()
6
+ const VALID_MODES = ['accurate', 'balanced', 'fast', 'accurate_server']
7
+
8
+ /**
9
+ * Probe
10
+ * @classdesc Probe Class for Innovatrics IFace SDK
11
+ *
12
+ * @see {@link IFace}
13
+ */
14
+ export class Probe {
15
+ source
16
+ url
17
+ imageData
18
+ /**
19
+ * Instantiate an IFace Probe
20
+ * @constructor
21
+ * @param {string} source - The image source for the probe.
22
+ * @param {string} [type] - The type of data in the image source, valid values
23
+ * are 'url', 'base64', and 'dataurl'. If not provided 'base64' is the default.
24
+ *
25
+ * @example
26
+ * const candidate = new Candidate('/person/123.jpg', '123', 'url')
27
+ */
28
+ constructor (source, type = 'base64') {
29
+ // TODO:
30
+ // If the source is typeof object and/or type=object we should do some duck
31
+ // typing and accept any of...
32
+ // - The result of an ICAO check that contains a cropped image
33
+ // - An Image tag (fetch the source)
34
+ // - A Blob
35
+ // - An Array Buffer
36
+ // - An object that has a fullCapture method (call function in .data())
37
+ switch (type) {
38
+ case 'url':
39
+ this.url = source
40
+ break
41
+ case 'base64':
42
+ this.imageData = source
43
+ break
44
+ case 'dataurl':
45
+ this.imageData = source.split(',')[1]
46
+ break
47
+ case undefined:
48
+ case null:
49
+ default:
50
+ throw new Error('Invalid image source type provided')
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Fetch the image data and base64 encode it
56
+ * @async
57
+ * @example
58
+ * const probe = new Probe("/img/photo.jpg")
59
+ * await probe.data()
60
+ * console.log(probe.imageData)
61
+ */
62
+ async data () {
63
+ if (!this.imageData) {
64
+ const response = await fetch(this.url)
65
+ const blob = await response.blob()
66
+ this.imageData = await blobToBase64(blob)
67
+ }
68
+ }
69
+
70
+ toJSON () {
71
+ return this.imageData
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Candidate
77
+ * @classdesc Candidate Class for Innovatrics IFace SDK
78
+ *
79
+ * @see {@link IFace}
80
+ */
81
+ export class Candidate extends Probe {
82
+ id
83
+
84
+ // TODO: Support setting different modes on individual candidates
85
+ // an example usecase for this is to set 'accurate_server' the most recent
86
+ // photo, and 'fast' for the remaining set (since they likely have already
87
+ // been verified at some point) In the plugin we would have to re-template the
88
+ // probe for each additional mode and it doesn't make sense to do so until
89
+ // the plugin is checking each photo against the probe in a separate thread.
90
+ mode
91
+
92
+ /**
93
+ * Instantiate an IFace Candidate
94
+ * @constructor
95
+ * @param {string} source - The image source for the candidate.
96
+ * @param {string} id - The ID for the candidate when the match results are
97
+ * returned this Id will be returned with it's respective results.
98
+ * @param {string} [type] - The type of data in the image source, valid values
99
+ * are 'url', 'base64', and 'dataurl'. If not provided 'base64' is the default.
100
+ *
101
+ * @example
102
+ * const candidate = new Candidate('/person/123.jpg', '123', 'url')
103
+ */
104
+ constructor (source, id, type = 'base64') {
105
+ super(source, type)
106
+ if (id) {
107
+ this.id = id
108
+ } else {
109
+ throw new Error('Candidate ID not provided')
110
+ }
111
+ }
112
+
113
+ toJSON () {
114
+ return {
115
+ id: this.id,
116
+ base64: this.imageData
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * IFace
123
+ * @classdesc Class for Innovatrics IFace SDK
124
+ *
125
+ */
126
+ export class IFace extends ICAO {
127
+ /**
128
+ * Instantiate an IFace plugin
129
+ * @constructor
130
+ * @param {string} [baseURL] - Protocol, domain, and port for the service.
131
+ *
132
+ * @example
133
+ * const iface = new IFace()
134
+ */
135
+ constructor (baseUrl = BASE_URL) {
136
+ super('capture_verify_iface', baseUrl)
137
+ }
138
+
139
+ /**
140
+ * Return list of available face matching modes
141
+ *
142
+ * @returns {string[]} List of available face matching modes
143
+ *
144
+ * @async
145
+ * @example
146
+ * const iface = new IFace()
147
+ * const modes = await iface.matchModes()
148
+ * // do something with modes, such as build a <select> element
149
+ * const select = document.createElement('select')
150
+ * modes.forEach(mode => {
151
+ * const option = document.createElement('option')
152
+ * option.value = mode
153
+ * select.appendChild(mode)
154
+ * })
155
+ * document.body.appendChild(select)
156
+ */
157
+ async matchModes () {
158
+ // This is async as we may later get this from the plugin at runtime
159
+ return VALID_MODES
160
+ }
161
+
162
+ matchModesHandler (callback) {
163
+ callback(null, VALID_MODES)
164
+ }
165
+
166
+ /**
167
+ * Check the ICAO compliance of an image
168
+ * @param {string} image - A base64 encoded image.
169
+ *
170
+ * @param {string|bool} [crop] - If falsy image will not be cropped, otherwise
171
+ * the value will be interpreted as the preferred crop method which will vary
172
+ * by plugin implementation.
173
+ *
174
+ * @returns {object} Results of the ICAO check, this may vary by plugin
175
+ * implementation.
176
+ *
177
+ * @async
178
+ * @example
179
+ * // Take a photo with a Canon Camera and perform ICAO checks on the
180
+ * // returned image
181
+ * const camera = new CanonCamera()
182
+ * const photo = await camera.takePhoto('base64')
183
+ * const iface = new IFace()
184
+ * const results = await iface.icao(photo)
185
+ * const img = document.createElement('img')
186
+ * img.src = 'data:image/jpeg;base64,' + results.cropped
187
+ * console.log(`found ${result.faces_found} faces in the image`)
188
+ * document.body.appendChild(img)
189
+ */
190
+ async icao (image, cropMethod = false) {
191
+ return await this.check(image, cropMethod)
192
+ }
193
+
194
+ /**
195
+ * Perform a facial match against one or more photos.
196
+ *
197
+ * @param {object|string} probe - Either a Probe object or a base64 encoded
198
+ * image that is being compared or searched against one or more candidates
199
+ *
200
+ * @param {object[]} candidates - An array of candidate objects against which
201
+ * the probe photo is compared.
202
+ * @param {string} candidates[].id - Id of the photo, when the results of the
203
+ * matched are returned, this id will be returned so results can be matched
204
+ * back to their original image. Must be less than 16 characters.
205
+ * @param {string} candidates[].base64 - Base64 encoded string containing the
206
+ * photo.
207
+ * @param {string} [candidates[].mode] - Matching mode to use for this photo.
208
+ * If left unspecified will use the mode provided in the mode parameter.
209
+ *
210
+ * @param {string} [mode=fast] - Matching mode to use for all images, can be
211
+ * overriden on each candidate if desired. Valid values are: 'accurate',
212
+ * 'balanced', 'fast', and 'accurate_server'.
213
+ *
214
+ * @async
215
+ * @example
216
+ * // Create an iface object
217
+ * const iface = new IFace()
218
+ *
219
+ * // Obtain a photo for the probe photo
220
+ * const probe = await camera.takePhoto('base64')
221
+ *
222
+ * // Create a candidate set from the person's previous photos
223
+ * const candidates = [
224
+ * new Candidate('/person/1/photo/2.jpg', '3', 'url'),
225
+ * new Candidate('/person/1/photo/1.jpg', '2', 'url')
226
+ * ]
227
+ *
228
+ * // Match the probe to all the candidates using the 'balanced' mode
229
+ * const results = await iface.match(probe, candidates, 'balanced')
230
+ *
231
+ * // use the results
232
+ * console.log(results)
233
+ */
234
+ async match (probe, candidates, mode = 'fast') {
235
+ if (VALID_MODES.indexOf(mode) === -1) {
236
+ throw new Error('Invalid mode provided')
237
+ }
238
+
239
+ // If this is a Probe object fetch it's image data
240
+ if (typeof probe === 'object' && probe.constructor.name === 'Probe') {
241
+ await probe.data()
242
+ }
243
+
244
+ // And fetch data for candidates
245
+ const asyncOperations = candidates.map(async candidate => {
246
+ if (typeof candidate === 'object' &&
247
+ candidate.constructor.name === 'Candidate') {
248
+ await candidate.data()
249
+ }
250
+ })
251
+
252
+ // Wait for all async operations to complete
253
+ await Promise.all(asyncOperations)
254
+
255
+ const body = {
256
+ probe,
257
+ mode,
258
+ candidates
259
+ }
260
+ const options = {
261
+ method: 'POST',
262
+ mode: 'cors',
263
+ headers: {
264
+ 'Content-Type': 'application/json'
265
+ },
266
+ body: JSON.stringify(body)
267
+ }
268
+ const url = `${this.baseUrl}/plugin/${this.id}/face/match`
269
+ const response = await fetch(url, options)
270
+ return await response.json()
271
+ }
272
+ }
273
+
274
+ export default IFace
package/Logs.js ADDED
@@ -0,0 +1,129 @@
1
+ import { BASE_URL } from './Common.js'
2
+
3
+ export const TRACE = 10
4
+ export const DEBUG = 20
5
+ export const INFO = 30
6
+ export const WARN = 40
7
+ export const ERROR = 50
8
+ export const FATAL = 60
9
+
10
+ /**
11
+ * @constant
12
+ * @type {Object.<string, number>}
13
+ *
14
+ */
15
+ export const LOG_LEVELS = {
16
+ trace: TRACE,
17
+ debug: DEBUG,
18
+ info: INFO,
19
+ warn: WARN,
20
+ error: ERROR,
21
+ fatal: FATAL
22
+ }
23
+
24
+ /**
25
+ * @classdesc Plugins write
26
+ * {@link https://github.com/trentm/node-bunyan?tab=readme-ov-file#log-record-fields|Bunyan}
27
+ * formated logs to separate log files. This class provides
28
+ * utilities for viewing and following those logs as they're written.
29
+ */
30
+ export class Logs {
31
+ baseUrl
32
+ #reader
33
+ #controller
34
+ /**
35
+ * Instantiate a Log Object
36
+ * @constructor
37
+ * @param {string} [baseURL] - Protocol, domain, and port for the service.
38
+ */
39
+ constructor (baseUrl = BASE_URL) {
40
+ this.baseUrl = baseUrl
41
+ }
42
+
43
+ /**
44
+ * End the log stream and stop following logs.
45
+ */
46
+ end () {
47
+ this.#reader?.cancel()
48
+ this.#controller?.abort()
49
+ }
50
+
51
+ /**
52
+ * Add a handler for following logs. Note that each invocation of this method
53
+ * opens a connection to the server and holds it open. Web browsers limit the
54
+ * number of connections to a single domain. Avoid using this method more than
55
+ * once per client.
56
+ * @param {string[]} plugins - Follow logs for the provided Plugin IDs.
57
+ * @param {string|number} level - Log Level.
58
+ * @param {function} callback - Invoked with two arguments. The first argument
59
+ * is an Error object (if an error occurred) or null. The second argument is
60
+ * an log object.
61
+ */
62
+ follow (plugins, level, callback) {
63
+ let pluginList
64
+ if (typeof plugins === 'object' && plugins.length > 0) {
65
+ pluginList = plugins.join(',')
66
+ } else {
67
+ throw new Error('Invalid value provided for plugins argument')
68
+ }
69
+
70
+ const logLevel = (typeof level === 'number') ? level : LOG_LEVELS[level]
71
+
72
+ if (!logLevel) {
73
+ throw new Error('Invalid value provided for log level argument')
74
+ }
75
+
76
+ if (typeof callback !== 'function') {
77
+ throw new Error('Invalid value provided for callback argument')
78
+ }
79
+
80
+ const url = `${this.baseUrl}/plugin/logs/follow?plugins=${pluginList}&level=${logLevel}`
81
+
82
+ this.#controller = new AbortController()
83
+ const signal = this.#controller.signal
84
+
85
+ fetch(url, { signal })
86
+ .then(({ body }) => {
87
+ let buffer = ''
88
+ if (!body) {
89
+ return callback(new Error('No response body'))
90
+ }
91
+ const readData = data => {
92
+ if (!data.done) {
93
+ callback(null, data.value)
94
+ this.#reader.read().then(readData).catch(e => callback(e))
95
+ }
96
+ }
97
+ this.#reader = body
98
+ .pipeThrough(new TextDecoderStream())
99
+ .pipeThrough(new TransformStream({
100
+ transform (chunk, controller) {
101
+ buffer += chunk
102
+ const parts = buffer.split('\n')
103
+ parts.slice(0, -1).forEach(part => controller.enqueue(part))
104
+ buffer = parts[parts.length - 1]
105
+ },
106
+ flush (controller) {
107
+ if (buffer) {
108
+ controller.enqueue(buffer)
109
+ }
110
+ }
111
+ }))
112
+ .pipeThrough(new TransformStream({
113
+ transform (chunk, controller) {
114
+ controller.enqueue(JSON.parse(chunk))
115
+ }
116
+ }))
117
+ .getReader()
118
+ this.#reader
119
+ .read()
120
+ .then(readData)
121
+ .catch(e => callback(e))
122
+ }).catch(e => {
123
+ // Don't emit an error when aborting the fetch operation
124
+ if (e.name !== 'AbortError') {
125
+ callback(e)
126
+ }
127
+ })
128
+ }
129
+ }
package/MockCamera.js ADDED
@@ -0,0 +1,10 @@
1
+ import Camera from './Camera.js'
2
+ import { BASE_URL } from './Common.js'
3
+
4
+ class MockCamera extends Camera {
5
+ constructor (baseUrl = BASE_URL) {
6
+ super('capture_camera_mock', baseUrl)
7
+ }
8
+ }
9
+
10
+ export default MockCamera
package/Plugin.js ADDED
@@ -0,0 +1,298 @@
1
+ import Device from './Device.js'
2
+
3
+ import { BASE_URL, DELETE, asyncToCallback } from './Common.js'
4
+
5
+ /**
6
+ * @classdesc CaptureBridge utilizes a plugin system for interacting with
7
+ * hardware devices and vendor SDKs. Clients will interact with plugins via the
8
+ * REST API using their unique plugin ID. This class provides basic methods
9
+ * for working with these plugins such as obtaining a list of compatible devices
10
+ * and managing the plugin's configuration.
11
+ *
12
+ * @see {@link CaptureBridge#plugins} Get all installed plugin.
13
+ * @see {@link CaptureBridge#plugin} Get a single plugin by ID.
14
+ */
15
+ class Plugin {
16
+ /**
17
+ * @property {string} id - ID of the plugin, used when building REST endpoint paths.
18
+ * @property {string} name - Human friendly name for the plugin.
19
+ * @property {string} description - Human friendly description of plugin.
20
+ * @property {string} version - {@link https://semver.org/|Semantic version} version of the plugin.
21
+ * @property {string[]} methods - A plugin's list of supported RPC methods.
22
+ * @property {boolean} ready - True if plugin has been loaded into memory and is ready to receive requests.
23
+ * @property {object} defaultDevice - Default device to use for the plugin.
24
+ * @property {string[]} configMethods - Methods required to implement configuration.
25
+ */
26
+ baseUrl
27
+ id
28
+ name
29
+ description
30
+ version
31
+ methods = []
32
+ ready = false
33
+ defaultDevice = null
34
+ configMethods = ['config_schema', 'get_config', 'set_config']
35
+
36
+ /**
37
+ * Instantiate a plugin.
38
+ * @constructor
39
+ * @param {object|string} plugin - plugin object as received from the API or a plugin ID string.
40
+ * @param {string} plugin.id - ID of the plugin, used when building endpoint paths to call methods on the plugin.
41
+ * @param {string} plugin.name - Human friendly name of plugin.
42
+ * @param {string} plugin.description - Human friendly description of plugin.
43
+ * @param {string} plugin.version - {@link https://semver.org/|Semantic version} version of the plugin.
44
+ * @param {string[]} plugin.methods - plugin's capabilities.
45
+ * @param {boolean} plugin.ready - True if plugin has been loaded into memory and is ready.
46
+ * @param {string} [baseURL=BASE_URL] - Override the default protocol, domain, and port for the service.
47
+ * @throws Will throw an Error the plugin argument is not a plugin ID (String) or an object.
48
+ */
49
+ constructor (plugin, baseUrl = BASE_URL) {
50
+ this.baseUrl = baseUrl
51
+ if (typeof plugin === 'string') {
52
+ this.id = plugin
53
+ } else if (typeof plugin === 'object') {
54
+ Object.assign(this, {}, plugin)
55
+ } else {
56
+ throw new Error('Invalid properties supplied to Plugin constructor')
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get all devices for this plugin
62
+ *
63
+ * @method
64
+ * @async
65
+ * @see {@link https://capture.local.valididcloud.com:9001/doc/#api-Plugin-GetPluginDevices|API Endpoint (/plugin/:pluginId/device)}
66
+ * @returns {object[]} Array of {@link Device} objects.
67
+ * @example
68
+ * const plugin = await captureBridge.plugin('capture_camera_canon')
69
+ * const devices = await plugin.devices()
70
+ */
71
+ async devices () {
72
+ const response = await fetch(`${this.baseUrl}/plugin/${this.id}/device`)
73
+ const devices = await response.json()
74
+ return devices.map(d => new Device(d, this, this.baseUrl))
75
+ }
76
+
77
+ /**
78
+ * Get all Devices for this plugin
79
+ *
80
+ * @method
81
+ * @see {@link https://capture.local.valididcloud.com:9001/doc/#api-Plugin-GetPluginDevices|API Endpoint (/plugin/:pluginId/device)}
82
+ * @param {function} callback - Invoked with two arguments. The first argument
83
+ * is an Error object (if an error occurred) or null. The second argument is
84
+ * an Array of {@link Device} objects. If no devices are available the second
85
+ * argument will be an empty Array.
86
+ * @example
87
+ * captureBridge.pluginHandler('capture_camera_canon', (error, plugin) => {
88
+ * plugin.devicesHandler((error, devices) => console.log(devices))
89
+ * })
90
+ */
91
+ devicesHandler (callback) {
92
+ asyncToCallback(this, this.devices, callback)
93
+ }
94
+
95
+ /**
96
+ * Get a device by ID for this plugin
97
+ *
98
+ * @method
99
+ * @async
100
+ * @see {@link https://capture.local.valididcloud.com:9001/doc/#api-Plugin-GetPluginDevices|API Endpoint (/plugin/:pluginId/device)}
101
+ * @param {string} id - Device ID
102
+ * @returns {object?} Requested {@link Device} or null
103
+ * @example
104
+ * const plugin = await captureBridge.plugin('capture_camera_canon')
105
+ * const device = await plugin.device('AWOOO56709')
106
+ */
107
+ async device (id) {
108
+ const response = await fetch(`${this.baseUrl}/plugin/${this.id}/device`)
109
+ const devices = await response.json()
110
+ const device = devices.find(d => d.id === id)
111
+ return device ? new Device(device, this, this.baseUrl) : null
112
+ }
113
+
114
+ /**
115
+ * Get a Device by ID for this plugin
116
+ * @method
117
+ * @see {@link https://capture.local.valididcloud.com:9001/doc/#api-Plugin-GetPluginDevices|API Endpoint (/plugin/:pluginId/device)}
118
+ * @param {string} id - Device ID
119
+ * @param {function} callback - Invoked with two arguments. The first argument
120
+ * is an Error object (if an error occurred) or null. The second argument is
121
+ * a {@link Device} object. If the requested device is not available the
122
+ * second argument will be null.
123
+ * @example
124
+ * captureBridge.pluginHandler('capture_camera_canon', (error, plugin) => {
125
+ * plugin.deviceHandler('AWOOO56709', (error, device) => console.log(device))
126
+ * })
127
+ */
128
+ deviceHandler (id, callback) {
129
+ asyncToCallback(this, this.device, callback, id)
130
+ }
131
+
132
+ async configSchema () {
133
+ if (!await this.supportsConfig()) {
134
+ throw new Error('Plugin does not support config')
135
+ }
136
+ const response = await fetch(`${this.baseUrl}/plugin/${this.id}/config/schema`)
137
+ return await response.json()
138
+ }
139
+
140
+ configSchemaHandler (callback) {
141
+ asyncToCallback(this, this.configSchema, callback)
142
+ }
143
+
144
+ async config () {
145
+ if (!await this.supportsConfig()) {
146
+ throw new Error('Plugin does not support config')
147
+ }
148
+ const response = await fetch(`${this.baseUrl}/plugin/${this.id}/config`)
149
+ return await response.json()
150
+ }
151
+
152
+ configHandler (callback) {
153
+ asyncToCallback(this, this.config, callback)
154
+ }
155
+
156
+ async saveConfig (config) {
157
+ if (!await this.supportsConfig()) {
158
+ throw new Error('Plugin does not support config')
159
+ }
160
+ const options = {
161
+ method: 'POST',
162
+ mode: 'cors',
163
+ headers: {
164
+ 'Content-Type': 'application/json'
165
+ },
166
+ body: JSON.stringify(config)
167
+ }
168
+ const url = `${this.baseUrl}/plugin/${this.id}/config`
169
+ const response = await fetch(url, options)
170
+ return await response.json()
171
+ }
172
+
173
+ async saveConfigHandler (config, callback) {
174
+ asyncToCallback(this, this.saveConfig, callback, config)
175
+ }
176
+
177
+ async supportsConfig () {
178
+ await this.update()
179
+ return this.configMethods.every(m => this.methods.includes(m))
180
+ }
181
+
182
+ supportsConfigHandler (callback) {
183
+ asyncToCallback(this, this.configHandler, callback)
184
+ }
185
+
186
+ async shutdown () {
187
+ const url = `${this.baseUrl}/plugin/${this.id}`
188
+ const response = await fetch(url, DELETE)
189
+ return await response.json()
190
+ }
191
+
192
+ shutdownHandler (callback) {
193
+ asyncToCallback(this, this.shutdown, callback)
194
+ }
195
+
196
+ async startup () {
197
+ // See: ValidRD/wa_capture_bridge/issues/48
198
+ // We need an explicit endpoint for this instead
199
+ const devices = await this.devices()
200
+ if (devices) {
201
+ await this.update(true)
202
+ return { status: 'starting' }
203
+ }
204
+ return { status: 'failed' }
205
+ }
206
+
207
+ startupHandler (callback) {
208
+ asyncToCallback(this, this.startup, callback)
209
+ }
210
+
211
+ /**
212
+ * This class can be instantiated from either a plugin ID or a full plugin
213
+ * object from an API request.
214
+ * Because constructors cannot be async we use this method to hydrate the
215
+ * plugin properties if needed.
216
+ *
217
+ * @private
218
+ * @method
219
+ */
220
+ async update (force = false) {
221
+ if (force || this.methods.length === 0) {
222
+ const response = await fetch(`${this.baseUrl}/plugin`)
223
+ const plugins = await response.json()
224
+ const plugin = plugins.find(p => p.id === this.id)
225
+ Object.assign(this, plugin)
226
+ }
227
+ }
228
+
229
+ updateHandler (force = false, callback) {
230
+ asyncToCallback(this, this.update, callback, force)
231
+ }
232
+
233
+ async rpc (request) {
234
+ const options = {
235
+ method: 'POST',
236
+ mode: 'cors',
237
+ headers: {
238
+ 'Content-Type': 'application/json'
239
+ },
240
+ body: JSON.stringify(request)
241
+ }
242
+ const url = `${this.baseUrl}/plugin/${this.id}/rpc`
243
+ const response = await fetch(url, options)
244
+ return await response.json()
245
+ }
246
+
247
+ rpcHandler (request, callback) {
248
+ asyncToCallback(this, this.rpc, callback, request)
249
+ }
250
+
251
+ async ping () {
252
+ const response = await fetch(`${this.baseUrl}/plugin/${this.id}/ping`)
253
+ return await response.json()
254
+ }
255
+
256
+ pingHandler (callback) {
257
+ asyncToCallback(this, this.ping, callback)
258
+ }
259
+
260
+ /**
261
+ * Classes that extend this one will add their own methods to perform
262
+ * operations on a device. They will call this method first to get a device
263
+ * object to use for making calls to the appropriate endpoint.
264
+ *
265
+ * This method really should be "protected" in classical OOP terms but
266
+ * Javascript does not currently have support for such scopes, and it's not
267
+ * marked using the ES6 private modifier so that it can be inherited by it's
268
+ * children.
269
+ * @private
270
+ * @method
271
+ */
272
+ async setupDevice (deviceOpt) {
273
+ await this.update()
274
+
275
+ if (deviceOpt instanceof Device) {
276
+ this.defaultDevice = deviceOpt
277
+ return this.defaultDevice
278
+ }
279
+
280
+ if (typeof deviceOpt === 'string') {
281
+ this.defaultDevice = this.device(deviceOpt)
282
+ return this.defaultDevice
283
+ }
284
+
285
+ if (!this.defaultDevice) {
286
+ const devices = await this.devices()
287
+ if (!devices.length) {
288
+ throw new Error('No devices found for the plugin')
289
+ }
290
+ this.defaultDevice = devices[0]
291
+ return this.defaultDevice
292
+ }
293
+
294
+ return this.defaultDevice
295
+ }
296
+ }
297
+
298
+ export default Plugin