node-switchbot 2.4.0 → 2.5.0-beta.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.
Files changed (180) hide show
  1. package/.github/npm-version-script.cjs +81 -0
  2. package/CHANGELOG.md +1 -1
  3. package/dist/advertising.d.ts +14 -148
  4. package/dist/advertising.d.ts.map +1 -1
  5. package/dist/advertising.js +36 -92
  6. package/dist/advertising.js.map +1 -1
  7. package/dist/device/woblindtilt.d.ts +98 -13
  8. package/dist/device/woblindtilt.d.ts.map +1 -1
  9. package/dist/device/woblindtilt.js +192 -86
  10. package/dist/device/woblindtilt.js.map +1 -1
  11. package/dist/device/woblindtilt.test.d.ts +2 -0
  12. package/dist/device/woblindtilt.test.d.ts.map +1 -0
  13. package/dist/device/woblindtilt.test.js +26 -0
  14. package/dist/device/woblindtilt.test.js.map +1 -0
  15. package/dist/device/wobulb.d.ts +17 -34
  16. package/dist/device/wobulb.d.ts.map +1 -1
  17. package/dist/device/wobulb.js +53 -61
  18. package/dist/device/wobulb.js.map +1 -1
  19. package/dist/device/wobulb.test.d.ts +2 -0
  20. package/dist/device/wobulb.test.d.ts.map +1 -0
  21. package/dist/device/wobulb.test.js +52 -0
  22. package/dist/device/wobulb.test.js.map +1 -0
  23. package/dist/device/woceilinglight.d.ts +21 -51
  24. package/dist/device/woceilinglight.d.ts.map +1 -1
  25. package/dist/device/woceilinglight.js +53 -64
  26. package/dist/device/woceilinglight.js.map +1 -1
  27. package/dist/device/woceilinglight.test.d.ts +2 -0
  28. package/dist/device/woceilinglight.test.d.ts.map +1 -0
  29. package/dist/device/woceilinglight.test.js +63 -0
  30. package/dist/device/woceilinglight.test.js.map +1 -0
  31. package/dist/device/wocontact.d.ts +1 -14
  32. package/dist/device/wocontact.d.ts.map +1 -1
  33. package/dist/device/wocontact.js +13 -13
  34. package/dist/device/wocontact.js.map +1 -1
  35. package/dist/device/wocontact.test.d.ts +2 -0
  36. package/dist/device/wocontact.test.d.ts.map +1 -0
  37. package/dist/device/wocontact.test.js +34 -0
  38. package/dist/device/wocontact.test.js.map +1 -0
  39. package/dist/device/wocurtain.d.ts +5 -16
  40. package/dist/device/wocurtain.d.ts.map +1 -1
  41. package/dist/device/wocurtain.js +58 -47
  42. package/dist/device/wocurtain.js.map +1 -1
  43. package/dist/device/wocurtain.test.d.ts +2 -0
  44. package/dist/device/wocurtain.test.d.ts.map +1 -0
  45. package/dist/device/wocurtain.test.js +33 -0
  46. package/dist/device/wocurtain.test.js.map +1 -0
  47. package/dist/device/wohand.d.ts +2 -10
  48. package/dist/device/wohand.d.ts.map +1 -1
  49. package/dist/device/wohand.js +31 -33
  50. package/dist/device/wohand.js.map +1 -1
  51. package/dist/device/wohand.test.d.ts +2 -0
  52. package/dist/device/wohand.test.d.ts.map +1 -0
  53. package/dist/device/wohand.test.js +62 -0
  54. package/dist/device/wohand.test.js.map +1 -0
  55. package/dist/device/wohand2.test.d.ts +2 -0
  56. package/dist/device/wohand2.test.d.ts.map +1 -0
  57. package/dist/device/wohand2.test.js +50 -0
  58. package/dist/device/wohand2.test.js.map +1 -0
  59. package/dist/device/wohub2.d.ts +1 -19
  60. package/dist/device/wohub2.d.ts.map +1 -1
  61. package/dist/device/wohub2.js +6 -2
  62. package/dist/device/wohub2.js.map +1 -1
  63. package/dist/device/wohumi.d.ts +2 -11
  64. package/dist/device/wohumi.d.ts.map +1 -1
  65. package/dist/device/wohumi.js +31 -33
  66. package/dist/device/wohumi.js.map +1 -1
  67. package/dist/device/wohumi.test.d.ts +2 -0
  68. package/dist/device/wohumi.test.d.ts.map +1 -0
  69. package/dist/device/wohumi.test.js +61 -0
  70. package/dist/device/wohumi.test.js.map +1 -0
  71. package/dist/device/woiosensorth.d.ts +1 -19
  72. package/dist/device/woiosensorth.d.ts.map +1 -1
  73. package/dist/device/woiosensorth.js +14 -10
  74. package/dist/device/woiosensorth.js.map +1 -1
  75. package/dist/device/woiosensorth.test.d.ts +2 -0
  76. package/dist/device/woiosensorth.test.d.ts.map +1 -0
  77. package/dist/device/woiosensorth.test.js +39 -0
  78. package/dist/device/woiosensorth.test.js.map +1 -0
  79. package/dist/device/woplugmini.d.ts +12 -38
  80. package/dist/device/woplugmini.d.ts.map +1 -1
  81. package/dist/device/woplugmini.js +35 -40
  82. package/dist/device/woplugmini.js.map +1 -1
  83. package/dist/device/woplugmini.test.d.ts +2 -0
  84. package/dist/device/woplugmini.test.d.ts.map +1 -0
  85. package/dist/device/woplugmini.test.js +91 -0
  86. package/dist/device/woplugmini.test.js.map +1 -0
  87. package/dist/device/wopresence.d.ts +2 -14
  88. package/dist/device/wopresence.d.ts.map +1 -1
  89. package/dist/device/wopresence.js +11 -7
  90. package/dist/device/wopresence.js.map +1 -1
  91. package/dist/device/wopresence.test.d.ts +2 -0
  92. package/dist/device/wopresence.test.d.ts.map +1 -0
  93. package/dist/device/wopresence.test.js +42 -0
  94. package/dist/device/wopresence.test.js.map +1 -0
  95. package/dist/device/wosensorth.d.ts +3 -37
  96. package/dist/device/wosensorth.d.ts.map +1 -1
  97. package/dist/device/wosensorth.js +24 -20
  98. package/dist/device/wosensorth.js.map +1 -1
  99. package/dist/device/wosensorth.test.d.ts +2 -0
  100. package/dist/device/wosensorth.test.d.ts.map +1 -0
  101. package/dist/device/wosensorth.test.js +59 -0
  102. package/dist/device/wosensorth.test.js.map +1 -0
  103. package/dist/device/wosmartlock.d.ts +19 -33
  104. package/dist/device/wosmartlock.d.ts.map +1 -1
  105. package/dist/device/wosmartlock.js +104 -83
  106. package/dist/device/wosmartlock.js.map +1 -1
  107. package/dist/device/wosmartlock.test.d.ts +2 -0
  108. package/dist/device/wosmartlock.test.d.ts.map +1 -0
  109. package/dist/device/wosmartlock.test.js +84 -0
  110. package/dist/device/wosmartlock.test.js.map +1 -0
  111. package/dist/device/wosmartlockpro.d.ts +13 -27
  112. package/dist/device/wosmartlockpro.d.ts.map +1 -1
  113. package/dist/device/wosmartlockpro.js +90 -70
  114. package/dist/device/wosmartlockpro.js.map +1 -1
  115. package/dist/device/wosmartlockpro.test.d.ts +2 -0
  116. package/dist/device/wosmartlockpro.test.d.ts.map +1 -0
  117. package/dist/device/wosmartlockpro.test.js +124 -0
  118. package/dist/device/wosmartlockpro.test.js.map +1 -0
  119. package/dist/device/wostrip.d.ts +18 -32
  120. package/dist/device/wostrip.d.ts.map +1 -1
  121. package/dist/device/wostrip.js +58 -70
  122. package/dist/device/wostrip.js.map +1 -1
  123. package/dist/device/wostrip.test.d.ts +2 -0
  124. package/dist/device/wostrip.test.d.ts.map +1 -0
  125. package/dist/device/wostrip.test.js +115 -0
  126. package/dist/device/wostrip.test.js.map +1 -0
  127. package/dist/device.d.ts +237 -39
  128. package/dist/device.d.ts.map +1 -1
  129. package/dist/device.js +478 -366
  130. package/dist/device.js.map +1 -1
  131. package/dist/index.d.ts +1 -1
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +1 -1
  134. package/dist/index.js.map +1 -1
  135. package/dist/parameter-checker.d.ts.map +1 -1
  136. package/dist/parameter-checker.js.map +1 -1
  137. package/dist/settings.d.ts +8 -0
  138. package/dist/settings.d.ts.map +1 -0
  139. package/dist/settings.js +12 -0
  140. package/dist/settings.js.map +1 -0
  141. package/dist/switchbot.d.ts +30 -10
  142. package/dist/switchbot.d.ts.map +1 -1
  143. package/dist/switchbot.js +39 -86
  144. package/dist/switchbot.js.map +1 -1
  145. package/dist/types/bledevicestatus.d.ts +288 -0
  146. package/dist/types/bledevicestatus.d.ts.map +1 -0
  147. package/dist/types/bledevicestatus.js +2 -0
  148. package/dist/types/bledevicestatus.js.map +1 -0
  149. package/dist/types/devicelist.d.ts +89 -0
  150. package/dist/types/devicelist.d.ts.map +1 -0
  151. package/dist/types/devicelist.js +6 -0
  152. package/dist/types/devicelist.js.map +1 -0
  153. package/dist/types/deviceresponse.d.ts +13 -0
  154. package/dist/types/deviceresponse.d.ts.map +1 -0
  155. package/dist/types/deviceresponse.js +2 -0
  156. package/dist/types/deviceresponse.js.map +1 -0
  157. package/dist/types/devicestatus.d.ts +149 -0
  158. package/dist/types/devicestatus.d.ts.map +1 -0
  159. package/dist/types/devicestatus.js +3 -0
  160. package/dist/types/devicestatus.js.map +1 -0
  161. package/dist/types/devicewebhookstatus.d.ts +156 -0
  162. package/dist/types/devicewebhookstatus.d.ts.map +1 -0
  163. package/dist/types/devicewebhookstatus.js +2 -0
  164. package/dist/types/devicewebhookstatus.js.map +1 -0
  165. package/dist/types/irdevicelist.d.ts +10 -0
  166. package/dist/types/irdevicelist.d.ts.map +1 -0
  167. package/dist/types/irdevicelist.js +2 -0
  168. package/dist/types/irdevicelist.js.map +1 -0
  169. package/dist/types/pushbody.d.ts +6 -0
  170. package/dist/types/pushbody.d.ts.map +1 -0
  171. package/dist/types/pushbody.js +2 -0
  172. package/dist/types/pushbody.js.map +1 -0
  173. package/dist/{types.d.ts → types/types.d.ts} +9 -0
  174. package/dist/types/types.d.ts.map +1 -0
  175. package/dist/types/types.js.map +1 -0
  176. package/jest.config.js +3 -0
  177. package/package.json +22 -14
  178. package/dist/types.d.ts.map +0 -1
  179. package/dist/types.js.map +0 -1
  180. /package/dist/{types.js → types/types.js} +0 -0
