node-switchbot 3.6.0-beta.0 → 3.6.0-beta.10

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 (317) hide show
  1. package/.github/copilot-instructions.md +165 -0
  2. package/CHANGELOG.md +26 -0
  3. package/dist/device.d.ts +247 -13
  4. package/dist/device.d.ts.map +1 -1
  5. package/dist/device.js +658 -78
  6. package/dist/device.js.map +1 -1
  7. package/dist/device.test.d.ts +2 -0
  8. package/dist/device.test.d.ts.map +1 -0
  9. package/dist/device.test.js +152 -0
  10. package/dist/device.test.js.map +1 -0
  11. package/dist/index.d.ts +4 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +6 -7
  14. package/dist/index.js.map +1 -1
  15. package/dist/parameter-checker.d.ts +1 -0
  16. package/dist/parameter-checker.d.ts.map +1 -1
  17. package/dist/parameter-checker.js +19 -11
  18. package/dist/parameter-checker.js.map +1 -1
  19. package/dist/parameter-checker.test.d.ts +2 -0
  20. package/dist/parameter-checker.test.d.ts.map +1 -0
  21. package/dist/parameter-checker.test.js +56 -0
  22. package/dist/parameter-checker.test.js.map +1 -0
  23. package/dist/settings.d.ts.map +1 -1
  24. package/dist/settings.js +21 -16
  25. package/dist/settings.js.map +1 -1
  26. package/dist/settings.test.js +13 -1
  27. package/dist/settings.test.js.map +1 -1
  28. package/dist/switchbot-ble.d.ts +8 -11
  29. package/dist/switchbot-ble.d.ts.map +1 -1
  30. package/dist/switchbot-ble.js +88 -73
  31. package/dist/switchbot-ble.js.map +1 -1
  32. package/dist/switchbot-ble.test.d.ts +2 -0
  33. package/dist/switchbot-ble.test.d.ts.map +1 -0
  34. package/dist/switchbot-ble.test.js +32 -0
  35. package/dist/switchbot-ble.test.js.map +1 -0
  36. package/dist/switchbot-openapi.d.ts +4 -7
  37. package/dist/switchbot-openapi.d.ts.map +1 -1
  38. package/dist/switchbot-openapi.js +19 -10
  39. package/dist/switchbot-openapi.js.map +1 -1
  40. package/dist/switchbot-openapi.test.d.ts +2 -0
  41. package/dist/switchbot-openapi.test.d.ts.map +1 -0
  42. package/dist/switchbot-openapi.test.js +36 -0
  43. package/dist/switchbot-openapi.test.js.map +1 -0
  44. package/dist/types/ble-guards.d.ts +12 -0
  45. package/dist/types/ble-guards.d.ts.map +1 -0
  46. package/dist/types/ble-guards.js +10 -0
  47. package/dist/types/ble-guards.js.map +1 -0
  48. package/dist/types/ble-guards.test.d.ts +2 -0
  49. package/dist/types/ble-guards.test.d.ts.map +1 -0
  50. package/dist/types/ble-guards.test.js +62 -0
  51. package/dist/types/ble-guards.test.js.map +1 -0
  52. package/dist/types/{bledevicestatus.d.ts → ble.d.ts} +135 -116
  53. package/dist/types/ble.d.ts.map +1 -0
  54. package/dist/types/ble.js +2 -0
  55. package/dist/types/ble.js.map +1 -0
  56. package/dist/types/openapi.d.ts +623 -0
  57. package/dist/types/openapi.d.ts.map +1 -0
  58. package/dist/types/openapi.js +3 -0
  59. package/dist/types/openapi.js.map +1 -0
  60. package/docs/assets/hierarchy.js +1 -1
  61. package/docs/assets/navigation.js +1 -1
  62. package/docs/assets/search.js +1 -1
  63. package/docs/classes/Advertising.html +4 -4
  64. package/docs/classes/ErrorUtils.html +25 -0
  65. package/docs/classes/ParameterChecker.html +69 -0
  66. package/docs/classes/SwitchBotBLE.html +9 -11
  67. package/docs/classes/SwitchBotOpenAPI.html +9 -9
  68. package/docs/classes/SwitchbotDevice.html +12 -12
  69. package/docs/classes/ValidationUtils.html +51 -0
  70. package/docs/classes/WoAirPurifier.html +83 -0
  71. package/docs/classes/WoAirPurifierTable.html +83 -0
  72. package/docs/classes/WoBlindTilt.html +21 -21
  73. package/docs/classes/WoBulb.html +19 -19
  74. package/docs/classes/WoCeilingLight.html +22 -22
  75. package/docs/classes/WoContact.html +13 -13
  76. package/docs/classes/WoCurtain.html +18 -18
  77. package/docs/classes/WoHand.html +18 -18
  78. package/docs/classes/WoHub2.html +13 -13
  79. package/docs/classes/WoHub3.html +52 -0
  80. package/docs/classes/WoHumi.html +20 -20
  81. package/docs/classes/WoHumi2.html +20 -20
  82. package/docs/classes/WoIOSensorTH.html +13 -13
  83. package/docs/classes/WoKeypad.html +13 -13
  84. package/docs/classes/WoLeak.html +13 -13
  85. package/docs/classes/WoPlugMiniJP.html +18 -18
  86. package/docs/classes/WoPlugMiniUS.html +18 -18
  87. package/docs/classes/WoPresence.html +13 -13
  88. package/docs/classes/WoRelaySwitch1.html +15 -15
  89. package/docs/classes/WoRelaySwitch1PM.html +15 -15
  90. package/docs/classes/WoRemote.html +13 -13
  91. package/docs/classes/WoSensorTH.html +12 -12
  92. package/docs/classes/WoSensorTHPlus.html +12 -12
  93. package/docs/classes/WoSensorTHPro.html +12 -12
  94. package/docs/classes/WoSensorTHProCO2.html +12 -12
  95. package/docs/classes/WoSmartLock.html +23 -23
  96. package/docs/classes/WoSmartLockPro.html +23 -23
  97. package/docs/classes/WoStrip.html +20 -20
  98. package/docs/enums/LogLevel.html +2 -2
  99. package/docs/enums/SwitchBotBLEModel.html +5 -2
  100. package/docs/enums/SwitchBotBLEModelFriendlyName.html +9 -2
  101. package/docs/enums/SwitchBotBLEModelName.html +5 -2
  102. package/docs/enums/SwitchBotModel.html +5 -2
  103. package/docs/functions/updateBaseURL.html +3 -0
  104. package/docs/hierarchy.html +1 -1
  105. package/docs/interfaces/AdvertisementData.html +2 -2
  106. package/docs/interfaces/Chars.html +2 -2
  107. package/docs/interfaces/ColorLightServiceDataBase.html +17 -0
  108. package/docs/interfaces/ErrorObject.html +2 -2
  109. package/docs/interfaces/LockBaseServiceData.html +15 -0
  110. package/docs/interfaces/NobleTypes.html +2 -3
  111. package/docs/interfaces/Params.html +2 -2
  112. package/docs/interfaces/PlugMiniServiceDataBase.html +12 -0
  113. package/docs/interfaces/Rule.html +2 -2
  114. package/docs/interfaces/ServiceData.html +2 -2
  115. package/docs/interfaces/SwitchBotBLEDevice.html +5 -2
  116. package/docs/interfaces/SwitchBotScanner.html +6 -0
  117. package/docs/interfaces/TemperatureServiceDataBase.html +10 -0
  118. package/docs/interfaces/WebhookDetail.html +2 -2
  119. package/docs/interfaces/ad.html +2 -2
  120. package/docs/interfaces/body.html +2 -2
  121. package/docs/interfaces/bodyChange.html +2 -2
  122. package/docs/interfaces/deleteWebhookResponse.html +2 -2
  123. package/docs/interfaces/device.html +2 -2
  124. package/docs/interfaces/deviceList.html +2 -2
  125. package/docs/interfaces/deviceStatus.html +2 -2
  126. package/docs/interfaces/deviceStatusRequest.html +2 -2
  127. package/docs/interfaces/deviceWebhook.html +2 -2
  128. package/docs/interfaces/deviceWebhookContext.html +2 -2
  129. package/docs/interfaces/devices.html +2 -2
  130. package/docs/interfaces/infraredRemoteList.html +2 -2
  131. package/docs/interfaces/irdevice.html +2 -2
  132. package/docs/interfaces/pushRequest.html +5 -0
  133. package/docs/interfaces/pushResponse.html +3 -2
  134. package/docs/interfaces/pushResponseBody.html +3 -0
  135. package/docs/interfaces/queryWebhookResponse.html +2 -2
  136. package/docs/interfaces/setupWebhookResponse.html +2 -2
  137. package/docs/interfaces/updateWebhookResponse.html +2 -2
  138. package/docs/interfaces/webhookRequest.html +2 -2
  139. package/docs/modules.html +1 -1
  140. package/docs/types/BLEDeviceServiceData.html +1 -0
  141. package/docs/types/{indoorCam.html → IndoorCam.html} +1 -1
  142. package/docs/types/MacAddress.html +1 -1
  143. package/docs/types/airPurifier.html +1 -0
  144. package/docs/types/airPurifierPM25WebhookContext.html +1 -0
  145. package/docs/types/airPurifierServiceData.html +1 -0
  146. package/docs/types/airPurifierStatus.html +1 -0
  147. package/docs/types/airPurifierTable.html +1 -0
  148. package/docs/types/airPurifierTablePM25WebhookContext.html +1 -0
  149. package/docs/types/airPurifierTableServiceData.html +1 -0
  150. package/docs/types/airPurifierTableStatus.html +1 -0
  151. package/docs/types/airPurifierTableVOC.html +1 -0
  152. package/docs/types/airPurifierTableVOCStatus.html +1 -0
  153. package/docs/types/airPurifierTableVOCWebhookContext.html +1 -0
  154. package/docs/types/airPurifierTableWebhookContext.html +1 -0
  155. package/docs/types/airPurifierVOC.html +1 -0
  156. package/docs/types/airPurifierVOCStatus.html +1 -0
  157. package/docs/types/airPurifierVOCWebhookContext.html +1 -0
  158. package/docs/types/airPurifierWebhookContext.html +1 -0
  159. package/docs/types/batteryCirculatorFan.html +1 -1
  160. package/docs/types/batteryCirculatorFanServiceData.html +1 -1
  161. package/docs/types/batteryCirculatorFanStatus.html +1 -1
  162. package/docs/types/batteryCirculatorFanWebhookContext.html +1 -1
  163. package/docs/types/blindTilt.html +1 -1
  164. package/docs/types/blindTiltServiceData.html +1 -1
  165. package/docs/types/blindTiltStatus.html +1 -1
  166. package/docs/types/blindTiltWebhookContext.html +1 -1
  167. package/docs/types/bot.html +1 -1
  168. package/docs/types/botServiceData.html +1 -1
  169. package/docs/types/botStatus.html +1 -1
  170. package/docs/types/botWebhookContext.html +1 -1
  171. package/docs/types/ceilingLight.html +1 -1
  172. package/docs/types/ceilingLightPro.html +1 -1
  173. package/docs/types/ceilingLightProServiceData.html +1 -1
  174. package/docs/types/ceilingLightProStatus.html +1 -1
  175. package/docs/types/ceilingLightProWebhookContext.html +1 -1
  176. package/docs/types/ceilingLightServiceData.html +1 -1
  177. package/docs/types/ceilingLightStatus.html +1 -1
  178. package/docs/types/ceilingLightWebhookContext.html +1 -1
  179. package/docs/types/circulatorFanStatus.html +1 -1
  180. package/docs/types/circulatorFanWebhookContext.html +1 -1
  181. package/docs/types/colorBulb.html +1 -1
  182. package/docs/types/colorBulbServiceData.html +1 -1
  183. package/docs/types/colorBulbStatus.html +1 -1
  184. package/docs/types/colorBulbWebhookContext.html +1 -1
  185. package/docs/types/commandType.html +2 -0
  186. package/docs/types/contactSensor.html +1 -1
  187. package/docs/types/contactSensorServiceData.html +1 -1
  188. package/docs/types/contactSensorStatus.html +1 -1
  189. package/docs/types/contactSensorWebhookContext.html +1 -1
  190. package/docs/types/curtain.html +1 -1
  191. package/docs/types/curtain3.html +1 -1
  192. package/docs/types/curtain3ServiceData.html +1 -1
  193. package/docs/types/curtain3WebhookContext.html +1 -1
  194. package/docs/types/curtainServiceData.html +1 -1
  195. package/docs/types/curtainStatus.html +1 -1
  196. package/docs/types/curtainWebhookContext.html +1 -1
  197. package/docs/types/floorCleaningRobotS10.html +1 -1
  198. package/docs/types/floorCleaningRobotS10Status.html +1 -1
  199. package/docs/types/floorCleaningRobotS10WebhookContext.html +1 -1
  200. package/docs/types/hub2.html +1 -1
  201. package/docs/types/hub2ServiceData.html +1 -1
  202. package/docs/types/hub2Status.html +1 -1
  203. package/docs/types/hub2WebhookContext.html +1 -1
  204. package/docs/types/hub3ServiceData.html +1 -0
  205. package/docs/types/humidifier.html +1 -1
  206. package/docs/types/humidifier2ServiceData.html +1 -1
  207. package/docs/types/humidifier2Status.html +1 -1
  208. package/docs/types/humidifier2WebhookContext.html +1 -1
  209. package/docs/types/humidifierServiceData.html +1 -1
  210. package/docs/types/humidifierStatus.html +1 -1
  211. package/docs/types/humidifierWebhookContext.html +1 -1
  212. package/docs/types/indoorCameraWebhookContext.html +1 -1
  213. package/docs/types/keypad.html +1 -1
  214. package/docs/types/keypadDetectorServiceData.html +1 -1
  215. package/docs/types/keypadTouch.html +1 -1
  216. package/docs/types/keypadTouchWebhookContext.html +1 -1
  217. package/docs/types/keypadWebhookContext.html +1 -1
  218. package/docs/types/lock.html +1 -1
  219. package/docs/types/lockPro.html +1 -1
  220. package/docs/types/lockProServiceData.html +1 -1
  221. package/docs/types/lockProStatus.html +1 -1
  222. package/docs/types/lockProWebhookContext.html +1 -1
  223. package/docs/types/lockServiceData.html +1 -1
  224. package/docs/types/lockStatus.html +1 -1
  225. package/docs/types/lockWebhookContext.html +1 -1
  226. package/docs/types/meter.html +1 -1
  227. package/docs/types/meterPlus.html +1 -1
  228. package/docs/types/meterPlusServiceData.html +1 -1
  229. package/docs/types/meterPlusStatus.html +1 -1
  230. package/docs/types/meterPlusWebhookContext.html +1 -1
  231. package/docs/types/meterPro.html +1 -1
  232. package/docs/types/meterProCO2ServiceData.html +1 -1
  233. package/docs/types/meterProCO2Status.html +1 -1
  234. package/docs/types/meterProCO2WebhookContext.html +1 -1
  235. package/docs/types/meterProServiceData.html +1 -1
  236. package/docs/types/meterProStatus.html +1 -1
  237. package/docs/types/meterProWebhookContext.html +1 -1
  238. package/docs/types/meterServiceData.html +1 -1
  239. package/docs/types/meterStatus.html +1 -1
  240. package/docs/types/meterWebhookContext.html +1 -1
  241. package/docs/types/motionSensor.html +1 -1
  242. package/docs/types/motionSensorServiceData.html +1 -1
  243. package/docs/types/motionSensorStatus.html +1 -1
  244. package/docs/types/motionSensorWebhookContext.html +1 -1
  245. package/docs/types/onadvertisement.html +1 -1
  246. package/docs/types/ondiscover.html +1 -1
  247. package/docs/types/outdoorMeter.html +1 -1
  248. package/docs/types/outdoorMeterServiceData.html +1 -1
  249. package/docs/types/outdoorMeterStatus.html +1 -1
  250. package/docs/types/outdoorMeterWebhookContext.html +1 -1
  251. package/docs/types/panTiltCamWebhookContext.html +1 -1
  252. package/docs/types/pantiltCam.html +1 -1
  253. package/docs/types/pantiltCam2k.html +1 -1
  254. package/docs/types/plug.html +1 -1
  255. package/docs/types/plugMini.html +1 -1
  256. package/docs/types/plugMiniJPServiceData.html +1 -1
  257. package/docs/types/plugMiniJPWebhookContext.html +1 -1
  258. package/docs/types/plugMiniStatus.html +1 -1
  259. package/docs/types/plugMiniUSServiceData.html +1 -1
  260. package/docs/types/plugMiniUSWebhookContext.html +1 -1
  261. package/docs/types/plugStatus.html +1 -1
  262. package/docs/types/plugWebhookContext.html +1 -1
  263. package/docs/types/relaySwitch1Context.html +1 -1
  264. package/docs/types/relaySwitch1PMContext.html +1 -1
  265. package/docs/types/relaySwitch1PMServiceData.html +1 -1
  266. package/docs/types/relaySwitch1PMStatus.html +1 -1
  267. package/docs/types/relaySwitch1ServiceData.html +1 -1
  268. package/docs/types/relaySwitch1Status.html +1 -1
  269. package/docs/types/remote.html +1 -1
  270. package/docs/types/remoteServiceData.html +1 -1
  271. package/docs/types/robotVacuumCleanerS1.html +1 -1
  272. package/docs/types/robotVacuumCleanerS1Plus.html +1 -1
  273. package/docs/types/robotVacuumCleanerS1PlusStatus.html +1 -1
  274. package/docs/types/robotVacuumCleanerS1PlusWebhookContext.html +1 -1
  275. package/docs/types/robotVacuumCleanerS1Status.html +1 -1
  276. package/docs/types/robotVacuumCleanerS1WebhookContext.html +1 -1
  277. package/docs/types/robotVacuumCleanerServiceData.html +1 -1
  278. package/docs/types/stripLight.html +1 -1
  279. package/docs/types/stripLightServiceData.html +1 -1
  280. package/docs/types/stripLightStatus.html +1 -1
  281. package/docs/types/stripLightWebhookContext.html +1 -1
  282. package/docs/types/waterLeakDetector.html +1 -1
  283. package/docs/types/waterLeakDetectorServiceData.html +1 -1
  284. package/docs/types/waterLeakDetectorStatus.html +1 -1
  285. package/docs/types/waterLeakDetectorWebhookContext.html +1 -1
  286. package/docs/variables/parameterChecker.html +1 -0
  287. package/docs/variables/urls.html +1 -0
  288. package/package.json +6 -6
  289. package/dist/types/bledevicestatus.d.ts.map +0 -1
  290. package/dist/types/bledevicestatus.js +0 -2
  291. package/dist/types/bledevicestatus.js.map +0 -1
  292. package/dist/types/devicelist.d.ts +0 -90
  293. package/dist/types/devicelist.d.ts.map +0 -1
  294. package/dist/types/devicelist.js +0 -2
  295. package/dist/types/devicelist.js.map +0 -1
  296. package/dist/types/devicepush.d.ts +0 -13
  297. package/dist/types/devicepush.d.ts.map +0 -1
  298. package/dist/types/devicepush.js +0 -2
  299. package/dist/types/devicepush.js.map +0 -1
  300. package/dist/types/deviceresponse.d.ts +0 -12
  301. package/dist/types/deviceresponse.d.ts.map +0 -1
  302. package/dist/types/deviceresponse.js +0 -2
  303. package/dist/types/deviceresponse.js.map +0 -1
  304. package/dist/types/devicestatus.d.ts +0 -194
  305. package/dist/types/devicestatus.d.ts.map +0 -1
  306. package/dist/types/devicestatus.js +0 -3
  307. package/dist/types/devicestatus.js.map +0 -1
  308. package/dist/types/devicewebhookstatus.d.ts +0 -236
  309. package/dist/types/devicewebhookstatus.d.ts.map +0 -1
  310. package/dist/types/devicewebhookstatus.js +0 -2
  311. package/dist/types/devicewebhookstatus.js.map +0 -1
  312. package/dist/types/irdevicelist.d.ts +0 -10
  313. package/dist/types/irdevicelist.d.ts.map +0 -1
  314. package/dist/types/irdevicelist.js +0 -2
  315. package/dist/types/irdevicelist.js.map +0 -1
  316. package/docs/interfaces/switchbot.html +0 -3
  317. package/jest.config.js +0 -3
