node-switchbot 1.0.7

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.
@@ -0,0 +1,498 @@
1
+ /* ------------------------------------------------------------------
2
+ * node-linking - switchbot-device.js
3
+ *
4
+ * Copyright (c) 2019-2020, Futomi Hatano, All rights reserved.
5
+ * Released under the MIT license
6
+ * Date: 2020-02-19
7
+ * ---------------------------------------------------------------- */
8
+ 'use strict';
9
+ const parameterChecker = require('./parameter-checker.js');
10
+ const switchbotAdvertising = require('./switchbot-advertising.js');
11
+
12
+ class SwitchbotDevice {
13
+ /* ------------------------------------------------------------------
14
+ * Constructor
15
+ *
16
+ * [Arguments]
17
+ * - peripheral | Object | Required | The `peripheral` object of noble,
18
+ * | | | which represents this device
19
+ * - noble | Noble | Required | The Nobel object created by the noble module.
20
+ * ---------------------------------------------------------------- */
21
+ constructor(peripheral, noble) {
22
+ this._peripheral = peripheral;
23
+ this._noble = noble;
24
+ this._chars = null;
25
+
26
+ this._SERV_UUID_PRIMARY = 'cba20d00224d11e69fb80002a5d5c51b';
27
+ this._CHAR_UUID_WRITE = 'cba20002224d11e69fb80002a5d5c51b';
28
+ this._CHAR_UUID_NOTIFY = 'cba20003224d11e69fb80002a5d5c51b';
29
+ this._CHAR_UUID_DEVICE = '2a00';
30
+
31
+ this._READ_TIMEOUT_MSEC = 3000;
32
+ this._WRITE_TIMEOUT_MSEC = 3000;
33
+ this._COMMAND_TIMEOUT_MSEC = 3000;
34
+
35
+ // Save the device information
36
+ let ad = switchbotAdvertising.parse(peripheral);
37
+ this._id = ad.id;
38
+ this._address = ad.address;
39
+ this._model = ad.serviceData.model;
40
+ this._modelName = ad.serviceData.modelName;
41
+
42
+ this._was_connected_explicitly = false;
43
+ this._connected = false;
44
+
45
+ this._onconnect = () => { };
46
+ this._ondisconnect = () => { };
47
+ this._ondisconnect_internal = () => { };
48
+ this._onnotify_internal = () => { };
49
+ }
50
+
51
+ // Getters
52
+ get id() {
53
+ return this._id;
54
+ }
55
+ get address() {
56
+ return this._address;
57
+ }
58
+ get model() {
59
+ return this._model;
60
+ }
61
+ get modelName() {
62
+ return this._modelName;
63
+ }
64
+ get connectionState() {
65
+ if (!this._connected && this._peripheral.state === 'disconnecting') {
66
+ return 'disconnected';
67
+ } else {
68
+ return this._peripheral.state;
69
+ }
70
+ }
71
+
72
+ // Setters
73
+ set onconnect(func) {
74
+ if (!func || typeof (func) !== 'function') {
75
+ throw new Error('The `onconnect` must be a function.');
76
+ }
77
+ this._onconnect = func;
78
+ }
79
+ set ondisconnect(func) {
80
+ if (!func || typeof (func) !== 'function') {
81
+ throw new Error('The `ondisconnect` must be a function.');
82
+ }
83
+ this._ondisconnect = func;
84
+ }
85
+
86
+ /* ------------------------------------------------------------------
87
+ * connect()
88
+ * - Connect the device
89
+ *
90
+ * [Arguments]
91
+ * - none
92
+ *
93
+ * [Returen value]
94
+ * - Promise object
95
+ * Nothing will be passed to the `resolve()`.
96
+ * ---------------------------------------------------------------- */
97
+ connect() {
98
+ this._was_connected_explicitly = true;
99
+ return this._connect();
100
+ }
101
+
102
+ _connect() {
103
+ return new Promise((resolve, reject) => {
104
+ // Check the bluetooth state
105
+ if (this._noble.state !== 'poweredOn') {
106
+ reject(new Error('The Bluetooth status is ' + this._noble.state + ', not poweredOn.'));
107
+ return;
108
+ }
109
+
110
+ // Check the connection state
111
+ let state = this.connectionState;
112
+ if (state === 'connected') {
113
+ resolve();
114
+ return;
115
+ } else if (state === 'connecting' || state === 'disconnecting') {
116
+ reject(new Error('Now ' + state + '. Wait for a few seconds then try again.'));
117
+ return;
118
+ }
119
+
120
+ // Set event handlers for events fired on the `Peripheral` object
121
+ this._peripheral.once('connect', () => {
122
+ this._connected = true;
123
+ this._onconnect();
124
+ });
125
+
126
+ this._peripheral.once('disconnect', () => {
127
+ this._connected = false;
128
+ this._chars = null;
129
+ this._peripheral.removeAllListeners();
130
+ this._ondisconnect_internal();
131
+ this._ondisconnect();
132
+ });
133
+
134
+ // Connect
135
+ this._peripheral.connect((error) => {
136
+ if (error) {
137
+ reject(error);
138
+ return;
139
+ }
140
+ this._getCharacteristics().then((chars) => {
141
+ this._chars = chars;
142
+ return this._subscribe();
143
+ }).then(() => {
144
+ resolve();
145
+ }).catch((error) => {
146
+ this._peripheral.disconnect();
147
+ reject(error);
148
+ });
149
+ });
150
+ });
151
+ }
152
+
153
+ _getCharacteristics() {
154
+ return new Promise((resolve, reject) => {
155
+ // Set timeout timer
156
+ let timer = setTimeout(() => {
157
+ this._ondisconnect_internal = () => { };
158
+ timer = null;
159
+ reject(new Error('Failed to discover services and characteristics: TIMEOUT'));
160
+ }, 5000);
161
+
162
+ // Watch the connection state
163
+ this._ondisconnect_internal = () => {
164
+ if (timer) {
165
+ clearTimeout(timer);
166
+ timer = null;
167
+ this._ondisconnect_internal = () => { };
168
+ }
169
+ reject(new Error('Failed to discover services and characteristics: DISCONNECTED'));
170
+ };
171
+
172
+ // Discover services and characteristics
173
+ (async () => {
174
+ let service_list = await this._discoverServices();
175
+ if (!timer) {
176
+ throw new Error('');
177
+ }
178
+
179
+ let chars = {
180
+ write: null,
181
+ notify: null,
182
+ device: null
183
+ };
184
+
185
+ for (let service of service_list) {
186
+ let char_list = await this._discoverCharacteristics(service);
187
+ for (let char of char_list) {
188
+ if (char.uuid === this._CHAR_UUID_WRITE) {
189
+ chars.write = char;
190
+ } else if (char.uuid === this._CHAR_UUID_NOTIFY) {
191
+ chars.notify = char;
192
+ } else if (char.uuid === this._CHAR_UUID_DEVICE) {
193
+ // Some models of Bot don't seem to support this characteristic UUID
194
+ chars.device = char;
195
+ }
196
+ }
197
+ }
198
+
199
+ if (chars.write && chars.notify) {
200
+ resolve(chars);
201
+ } else {
202
+ reject(new Error('No characteristic was found.'));
203
+ }
204
+
205
+ })().catch((error) => {
206
+ if (timer) {
207
+ clearTimeout(timer);
208
+ timer = null;
209
+ this._ondisconnect_internal = () => { };
210
+ reject(error);
211
+ } else {
212
+ // Do nothing
213
+ }
214
+ });
215
+ });
216
+ }
217
+
218
+ _discoverServices() {
219
+ return new Promise((resolve, reject) => {
220
+ this._peripheral.discoverServices([], (error, service_list) => {
221
+ if (error) {
222
+ reject(error);
223
+ return;
224
+ }
225
+
226
+ let service = null;
227
+ for (let s of service_list) {
228
+ if (s.uuid === this._SERV_UUID_PRIMARY) {
229
+ service = s;
230
+ break;
231
+ }
232
+ }
233
+ if (service) {
234
+ resolve(service_list);
235
+ } else {
236
+ reject(new Error('No service was found.'));
237
+ }
238
+ });
239
+ });
240
+ }
241
+
242
+ _discoverCharacteristics(service) {
243
+ return new Promise((resolve, reject) => {
244
+ service.discoverCharacteristics([], (error, char_list) => {
245
+ if (error) {
246
+ reject(error);
247
+ } else {
248
+ resolve(char_list);
249
+ }
250
+ });
251
+ });
252
+ }
253
+
254
+ _subscribe() {
255
+ return new Promise((resolve, reject) => {
256
+ let char = this._chars.notify;
257
+ if (!char) {
258
+ reject(new Error('No notify characteristic was found.'));
259
+ return;
260
+ }
261
+ char.subscribe((error) => {
262
+ if (error) {
263
+ reject(error);
264
+ return;
265
+ }
266
+ char.on('data', (buf) => {
267
+ this._onnotify_internal(buf);
268
+ });
269
+ resolve();
270
+ })
271
+ });
272
+ }
273
+
274
+ _unsubscribe() {
275
+ return new Promise((resolve) => {
276
+ let char = this._chars.notify;
277
+ if (!char) {
278
+ resolve();
279
+ return;
280
+ }
281
+ char.removeAllListeners();
282
+ char.unsubscribe(() => {
283
+ resolve();
284
+ });
285
+ });
286
+ }
287
+
288
+ /* ------------------------------------------------------------------
289
+ * disconnect()
290
+ * - Disconnect the device
291
+ *
292
+ * [Arguments]
293
+ * - none
294
+ *
295
+ * [Returen value]
296
+ * - Promise object
297
+ * Nothing will be passed to the `resolve()`.
298
+ * ---------------------------------------------------------------- */
299
+ disconnect() {
300
+ return new Promise((resolve, reject) => {
301
+ this._was_connected_explicitly = false;
302
+ // Check the connection state
303
+ let state = this._peripheral.state;
304
+ if (state === 'disconnected') {
305
+ resolve();
306
+ return;
307
+ } else if (state === 'connecting' || state === 'disconnecting') {
308
+ reject(new Error('Now ' + state + '. Wait for a few seconds then try again.'));
309
+ return;
310
+ }
311
+
312
+ // Unsubscribe
313
+ this._unsubscribe().then(() => {
314
+ // Disconnect
315
+ this._peripheral.disconnect(() => {
316
+ resolve();
317
+ });
318
+ });
319
+ });
320
+ }
321
+
322
+ _disconnect() {
323
+ if (this._was_connected_explicitly) {
324
+ return new Promise((resolve, reject) => {
325
+ resolve();
326
+ });
327
+ } else {
328
+ return this.disconnect();
329
+ }
330
+ }
331
+
332
+ /* ------------------------------------------------------------------
333
+ * getDeviceName()
334
+ * - Retrieve the device name
335
+ *
336
+ * [Arguments]
337
+ * - none
338
+ *
339
+ * [Returen value]
340
+ * - Promise object
341
+ * The device name will be passed to the `resolve()`.
342
+ * ---------------------------------------------------------------- */
343
+ getDeviceName() {
344
+ return new Promise((resolve, reject) => {
345
+ let name = '';
346
+ this._connect().then(() => {
347
+ if (!this._chars.device) {
348
+ // Some models of Bot don't seem to support this characteristic UUID
349
+ throw new Error('The device does not support the characteristic UUID 0x' + this._CHAR_UUID_DEVICE + '.');
350
+ }
351
+ return this._read(this._chars.device);
352
+ }).then((buf) => {
353
+ name = buf.toString('utf8');
354
+ return this._disconnect();
355
+ }).then(() => {
356
+ resolve(name);
357
+ }).catch((error) => {
358
+ reject(error);
359
+ });
360
+ });
361
+ }
362
+
363
+ /* ------------------------------------------------------------------
364
+ * setDeviceName(name)
365
+ * - Set the device name
366
+ *
367
+ * [Arguments]
368
+ * - name | String | Required | Device name. The bytes length of the name
369
+ * | | | must be in the range of 1 to 20 bytes.
370
+ *
371
+ * [Returen value]
372
+ * - Promise object
373
+ * Nothing will be passed to the `resolve()`.
374
+ * ---------------------------------------------------------------- */
375
+ setDeviceName(name) {
376
+ return new Promise((resolve, reject) => {
377
+ // Check the parameters
378
+ let valid = parameterChecker.check({ name: name }, {
379
+ name: { required: true, type: 'string', minBytes: 1, maxBytes: 100 }
380
+ });
381
+
382
+ if (!valid) {
383
+ reject(new Error(parameterChecker.error.message));
384
+ return;
385
+ }
386
+
387
+ let buf = Buffer.from(name, 'utf8');
388
+ this._connect().then(() => {
389
+ if (!this._chars.device) {
390
+ // Some models of Bot don't seem to support this characteristic UUID
391
+ throw new Error('The device does not support the characteristic UUID 0x' + this._CHAR_UUID_DEVICE + '.');
392
+ }
393
+ return this._write(this._chars.device, buf);
394
+ }).then(() => {
395
+ return this._disconnect();
396
+ }).then(() => {
397
+ resolve();
398
+ }).catch((error) => {
399
+ reject(error);
400
+ });
401
+ });
402
+ }
403
+
404
+ // Write the specified Buffer data to the write characteristic
405
+ // and receive the response from the notify characteristic
406
+ // with connection handling
407
+ _command(req_buf) {
408
+ return new Promise((resolve, reject) => {
409
+ if (!Buffer.isBuffer(req_buf)) {
410
+ reject(new Error('The specified data is not acceptable for writing.'));
411
+ return;
412
+ }
413
+
414
+ let res_buf = null;
415
+
416
+ this._connect().then(() => {
417
+ return this._write(this._chars.write, req_buf);
418
+ }).then(() => {
419
+ return this._waitCommandResponse();
420
+ }).then((buf) => {
421
+ res_buf = buf;
422
+ return this._disconnect();
423
+ }).then(() => {
424
+ resolve(res_buf);
425
+ }).catch((error) => {
426
+ reject(error);
427
+ });
428
+ });
429
+ }
430
+
431
+ _waitCommandResponse() {
432
+ return new Promise((resolve, reject) => {
433
+ let timer = setTimeout(() => {
434
+ timer = null;
435
+ this._onnotify_internal = () => { };
436
+ reject(new Error('COMMAND_TIMEOUT'));
437
+ }, this._COMMAND_TIMEOUT_MSEC);
438
+
439
+ this._onnotify_internal = (buf) => {
440
+ if (timer) {
441
+ clearTimeout(timer);
442
+ timer = null;
443
+ }
444
+ this._onnotify_internal = () => { };
445
+ resolve(buf);
446
+ };
447
+ });
448
+ }
449
+
450
+ // Read data from the specified characteristic
451
+ _read(char) {
452
+ return new Promise((resolve, reject) => {
453
+ // Set a timeout timer
454
+ let timer = setTimeout(() => {
455
+ reject('READ_TIMEOUT');
456
+ }, this._READ_TIMEOUT_MSEC);
457
+
458
+ // Read charcteristic data
459
+ char.read((error, buf) => {
460
+ if (timer) {
461
+ clearTimeout(timer);
462
+ timer = null;
463
+ }
464
+ if (error) {
465
+ reject(error);
466
+ } else {
467
+ resolve(buf);
468
+ }
469
+ });
470
+ });
471
+ }
472
+
473
+ // Write the specified Buffer data to the specified characteristic
474
+ _write(char, buf) {
475
+ return new Promise((resolve, reject) => {
476
+ // Set a timeout timer
477
+ let timer = setTimeout(() => {
478
+ reject('WRITE_TIMEOUT');
479
+ }, this._WRITE_TIMEOUT_MSEC);
480
+
481
+ // write charcteristic data
482
+ char.write(buf, false, (error) => {
483
+ if (timer) {
484
+ clearTimeout(timer);
485
+ timer = null;
486
+ }
487
+ if (error) {
488
+ reject(error);
489
+ } else {
490
+ resolve();
491
+ }
492
+ });
493
+ });
494
+ }
495
+
496
+ }
497
+
498
+ module.exports = SwitchbotDevice;