package/dist/device.js CHANGED
@@ -1,51 +1,46 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { Advertising } from './advertising.js';
3
3
  import { parameterChecker } from './parameter-checker.js';
4
+ import { CHAR_UUID_DEVICE, CHAR_UUID_NOTIFY, CHAR_UUID_WRITE, COMMAND_TIMEOUT_MSEC, READ_TIMEOUT_MSEC, SERV_UUID_PRIMARY, WRITE_TIMEOUT_MSEC, } from './settings.js';
4
5
  export class SwitchbotDevice {
5
- _peripheral;
6
6
  _noble;
7
- _chars;
8
- _SERV_UUID_PRIMARY = 'cba20d00224d11e69fb80002a5d5c51b';
9
- _CHAR_UUID_WRITE = 'cba20002224d11e69fb80002a5d5c51b';
10
- _CHAR_UUID_NOTIFY = 'cba20003224d11e69fb80002a5d5c51b';
11
- _CHAR_UUID_DEVICE = '2a00';
12
- _READ_TIMEOUT_MSEC = 3000;
13
- _WRITE_TIMEOUT_MSEC = 3000;
14
- _COMMAND_TIMEOUT_MSEC = 3000;
7
+ _peripheral;
8
+ _characteristics;
15
9
  _id;
16
10
  _address;
17
11
  _model;
18
12
  _modelName;
19
- _was_connected_explicitly;
13
+ _explicitly;
20
14
  _connected;
21
- _onconnect;
22
- _ondisconnect;
23
- _ondisconnect_internal;
24
- _onnotify_internal;
25
- /* ------------------------------------------------------------------
26
- * Constructor
15
+ onnotify_internal;
16
+ ondisconnect_internal;
17
+ onconnect_internal;
18
+ /**
19
+ * Constructor for the Device class.
20
+ *
21
+ * @param {object} peripheral - The `peripheral` object from noble, representing this device.
22
+ * @param {Noble} noble - The Noble object created by the noble module.
27
23
  *
28
- * [Arguments]
29
- * - peripheral | Object | Required | The `peripheral` object of noble,
30
- * | | | which represents this device
31
- * - noble | Noble | Required | The Noble object created by the noble module.
32
- * ---------------------------------------------------------------- */
24
+ * This constructor initializes a new instance of the Device class with the specified peripheral and noble objects.
25
+ */
33
26
  constructor(peripheral, noble) {
34
27
  this._peripheral = peripheral;
35
28
  this._noble = noble;
36
- this._chars = null;
29
+ this._characteristics = null;
37
30
  // Save the device information
38
31
  const ad = Advertising.parse(peripheral);
39
32
  this._id = ad ? ad.id : null;
40
33
  this._address = ad ? ad.address : null;
41
34
  this._model = ad ? ad.serviceData.model : null;
42
35
  this._modelName = ad ? ad.serviceData.modelName : null;
43
- this._was_connected_explicitly = false;
36
+ this._explicitly = false;
44
37
  this._connected = false;
45
- this._onconnect = () => { };
46
- this._ondisconnect = () => { };
47
- this._ondisconnect_internal = () => { };
48
- this._onnotify_internal = () => { };
38
+ this._explicitly = false;
39
+ this._connected = false;
40
+ this.onconnect = () => { };
41
+ this.ondisconnect = () => { };
42
+ this.ondisconnect_internal = () => { };
43
+ this.onnotify_internal = () => { };
49
44
  }
50
45
  // Getters
51
46
  get id() {
@@ -68,306 +63,343 @@ export class SwitchbotDevice {
68
63
  return this._peripheral.state;
69
64
  }
70
65
  }
71
- // Setters
72
66
  get onconnect() {
73
- return this._onconnect;
67
+ return this.onconnect_internal;
74
68
  }
69
+ /**
70
+ * Sets the asynchronous connection handler.
71
+ *
72
+ * This setter assigns a function to be used as the asynchronous connection handler. The handler
73
+ * is expected to be a function that returns a Promise, which resolves once the connection process
74
+ * is complete. The resolution of the Promise does not carry any value.
75
+ *
76
+ * @param func A function that returns a Promise, representing the asynchronous operation of connecting.
77
+ * This function is expected to be called without any arguments.
78
+ * @throws Error if the provided argument is not a function, ensuring type safety.
79
+ */
75
80
  set onconnect(func) {
76
81
  if (!func || typeof func !== 'function') {
77
- throw new Error('The `onconnect` must be a function.');
82
+ throw new Error('The `onconnect` must be a function that returns a Promise<void>.');
78
83
  }
79
- this._onconnect = func;
84
+ this.onconnect_internal = async () => {
85
+ await func();
86
+ };
80
87
  }
81
88
  get ondisconnect() {
82
- return this._ondisconnect;
89
+ return this.ondisconnect_internal;
83
90
  }
91
+ /**
92
+ * Sets the asynchronous disconnection handler.
93
+ *
94
+ * This setter configures a function to act as the asynchronous disconnection handler. The handler
95
+ * should be a function that returns a Promise, which resolves when the disconnection process
96
+ * is complete. The resolution of the Promise does not carry any value.
97
+ *
98
+ * @param func A function that returns a Promise, representing the asynchronous operation of disconnecting.
99
+ * This function is expected to be called without any arguments.
100
+ * @throws Error if the provided argument is not a function, to ensure that the handler is correctly typed.
101
+ */
84
102
  set ondisconnect(func) {
85
103
  if (!func || typeof func !== 'function') {
86
- throw new Error('The `ondisconnect` must be a function.');
104
+ throw new Error('The `ondisconnect` must be a function that returns a Promise<void>.');
87
105
  }
88
- this._ondisconnect = func;
106
+ this.ondisconnect_internal = async () => {
107
+ await func();
108
+ };
89
109
  }
90
- /* ------------------------------------------------------------------
91
- * connect()
92
- * - Connect the device
110
+ /**
111
+ * Initiates an asynchronous connection process.
93
112
  *
94
- * [Arguments]
95
- * - none
113
+ * This method marks the device as being connected explicitly by setting a flag, then proceeds
114
+ * to initiate the actual connection process by calling an internal asynchronous method `connect_internalAsync`.
115
+ * The `connect_internalAsync` method is responsible for handling the low-level connection logic.
96
116
  *
97
- * [Return value]
98
- * - Promise object
99
- * Nothing will be passed to the `resolve()`.
100
- * ---------------------------------------------------------------- */
101
- connect() {
102
- this._was_connected_explicitly = true;
103
- return this._connect();
117
+ * @returns A Promise that resolves when the connection process initiated by `connect_internalAsync` completes.
118
+ * The resolution of this Promise does not carry any value, indicating that the focus is on
119
+ * the completion of the connection process rather than the result of the connection itself.
120
+ */
121
+ async connect() {
122
+ this._explicitly = true;
123
+ return await this.connect_internal();
104
124
  }
105
- _connect() {
106
- return new Promise((resolve, reject) => {
107
- // Check the bluetooth state
108
- if (this._noble._state !== 'poweredOn') {
109
- reject(new Error(`The Bluetooth status is ${this._noble._state}, not poweredOn.`));
110
- return;
111
- }
112
- // Check the connection state
113
- const state = this.connectionState;
114
- if (state === 'connected') {
115
- resolve();
116
- return;
125
+ /**
126
+ * Initiates the device connection process.
127
+ *
128
+ * This method is marked as deprecated and is scheduled for removal in a future version. It is recommended
129
+ * to use `disconnectAsync()` instead. The method returns a Promise that resolves when the device is successfully
130
+ * connected or rejects if the connection cannot be established.
131
+ *
132
+ * The connection process involves several steps:
133
+ * 1. Checks the Bluetooth adapter's state. If it's not powered on, the promise is rejected.
134
+ * 2. Checks the current connection state of the device. If already connected, the promise resolves immediately.
135
+ * If in the process of connecting or disconnecting, the promise is rejected advising to wait.
136
+ * 3. Sets up event handlers for 'connect' and 'disconnect' events on the peripheral device to manage connection state.
137
+ * 4. Initiates the connection. If an error occurs during this step, the promise is rejected.
138
+ * 5. Once connected, retrieves the device's characteristics and subscribes to them. If this process fails, the device
139
+ * is disconnected, and the promise is rejected.
140
+ *
141
+ * @returns A Promise<void> that resolves when the device is connected or rejects with an error.
142
+ */
143
+ async connect_internal() {
144
+ // Check the bluetooth state
145
+ if (this._noble._state !== 'poweredOn') {
146
+ throw new Error(`The Bluetooth status is ${this._noble._state}, not poweredOn.`);
147
+ }
148
+ // Check the connection state
149
+ const state = this.connectionState;
150
+ if (state === 'connected') {
151
+ return;
152
+ }
153
+ else if (state === 'connecting' || state === 'disconnecting') {
154
+ throw new Error(`Now ${state}. Wait for a few seconds then try again.`);
155
+ }
156
+ // Set event handlers for events fired on the `Peripheral` object
157
+ this._peripheral.once('connect', async () => {
158
+ this._connected = true;
159
+ await this.onconnect();
160
+ });
161
+ this._peripheral.once('disconnect', async () => {
162
+ this._connected = false;
163
+ this._characteristics = null;
164
+ this._peripheral.removeAllListeners();
165
+ try {
166
+ await this.ondisconnect_internal();
167
+ await this.ondisconnect();
117
168
  }
118
- else if (state === 'connecting' || state === 'disconnecting') {
119
- reject(new Error(`Now ${state}. Wait for a few seconds then try again.`));
120
- return;
169
+ catch (error) {
170
+ throw new Error('Error during disconnect:', error);
121
171
  }
122
- // Set event handlers for events fired on the `Peripheral` object
123
- this._peripheral.once('connect', () => {
124
- this._connected = true;
125
- this._onconnect();
126
- });
127
- this._peripheral.once('disconnect', () => {
128
- this._connected = false;
129
- this._chars = null;
130
- this._peripheral.removeAllListeners();
131
- this._ondisconnect_internal();
132
- this._ondisconnect();
133
- });
134
- // Connect
135
- this._peripheral.connect((error) => {
136
- if (error) {
137
- reject(error);
138
- return;
139
- }
140
- this._getCharacteristics()
141
- .then((chars) => {
142
- this._chars = chars;
143
- return this._subscribe();
144
- })
145
- .then(() => {
146
- resolve();
147
- })
148
- .catch((error) => {
149
- this._peripheral.disconnect();
150
- reject(error);
151
- });
152
- });
153
172
  });
173
+ // Connect
174
+ await this._peripheral.connectAsync();
175
+ const chars = await this.getCharacteristics();
176
+ this._characteristics = chars;
177
+ await this.subscribe();
154
178
  }
155
- _getCharacteristics() {
156
- return new Promise((resolve, reject) => {
157
- // Set timeout timer
158
- let timer = setTimeout(() => {
159
- this._ondisconnect_internal = () => { };
160
- timer = null;
161
- reject(new Error('Failed to discover services and characteristics: TIMEOUT'));
162
- }, 5000);
163
- // Watch the connection state
164
- this._ondisconnect_internal = () => {
165
- if (timer) {
166
- clearTimeout(timer);
167
- timer = null;
168
- this._ondisconnect_internal = () => { };
169
- }
170
- reject(new Error('Failed to discover services and characteristics: DISCONNECTED'));
171
- };
179
+ async getCharacteristics() {
180
+ // Set timeout timer
181
+ let timer = setTimeout(async () => {
182
+ await this.ondisconnect_internal();
183
+ timer = null;
184
+ throw new Error('Failed to discover services and characteristics: TIMEOUT');
185
+ }, 5000);
186
+ try {
172
187
  // Discover services and characteristics
173
- (async () => {
174
- const service_list = await this._discoverServices();
175
- if (!timer) {
176
- throw new Error('Service discovery timeout or disconnection occurred.');
177
- }
178
- const chars = {
179
- write: null,
180
- notify: null,
181
- device: null,
182
- };
183
- for (const service of service_list) {
184
- const char_list = await this._discoverCharacteristics(service);
185
- for (const char of char_list) {
186
- if (char.uuid === this._CHAR_UUID_WRITE) {
187
- chars.write = char;
188
- }
189
- else if (char.uuid === this._CHAR_UUID_NOTIFY) {
190
- chars.notify = char;
191
- }
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
- }
188
+ const service_list = await this.discoverServices();
189
+ if (!timer) {
190
+ throw new Error('Failed to discover services and characteristics.');
191
+ }
192
+ const chars = {
193
+ write: null,
194
+ notify: null,
195
+ device: null,
196
+ };
197
+ for (const service of service_list) {
198
+ const char_list = await this.discoverCharacteristics(service);
199
+ for (const char of char_list) {
200
+ if (char.uuid === CHAR_UUID_WRITE) {
201
+ chars.write = char;
196
202
  }
197
- }
198
- if (chars.write && chars.notify) {
199
- resolve(chars);
200
- }
201
- else {
202
- reject(new Error('No characteristic was found.'));
203
- }
204
- })().catch((error) => {
205
- if (timer) {
206
- clearTimeout(timer);
207
- timer = null;
208
- this._ondisconnect_internal = () => { };
209
- reject(error);
210
- }
211
- else {
212
- // Do nothing
213
- }
214
- });
215
- });
216
- }
217
- _discoverServices() {
218
- return new Promise((resolve, reject) => {
219
- this._peripheral.discoverServices([], (error, service_list) => {
220
- if (error) {
221
- reject(error);
222
- return;
223
- }
224
- let service = null;
225
- for (const s of service_list) {
226
- if (s.uuid === this._SERV_UUID_PRIMARY) {
227
- service = s;
228
- break;
203
+ else if (char.uuid === CHAR_UUID_NOTIFY) {
204
+ chars.notify = char;
205
+ }
206
+ else if (char.uuid === CHAR_UUID_DEVICE) {
207
+ // Some models of Bot don't seem to support this characteristic UUID
208
+ chars.device = char;
229
209
  }
230
210
  }
231
- if (service) {
232
- resolve(service_list);
233
- }
234
- else {
235
- reject(new Error('No service was found.'));
236
- }
237
- });
238
- });
211
+ }
212
+ if (!chars.write || !chars.notify) {
213
+ throw new Error('No characteristic was found.');
214
+ }
215
+ return chars;
216
+ }
217
+ catch (error) {
218
+ if (timer) {
219
+ clearTimeout(timer);
220
+ this.ondisconnect_internal = () => { };
221
+ }
222
+ throw error;
223
+ }
224
+ finally {
225
+ if (timer) {
226
+ clearTimeout(timer);
227
+ this.ondisconnect_internal = () => { };
228
+ }
229
+ }
239
230
  }
240
- _discoverCharacteristics(service) {
241
- return new Promise((resolve, reject) => {
242
- service.discoverCharacteristics([], (error, char_list) => {
243
- if (error) {
244
- reject(error);
245
- }
246
- else {
247
- resolve(char_list);
248
- }
249
- });
231
+ async discoverServices() {
232
+ return await this._peripheral.discoverServicesAsync([])
233
+ .then((service_list) => {
234
+ const services = service_list.filter((s) => s.uuid === SERV_UUID_PRIMARY);
235
+ if (services.length === 0) {
236
+ throw new Error('No service was found.');
237
+ }
238
+ return services;
239
+ })
240
+ .catch((error) => {
241
+ throw error;
250
242
  });
251
243
  }
252
- _subscribe() {
253
- return new Promise((resolve, reject) => {
254
- const char = this._chars ? this._chars.notify : null;
255
- if (!char) {
256
- reject(new Error('No notify characteristic was found.'));
257
- return;
258
- }
259
- char.subscribe((error) => {
260
- if (error) {
261
- reject(error);
262
- return;
263
- }
264
- char.on('data', (buf) => {
265
- this._onnotify_internal(buf);
266
- });
267
- resolve();
268
- });
244
+ /**
245
+ * Asynchronously discovers the characteristics of the specified service.
246
+ * This method is an asynchronous version of the deprecated `discoverCharacteristics` method.
247
+ * It attempts to discover the characteristics of the specified service and returns a Promise that resolves with the list of characteristics.
248
+ * If the discovery process fails, the Promise is rejected with an error.
249
+ * @param service The service object for which characteristics will be discovered.
250
+ * @returns A Promise that resolves with the list of characteristics or rejects with an error.
251
+ */
252
+ async discoverCharacteristics(service) {
253
+ return service.discoverCharacteristicsAsync([])
254
+ .then((char_list) => {
255
+ return char_list;
256
+ })
257
+ .catch((error) => {
258
+ throw error;
269
259
  });
270
260
  }
271
- _unsubscribe() {
272
- return new Promise((resolve) => {
273
- const char = this._chars ? this._chars.notify : null;
274
- if (!char) {
275
- resolve();
276
- return;
277
- }
278
- char.removeAllListeners();
279
- char.unsubscribe(() => {
280
- resolve();
261
+ async subscribe() {
262
+ const char = this._characteristics ? this._characteristics.notify : null;
263
+ if (!char) {
264
+ throw new Error('No notify characteristic was found.');
265
+ }
266
+ char.subscribeAsync()
267
+ .then(() => {
268
+ char.on('data', (buf) => {
269
+ this.onnotify_internal(buf);
281
270
  });
271
+ })
272
+ .catch((error) => {
273
+ throw error;
282
274
  });
283
275
  }
284
- /* ------------------------------------------------------------------
285
- * disconnect()
286
- * - Disconnect the device
276
+ /**
277
+ * Asynchronously unsubscribes from the device's notification characteristic.
287
278
  *
288
- * [Arguments]
289
- * - none
279
+ * This method checks if the notification characteristic object exists within the cached characteristics
280
+ * (`this.chars`). If the characteristic is found, it proceeds to remove all event listeners attached to it
281
+ * to prevent any further handling of incoming data notifications. Then, it asynchronously unsubscribes from
282
+ * the notification characteristic using `unsubscribeAsync()`, effectively stopping the device from sending
283
+ * notifications to the client.
290
284
  *
291
- * [Return value]
292
- * - Promise object
293
- * Nothing will be passed to the `resolve()`.
294
- * ---------------------------------------------------------------- */
295
- disconnect() {
296
- return new Promise((resolve, reject) => {
297
- this._was_connected_explicitly = false;
298
- // Check the connection state
299
- const state = this._peripheral.state;
300
- if (state === 'disconnected') {
301
- resolve();
302
- return;
303
- }
304
- else if (state === 'connecting' || state === 'disconnecting') {
305
- reject(new Error(`Now ${state}. Wait for a few seconds then try again.`));
306
- return;
307
- }
308
- // Unsubscribe
309
- this._unsubscribe().then(() => {
310
- // Disconnect
311
- this._peripheral.disconnect(() => {
312
- resolve();
313
- });
314
- });
315
- });
285
+ * If the notification characteristic is not found, the method simply returns without performing any action.
286
+ *
287
+ * @return A Promise that resolves to `void` upon successful unsubscription or if the characteristic is not found.
288
+ */
289
+ async unsubscribe() {
290
+ const char = this._characteristics ? this._characteristics.notify : null;
291
+ if (!char) {
292
+ return;
293
+ }
294
+ char.removeAllListeners();
295
+ await char.unsubscribeAsync();
316
296
  }
317
- _disconnect() {
318
- if (this._was_connected_explicitly) {
319
- return Promise.resolve();
297
+ /**
298
+ * Asynchronously disconnects from the device.
299
+ *
300
+ * This method handles the disconnection process by first checking the current
301
+ * connection state of the device. If the device is already disconnected, the
302
+ * method resolves immediately. If the device is in the process of connecting or
303
+ * disconnecting, it throws an error indicating that the operation should be retried
304
+ * after a brief wait. Otherwise, it proceeds to unsubscribe from any subscriptions
305
+ * and then initiates the disconnection process.
306
+ *
307
+ * Note: This method sets a flag to indicate that the disconnection was not initiated
308
+ * by the user explicitly.
309
+ *
310
+ * @returns A Promise that resolves when the disconnection process has completed.
311
+ * The Promise does not pass any value upon resolution.
312
+ */
313
+ async disconnect() {
314
+ this._explicitly = false;
315
+ const state = this._peripheral.state;
316
+ if (state === 'disconnected') {
317
+ return; // Resolves the promise implicitly
320
318
  }
321
- else {
322
- return this.disconnect();
319
+ else if (state === 'connecting' || state === 'disconnecting') {
320
+ throw new Error(`Now ${state}. Wait for a few seconds then try again.`);
323
321
  }
322
+ await this.unsubscribe(); // Wait for unsubscribe to complete
323
+ await this._peripheral.disconnectAsync();
324
324
  }
325
- /* ------------------------------------------------------------------
326
- * getDeviceName()
327
- * - Retrieve the device name
325
+ /**
326
+ * Disconnects from the device asynchronously if the connection was not initiated by the user.
328
327
  *
329
- * [Arguments]
330
- * - none
328
+ * This method checks the `explicitly` flag to determine if the connection was initiated by the user.
329
+ * If not, it proceeds to disconnect from the device by calling `this.disconnectAsync()`. After the disconnection,
330
+ * it sets `explicitly` to true to prevent future disconnections when the connection is user-initiated.
331
331
  *
332
- * [Return value]
333
- * - Promise object
334
- * The device name will be passed to the `resolve()`.
335
- * ---------------------------------------------------------------- */
336
- getDeviceName() {
337
- return new Promise((resolve, reject) => {
338
- let name = '';
339
- this._connect()
340
- .then(() => {
341
- if (!this._chars || !this._chars.device) {
342
- // Some models of Bot don't seem to support this characteristic UUID
343
- throw new Error(`The device does not support the characteristic UUID 0x${this._CHAR_UUID_DEVICE}.`);
344
- }
345
- return this._read(this._chars.device);
346
- })
347
- .then((buf) => {
348
- name = buf.toString('utf8');
349
- return this._disconnect();
350
- })
351
- .then(() => {
352
- resolve(name);
353
- })
354
- .catch((error) => {
355
- reject(error);
356
- });
332
+ * This approach ensures that automatic disconnections only occur when the user has not explicitly initiated the connection,
333
+ * avoiding unnecessary disconnections in user-initiated sessions.
334
+ *
335
+ * @returns A Promise that resolves once the device has been successfully disconnected, applicable only when the
336
+ * connection was not user-initiated.
337
+ */
338
+ async disconnect_internal() {
339
+ if (!this._explicitly) {
340
+ await this.disconnect();
341
+ this._explicitly = true; // Ensure this condition is updated to prevent re-entry or incorrect logic flow.
342
+ }
343
+ }
344
+ /**
345
+ * Asynchronously retrieves the device name.
346
+ * This method is designed to fetch the name of the device asynchronously and return it as a promise.
347
+ * It is marked as deprecated and will be removed in a future version. Use `getDeviceNameAsync` instead.
348
+ *
349
+ * @deprecated since version 2.4.0. Will be removed in version 3.0.0. Use `getDeviceNameAsync()` instead.
350
+ * @returns A Promise that resolves with the device name.
351
+ */
352
+ async getDeviceName() {
353
+ let name = '';
354
+ await this.connect_internal()
355
+ .then(async () => {
356
+ if (!this._characteristics || !this._characteristics.device) {
357
+ // Some models of Bot don't seem to support this characteristic UUID
358
+ throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`);
359
+ }
360
+ return await this.read(this._characteristics.device);
361
+ })
362
+ .then((buf) => {
363
+ name = buf.toString('utf8');
364
+ return this.disconnect_internal();
365
+ })
366
+ .then(() => {
367
+ return name;
368
+ })
369
+ .catch((error) => {
370
+ throw new Error(error);
357
371
  });
358
372
  }
359
- /* ------------------------------------------------------------------
360
- * setDeviceName(name)
361
- * - Set the device name
373
+ /**
374
+ * Asynchronously retrieves the device name.
375
+ * This method initiates a connection to the device, checks for the presence of a specific characteristic UUID,
376
+ * reads the characteristic to obtain the device name, and then disconnects from the device.
377
+ * It ensures that the device supports the required characteristic for fetching the name. If the characteristic
378
+ * is not supported, it throws an error. The method encapsulates the entire process of connecting, reading,
379
+ * and disconnecting, making it convenient to get the device name with a single call.
362
380
  *
363
- * [Arguments]
364
- * - name | String | Required | Device name. The bytes length of the name
365
- * | | | must be in the range of 1 to 20 bytes.
381
+ * @returns A Promise that resolves with the device name as a string.
382
+ */
383
+ async getDeviceNameAsnyc() {
384
+ await this.connect_internal();
385
+ if (!this._characteristics || !this._characteristics.device) {
386
+ throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`);
387
+ }
388
+ const buf = await this.read(this._characteristics.device);
389
+ const name = buf.toString('utf8');
390
+ await this.disconnect_internal();
391
+ return name;
392
+ }
393
+ /**
394
+ * Asynchronously sets the device name to the specified value.
395
+ * Validates the new name to ensure it meets the criteria of being a string with a byte length between 1 and 100 bytes.
396
+ * If the validation fails, the promise is rejected with an error detailing the validation issue.
397
+ * Upon successful validation, the device name is updated, and the promise resolves without passing any value.
366
398
  *
367
- * [Return value]
368
- * - Promise object
369
- * Nothing will be passed to the `resolve()`.
370
- * ---------------------------------------------------------------- */
399
+ * @param name The new device name as a string. Must be 1 to 100 bytes in length.
400
+ * @returns A Promise that resolves to `void` upon successful update of the device name.
401
+ * @deprecated since version 2.4.0. Will be removed in version 3.0.0. Use `setDeviceNameAsync()` instead.
402
+ */
371
403
  setDeviceName(name) {
372
404
  return new Promise((resolve, reject) => {
373
405
  // Check the parameters
@@ -379,116 +411,196 @@ export class SwitchbotDevice {
379
411
  return;
380
412
  }
381
413
  const buf = Buffer.from(name, 'utf8');
382
- this._connect()
414
+ this.connect_internal()
383
415
  .then(() => {
384
- if (!this._chars || !this._chars.device) {
416
+ if (!this._characteristics || !this._characteristics.device) {
385
417
  // Some models of Bot don't seem to support this characteristic UUID
386
- throw new Error(`The device does not support the characteristic UUID 0x${this._CHAR_UUID_DEVICE}.`);
418
+ throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`);
387
419
  }
388
- return this._write(this._chars.device, buf);
420
+ return this.write(this._characteristics.device, buf);
389
421
  })
390
422
  .then(() => {
391
- return this._disconnect();
423
+ return this.disconnect_internal();
392
424
  })
393
425
  .then(() => {
426
+ return;
394
427
  resolve();
395
428
  })
396
429
  .catch((error) => {
397
- reject(error);
430
+ throw new Error(error);
398
431
  });
399
432
  });
400
433
  }
401
- // Write the specified Buffer data to the write characteristic
402
- // and receive the response from the notify characteristic
403
- // with connection handling
404
- _command(req_buf) {
405
- return new Promise((resolve, reject) => {
406
- if (!Buffer.isBuffer(req_buf)) {
407
- reject(new Error('The specified data is not acceptable for writing.'));
408
- return;
409
- }
410
- let res_buf;
411
- this._connect()
412
- .then(() => {
413
- if (!this._chars || !this._chars.write) {
414
- return reject(new Error('No characteristics available.'));
415
- }
416
- return this._write(this._chars.write, req_buf);
417
- })
418
- .then(() => {
419
- return this._waitCommandResponse();
420
- })
421
- .then((buf) => {
422
- res_buf = buf;
423
- return this._disconnect();
424
- })
425
- .then(() => {
426
- resolve(res_buf);
427
- })
428
- .catch((error) => {
429
- reject(error);
430
- });
431
- });
434
+ /**
435
+ * Asynchronously sets the device name to the specified value.
436
+ * This method begins by validating the provided name to ensure it meets the criteria of being a string with a byte length between 1 and 100 bytes.
437
+ * If the name does not pass validation, the method throws an error with a message detailing the validation issue.
438
+ * After passing validation, the method converts the name into a UTF-8 encoded buffer.
439
+ * It then initiates a connection to the device. If the device does not support the required characteristic UUID for setting the device name,
440
+ * an error is thrown indicating the lack of support.
441
+ * Upon successfully connecting and verifying support, the method writes the new name to the device using the appropriate characteristic.
442
+ * Finally, it disconnects from the device, completing the name update process.
443
+ *
444
+ * @param name The new device name as a string. Must be 1 to 100 bytes in length.
445
+ * @returns A Promise that resolves to `void` upon successful update of the device name.
446
+ */
447
+ async setDeviceNameAsync(name) {
448
+ // Check the parameters
449
+ const valid = parameterChecker.check({ name }, {
450
+ name: { required: true, type: 'string', minBytes: 1, maxBytes: 100 },
451
+ }, true);
452
+ if (!valid) {
453
+ throw new Error(parameterChecker.error.message);
454
+ }
455
+ const buf = Buffer.from(name, 'utf8');
456
+ await this.connect_internal();
457
+ if (!this._characteristics || !this._characteristics.device) {
458
+ // Some models of Bot don't seem to support this characteristic UUID
459
+ throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`);
460
+ }
461
+ await this.write(this._characteristics.device, buf);
462
+ await this.disconnect_internal();
463
+ }
464
+ /**
465
+ * Asynchronously sends a command to the device and awaits a response.
466
+ * This method encapsulates the process of sending a command encapsulated in a Buffer to the device,
467
+ * and then waiting for the device to respond. The method ensures that the device is connected before sending the command,
468
+ * writes the command to the device's write characteristic, waits for a response on the notify characteristic,
469
+ * and finally disconnects from the device. The response from the device is returned as a Buffer.
470
+ *
471
+ * @param req_buf A Buffer containing the command to be sent to the device.
472
+ * @returns A Promise that resolves with a Buffer containing the device's response to the command.
473
+ */
474
+ async command(req_buf) {
475
+ if (!Buffer.isBuffer(req_buf)) {
476
+ throw new TypeError('The specified data is not acceptable for writing.');
477
+ }
478
+ await this.connect_internal();
479
+ if (!this._characteristics || !this._characteristics.write) {
480
+ throw new Error('No characteristics available.');
481
+ }
482
+ await this.write(this._characteristics.write, req_buf);
483
+ const res_buf = await this._waitCommandResponseAsync();
484
+ await this.disconnect_internal();
485
+ return res_buf;
432
486
  }
487
+ /**
488
+ * Waits for a response from the device after sending a command.
489
+ * This method sets up a promise that resolves when a response is received from the device or rejects if a timeout occurs.
490
+ * A timer is started to enforce a command timeout. If the response is received before the timeout, the timer is cleared.
491
+ * The method sets an internal handler (`onnotify_internal`) to process the incoming response.
492
+ * If the response is not received within the specified timeout period, the promise is rejected with a 'COMMAND_TIMEOUT' error.
493
+ * Once a response is received or a timeout occurs, the internal handler is reset to an empty function to prevent memory leaks.
494
+ * @deprecated since version 2.4.0. Will be removed in version 3.0.0. Use `_waitCommandResponseAsync()` instead.
495
+ *
496
+ * @returns A Promise that resolves with the received Buffer or rejects with an error if a timeout occurs.
497
+ */
433
498
  _waitCommandResponse() {
434
499
  return new Promise((resolve, reject) => {
435
500
  let timer = setTimeout(() => {
436
501
  timer = undefined;
437
- this._onnotify_internal = () => { };
502
+ this.onnotify_internal = () => { };
438
503
  reject(new Error('COMMAND_TIMEOUT'));
439
- }, this._COMMAND_TIMEOUT_MSEC);
440
- this._onnotify_internal = (buf) => {
504
+ }, COMMAND_TIMEOUT_MSEC);
505
+ this.onnotify_internal = (buf) => {
441
506
  if (timer) {
442
507
  clearTimeout(timer);
443
508
  timer = undefined;
444
509
  }
445
- this._onnotify_internal = () => { };
446
- resolve(buf);
510
+ this.onnotify_internal = () => { };
511
+ return buf;
447
512
  };
448
513
  });
449
514
  }
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(new Error('READ_TIMEOUT'));
456
- }, this._READ_TIMEOUT_MSEC);
457
- // Read characteristic data
458
- char.read((error, buf) => {
459
- if (timer) {
460
- clearTimeout(timer);
461
- timer = undefined;
462
- }
463
- if (error) {
464
- reject(error);
465
- }
466
- else {
467
- resolve(buf);
468
- }
469
- });
515
+ /**
516
+ * Asynchronously waits for a response from the device after sending a command.
517
+ * This method sets up a promise that resolves when a response is received from the device or rejects if a timeout occurs.
518
+ * A timer is started to enforce a command timeout. If the response is received before the timeout, the timer is cleared.
519
+ * The method sets an internal handler (`onnotify_internalAsync`) to process the incoming response.
520
+ * If the response is not received within the specified timeout period, the promise is rejected with a 'COMMAND_TIMEOUT' error.
521
+ * Once a response is received or a timeout occurs, the internal handler is reset to an empty function to prevent memory leaks.
522
+ *
523
+ * @returns A Promise that resolves with the received Buffer or rejects with an error if a timeout occurs.
524
+ */
525
+ async _waitCommandResponseAsync() {
526
+ const timeout = READ_TIMEOUT_MSEC; // Timeout period in milliseconds
527
+ let timer = null;
528
+ // Setup a timeout to reject the operation if it takes too long
529
+ const timeoutPromise = new Promise((_, reject) => {
530
+ timer = setTimeout(() => reject(new Error('READ_TIMEOUT')), timeout);
470
531
  });
532
+ // Setup the read operation promise
533
+ const readPromise = await this.onnotify_internal();
534
+ // Wait for either the read operation to complete or the timeout to occur
535
+ const result = await Promise.race([readPromise, timeoutPromise]);
536
+ // Clear the timeout if the read operation completes successfully
537
+ if (timer) {
538
+ clearTimeout(timer);
539
+ }
540
+ return result;
471
541
  }
472
- // Write the specified Buffer data to the specified characteristic
473
- _write(char, buf) {
474
- return new Promise((resolve, reject) => {
475
- // Set a timeout timer
476
- let timer = setTimeout(() => {
477
- reject(new Error('WRITE_TIMEOUT'));
478
- }, this._WRITE_TIMEOUT_MSEC);
479
- // write characteristic data
480
- char.write(buf, false, (error) => {
481
- if (timer) {
482
- clearTimeout(timer);
483
- timer = undefined;
484
- }
485
- if (error) {
486
- reject(error);
487
- }
488
- else {
489
- resolve();
490
- }
491
- });
542
+ /**
543
+ * Asynchronously reads data from the specified characteristic of the device with a timeout.
544
+ * This method attempts to read data from the device's characteristic and sets a timeout to handle cases where the read operation takes too long.
545
+ * If the read operation does not complete within the specified timeout period, an error is thrown.
546
+ * Once the read operation completes successfully, the timeout is cleared to prevent it from triggering.
547
+ *
548
+ * @param char The characteristic of the device from which data will be read.
549
+ * @returns A Promise that resolves with the data read from the characteristic or rejects with an error if a timeout occurs.
550
+ *
551
+ * @throws Error if the read operation fails or if a timeout occurs.
552
+ */
553
+ async read(char) {
554
+ let timer = setTimeout(() => {
555
+ throw new Error('READ_TIMEOUT');
556
+ }, READ_TIMEOUT_MSEC);
557
+ // Setup the read operation promise
558
+ const readPromise = await char.readAsync()
559
+ .then((result) => {
560
+ if (timer) {
561
+ clearTimeout(timer);
562
+ timer = undefined;
563
+ }
564
+ else {
565
+ throw new Error('READ_TIMEOUT');
566
+ }
567
+ return result;
568
+ });
569
+ // Wait for either the read operation to complete or the timeout to occur
570
+ const result = await Promise.race([readPromise, timer]);
571
+ // Clear the timeout if the read operation completes successfully
572
+ if (timer) {
573
+ clearTimeout(timer);
574
+ }
575
+ return result;
576
+ }
577
+ /**
578
+ * Asynchronously writes data to a specified characteristic of the device.
579
+ * This method sends a buffer of data to the device's characteristic and sets a timeout to handle cases where the write operation takes too long.
580
+ * If the write operation does not complete within the specified timeout period, an error is thrown.
581
+ * Once the write operation completes successfully, the timeout is cleared to prevent it from triggering.
582
+ *
583
+ * @param char The characteristic of the device to which the data will be written.
584
+ * @param buf A Buffer containing the data to be written to the device.
585
+ * @returns A Promise that resolves when the write operation completes successfully or rejects with an error if a timeout occurs.
586
+ */
587
+ async write(char, buf) {
588
+ let timer = setTimeout(() => {
589
+ throw new Error('WRITE_TIMEOUT');
590
+ }, WRITE_TIMEOUT_MSEC);
591
+ // write characteristic data
592
+ await char.writeAsync(buf, false)
593
+ .then(() => {
594
+ if (timer) {
595
+ clearTimeout(timer);
596
+ timer = undefined;
597
+ }
598
+ else {
599
+ throw new Error('READ_TIMEOUT');
600
+ }
601
+ })
602
+ .catch((error) => {
603
+ throw new Error(`WRITE_TIMEOUT, ${error}`);
492
604
  });
493
605
  }
494
606
  }