package/dist/device.js CHANGED
@@ -1,20 +1,84 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import * as Crypto from 'node:crypto';
3
3
  import { EventEmitter } from 'node:events';
4
- import { parameterChecker } from './parameter-checker.js';
5
4
  import { CHAR_UUID_DEVICE, CHAR_UUID_NOTIFY, CHAR_UUID_WRITE, READ_TIMEOUT_MSEC, SERV_UUID_PRIMARY, WoSmartLockCommands, WoSmartLockProCommands, WRITE_TIMEOUT_MSEC } from './settings.js';
6
- const HUMIDIFIER_COMMAND_HEADER = '5701';
7
- const TURN_ON_KEY = `${HUMIDIFIER_COMMAND_HEADER}0101`;
8
- const TURN_OFF_KEY = `${HUMIDIFIER_COMMAND_HEADER}0102`;
9
- const INCREASE_KEY = `${HUMIDIFIER_COMMAND_HEADER}0103`;
10
- const DECREASE_KEY = `${HUMIDIFIER_COMMAND_HEADER}0104`;
11
- const SET_AUTO_MODE_KEY = `${HUMIDIFIER_COMMAND_HEADER}0105`;
12
- const SET_MANUAL_MODE_KEY = `${HUMIDIFIER_COMMAND_HEADER}0106`;
5
+ /**
6
+ * Command constants for various SwitchBot devices.
7
+ * Using readonly arrays to ensure immutability and better type safety.
8
+ */
9
+ const DEVICE_COMMANDS = {
10
+ BLIND_TILT: {
11
+ OPEN: [0x57, 0x0F, 0x45, 0x01, 0x05, 0xFF, 0x32],
12
+ CLOSE_UP: [0x57, 0x0F, 0x45, 0x01, 0x05, 0xFF, 0x64],
13
+ CLOSE_DOWN: [0x57, 0x0F, 0x45, 0x01, 0x05, 0xFF, 0x00],
14
+ PAUSE: [0x57, 0x0F, 0x45, 0x01, 0x00, 0xFF],
15
+ },
16
+ BULB: {
17
+ BASE: [0x57, 0x0F, 0x47, 0x01],
18
+ READ_STATE: [0x57, 0x0F, 0x48, 0x01],
19
+ TURN_ON: [0x01, 0x01],
20
+ TURN_OFF: [0x01, 0x02],
21
+ SET_BRIGHTNESS: [0x02, 0x14],
22
+ SET_COLOR_TEMP: [0x02, 0x17],
23
+ SET_RGB: [0x02, 0x12],
24
+ },
25
+ HUMIDIFIER: {
26
+ HEADER: '5701',
27
+ TURN_ON: '570101',
28
+ TURN_OFF: '570102',
29
+ INCREASE: '570103',
30
+ DECREASE: '570104',
31
+ SET_AUTO_MODE: '570105',
32
+ SET_MANUAL_MODE: '570106',
33
+ },
34
+ AIR_PURIFIER: {
35
+ TURN_ON: [0x57, 0x01, 0x01],
36
+ TURN_OFF: [0x57, 0x01, 0x02],
37
+ SET_MODE: [0x57, 0x02],
38
+ SET_SPEED: [0x57, 0x03],
39
+ },
40
+ // Common commands used across multiple devices
41
+ COMMON: {
42
+ POWER_ON: [0x57, 0x01, 0x01],
43
+ POWER_OFF: [0x57, 0x01, 0x02],
44
+ },
45
+ };
46
+ /**
47
+ * Air quality level constants for air purifier devices.
48
+ */
49
+ const AIR_QUALITY_LEVELS = {
50
+ EXCELLENT: 'excellent',
51
+ GOOD: 'good',
52
+ FAIR: 'fair',
53
+ POOR: 'poor',
54
+ };
55
+ /**
56
+ * Air purifier mode constants.
57
+ */
58
+ const AIR_PURIFIER_MODES = {
59
+ MANUAL: 'manual',
60
+ AUTO: 'auto',
61
+ SLEEP: 'sleep',
62
+ LEVEL_1: 'level_1',
63
+ LEVEL_2: 'level_2',
64
+ LEVEL_3: 'level_3',
65
+ };
66
+ // Legacy constants for backward compatibility
67
+ const BLIND_TILT_COMMANDS = DEVICE_COMMANDS.BLIND_TILT;
68
+ const BULB_COMMANDS = DEVICE_COMMANDS.BULB;
69
+ const HUMIDIFIER_COMMAND_HEADER = DEVICE_COMMANDS.HUMIDIFIER.HEADER;
70
+ const TURN_ON_KEY = DEVICE_COMMANDS.HUMIDIFIER.TURN_ON;
71
+ const TURN_OFF_KEY = DEVICE_COMMANDS.HUMIDIFIER.TURN_OFF;
72
+ const INCREASE_KEY = DEVICE_COMMANDS.HUMIDIFIER.INCREASE;
73
+ const DECREASE_KEY = DEVICE_COMMANDS.HUMIDIFIER.DECREASE;
74
+ const SET_AUTO_MODE_KEY = DEVICE_COMMANDS.HUMIDIFIER.SET_AUTO_MODE;
75
+ const SET_MANUAL_MODE_KEY = DEVICE_COMMANDS.HUMIDIFIER.SET_MANUAL_MODE;
13
76
  export var SwitchBotModel;
