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.
- package/LICENSE +14 -0
- package/README.md +950 -0
- package/lib/parameter-checker.js +471 -0
- package/lib/switchbot-advertising.js +255 -0
- package/lib/switchbot-device-wocontact.js +16 -0
- package/lib/switchbot-device-wocurtain.js +109 -0
- package/lib/switchbot-device-wohand.js +106 -0
- package/lib/switchbot-device-wohumi.js +106 -0
- package/lib/switchbot-device-wopresence.js +16 -0
- package/lib/switchbot-device-wosensorth.js +16 -0
- package/lib/switchbot-device.js +498 -0
- package/lib/switchbot.js +366 -0
- package/package.json +42 -0
|
@@ -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;
|