14
77
  (function (SwitchBotModel) {
15
78
  SwitchBotModel["HubMini"] = "W0202200";
16
79
  SwitchBotModel["HubPlus"] = "SwitchBot Hub S1";
17
80
  SwitchBotModel["Hub2"] = "W3202100";
81
+ SwitchBotModel["Hub3"] = "W3302100";
18
82
  SwitchBotModel["Bot"] = "SwitchBot S1";
19
83
  SwitchBotModel["Curtain"] = "W0701600";
20
84
  SwitchBotModel["Curtain3"] = "W2400000";
@@ -58,6 +122,8 @@ export var SwitchBotModel;
58
122
  SwitchBotModel["RelaySwitch1"] = "W5502300";
59
123
  SwitchBotModel["RelaySwitch1PM"] = "W5502310";
60
124
  SwitchBotModel["Unknown"] = "Unknown";
125
+ SwitchBotModel["AirPurifier"] = "W5302300";
126
+ SwitchBotModel["AirPurifierTable"] = "W5302310";
61
127
  })(SwitchBotModel || (SwitchBotModel = {}));
62
128
  export var SwitchBotBLEModel;
63
129
  (function (SwitchBotBLEModel) {
@@ -71,6 +137,7 @@ export var SwitchBotBLEModel;
71
137
  SwitchBotBLEModel["MeterPro"] = "4";
72
138
  SwitchBotBLEModel["MeterProCO2"] = "5";
73
139
  SwitchBotBLEModel["Hub2"] = "v";
140
+ SwitchBotBLEModel["Hub3"] = "V";
74
141
  SwitchBotBLEModel["OutdoorMeter"] = "w";
75
142
  SwitchBotBLEModel["MotionSensor"] = "s";
76
143
  SwitchBotBLEModel["ContactSensor"] = "d";
@@ -89,11 +156,14 @@ export var SwitchBotBLEModel;
89
156
  SwitchBotBLEModel["RelaySwitch1PM"] = "<";
90
157
  SwitchBotBLEModel["Remote"] = "b";
91
158
  SwitchBotBLEModel["Unknown"] = "Unknown";
159
+ SwitchBotBLEModel["AirPurifier"] = "+";
160
+ SwitchBotBLEModel["AirPurifierTable"] = "7";
92
161
  })(SwitchBotBLEModel || (SwitchBotBLEModel = {}));
93
162
  export var SwitchBotBLEModelName;
94
163
  (function (SwitchBotBLEModelName) {
95
164
  SwitchBotBLEModelName["Bot"] = "WoHand";
96
165
  SwitchBotBLEModelName["Hub2"] = "WoHub2";
166
+ SwitchBotBLEModelName["Hub3"] = "WoHub3";
97
167
  SwitchBotBLEModelName["ColorBulb"] = "WoBulb";
98
168
  SwitchBotBLEModelName["Curtain"] = "WoCurtain";
99
169
  SwitchBotBLEModelName["Curtain3"] = "WoCurtain3";
@@ -118,12 +188,15 @@ export var SwitchBotBLEModelName;
118
188
  SwitchBotBLEModelName["RelaySwitch1"] = "WoRelaySwitch1Plus";
119
189
  SwitchBotBLEModelName["RelaySwitch1PM"] = "WoRelaySwitch1PM";
120
190
  SwitchBotBLEModelName["Remote"] = "WoRemote";
191
+ SwitchBotBLEModelName["AirPurifier"] = "WoAirPurifier";
192
+ SwitchBotBLEModelName["AirPurifierTable"] = "WoAirPurifierTable";
121
193
  SwitchBotBLEModelName["Unknown"] = "Unknown";
122
194
  })(SwitchBotBLEModelName || (SwitchBotBLEModelName = {}));
123
195
  export var SwitchBotBLEModelFriendlyName;
124
196
  (function (SwitchBotBLEModelFriendlyName) {
125
197
  SwitchBotBLEModelFriendlyName["Bot"] = "Bot";
126
198
  SwitchBotBLEModelFriendlyName["Hub2"] = "Hub 2";
199
+ SwitchBotBLEModelFriendlyName["Hub3"] = "Hub 3";
127
200
  SwitchBotBLEModelFriendlyName["ColorBulb"] = "Color Bulb";
128
201
  SwitchBotBLEModelFriendlyName["Curtain"] = "Curtain";
129
202
  SwitchBotBLEModelFriendlyName["Curtain3"] = "Curtain 3";
@@ -150,7 +223,13 @@ export var SwitchBotBLEModelFriendlyName;
150
223
  SwitchBotBLEModelFriendlyName["RelaySwitch1"] = "Relay Switch 1";
151
224
  SwitchBotBLEModelFriendlyName["RelaySwitch1PM"] = "Relay Switch 1PM";
152
225
  SwitchBotBLEModelFriendlyName["Remote"] = "Remote";
226
+ SwitchBotBLEModelFriendlyName["AirPurifier"] = "Air Purifier";
227
+ SwitchBotBLEModelFriendlyName["AirPurifierTable"] = "Air Purifier Table";
153
228
  SwitchBotBLEModelFriendlyName["Unknown"] = "Unknown";
229
+ SwitchBotBLEModelFriendlyName["AirPurifierVOC"] = "Air Purifier VOC";
230
+ SwitchBotBLEModelFriendlyName["AirPurifierTableVOC"] = "Air Purifier Table VOC";
231
+ SwitchBotBLEModelFriendlyName["AirPurifierPM2_5"] = "Air Purifier PM2.5";
232
+ SwitchBotBLEModelFriendlyName["AirPurifierTablePM2_5"] = "Air Purifier Table PM2.5";
154
233
  })(SwitchBotBLEModelFriendlyName || (SwitchBotBLEModelFriendlyName = {}));
155
234
  /**
156
235
  * Enum for log levels.
@@ -166,6 +245,176 @@ export var LogLevel;
166
245
  LogLevel["DEBUG"] = "debug";
167
246
  LogLevel["INFO"] = "info";
168
247
  })(LogLevel || (LogLevel = {}));
248
+ /**
249
+ * Utility class for comprehensive input validation with improved error messages.
250
+ */
251
+ export class ValidationUtils {
252
+ /**
253
+ * Validates percentage value (0-100).
254
+ * @param value - The value to validate
255
+ * @param paramName - The parameter name for error reporting
256
+ * @throws {RangeError} When value is not within valid range
257
+ * @throws {TypeError} When value is not a number
258
+ */
259
+ static validatePercentage(value, paramName = 'value') {
260
+ if (typeof value !== 'number' || Number.isNaN(value)) {
261
+ throw new TypeError(`${paramName} must be a valid number, got: ${value}`);
262
+ }
263
+ if (value < 0 || value > 100) {
264
+ throw new RangeError(`${paramName} must be between 0 and 100 inclusive, got: ${value}`);
265
+ }
266
+ }
267
+ /**
268
+ * Validates RGB color value (0-255).
269
+ * @param value - The color value to validate
270
+ * @param colorName - The color name for error reporting
271
+ * @throws {RangeError} When value is not within valid range
272
+ * @throws {TypeError} When value is not a number
273
+ */
274
+ static validateRGB(value, colorName = 'color') {
275
+ if (typeof value !== 'number' || Number.isNaN(value)) {
276
+ throw new TypeError(`${colorName} must be a valid number, got: ${value}`);
277
+ }
278
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
279
+ throw new RangeError(`${colorName} must be an integer between 0 and 255 inclusive, got: ${value}`);
280
+ }
281
+ }
282
+ /**
283
+ * Validates buffer and throws descriptive error.
284
+ * @param buffer - The buffer to validate
285
+ * @param expectedLength - Optional expected length
286
+ * @param paramName - The parameter name for error reporting
287
+ * @throws {TypeError} When buffer is not a Buffer
288
+ * @throws {RangeError} When buffer length doesn't match expected
289
+ */
290
+ static validateBuffer(buffer, expectedLength, paramName = 'buffer') {
291
+ if (!Buffer.isBuffer(buffer)) {
292
+ throw new TypeError(`${paramName} must be a Buffer instance, got: ${typeof buffer}`);
293
+ }
294
+ if (expectedLength !== undefined && buffer.length !== expectedLength) {
295
+ throw new RangeError(`${paramName} must have exactly ${expectedLength} bytes, got: ${buffer.length} bytes`);
296
+ }
297
+ }
298
+ /**
299
+ * Validates string input with comprehensive checks.
300
+ * @param value - The value to validate
301
+ * @param paramName - The parameter name for error reporting
302
+ * @param minLength - Minimum required length
303
+ * @param maxLength - Optional maximum length
304
+ * @throws {TypeError} When value is not a string
305
+ * @throws {RangeError} When string length is invalid
306
+ */
307
+ static validateString(value, paramName = 'value', minLength = 1, maxLength) {
308
+ if (typeof value !== 'string') {
309
+ throw new TypeError(`${paramName} must be a string, got: ${typeof value}`);
310
+ }
311
+ if (value.length < minLength) {
312
+ throw new RangeError(`${paramName} must have at least ${minLength} character(s), got: ${value.length}`);
313
+ }
314
+ if (maxLength !== undefined && value.length > maxLength) {
315
+ throw new RangeError(`${paramName} must have at most ${maxLength} character(s), got: ${value.length}`);
316
+ }
317
+ }
318
+ /**
319
+ * Validates numeric range with enhanced checks.
320
+ * @param value - The value to validate
321
+ * @param min - Minimum allowed value
322
+ * @param max - Maximum allowed value
323
+ * @param paramName - The parameter name for error reporting
324
+ * @param mustBeInteger - Whether the value must be an integer
325
+ * @throws {TypeError} When value is not a number
326
+ * @throws {RangeError} When value is outside valid range
327
+ */
328
+ static validateRange(value, min, max, paramName = 'value', mustBeInteger = false) {
329
+ if (typeof value !== 'number' || Number.isNaN(value)) {
330
+ throw new TypeError(`${paramName} must be a valid number, got: ${value}`);
331
+ }
332
+ if (mustBeInteger && !Number.isInteger(value)) {
333
+ throw new TypeError(`${paramName} must be an integer, got: ${value}`);
334
+ }
335
+ if (value < min || value > max) {
336
+ throw new RangeError(`${paramName} must be between ${min} and ${max} inclusive, got: ${value}`);
337
+ }
338
+ }
339
+ /**
340
+ * Validates MAC address format.
341
+ * @param address - The MAC address to validate
342
+ * @param paramName - The parameter name for error reporting
343
+ * @throws {TypeError} When address is not a string
344
+ * @throws {Error} When address format is invalid
345
+ */
346
+ static validateMacAddress(address, paramName = 'address') {
347
+ if (typeof address !== 'string') {
348
+ throw new TypeError(`${paramName} must be a string`);
349
+ }
350
+ const macRegex = /^(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}$|^[0-9A-F]{12}$/i;
351
+ if (!macRegex.test(address)) {
352
+ throw new Error(`${paramName} must be a valid MAC address format, got: ${address}`);
353
+ }
354
+ }
355
+ /**
356
+ * Validates that a value is one of the allowed enum values.
357
+ * @param value - The value to validate
358
+ * @param allowedValues - Array of allowed values
359
+ * @param paramName - The parameter name for error reporting
360
+ * @throws {Error} When value is not in allowed values
361
+ */
362
+ static validateEnum(value, allowedValues, paramName = 'value') {
363
+ if (!allowedValues.includes(value)) {
364
+ throw new Error(`${paramName} must be one of: ${allowedValues.join(', ')}, got: ${value}`);
365
+ }
366
+ }
367
+ }
368
+ /**
369
+ * Enhanced error handling utilities.
370
+ */
371
+ export class ErrorUtils {
372
+ /**
373
+ * Creates a timeout error with context.
374
+ * @param operation - The operation that timed out
375
+ * @param timeoutMs - The timeout duration in milliseconds
376
+ * @returns A descriptive timeout error
377
+ */
378
+ static createTimeoutError(operation, timeoutMs) {
379
+ return new Error(`Operation '${operation}' timed out after ${timeoutMs}ms`);
380
+ }
381
+ /**
382
+ * Creates a connection error with context.
383
+ * @param deviceId - The device ID that failed to connect
384
+ * @param cause - The underlying cause of the connection failure
385
+ * @returns A descriptive connection error
386
+ */
387
+ static createConnectionError(deviceId, cause) {
388
+ const message = `Failed to connect to device ${deviceId}`;
389
+ return cause ? new Error(`${message}: ${cause.message}`) : new Error(message);
390
+ }
391
+ /**
392
+ * Creates a command error with context.
393
+ * @param command - The command that failed
394
+ * @param deviceId - The device ID
395
+ * @param cause - The underlying cause
396
+ * @returns A descriptive command error
397
+ */
398
+ static createCommandError(command, deviceId, cause) {
399
+ const message = `Command '${command}' failed for device ${deviceId}`;
400
+ return cause ? new Error(`${message}: ${cause.message}`) : new Error(message);
401
+ }
402
+ /**
403
+ * Wraps an async operation with timeout and enhanced error handling.
404
+ * @param operation - The async operation to wrap
405
+ * @param timeoutMs - Timeout in milliseconds
406
+ * @param operationName - Name of the operation for error messages
407
+ * @returns Promise that resolves with the operation result or rejects with timeout
408
+ */
409
+ static async withTimeout(operation, timeoutMs, operationName) {
410
+ const timeoutPromise = new Promise((_, reject) => {
411
+ setTimeout(() => {
412
+ reject(this.createTimeoutError(operationName, timeoutMs));
413
+ }, timeoutMs);
414
+ });
415
+ return Promise.race([operation, timeoutPromise]);
416
+ }
417
+ }
169
418
  /**
170
419
  * Represents a Switchbot Device.
171
420
  */
@@ -345,8 +594,9 @@ export class SwitchbotDevice extends EventEmitter {
345
594
  * @param service The service to discover characteristics for.
346
595
  * @returns A Promise that resolves with the list of characteristics.
347
596
  */
348
- async discoverCharacteristics(service) {
349
- return await service.discoverCharacteristicsAsync([]);
597
+ // Discover characteristics without extra async/await
598
+ discoverCharacteristics(service) {
599
+ return service.discoverCharacteristicsAsync([]);
350
600
  }
351
601
  /**
352
602
  * Subscribes to the notify characteristic.
@@ -404,12 +654,20 @@ export class SwitchbotDevice extends EventEmitter {
404
654
  */
405
655
  async getDeviceName() {
406
656
  await this.internalConnect();
407
- if (!this.characteristics?.device) {
408
- throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`);
657
+ try {
658
+ if (!this.characteristics?.device) {
659
+ throw new Error(`Characteristic ${CHAR_UUID_DEVICE} not supported`);
660
+ }
661
+ const buf = await this.readCharacteristic(this.characteristics.device);
662
+ return buf.toString('utf8');
663
+ }
664
+ catch (error) {
665
+ const deviceContext = `device ${this.deviceId || 'unknown'}`;
666
+ throw ErrorUtils.createCommandError('getDeviceName', deviceContext, error);
667
+ }
668
+ finally {
669
+ await this.internalDisconnect();
409
670
  }
410
- const buf = await this.readCharacteristic(this.characteristics.device);
411
- await this.internalDisconnect();
412
- return buf.toString('utf8');
413
671
  }
414
672
  /**
415
673
  * Sets the device name.
@@ -417,17 +675,26 @@ export class SwitchbotDevice extends EventEmitter {
417
675
  * @returns A Promise that resolves when the name is set.
418
676
  */
419
677
  async setDeviceName(name) {
420
- const valid = parameterChecker.check({ name }, { name: { required: true, type: 'string', minBytes: 1, maxBytes: 100 } }, true);
421
- if (!valid) {
422
- throw new Error(parameterChecker.error.message);
678
+ ValidationUtils.validateString(name, 'name', 1);
679
+ // Additional validation for device name length
680
+ const nameBuffer = Buffer.from(name, 'utf8');
681
+ if (nameBuffer.length > 100) {
682
+ throw new RangeError('Device name cannot exceed 100 bytes when encoded as UTF-8');
423
683
  }
424
- const buf = Buffer.from(name, 'utf8');
425
684
  await this.internalConnect();
426
- if (!this.characteristics?.device) {
427
- throw new Error(`The device does not support the characteristic UUID 0x${CHAR_UUID_DEVICE}.`);
685
+ try {
686
+ if (!this.characteristics?.device) {
687
+ throw new Error(`Characteristic ${CHAR_UUID_DEVICE} not supported`);
688
+ }
689
+ await this.writeCharacteristic(this.characteristics.device, nameBuffer);
690
+ }
691
+ catch (error) {
692
+ const deviceContext = `device ${this.deviceId || 'unknown'}`;
693
+ throw ErrorUtils.createCommandError('setDeviceName', deviceContext, error);
694
+ }
695
+ finally {
696
+ await this.internalDisconnect();
428
697
  }
429
- await this.writeCharacteristic(this.characteristics.device, buf);
430
- await this.internalDisconnect();
431
698
  }
432
699
  /**
433
700
  * Sends a command to the device and awaits a response.
@@ -435,17 +702,24 @@ export class SwitchbotDevice extends EventEmitter {
435
702
  * @returns A Promise that resolves with the response buffer.
436
703
  */
437
704
  async command(reqBuf) {
438
- if (!Buffer.isBuffer(reqBuf)) {
439
- throw new TypeError('The specified data is not acceptable for writing.');
440
- }
705
+ ValidationUtils.validateBuffer(reqBuf, undefined, 'reqBuf');
441
706
  await this.internalConnect();
442
707
  if (!this.characteristics?.write) {
443
- throw new Error('No characteristics available.');
708
+ throw new Error('No write characteristic available for command execution');
709
+ }
710
+ try {
711
+ await this.writeCharacteristic(this.characteristics.write, reqBuf);
712
+ const resBuf = await this.waitForCommandResponse();
713
+ return resBuf;
714
+ }
715
+ catch (error) {
716
+ const deviceContext = `device ${this.deviceId || 'unknown'}`;
717
+ // Use ErrorUtils for enriched error context
718
+ throw ErrorUtils.createCommandError('execute command', deviceContext, error);
719
+ }
720
+ finally {
721
+ await this.internalDisconnect();
444
722
  }
445
- await this.writeCharacteristic(this.characteristics.write, reqBuf);
446
- const resBuf = await this.waitForCommandResponse();
447
- await this.internalDisconnect();
448
- return resBuf;
449
723
  }
450
724
  /**
451
725
  * Waits for a response from the device after sending a command.
@@ -468,41 +742,33 @@ export class SwitchbotDevice extends EventEmitter {
468
742
  return await Promise.race([readPromise, timeoutPromise]);
469
743
  }
470
744
  /**
471
- * Reads data from a characteristic with a timeout.
745
+ * Reads data from a characteristic with enhanced timeout and error handling.
472
746
  * @param char The characteristic to read from.
473
747
  * @returns A Promise that resolves with the data buffer.
474
748
  */
475
749
  async readCharacteristic(char) {
476
- const timer = setTimeout(() => {
477
- throw new Error('READ_TIMEOUT');
478
- }, READ_TIMEOUT_MSEC);
479
750
  try {
480
- const result = await char.readAsync();
481
- clearTimeout(timer);
482
- return result;
751
+ return await ErrorUtils.withTimeout(char.readAsync(), READ_TIMEOUT_MSEC, `read characteristic ${char.uuid}`);
483
752
  }
484
753
  catch (error) {
485
- clearTimeout(timer);
486
- throw error;
754
+ const deviceContext = `device ${this.deviceId || 'unknown'}`;
755
+ throw ErrorUtils.createCommandError(`read characteristic ${char.uuid}`, deviceContext, error);
487
756
  }
488
757
  }
489
758
  /**
490
- * Writes data to a characteristic with a timeout.
759
+ * Writes data to a characteristic with enhanced timeout and error handling.
491
760
  * @param char The characteristic to write to.
492
761
  * @param buf The data buffer.
493
762
  * @returns A Promise that resolves when the write is complete.
494
763
  */
495
764
  async writeCharacteristic(char, buf) {
496
- const timer = setTimeout(() => {
497
- throw new Error('WRITE_TIMEOUT');
498
- }, WRITE_TIMEOUT_MSEC);
765
+ ValidationUtils.validateBuffer(buf, undefined, 'write buffer');
499
766
  try {
500
- await char.writeAsync(buf, false);
501
- clearTimeout(timer);
767
+ return await ErrorUtils.withTimeout(char.writeAsync(buf, false), WRITE_TIMEOUT_MSEC, `write to characteristic ${char.uuid}`);
502
768
  }
503
769
  catch (error) {
504
- clearTimeout(timer);
505
- throw error;
770
+ const deviceContext = `device ${this.deviceId || 'unknown'}`;
771
+ throw ErrorUtils.createCommandError(`write to characteristic ${char.uuid}`, deviceContext, error);
506
772
  }
507
773
  }
508
774
  }
@@ -556,10 +822,11 @@ export class Advertising {
556
822
  * Validates if the buffer is a valid Buffer object with a minimum length.
557
823
  *
558
824
  * @param {any} buffer - The buffer to validate.
825
+ * @param {number} minLength - The minimum required length.
559
826
  * @returns {boolean} - True if the buffer is valid, false otherwise.
560
827
  */
561
- static validateBuffer(buffer) {
562
- return buffer && Buffer.isBuffer(buffer) && buffer.length >= 3;
828
+ static validateBuffer(buffer, minLength = 3) {
829
+ return buffer && Buffer.isBuffer(buffer) && buffer.length >= minLength;
563
830
  }
564
831
  /**
565
832
  * Parses the service data based on the device model.
@@ -591,8 +858,14 @@ export class Advertising {
591
858
  return WoSensorTHProCO2.parseServiceData(serviceData, manufacturerData, emitLog);
592
859
  case SwitchBotBLEModel.Hub2:
593
860
  return WoHub2.parseServiceData(manufacturerData, emitLog);
861
+ case SwitchBotBLEModel.Hub3:
862
+ return WoHub3.parseServiceData(manufacturerData, emitLog);
594
863
  case SwitchBotBLEModel.OutdoorMeter:
595
864
  return WoIOSensorTH.parseServiceData(serviceData, manufacturerData, emitLog);
865
+ case SwitchBotBLEModel.AirPurifier:
866
+ return WoAirPurifier.parseServiceData(serviceData, manufacturerData, emitLog);
867
+ case SwitchBotBLEModel.AirPurifierTable:
868
+ return WoAirPurifierTable.parseServiceData(serviceData, manufacturerData, emitLog);
596
869
  case SwitchBotBLEModel.MotionSensor:
597
870
  return WoPresence.parseServiceData(serviceData, emitLog);
598
871
  case SwitchBotBLEModel.ContactSensor:
@@ -696,21 +969,21 @@ export class WoBlindTilt extends SwitchbotDevice {
696
969
  * @returns {Promise<void>}
697
970
  */
698
971
  async open() {
699
- await this.operateBlindTilt([0x57, 0x0F, 0x45, 0x01, 0x05, 0xFF, 0x32]);
972
+ await this.operateBlindTilt([...BLIND_TILT_COMMANDS.OPEN]);
700
973
  }
701
974
  /**
702
975
  * Closes the blind tilt up to the nearest endpoint.
703
976
  * @returns {Promise<void>}
704
977
  */
705
978
  async closeUp() {
706
- await this.operateBlindTilt([0x57, 0x0F, 0x45, 0x01, 0x05, 0xFF, 0x64]);
979
+ await this.operateBlindTilt([...BLIND_TILT_COMMANDS.CLOSE_UP]);
707
980
  }
708
981
  /**
709
982
  * Closes the blind tilt down to the nearest endpoint.
710
983
  * @returns {Promise<void>}
711
984
  */
712
985
  async closeDown() {
713
- await this.operateBlindTilt([0x57, 0x0F, 0x45, 0x01, 0x05, 0xFF, 0x00]);
986
+ await this.operateBlindTilt([...BLIND_TILT_COMMANDS.CLOSE_DOWN]);
714
987
  }
715
988
  /**
716
989
  * Closes the blind tilt to the nearest endpoint.
@@ -794,7 +1067,7 @@ export class WoBlindTilt extends SwitchbotDevice {
794
1067
  * @returns {Promise<void>}
795
1068
  */
796
1069
  async pause() {
797
- await this.operateBlindTilt([0x57, 0x0F, 0x45, 0x01, 0x00, 0xFF]);
1070
+ await this.operateBlindTilt([...BLIND_TILT_COMMANDS.PAUSE]);
798
1071
  }
799
1072
  /**
800
1073
  * Runs the blind tilt to the specified position.
@@ -803,13 +1076,10 @@ export class WoBlindTilt extends SwitchbotDevice {
803
1076
  * @returns {Promise<void>}
804
1077
  */
805
1078
  async runToPos(percent, mode) {
806
- if (typeof percent !== 'number' || percent < 0 || percent > 100) {
807
- throw new RangeError('Percent must be a number between 0 and 100');
808
- }
809
- if (typeof mode !== 'number' || mode < 0 || mode > 1) {
810
- throw new RangeError('Mode must be a number between 0 and 1');
811
- }
812
- await this.operateBlindTilt([0x57, 0x0F, 0x45, 0x01, 0x05, mode, percent]);
1079
+ ValidationUtils.validatePercentage(percent, 'percent');
1080
+ ValidationUtils.validateRange(mode, 0, 1, 'mode', true);
1081
+ const adjustedPercent = this.reverse ? 100 - percent : percent;
1082
+ await this.operateBlindTilt([0x57, 0x0F, 0x45, 0x01, 0x05, mode, adjustedPercent]);
813
1083
  }
814
1084
  /**
815
1085
  * Sends a command to operate the blind tilt and handles the response.
@@ -876,7 +1146,7 @@ export class WoBulb extends SwitchbotDevice {
876
1146
  * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the bulb is ON (true) or OFF (false).
877
1147
  */
878
1148
  async readState() {
879
- return this.operateBulb([0x57, 0x0F, 0x48, 0x01]);
1149
+ return this.operateBulb([...BULB_COMMANDS.READ_STATE]);
880
1150
  }
881
1151
  /**
882
1152
  * Sets the state of the bulb.
@@ -885,22 +1155,21 @@ export class WoBulb extends SwitchbotDevice {
885
1155
  * @private
886
1156
  */
887
1157
  async setState(reqByteArray) {
888
- const base = [0x57, 0x0F, 0x47, 0x01];
889
- return this.operateBulb(base.concat(reqByteArray));
1158
+ return this.operateBulb([...BULB_COMMANDS.BASE, ...reqByteArray]);
890
1159
  }
891
1160
  /**
892
1161
  * Turns on the bulb.
893
1162
  * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the bulb is ON (true).
894
1163
  */
895
1164
  async turnOn() {
896
- return this.setState([0x01, 0x01]);
1165
+ return this.setState([...BULB_COMMANDS.TURN_ON]);
897
1166
  }
898
1167
  /**
899
1168
  * Turns off the bulb.
900
1169
  * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the bulb is OFF (false).
901
1170
  */
902
1171
  async turnOff() {
903
- return this.setState([0x01, 0x02]);
1172
+ return this.setState([...BULB_COMMANDS.TURN_OFF]);
904
1173
  }
905
1174
  /**
906
1175
  * Sets the brightness of the bulb.
@@ -908,10 +1177,8 @@ export class WoBulb extends SwitchbotDevice {
908
1177
  * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the operation was successful.
909
1178
  */
910
1179
  async setBrightness(brightness) {
911
- if (brightness < 0 || brightness > 100) {
912
- throw new RangeError('Brightness must be between 0 and 100');
913
- }
914
- return this.setState([0x02, 0x14, brightness]);
1180
+ ValidationUtils.validatePercentage(brightness, 'brightness');
1181
+ return this.setState([...BULB_COMMANDS.SET_BRIGHTNESS, brightness]);
915
1182
  }
916
1183
  /**
917
1184
  * Sets the color temperature of the bulb.
@@ -919,10 +1186,8 @@ export class WoBulb extends SwitchbotDevice {
919
1186
  * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the operation was successful.
920
1187
  */
921
1188
  async setColorTemperature(color_temperature) {
922
- if (color_temperature < 0 || color_temperature > 100) {
923
- throw new RangeError('Color temperature must be between 0 and 100');
924
- }
925
- return this.setState([0x02, 0x17, color_temperature]);
1189
+ ValidationUtils.validatePercentage(color_temperature, 'color_temperature');
1190
+ return this.setState([...BULB_COMMANDS.SET_COLOR_TEMP, color_temperature]);
926
1191
  }
927
1192
  /**
928
1193
  * Sets the RGB color of the bulb.
@@ -933,10 +1198,11 @@ export class WoBulb extends SwitchbotDevice {
933
1198
  * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the operation was successful.
934
1199
  */
935
1200
  async setRGB(brightness, red, green, blue) {
936
- if (brightness < 0 || brightness > 100 || red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) {
937
- throw new RangeError('Invalid RGB or brightness values');
938
- }
939
- return this.setState([0x02, 0x12, brightness, red, green, blue]);
1201
+ ValidationUtils.validatePercentage(brightness, 'brightness');
1202
+ ValidationUtils.validateRGB(red, 'red');
1203
+ ValidationUtils.validateRGB(green, 'green');
1204
+ ValidationUtils.validateRGB(blue, 'blue');
1205
+ return this.setState([...BULB_COMMANDS.SET_RGB, brightness, red, green, blue]);
940
1206
  }
941
1207
  /**
942
1208
  * Sends a command to the bulb.
@@ -1406,6 +1672,43 @@ export class WoHub2 extends SwitchbotDevice {
1406
1672
  super(peripheral, noble);
1407
1673
  }
1408
1674
  }
1675
+ /**
1676
+ * Class representing a WoHub3 device.
1677
+ * @see https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/devicetypes/meter.md
1678
+ */
1679
+ export class WoHub3 extends SwitchbotDevice {
1680
+ /**
1681
+ * Parses the service data for WoHub3.
1682
+ * @param {Buffer} manufacturerData - The manufacturer data buffer.
1683
+ * @param {Function} emitLog - The function to emit log messages.
1684
+ * @returns {Promise<hub3ServiceData | null>} - Parsed service data or null if invalid.
1685
+ */
1686
+ static async parseServiceData(manufacturerData, emitLog) {
1687
+ if (manufacturerData.length !== 16) {
1688
+ emitLog('debugerror', `[parseServiceDataForWoHub3] Buffer length ${manufacturerData.length} !== 16!`);
1689
+ return null;
1690
+ }
1691
+ const [byte0, byte1, byte2, , , , , , , , , , byte12] = manufacturerData;
1692
+ const tempSign = byte1 & 0b10000000 ? 1 : -1;
1693
+ const tempC = tempSign * ((byte1 & 0b01111111) + (byte0 & 0b00001111) / 10);
1694
+ const tempF = Math.round(((tempC * 9) / 5 + 32) * 10) / 10;
1695
+ const lightLevel = byte12 & 0b11111;
1696
+ const data = {
1697
+ model: SwitchBotBLEModel.Hub3,
1698
+ modelName: SwitchBotBLEModelName.Hub3,
1699
+ modelFriendlyName: SwitchBotBLEModelFriendlyName.Hub3,
1700
+ celsius: tempC,
1701
+ fahrenheit: tempF,
1702
+ fahrenheit_mode: !!(byte2 & 0b10000000),
1703
+ humidity: byte2 & 0b01111111,
1704
+ lightLevel,
1705
+ };
1706
+ return data;
1707
+ }
1708
+ constructor(peripheral, noble) {
1709
+ super(peripheral, noble);
1710
+ }
1711
+ }
1409
1712
  /**
1410
1713
  * Class representing a WoHumi device.
1411
1714
  * @see https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/tree/latest/devicetypes
@@ -2750,6 +3053,7 @@ export class WoStrip extends SwitchbotDevice {
2750
3053
  red: byte3,
2751
3054
  green: byte4,
2752
3055
  blue: byte5,
3056
+ color_temperature: 0, // Add a default value or extract from serviceData if available
2753
3057
  delay: byte8 & 0b10000000,
2754
3058
  preset: byte8 & 0b00001000,
2755
3059
  color_mode: byte8 & 0b00000111,
@@ -2842,4 +3146,280 @@ export class WoStrip extends SwitchbotDevice {
2842
3146
  }
2843
3147
  }
2844
3148
  }
3149
+ /**
3150
+ * Class representing a SwitchBot Air Purifier device.
3151
+ * @extends SwitchbotDevice
3152
+ */
3153
+ export class WoAirPurifier extends SwitchbotDevice {
3154
+ /**
3155
+ * Parses service data for air purifier devices.
3156
+ * @param {Buffer | null} serviceData - The service data buffer.
3157
+ * @param {Buffer | null} manufacturerData - The manufacturer data buffer.
3158
+ * @param {Function} emitLog - The function to emit log messages.
3159
+ * @returns {airPurifierServiceData | null} - The parsed service data or null.
3160
+ */
3161
+ static parseServiceData(serviceData, manufacturerData, emitLog) {
3162
+ if (!manufacturerData || manufacturerData.length < 14) {
3163
+ return null;
3164
+ }
3165
+ const deviceData = manufacturerData.subarray(6);
3166
+ if (deviceData.length < 8) {
3167
+ return null;
3168
+ }
3169
+ const sequenceNumber = deviceData[0];
3170
+ const isOn = Boolean(deviceData[1] & 0b10000000);
3171
+ const mode = deviceData[1] & 0b00000111;
3172
+ const isAqiValid = Boolean(deviceData[2] & 0b00000100);
3173
+ const childLock = Boolean(deviceData[2] & 0b00000010);
3174
+ const speed = deviceData[3] & 0b01111111;
3175
+ const aqiLevelRaw = (deviceData[4] & 0b00000110) >> 1;
3176
+ const workTime = (deviceData[5] << 8) | deviceData[6];
3177
+ const errCode = deviceData[7];
3178
+ // Map AQI level to string using the defined constant
3179
+ const aqiLevelValues = [
3180
+ AIR_QUALITY_LEVELS.EXCELLENT,
3181
+ AIR_QUALITY_LEVELS.GOOD,
3182
+ AIR_QUALITY_LEVELS.FAIR,
3183
+ AIR_QUALITY_LEVELS.POOR,
3184
+ ];
3185
+ const aqiLevel = aqiLevelValues[aqiLevelRaw] || 'unknown';
3186
+ // Determine mode based on mode value and speed
3187
+ let modeString = null;
3188
+ if (mode === 1) {
3189
+ if (speed >= 0 && speed <= 33) {
3190
+ modeString = AIR_PURIFIER_MODES.LEVEL_1;
3191
+ }
3192
+ else if (speed >= 34 && speed <= 66) {
3193
+ modeString = AIR_PURIFIER_MODES.LEVEL_2;
3194
+ }
3195
+ else {
3196
+ modeString = AIR_PURIFIER_MODES.LEVEL_3;
3197
+ }
3198
+ }
3199
+ else if (mode > 1 && mode <= 4) {
3200
+ const modeMap = [null, null, 'auto', 'sleep', 'manual'];
3201
+ modeString = modeMap[mode + 2] || null;
3202
+ }
3203
+ if (emitLog) {
3204
+ emitLog('debug', `Air Purifier Service Data: isOn=${isOn}, mode=${modeString}, speed=${speed}, AQI=${aqiLevel}`);
3205
+ }
3206
+ return {
3207
+ model: SwitchBotBLEModel.AirPurifier,
3208
+ modelName: SwitchBotBLEModelName.AirPurifier,
3209
+ modelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifier,
3210
+ isOn,
3211
+ mode: modeString,
3212
+ isAqiValid,
3213
+ child_lock: childLock,
3214
+ speed,
3215
+ aqi_level: aqiLevel,
3216
+ filter_element_working_time: workTime,
3217
+ err_code: errCode,
3218
+ sequence_number: sequenceNumber,
3219
+ };
3220
+ }
3221
+ /**
3222
+ * Sets the state of the air purifier.
3223
+ * @param {number[]} reqByteArray - The request byte array.
3224
+ * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the operation was successful.
3225
+ * @private
3226
+ */
3227
+ async setState(reqByteArray) {
3228
+ return this.operateAirPurifier(reqByteArray);
3229
+ }
3230
+ /**
3231
+ * Turns the air purifier on.
3232
+ * @returns {Promise<boolean>} - Resolves with true if the air purifier is turned on.
3233
+ */
3234
+ async turnOn() {
3235
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.TURN_ON]);
3236
+ }
3237
+ /**
3238
+ * Turns the air purifier off.
3239
+ * @returns {Promise<boolean>} - Resolves with true if the air purifier is turned off.
3240
+ */
3241
+ async turnOff() {
3242
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.TURN_OFF]);
3243
+ }
3244
+ /**
3245
+ * Sets the speed of the air purifier.
3246
+ * @param {number} speed - The speed value (0-100).
3247
+ * @returns {Promise<boolean>} - Resolves with true if the operation was successful.
3248
+ */
3249
+ async setSpeed(speed) {
3250
+ if (typeof speed !== 'number' || speed < 0 || speed > 100) {
3251
+ throw new TypeError(`Invalid speed value: ${speed}`);
3252
+ }
3253
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.SET_SPEED, speed]);
3254
+ }
3255
+ /**
3256
+ * Sets the mode of the air purifier.
3257
+ * @param {number} mode - The mode value (1-4).
3258
+ * @returns {Promise<boolean>} - Resolves with true if the operation was successful.
3259
+ */
3260
+ async setMode(mode) {
3261
+ if (typeof mode !== 'number' || mode < 1 || mode > 4) {
3262
+ throw new TypeError(`Invalid mode value: ${mode}`);
3263
+ }
3264
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.SET_MODE, mode]);
3265
+ }
3266
+ /**
3267
+ * Operates the air purifier with the given byte array.
3268
+ * @public
3269
+ * @param {number[]} bytes - The byte array to send.
3270
+ * @returns {Promise<boolean>} - Resolves with true if the operation was successful.
3271
+ */
3272
+ async operateAirPurifier(bytes) {
3273
+ const req_buf = Buffer.from(bytes);
3274
+ const res_buf = await this.command(req_buf);
3275
+ if (res_buf.length !== 2) {
3276
+ throw new Error(`Expecting a 2-byte response, got instead: 0x${res_buf.toString('hex')}`);
3277
+ }
3278
+ const code = res_buf.readUInt8(1);
3279
+ if (code === 0x00 || code === 0x80) {
3280
+ return code === 0x80;
3281
+ }
3282
+ else {
3283
+ throw new Error(`The device returned an error: 0x${res_buf.toString('hex')}`);
3284
+ }
3285
+ }
3286
+ }
3287
+ /**
3288
+ * Class representing a SwitchBot Air Purifier Table device.
3289
+ * @extends SwitchbotDevice
3290
+ */
3291
+ export class WoAirPurifierTable extends SwitchbotDevice {
3292
+ /**
3293
+ * Parses service data for air purifier table devices.
3294
+ * @param {Buffer | null} serviceData - The service data buffer.
3295
+ * @param {Buffer | null} manufacturerData - The manufacturer data buffer.
3296
+ * @param {Function} emitLog - The function to emit log messages.
3297
+ * @returns {airPurifierTableServiceData | null} - The parsed service data or null.
3298
+ */
3299
+ static parseServiceData(serviceData, manufacturerData, emitLog) {
3300
+ if (!manufacturerData || manufacturerData.length < 14) {
3301
+ return null;
3302
+ }
3303
+ const deviceData = manufacturerData.subarray(6);
3304
+ if (deviceData.length < 8) {
3305
+ return null;
3306
+ }
3307
+ const sequenceNumber = deviceData[0];
3308
+ const isOn = Boolean(deviceData[1] & 0b10000000);
3309
+ const mode = deviceData[1] & 0b00000111;
3310
+ const isAqiValid = Boolean(deviceData[2] & 0b00000100);
3311
+ const childLock = Boolean(deviceData[2] & 0b00000010);
3312
+ const speed = deviceData[3] & 0b01111111;
3313
+ const aqiLevelRaw = (deviceData[4] & 0b00000110) >> 1;
3314
+ const workTime = (deviceData[5] << 8) | deviceData[6];
3315
+ const errCode = deviceData[7];
3316
+ // Map AQI level to string using the defined constant
3317
+ const aqiLevelValues = [
3318
+ AIR_QUALITY_LEVELS.EXCELLENT,
3319
+ AIR_QUALITY_LEVELS.GOOD,
3320
+ AIR_QUALITY_LEVELS.FAIR,
3321
+ AIR_QUALITY_LEVELS.POOR,
3322
+ ];
3323
+ const aqiLevel = aqiLevelValues[aqiLevelRaw] || 'unknown';
3324
+ // Determine mode based on mode value and speed
3325
+ let modeString = null;
3326
+ if (mode === 1) {
3327
+ if (speed >= 0 && speed <= 33) {
3328
+ modeString = AIR_PURIFIER_MODES.LEVEL_1;
3329
+ }
3330
+ else if (speed >= 34 && speed <= 66) {
3331
+ modeString = AIR_PURIFIER_MODES.LEVEL_2;
3332
+ }
3333
+ else {
3334
+ modeString = AIR_PURIFIER_MODES.LEVEL_3;
3335
+ }
3336
+ }
3337
+ else if (mode > 1 && mode <= 4) {
3338
+ const modeMap = [null, null, 'auto', 'sleep', 'manual'];
3339
+ modeString = modeMap[mode + 2] || null;
3340
+ }
3341
+ if (emitLog) {
3342
+ emitLog('debug', `Air Purifier Table Service Data: isOn=${isOn}, mode=${modeString}, speed=${speed}, AQI=${aqiLevel}`);
3343
+ }
3344
+ return {
3345
+ model: SwitchBotBLEModel.AirPurifierTable,
3346
+ modelName: SwitchBotBLEModelName.AirPurifierTable,
3347
+ modelFriendlyName: SwitchBotBLEModelFriendlyName.AirPurifierTable,
3348
+ isOn,
3349
+ mode: modeString,
3350
+ isAqiValid,
3351
+ child_lock: childLock,
3352
+ speed,
3353
+ aqi_level: aqiLevel,
3354
+ filter_element_working_time: workTime,
3355
+ err_code: errCode,
3356
+ sequence_number: sequenceNumber,
3357
+ };
3358
+ }
3359
+ /**
3360
+ * Sets the state of the air purifier table.
3361
+ * @param {number[]} reqByteArray - The request byte array.
3362
+ * @returns {Promise<boolean>} - Resolves with a boolean indicating whether the operation was successful.
3363
+ * @private
3364
+ */
3365
+ async setState(reqByteArray) {
3366
+ return this.operateAirPurifierTable(reqByteArray);
3367
+ }
3368
+ /**
3369
+ * Turns the air purifier table on.
3370
+ * @returns {Promise<boolean>} - Resolves with true if the air purifier table is turned on.
3371
+ */
3372
+ async turnOn() {
3373
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.TURN_ON]);
3374
+ }
3375
+ /**
3376
+ * Turns the air purifier table off.
3377
+ * @returns {Promise<boolean>} - Resolves with true if the air purifier table is turned off.
3378
+ */
3379
+ async turnOff() {
3380
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.TURN_OFF]);
3381
+ }
3382
+ /**
3383
+ * Sets the speed of the air purifier table.
3384
+ * @param {number} speed - The speed value (0-100).
3385
+ * @returns {Promise<boolean>} - Resolves with true if the operation was successful.
3386
+ */
3387
+ async setSpeed(speed) {
3388
+ if (typeof speed !== 'number' || speed < 0 || speed > 100) {
3389
+ throw new TypeError(`Invalid speed value: ${speed}`);
3390
+ }
3391
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.SET_SPEED, speed]);
3392
+ }
3393
+ /**
3394
+ * Sets the mode of the air purifier table.
3395
+ * @param {number} mode - The mode value (1-4).
3396
+ * @returns {Promise<boolean>} - Resolves with true if the operation was successful.
3397
+ */
3398
+ async setMode(mode) {
3399
+ if (typeof mode !== 'number' || mode < 1 || mode > 4) {
3400
+ throw new TypeError(`Invalid mode value: ${mode}`);
3401
+ }
3402
+ return this.setState([...DEVICE_COMMANDS.AIR_PURIFIER.SET_MODE, mode]);
3403
+ }
3404
+ /**
3405
+ * Operates the air purifier table with the given byte array.
3406
+ * @public
3407
+ * @param {number[]} bytes - The byte array to send.
3408
+ * @returns {Promise<boolean>} - Resolves with true if the operation was successful.
3409
+ */
3410
+ async operateAirPurifierTable(bytes) {
3411
+ const req_buf = Buffer.from(bytes);
3412
+ const res_buf = await this.command(req_buf);
3413
+ if (res_buf.length !== 2) {
3414
+ throw new Error(`Expecting a 2-byte response, got instead: 0x${res_buf.toString('hex')}`);
3415
+ }
3416
+ const code = res_buf.readUInt8(1);
3417
+ if (code === 0x00 || code === 0x80) {
3418
+ return code === 0x80;
3419
+ }
3420
+ else {
3421
+ throw new Error(`The device returned an error: 0x${res_buf.toString('hex')}`);
3422
+ }
3423
+ }
3424
+ }
2845
3425
  //# sourceMappingURL=device.js.map