node-switchbot 4.0.0-beta.1 → 4.0.0-beta.11

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 (379) hide show
  1. package/.github/copilot-instructions.md +19 -5
  2. package/BLE.md +117 -4
  3. package/CHANGELOG.md +45 -0
  4. package/README.md +7 -1
  5. package/dist/api.d.ts +3 -3
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +9 -6
  8. package/dist/api.js.map +1 -1
  9. package/dist/ble.d.ts +38 -4
  10. package/dist/ble.d.ts.map +1 -1
  11. package/dist/ble.js +409 -53
  12. package/dist/ble.js.map +1 -1
  13. package/dist/devices/base.d.ts +83 -5
  14. package/dist/devices/base.d.ts.map +1 -1
  15. package/dist/devices/base.js +371 -34
  16. package/dist/devices/base.js.map +1 -1
  17. package/dist/devices/device-override-state-during-connection.d.ts +27 -0
  18. package/dist/devices/device-override-state-during-connection.d.ts.map +1 -0
  19. package/dist/devices/device-override-state-during-connection.js +45 -0
  20. package/dist/devices/device-override-state-during-connection.js.map +1 -0
  21. package/dist/devices/index.d.ts +29 -0
  22. package/dist/devices/index.d.ts.map +1 -1
  23. package/dist/devices/index.js +29 -0
  24. package/dist/devices/index.js.map +1 -1
  25. package/dist/devices/sequence-device.d.ts +36 -0
  26. package/dist/devices/sequence-device.d.ts.map +1 -0
  27. package/dist/devices/sequence-device.js +75 -0
  28. package/dist/devices/sequence-device.js.map +1 -0
  29. package/dist/devices/wo-air-purifier.d.ts +2 -2
  30. package/dist/devices/wo-air-purifier.d.ts.map +1 -1
  31. package/dist/devices/wo-air-purifier.js +2 -2
  32. package/dist/devices/wo-air-purifier.js.map +1 -1
  33. package/dist/devices/wo-art-frame.d.ts +8 -0
  34. package/dist/devices/wo-art-frame.d.ts.map +1 -0
  35. package/dist/devices/wo-art-frame.js +12 -0
  36. package/dist/devices/wo-art-frame.js.map +1 -0
  37. package/dist/devices/wo-bulb.d.ts +10 -0
  38. package/dist/devices/wo-bulb.d.ts.map +1 -1
  39. package/dist/devices/wo-bulb.js +69 -0
  40. package/dist/devices/wo-bulb.js.map +1 -1
  41. package/dist/devices/wo-circulator-fan.d.ts +8 -0
  42. package/dist/devices/wo-circulator-fan.d.ts.map +1 -0
  43. package/dist/devices/wo-circulator-fan.js +12 -0
  44. package/dist/devices/wo-circulator-fan.js.map +1 -0
  45. package/dist/devices/wo-climate-panel.d.ts +8 -0
  46. package/dist/devices/wo-climate-panel.d.ts.map +1 -0
  47. package/dist/devices/wo-climate-panel.js +12 -0
  48. package/dist/devices/wo-climate-panel.js.map +1 -0
  49. package/dist/devices/wo-curtain.d.ts +3 -3
  50. package/dist/devices/wo-curtain.d.ts.map +1 -1
  51. package/dist/devices/wo-curtain.js +12 -9
  52. package/dist/devices/wo-curtain.js.map +1 -1
  53. package/dist/devices/wo-floor-lamp.d.ts +8 -0
  54. package/dist/devices/wo-floor-lamp.d.ts.map +1 -0
  55. package/dist/devices/wo-floor-lamp.js +12 -0
  56. package/dist/devices/wo-floor-lamp.js.map +1 -0
  57. package/dist/devices/wo-garage-door-opener.d.ts +8 -0
  58. package/dist/devices/wo-garage-door-opener.d.ts.map +1 -0
  59. package/dist/devices/wo-garage-door-opener.js +12 -0
  60. package/dist/devices/wo-garage-door-opener.js.map +1 -0
  61. package/dist/devices/wo-hand.d.ts +53 -2
  62. package/dist/devices/wo-hand.d.ts.map +1 -1
  63. package/dist/devices/wo-hand.js +121 -3
  64. package/dist/devices/wo-hand.js.map +1 -1
  65. package/dist/devices/wo-hubmini-matter.d.ts +8 -0
  66. package/dist/devices/wo-hubmini-matter.d.ts.map +1 -0
  67. package/dist/devices/wo-hubmini-matter.js +12 -0
  68. package/dist/devices/wo-hubmini-matter.js.map +1 -0
  69. package/dist/devices/wo-humi2.d.ts +12 -0
  70. package/dist/devices/wo-humi2.d.ts.map +1 -1
  71. package/dist/devices/wo-humi2.js +18 -0
  72. package/dist/devices/wo-humi2.js.map +1 -1
  73. package/dist/devices/wo-keypad-vision-pro.d.ts +18 -0
  74. package/dist/devices/wo-keypad-vision-pro.d.ts.map +1 -0
  75. package/dist/devices/wo-keypad-vision-pro.js +15 -0
  76. package/dist/devices/wo-keypad-vision-pro.js.map +1 -0
  77. package/dist/devices/wo-keypad-vision.d.ts +8 -0
  78. package/dist/devices/wo-keypad-vision.d.ts.map +1 -0
  79. package/dist/devices/wo-keypad-vision.js +12 -0
  80. package/dist/devices/wo-keypad-vision.js.map +1 -0
  81. package/dist/devices/wo-lock-lite.d.ts +7 -0
  82. package/dist/devices/wo-lock-lite.d.ts.map +1 -0
  83. package/dist/devices/wo-lock-lite.js +11 -0
  84. package/dist/devices/wo-lock-lite.js.map +1 -0
  85. package/dist/devices/wo-lock-pro-wifi.d.ts +7 -0
  86. package/dist/devices/wo-lock-pro-wifi.d.ts.map +1 -0
  87. package/dist/devices/wo-lock-pro-wifi.js +11 -0
  88. package/dist/devices/wo-lock-pro-wifi.js.map +1 -0
  89. package/dist/devices/wo-lock-pro.d.ts +11 -2
  90. package/dist/devices/wo-lock-pro.d.ts.map +1 -1
  91. package/dist/devices/wo-lock-pro.js +65 -3
  92. package/dist/devices/wo-lock-pro.js.map +1 -1
  93. package/dist/devices/wo-lock-vision-pro.d.ts +7 -0
  94. package/dist/devices/wo-lock-vision-pro.d.ts.map +1 -0
  95. package/dist/devices/wo-lock-vision-pro.js +11 -0
  96. package/dist/devices/wo-lock-vision-pro.js.map +1 -0
  97. package/dist/devices/wo-lock-vision.d.ts +7 -0
  98. package/dist/devices/wo-lock-vision.d.ts.map +1 -0
  99. package/dist/devices/wo-lock-vision.js +11 -0
  100. package/dist/devices/wo-lock-vision.js.map +1 -0
  101. package/dist/devices/wo-lock.d.ts +7 -2
  102. package/dist/devices/wo-lock.d.ts.map +1 -1
  103. package/dist/devices/wo-lock.js +58 -3
  104. package/dist/devices/wo-lock.js.map +1 -1
  105. package/dist/devices/wo-plug-mini-us.d.ts +2 -2
  106. package/dist/devices/wo-plug-mini-us.d.ts.map +1 -1
  107. package/dist/devices/wo-plug-mini-us.js +2 -2
  108. package/dist/devices/wo-plug-mini-us.js.map +1 -1
  109. package/dist/devices/wo-relay-switch-1.d.ts +4 -2
  110. package/dist/devices/wo-relay-switch-1.d.ts.map +1 -1
  111. package/dist/devices/wo-relay-switch-1.js +36 -4
  112. package/dist/devices/wo-relay-switch-1.js.map +1 -1
  113. package/dist/devices/wo-relay-switch-2pm.d.ts +21 -0
  114. package/dist/devices/wo-relay-switch-2pm.d.ts.map +1 -0
  115. package/dist/devices/wo-relay-switch-2pm.js +39 -0
  116. package/dist/devices/wo-relay-switch-2pm.js.map +1 -0
  117. package/dist/devices/wo-rgbic-bulb.d.ts +29 -0
  118. package/dist/devices/wo-rgbic-bulb.d.ts.map +1 -0
  119. package/dist/devices/wo-rgbic-bulb.js +84 -0
  120. package/dist/devices/wo-rgbic-bulb.js.map +1 -0
  121. package/dist/devices/wo-rgbicww-floor-lamp.d.ts +8 -0
  122. package/dist/devices/wo-rgbicww-floor-lamp.d.ts.map +1 -0
  123. package/dist/devices/wo-rgbicww-floor-lamp.js +12 -0
  124. package/dist/devices/wo-rgbicww-floor-lamp.js.map +1 -0
  125. package/dist/devices/wo-rgbicww-strip-light.d.ts +8 -0
  126. package/dist/devices/wo-rgbicww-strip-light.d.ts.map +1 -0
  127. package/dist/devices/wo-rgbicww-strip-light.js +12 -0
  128. package/dist/devices/wo-rgbicww-strip-light.js.map +1 -0
  129. package/dist/devices/wo-roller-shade.d.ts +8 -0
  130. package/dist/devices/wo-roller-shade.d.ts.map +1 -0
  131. package/dist/devices/wo-roller-shade.js +12 -0
  132. package/dist/devices/wo-roller-shade.js.map +1 -0
  133. package/dist/devices/wo-smart-thermostat-radiator.d.ts +8 -0
  134. package/dist/devices/wo-smart-thermostat-radiator.d.ts.map +1 -0
  135. package/dist/devices/wo-smart-thermostat-radiator.js +12 -0
  136. package/dist/devices/wo-smart-thermostat-radiator.js.map +1 -0
  137. package/dist/devices/wo-strip-light-3.d.ts +8 -0
  138. package/dist/devices/wo-strip-light-3.d.ts.map +1 -0
  139. package/dist/devices/wo-strip-light-3.js +12 -0
  140. package/dist/devices/wo-strip-light-3.js.map +1 -0
  141. package/dist/devices/wo-vacuum-k10-plus.d.ts +7 -0
  142. package/dist/devices/wo-vacuum-k10-plus.d.ts.map +1 -0
  143. package/dist/devices/wo-vacuum-k10-plus.js +11 -0
  144. package/dist/devices/wo-vacuum-k10-plus.js.map +1 -0
  145. package/dist/devices/wo-vacuum-k10-pro-combo.d.ts +7 -0
  146. package/dist/devices/wo-vacuum-k10-pro-combo.d.ts.map +1 -0
  147. package/dist/devices/wo-vacuum-k10-pro-combo.js +11 -0
  148. package/dist/devices/wo-vacuum-k10-pro-combo.js.map +1 -0
  149. package/dist/devices/wo-vacuum-k10-pro.d.ts +7 -0
  150. package/dist/devices/wo-vacuum-k10-pro.d.ts.map +1 -0
  151. package/dist/devices/wo-vacuum-k10-pro.js +11 -0
  152. package/dist/devices/wo-vacuum-k10-pro.js.map +1 -0
  153. package/dist/devices/wo-vacuum-k11-plus.d.ts +7 -0
  154. package/dist/devices/wo-vacuum-k11-plus.d.ts.map +1 -0
  155. package/dist/devices/wo-vacuum-k11-plus.js +11 -0
  156. package/dist/devices/wo-vacuum-k11-plus.js.map +1 -0
  157. package/dist/devices/wo-vacuum-k20.d.ts +7 -0
  158. package/dist/devices/wo-vacuum-k20.d.ts.map +1 -0
  159. package/dist/devices/wo-vacuum-k20.js +11 -0
  160. package/dist/devices/wo-vacuum-k20.js.map +1 -0
  161. package/dist/devices/wo-vacuum-s10.d.ts +7 -0
  162. package/dist/devices/wo-vacuum-s10.d.ts.map +1 -0
  163. package/dist/devices/wo-vacuum-s10.js +11 -0
  164. package/dist/devices/wo-vacuum-s10.js.map +1 -0
  165. package/dist/devices/wo-vacuum-s20.d.ts +7 -0
  166. package/dist/devices/wo-vacuum-s20.d.ts.map +1 -0
  167. package/dist/devices/wo-vacuum-s20.js +11 -0
  168. package/dist/devices/wo-vacuum-s20.js.map +1 -0
  169. package/dist/devices/wo-vacuum.d.ts +44 -0
  170. package/dist/devices/wo-vacuum.d.ts.map +1 -0
  171. package/dist/devices/wo-vacuum.js +117 -0
  172. package/dist/devices/wo-vacuum.js.map +1 -0
  173. package/dist/index.d.ts +4 -2
  174. package/dist/index.d.ts.map +1 -1
  175. package/dist/index.js +3 -1
  176. package/dist/index.js.map +1 -1
  177. package/dist/settings.d.ts +27 -0
  178. package/dist/settings.d.ts.map +1 -1
  179. package/dist/settings.js +76 -6
  180. package/dist/settings.js.map +1 -1
  181. package/dist/switchbot.d.ts.map +1 -1
  182. package/dist/switchbot.js +86 -9
  183. package/dist/switchbot.js.map +1 -1
  184. package/dist/types/ble.d.ts +20 -1
  185. package/dist/types/ble.d.ts.map +1 -1
  186. package/dist/types/ble.js.map +1 -1
  187. package/dist/types/device.d.ts +49 -3
  188. package/dist/types/device.d.ts.map +1 -1
  189. package/dist/types/index.d.ts +22 -0
  190. package/dist/types/index.d.ts.map +1 -1
  191. package/dist/types/index.js.map +1 -1
  192. package/dist/utils/bot-ble.d.ts +36 -0
  193. package/dist/utils/bot-ble.d.ts.map +1 -0
  194. package/dist/utils/bot-ble.js +109 -0
  195. package/dist/utils/bot-ble.js.map +1 -0
  196. package/dist/utils/circuit-breaker.d.ts +98 -0
  197. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  198. package/dist/utils/circuit-breaker.js +187 -0
  199. package/dist/utils/circuit-breaker.js.map +1 -0
  200. package/dist/utils/connection-tracker.d.ts +66 -0
  201. package/dist/utils/connection-tracker.d.ts.map +1 -0
  202. package/dist/utils/connection-tracker.js +184 -0
  203. package/dist/utils/connection-tracker.js.map +1 -0
  204. package/dist/utils/fallback-handler.d.ts +68 -0
  205. package/dist/utils/fallback-handler.d.ts.map +1 -0
  206. package/dist/utils/fallback-handler.js +131 -0
  207. package/dist/utils/fallback-handler.js.map +1 -0
  208. package/dist/utils/index.d.ts +10 -0
  209. package/dist/utils/index.d.ts.map +1 -1
  210. package/dist/utils/index.js +41 -4
  211. package/dist/utils/index.js.map +1 -1
  212. package/dist/utils/retry.d.ts +55 -0
  213. package/dist/utils/retry.d.ts.map +1 -0
  214. package/dist/utils/retry.js +95 -0
  215. package/dist/utils/retry.js.map +1 -0
  216. package/docs/assets/hierarchy.js +1 -1
  217. package/docs/assets/navigation.js +1 -1
  218. package/docs/assets/search.js +1 -1
  219. package/docs/classes/APIError.html +2 -2
  220. package/docs/classes/APINotAvailableError.html +2 -2
  221. package/docs/classes/BLEConnection.html +16 -10
  222. package/docs/classes/BLENotAvailableError.html +2 -2
  223. package/docs/classes/BLEScanner.html +11 -9
  224. package/docs/classes/CommandFailedError.html +2 -2
  225. package/docs/classes/ConnectionTimeoutError.html +2 -2
  226. package/docs/classes/DeviceManager.html +13 -13
  227. package/docs/classes/DeviceNotFoundError.html +2 -2
  228. package/docs/classes/DeviceOverrideStateDuringConnection.html +56 -0
  229. package/docs/classes/DiscoveryError.html +2 -2
  230. package/docs/classes/OpenAPIClient.html +24 -24
  231. package/docs/classes/SequenceDevice.html +58 -0
  232. package/docs/classes/SwitchBot.html +11 -11
  233. package/docs/classes/SwitchBotDevice.html +43 -15
  234. package/docs/classes/SwitchBotError.html +2 -2
  235. package/docs/classes/ValidationError.html +2 -2
  236. package/docs/classes/WoAirPurifier.html +48 -18
  237. package/docs/classes/WoAirPurifierTable.html +48 -18
  238. package/docs/classes/WoArtFrame.html +71 -0
  239. package/docs/classes/WoBlindTilt.html +48 -20
  240. package/docs/classes/WoBulb.html +52 -19
  241. package/docs/classes/WoCeilingLight.html +52 -19
  242. package/docs/classes/WoCirculatorFan.html +66 -0
  243. package/docs/classes/WoClimatePanel.html +66 -0
  244. package/docs/classes/WoContact.html +42 -14
  245. package/docs/classes/WoCurtain.html +46 -18
  246. package/docs/classes/WoFloorLamp.html +71 -0
  247. package/docs/classes/WoGarageDoorOpener.html +64 -0
  248. package/docs/classes/WoHand.html +63 -19
  249. package/docs/classes/WoHub2.html +42 -14
  250. package/docs/classes/WoHub3.html +42 -14
  251. package/docs/classes/WoHubMiniMatter.html +56 -0
  252. package/docs/classes/WoHumi.html +46 -18
  253. package/docs/classes/WoHumi2.html +52 -18
  254. package/docs/classes/WoIOSensorTH.html +42 -14
  255. package/docs/classes/WoKeypad.html +42 -14
  256. package/docs/classes/WoKeypadVision.html +56 -0
  257. package/docs/classes/WoKeypadVisionPro.html +56 -0
  258. package/docs/classes/WoLeak.html +42 -14
  259. package/docs/classes/WoPlugMiniJP.html +45 -17
  260. package/docs/classes/WoPlugMiniUS.html +45 -17
  261. package/docs/classes/WoPresence.html +42 -14
  262. package/docs/classes/WoRGBICBulb.html +82 -0
  263. package/docs/classes/WoRGBICWWFloorLamp.html +82 -0
  264. package/docs/classes/WoRGBICWWStripLight.html +82 -0
  265. package/docs/classes/WoRelaySwitch1.html +47 -17
  266. package/docs/classes/WoRelaySwitch1PM.html +47 -17
  267. package/docs/classes/WoRelaySwitch2PM.html +68 -0
  268. package/docs/classes/WoRemote.html +42 -14
  269. package/docs/classes/WoRollerShade.html +64 -0
  270. package/docs/classes/WoSensorTH.html +42 -14
  271. package/docs/classes/WoSensorTHPlus.html +42 -14
  272. package/docs/classes/WoSensorTHPro.html +42 -14
  273. package/docs/classes/WoSensorTHProCO2.html +42 -14
  274. package/docs/classes/WoSmartLock.html +49 -16
  275. package/docs/classes/WoSmartLockLite.html +64 -0
  276. package/docs/classes/WoSmartLockPro.html +52 -17
  277. package/docs/classes/WoSmartLockProWiFi.html +68 -0
  278. package/docs/classes/WoSmartLockVision.html +64 -0
  279. package/docs/classes/WoSmartLockVisionPro.html +68 -0
  280. package/docs/classes/WoSmartThermostatRadiator.html +66 -0
  281. package/docs/classes/WoStrip.html +52 -19
  282. package/docs/classes/WoStripLight3.html +71 -0
  283. package/docs/classes/WoVacuum.html +71 -0
  284. package/docs/classes/WoVacuumK10Plus.html +71 -0
  285. package/docs/classes/WoVacuumK10Pro.html +71 -0
  286. package/docs/classes/WoVacuumK10ProCombo.html +71 -0
  287. package/docs/classes/WoVacuumK11Plus.html +71 -0
  288. package/docs/classes/WoVacuumK20.html +71 -0
  289. package/docs/classes/WoVacuumS10.html +71 -0
  290. package/docs/classes/WoVacuumS20.html +71 -0
  291. package/docs/enums/LogLevel.html +2 -2
  292. package/docs/enums/SwitchBotBLEModel.html +2 -2
  293. package/docs/enums/SwitchBotBLEModelName.html +2 -2
  294. package/docs/functions/updateBaseURL.html +1 -1
  295. package/docs/hierarchy.html +1 -1
  296. package/docs/index.html +2 -2
  297. package/docs/interfaces/APICommandRequest.html +2 -2
  298. package/docs/interfaces/APICommandResponse.html +2 -2
  299. package/docs/interfaces/APIDevice.html +2 -2
  300. package/docs/interfaces/APIDeviceStatus.html +2 -2
  301. package/docs/interfaces/APIErrorResponse.html +2 -2
  302. package/docs/interfaces/APIResponse.html +2 -2
  303. package/docs/interfaces/AirPurifierCommands.html +2 -2
  304. package/docs/interfaces/AirPurifierServiceData.html +20 -5
  305. package/docs/interfaces/AirPurifierStatus.html +7 -7
  306. package/docs/interfaces/BLEAdvertisement.html +3 -2
  307. package/docs/interfaces/BLEScanOptions.html +5 -5
  308. package/docs/interfaces/BLEServiceData.html +22 -5
  309. package/docs/interfaces/BlindTiltCommands.html +2 -2
  310. package/docs/interfaces/BlindTiltServiceData.html +21 -5
  311. package/docs/interfaces/BlindTiltStatus.html +6 -6
  312. package/docs/interfaces/BotCommands.html +6 -2
  313. package/docs/interfaces/BotServiceData.html +20 -5
  314. package/docs/interfaces/BotStatus.html +6 -6
  315. package/docs/interfaces/BulbCommands.html +4 -2
  316. package/docs/interfaces/BulbServiceData.html +21 -5
  317. package/docs/interfaces/BulbStatus.html +6 -6
  318. package/docs/interfaces/CeilingLightCommands.html +4 -2
  319. package/docs/interfaces/CeilingLightServiceData.html +21 -5
  320. package/docs/interfaces/CeilingLightStatus.html +6 -6
  321. package/docs/interfaces/CommandResult.html +6 -6
  322. package/docs/interfaces/ContactServiceData.html +22 -5
  323. package/docs/interfaces/ContactStatus.html +6 -6
  324. package/docs/interfaces/CurtainCommands.html +2 -2
  325. package/docs/interfaces/CurtainServiceData.html +22 -5
  326. package/docs/interfaces/CurtainStatus.html +6 -6
  327. package/docs/interfaces/DeviceInfo.html +23 -13
  328. package/docs/interfaces/DeviceListResponse.html +2 -2
  329. package/docs/interfaces/DeviceStatus.html +6 -6
  330. package/docs/interfaces/DiscoveryOptions.html +7 -7
  331. package/docs/interfaces/HubServiceData.html +22 -5
  332. package/docs/interfaces/HubStatus.html +6 -6
  333. package/docs/interfaces/HumidifierCommands.html +2 -2
  334. package/docs/interfaces/HumidifierServiceData.html +23 -6
  335. package/docs/interfaces/HumidifierStatus.html +6 -6
  336. package/docs/interfaces/KeypadStatus.html +6 -6
  337. package/docs/interfaces/LeakServiceData.html +22 -5
  338. package/docs/interfaces/LeakStatus.html +6 -6
  339. package/docs/interfaces/LockCommands.html +6 -2
  340. package/docs/interfaces/LockServiceData.html +20 -6
  341. package/docs/interfaces/LockStatus.html +6 -6
  342. package/docs/interfaces/MeterServiceData.html +22 -5
  343. package/docs/interfaces/MeterStatus.html +6 -6
  344. package/docs/interfaces/MotionServiceData.html +22 -5
  345. package/docs/interfaces/MotionStatus.html +6 -6
  346. package/docs/interfaces/PlugCommands.html +2 -2
  347. package/docs/interfaces/PlugServiceData.html +21 -5
  348. package/docs/interfaces/PlugStatus.html +6 -6
  349. package/docs/interfaces/PresenceServiceData.html +22 -5
  350. package/docs/interfaces/PresenceStatus.html +6 -6
  351. package/docs/interfaces/RelaySwitchCommands.html +2 -2
  352. package/docs/interfaces/RelaySwitchServiceData.html +21 -5
  353. package/docs/interfaces/RelaySwitchStatus.html +6 -6
  354. package/docs/interfaces/RemoteStatus.html +6 -6
  355. package/docs/interfaces/SceneListResponse.html +2 -2
  356. package/docs/interfaces/StripCommands.html +4 -2
  357. package/docs/interfaces/StripServiceData.html +21 -5
  358. package/docs/interfaces/StripStatus.html +6 -6
  359. package/docs/interfaces/SwitchBotConfig.html +21 -9
  360. package/docs/interfaces/VacuumCommands.html +8 -0
  361. package/docs/interfaces/VacuumStatus.html +16 -0
  362. package/docs/interfaces/WebhookConfig.html +2 -2
  363. package/docs/interfaces/WebhookDetails.html +2 -2
  364. package/docs/interfaces/WebhookQueryResponse.html +2 -2
  365. package/docs/interfaces/WebhookSetupResponse.html +2 -2
  366. package/docs/media/BLE.md +117 -4
  367. package/docs/modules.html +1 -1
  368. package/docs/types/ConnectionType.html +1 -1
  369. package/docs/types/PhysicalDeviceType.html +1 -1
  370. package/docs/types/VirtualDeviceType.html +1 -1
  371. package/docs/variables/urls.html +1 -1
  372. package/package.json +11 -7
  373. package/tmp-switchbot-scan.mjs +79 -0
  374. package/todo/PYSWITCHBOT_COMPARISON.md +484 -0
  375. package/todo/README.md +68 -0
  376. package/todo/completed.md +309 -0
  377. package/todo/todo.md +302 -0
  378. package/tsconfig.build.json +17 -0
  379. package/PRODUCTION_READY.md +0 -135
package/dist/ble.js CHANGED
@@ -2,10 +2,12 @@
2
2
  *
3
3
  * ble.ts: SwitchBot v4.0.0 - BLE Discovery and Communication
4
4
  */
5
+ import { Buffer } from 'node:buffer';
6
+ import { createCipheriv } from 'node:crypto';
5
7
  import { EventEmitter } from 'node:events';
6
- import { BLENotAvailableError, DeviceNotFoundError } from './errors.js';
8
+ import { BLENotAvailableError, CommandFailedError, DeviceNotFoundError } from './errors.js';
7
9
  import { BLE_COMMAND_TIMEOUT, BLE_CONNECT_TIMEOUT, BLE_NOTIFY_CHARACTERISTIC_UUID, BLE_SCAN_TIMEOUT, BLE_SERVICE_UUID, BLE_WRITE_CHARACTERISTIC_UUID, DEVICE_MODEL_MAP } from './settings.js';
8
- import { Logger, normalizeMAC, withTimeout } from './utils/index.js';
10
+ import { Logger, macToDeviceId, normalizeMAC, withTimeout, extractMacFromManufacturerData } from './utils/index.js';
9
11
  /**
10
12
  * BLE Scanner for discovering SwitchBot devices
11
13
  */
@@ -14,6 +16,11 @@ export class BLEScanner extends EventEmitter {
14
16
  logger;
15
17
  scanning = false;
16
18
  discoveredDevices = new Map();
19
+ discoveredModelCache = new Map();
20
+ nobleStateHandler;
21
+ nobleDiscoverHandler;
22
+ nobleScanStartHandler;
23
+ nobleScanStopHandler;
17
24
  noblePromise = null;
18
25
  constructor(options = {}) {
19
26
  super();
@@ -32,20 +39,23 @@ export class BLEScanner extends EventEmitter {
32
39
  * Initialize Noble lazily
33
40
  */
34
41
  async initializeNoble() {
35
- if (this.noble || this.noblePromise) {
42
+ if (this.noble) {
36
43
  return;
37
44
  }
38
- if (!this.noblePromise) {
39
- this.noblePromise = (async () => {
40
- try {
41
- const module = await import('@stoprocent/noble');
42
- return module.default;
43
- }
44
- catch (error) {
45
- throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
46
- }
47
- })();
45
+ if (this.noblePromise) {
46
+ // Wait for existing initialization to complete
47
+ this.noble = await this.noblePromise;
48
+ return;
48
49
  }
50
+ this.noblePromise = (async () => {
51
+ try {
52
+ const module = await import('@stoprocent/noble');
53
+ return module.default;
54
+ }
55
+ catch (error) {
56
+ throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
57
+ }
58
+ })();
49
59
  try {
50
60
  this.noble = await this.noblePromise;
51
61
  this.setupNobleHandlers();
@@ -61,61 +71,133 @@ export class BLEScanner extends EventEmitter {
61
71
  if (!this.noble) {
62
72
  await this.initializeNoble();
63
73
  }
74
+ if (!this.noble) {
75
+ throw new BLENotAvailableError('BLE not available - noble failed to initialize');
76
+ }
64
77
  }
65
78
  /**
66
79
  * Setup Noble event handlers
67
80
  */
68
81
  setupNobleHandlers() {
69
- this.noble.on('stateChange', (state) => {
82
+ // Prevent duplicate listeners if handlers are re-initialized
83
+ this.removeNobleHandlers();
84
+ // Store handlers as properties for later cleanup
85
+ this.nobleStateHandler = (state) => {
70
86
  this.logger.debug('Noble state changed:', state);
71
87
  this.emit('state-change', state);
72
88
  if (state === 'poweredOn') {
73
89
  this.emit('ready');
74
90
  }
75
- });
76
- this.noble.on('discover', (peripheral) => {
91
+ };
92
+ this.nobleDiscoverHandler = (peripheral) => {
77
93
  this.handleDiscovery(peripheral);
78
- });
79
- this.noble.on('scanStart', () => {
94
+ };
95
+ this.nobleScanStartHandler = () => {
80
96
  this.scanning = true;
81
97
  this.logger.info('BLE scan started');
82
98
  this.emit('scan-start');
83
- });
84
- this.noble.on('scanStop', () => {
99
+ };
100
+ this.nobleScanStopHandler = () => {
85
101
  this.scanning = false;
86
102
  this.logger.info('BLE scan stopped');
87
103
  this.emit('scan-stop');
88
- });
104
+ };
105
+ this.noble.on('stateChange', this.nobleStateHandler);
106
+ this.noble.on('discover', this.nobleDiscoverHandler);
107
+ this.noble.on('scanStart', this.nobleScanStartHandler);
108
+ this.noble.on('scanStop', this.nobleScanStopHandler);
109
+ }
110
+ /**
111
+ * Remove Noble event handlers
112
+ */
113
+ removeNobleHandlers() {
114
+ if (this.nobleStateHandler) {
115
+ this.noble.off('stateChange', this.nobleStateHandler);
116
+ }
117
+ if (this.nobleDiscoverHandler) {
118
+ this.noble.off('discover', this.nobleDiscoverHandler);
119
+ }
120
+ if (this.nobleScanStartHandler) {
121
+ this.noble.off('scanStart', this.nobleScanStartHandler);
122
+ }
123
+ if (this.nobleScanStopHandler) {
124
+ this.noble.off('scanStop', this.nobleScanStopHandler);
125
+ }
126
+ this.nobleStateHandler = undefined;
127
+ this.nobleDiscoverHandler = undefined;
128
+ this.nobleScanStartHandler = undefined;
129
+ this.nobleScanStopHandler = undefined;
89
130
  }
90
131
  /**
91
132
  * Handle device discovery
92
133
  */
93
134
  handleDiscovery(peripheral) {
94
135
  try {
95
- const { advertisement, address, rssi } = peripheral;
96
- // Check if this is a SwitchBot device
97
- if (!advertisement.serviceData || advertisement.serviceData.length === 0) {
136
+ // Validate peripheral has required properties
137
+ if (!peripheral || typeof peripheral !== 'object') {
138
+ return;
139
+ }
140
+ const { advertisement, address, rssi, connectable } = peripheral;
141
+ // Skip non-connectable devices
142
+ if (connectable === false) {
143
+ return;
144
+ }
145
+ // Skip devices with invalid RSSI (typical range: -120 to 0 dBm)
146
+ if (typeof rssi !== 'number' || rssi < -120 || rssi > 0) {
147
+ return;
148
+ }
149
+ // Validate advertisement object exists and has service data
150
+ if (!advertisement || typeof advertisement !== 'object') {
151
+ return;
152
+ }
153
+ if (!Array.isArray(advertisement.serviceData) || advertisement.serviceData.length === 0) {
98
154
  return;
99
155
  }
100
156
  for (const serviceDataItem of advertisement.serviceData) {
101
- // SwitchBot service UUID
102
- if (serviceDataItem.uuid !== 'fd3d' && serviceDataItem.uuid !== '0000fd3d-0000-1000-8000-00805f9b34fb') {
157
+ // Validate service data item has required properties
158
+ if (!serviceDataItem || typeof serviceDataItem !== 'object') {
159
+ continue;
160
+ }
161
+ // SwitchBot service UUID (current: fd3d, legacy: 000d)
162
+ const uuid = typeof serviceDataItem.uuid === 'string' ? serviceDataItem.uuid.toLowerCase() : '';
163
+ const isSwitchBotUUID = uuid === 'fd3d' ||
164
+ uuid === '0000fd3d-0000-1000-8000-00805f9b34fb' ||
165
+ uuid === '000d' ||
166
+ uuid === '0000000d-0000-1000-8000-00805f9b34fb';
167
+ if (!isSwitchBotUUID) {
103
168
  continue;
104
169
  }
105
170
  const serviceData = this.parseServiceData(serviceDataItem.data);
106
171
  if (!serviceData) {
107
172
  continue;
108
173
  }
109
- const mac = normalizeMAC(address);
174
+ let normalizedAddress = typeof address === 'string' && address.length > 0 ? normalizeMAC(address) : undefined;
175
+ // Fallback to manufacturer data MAC if service data address is empty
176
+ if (!normalizedAddress) {
177
+ const manufacturerMac = extractMacFromManufacturerData(peripheral.advertisement?.manufacturerData);
178
+ if (manufacturerMac) {
179
+ normalizedAddress = manufacturerMac;
180
+ }
181
+ }
182
+ const fallbackId = typeof peripheral.id === 'string' && peripheral.id.length > 0
183
+ ? peripheral.id
184
+ : (normalizedAddress ? macToDeviceId(normalizedAddress) : undefined);
185
+ if (!fallbackId) {
186
+ this.logger.debug('Skipping BLE discovery with no address and no peripheral id');
187
+ continue;
188
+ }
110
189
  const advertisement = {
111
- id: peripheral.id,
112
- address: mac,
190
+ id: fallbackId,
191
+ address: normalizedAddress,
192
+ isAddressable: normalizedAddress !== undefined,
113
193
  rssi,
114
194
  serviceData,
115
195
  };
116
- this.discoveredDevices.set(mac, advertisement);
196
+ const discoveryKey = normalizedAddress ?? `id:${fallbackId}`;
197
+ this.discoveredDevices.set(discoveryKey, advertisement);
198
+ this.discoveredModelCache.set(discoveryKey, serviceData.model);
117
199
  this.emit('discover', advertisement);
118
- this.logger.debug(`Discovered ${serviceData.modelName} at ${mac}`);
200
+ this.logger.debug(`Discovered ${serviceData.modelName} at ${normalizedAddress ?? fallbackId}`);
119
201
  }
120
202
  }
121
203
  catch (error) {
@@ -146,6 +228,45 @@ export class BLEScanner extends EventEmitter {
146
228
  if (data.length > 2) {
147
229
  serviceData.battery = data[2] & 0x7F;
148
230
  }
231
+ // Model-specific advertisement parsing for status without active connection
232
+ if (model === 'H' && data.length > 1) {
233
+ // Bot (WoHand): infer mode/state from status bits
234
+ serviceData.mode = (data[1] & 0x80) ? 'switch' : 'press';
235
+ serviceData.state = (data[1] & 0x40) !== 0;
236
+ }
237
+ if ((model === 'c' || model === '{') && data.length > 4) {
238
+ // Curtain/Curtain3
239
+ serviceData.inMotion = (data[1] & 0x40) !== 0;
240
+ serviceData.position = Math.min(100, Math.max(0, data[3] & 0x7F));
241
+ serviceData.lightLevel = data[4] & 0x7F;
242
+ serviceData.calibration = (data[1] & 0x20) !== 0;
243
+ if (data.length > 5) {
244
+ serviceData.deviceChain = data[5] & 0x03;
245
+ }
246
+ }
247
+ if ((model === 'o' || model === '\x11') && data.length > 1) {
248
+ // Lock / Lock Pro
249
+ const lockStatus = data[1] & 0x0F;
250
+ serviceData.status = lockStatus;
251
+ serviceData.doorOpen = (data[1] & 0x10) !== 0;
252
+ serviceData.calibration = (data[1] & 0x80) !== 0;
253
+ serviceData.lockState = lockStatus === 1
254
+ ? 'unlocked'
255
+ : lockStatus === 2
256
+ ? 'jammed'
257
+ : 'locked';
258
+ if (data.length > 3) {
259
+ serviceData.sequenceNumber = data[3];
260
+ }
261
+ }
262
+ if ((model === '\x0D' || model === '\x0E') && data.length > 1) {
263
+ // Relay Switch 1PM / Relay Switch 1
264
+ serviceData.state = (data[1] & 0x01) === 0x01;
265
+ serviceData.channel2State = (data[1] & 0x02) === 0x02;
266
+ if (data.length > 3) {
267
+ serviceData.sequenceNumber = data[3];
268
+ }
269
+ }
149
270
  return serviceData;
150
271
  }
151
272
  catch (error) {
@@ -160,6 +281,9 @@ export class BLEScanner extends EventEmitter {
160
281
  await this.ensureNoble();
161
282
  const { duration = BLE_SCAN_TIMEOUT, active = true } = options;
162
283
  this.logger.info('Starting BLE scan', { duration, active });
284
+ if (!this.noble) {
285
+ throw new BLENotAvailableError('BLE not available - noble failed to initialize');
286
+ }
163
287
  if (this.noble.state !== 'poweredOn') {
164
288
  await new Promise((resolve, reject) => {
165
289
  const timeout = setTimeout(() => {
@@ -173,6 +297,7 @@ export class BLEScanner extends EventEmitter {
173
297
  }
174
298
  // Clear previous discoveries if starting fresh
175
299
  this.discoveredDevices.clear();
300
+ this.discoveredModelCache.clear();
176
301
  // Start scanning
177
302
  this.noble.startScanning([], active);
178
303
  // Auto-stop after duration
@@ -192,6 +317,15 @@ export class BLEScanner extends EventEmitter {
192
317
  this.noble.stopScanning();
193
318
  }
194
319
  }
320
+ /**
321
+ * Cleanup all resources
322
+ */
323
+ destroy() {
324
+ this.stopScan();
325
+ this.removeNobleHandlers();
326
+ this.discoveredDevices.clear();
327
+ this.discoveredModelCache.clear();
328
+ }
195
329
  /**
196
330
  * Get all discovered devices
197
331
  */
@@ -199,10 +333,19 @@ export class BLEScanner extends EventEmitter {
199
333
  return Array.from(this.discoveredDevices.values());
200
334
  }
201
335
  /**
202
- * Get discovered device by MAC
336
+ * Get discovered device by MAC or BLE ID
203
337
  */
204
- getDevice(mac) {
205
- return this.discoveredDevices.get(normalizeMAC(mac));
338
+ getDevice(mac, bleId) {
339
+ // Try MAC lookup first
340
+ const byMac = this.discoveredDevices.get(normalizeMAC(mac));
341
+ if (byMac) {
342
+ return byMac;
343
+ }
344
+ // Fall back to ID-based lookup
345
+ if (bleId) {
346
+ return this.discoveredDevices.get(`id:${bleId}`);
347
+ }
348
+ return undefined;
206
349
  }
207
350
  /**
208
351
  * Check if currently scanning
@@ -213,17 +356,20 @@ export class BLEScanner extends EventEmitter {
213
356
  /**
214
357
  * Wait for specific device
215
358
  */
216
- async waitForDevice(mac, timeoutMs = BLE_SCAN_TIMEOUT) {
359
+ async waitForDevice(mac, timeoutMs = BLE_SCAN_TIMEOUT, bleId) {
217
360
  const normalizedMac = normalizeMAC(mac);
218
- // Check if already discovered
219
- const existing = this.discoveredDevices.get(normalizedMac);
361
+ // Check if already discovered (try MAC first, then ID)
362
+ const existing = this.discoveredDevices.get(normalizedMac) || (bleId ? this.discoveredDevices.get(`id:${bleId}`) : undefined);
220
363
  if (existing) {
221
364
  return existing;
222
365
  }
223
366
  // Wait for discovery
224
367
  return withTimeout(new Promise((resolve) => {
225
368
  const handler = (advertisement) => {
226
- if (normalizeMAC(advertisement.address) === normalizedMac) {
369
+ // Match by address (if available) or by ID
370
+ const matches = (advertisement.address && normalizeMAC(advertisement.address) === normalizedMac) ||
371
+ (bleId && advertisement.id === bleId);
372
+ if (matches) {
227
373
  this.off('discover', handler);
228
374
  resolve(advertisement);
229
375
  }
@@ -240,6 +386,11 @@ export class BLEConnection {
240
386
  logger;
241
387
  connections = new Map(); // Map of MAC -> Peripheral
242
388
  characteristics = new Map();
389
+ disconnectTimers = new Map();
390
+ operationLocks = new Map();
391
+ encryptionConfig = new Map();
392
+ notificationHandlers = new Map();
393
+ persistentConnectionMs = 8500;
243
394
  noblePromise = null;
244
395
  constructor(options = {}) {
245
396
  this.logger = new Logger('BLEConnection', options.logLevel);
@@ -254,20 +405,23 @@ export class BLEConnection {
254
405
  * Initialize Noble lazily
255
406
  */
256
407
  async initializeNoble() {
257
- if (this.noble || this.noblePromise) {
408
+ if (this.noble) {
258
409
  return;
259
410
  }
260
- if (!this.noblePromise) {
261
- this.noblePromise = (async () => {
262
- try {
263
- const module = await import('@stoprocent/noble');
264
- return module.default;
265
- }
266
- catch (error) {
267
- throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
268
- }
269
- })();
411
+ if (this.noblePromise) {
412
+ // Wait for existing initialization to complete
413
+ this.noble = await this.noblePromise;
414
+ return;
270
415
  }
416
+ this.noblePromise = (async () => {
417
+ try {
418
+ const module = await import('@stoprocent/noble');
419
+ return module.default;
420
+ }
421
+ catch (error) {
422
+ throw new BLENotAvailableError('BLE not supported on this platform or @stoprocent/noble not installed');
423
+ }
424
+ })();
271
425
  try {
272
426
  this.noble = await this.noblePromise;
273
427
  }
@@ -282,6 +436,178 @@ export class BLEConnection {
282
436
  if (!this.noble) {
283
437
  await this.initializeNoble();
284
438
  }
439
+ if (!this.noble) {
440
+ throw new BLENotAvailableError('BLE not available - noble failed to initialize');
441
+ }
442
+ }
443
+ async withMacLock(mac, fn) {
444
+ const normalizedMac = normalizeMAC(mac);
445
+ const previousLock = this.operationLocks.get(normalizedMac) ?? Promise.resolve();
446
+ let releaseCurrent = () => { };
447
+ const currentLock = new Promise((resolve) => {
448
+ releaseCurrent = resolve;
449
+ });
450
+ const chainedLock = previousLock.then(() => currentLock);
451
+ this.operationLocks.set(normalizedMac, chainedLock);
452
+ await previousLock;
453
+ try {
454
+ return await fn();
455
+ }
456
+ finally {
457
+ releaseCurrent();
458
+ if (this.operationLocks.get(normalizedMac) === chainedLock) {
459
+ this.operationLocks.delete(normalizedMac);
460
+ }
461
+ }
462
+ }
463
+ clearDisconnectTimer(mac) {
464
+ const existingTimer = this.disconnectTimers.get(mac);
465
+ if (existingTimer) {
466
+ clearTimeout(existingTimer);
467
+ this.disconnectTimers.delete(mac);
468
+ }
469
+ }
470
+ scheduleDisconnect(mac) {
471
+ this.clearDisconnectTimer(mac);
472
+ const timer = setTimeout(() => {
473
+ this.disconnect(mac).catch((error) => {
474
+ this.logger.debug(`Auto-disconnect failed for ${mac}`, error);
475
+ });
476
+ }, this.persistentConnectionMs);
477
+ this.disconnectTimers.set(mac, timer);
478
+ }
479
+ setPersistentConnectionTimeout(timeoutMs) {
480
+ this.persistentConnectionMs = Math.max(1000, timeoutMs);
481
+ }
482
+ setEncryption(mac, keyHex, ivHex, mode = 'auto') {
483
+ const normalizedMac = normalizeMAC(mac);
484
+ const key = Buffer.from(keyHex, 'hex');
485
+ const iv = Buffer.from(ivHex, 'hex');
486
+ if (key.length !== 16) {
487
+ throw new CommandFailedError('Invalid BLE encryption key length (expected 16 bytes)', 'ble');
488
+ }
489
+ const resolvedMode = mode === 'auto'
490
+ ? (iv.length === 12 ? 'gcm' : 'ctr')
491
+ : mode;
492
+ const expectedIvLength = resolvedMode === 'gcm' ? 12 : 16;
493
+ if (iv.length !== expectedIvLength) {
494
+ throw new CommandFailedError(`Invalid IV length for ${resolvedMode.toUpperCase()} mode`, 'ble');
495
+ }
496
+ this.encryptionConfig.set(normalizedMac, { key, iv: Buffer.from(iv), mode: resolvedMode });
497
+ }
498
+ clearEncryption(mac) {
499
+ this.encryptionConfig.delete(normalizeMAC(mac));
500
+ }
501
+ incrementIv(iv) {
502
+ const nextIv = Buffer.from(iv);
503
+ for (let i = nextIv.length - 1; i >= 0; i--) {
504
+ if ((nextIv[i] ?? 0) === 0xFF) {
505
+ nextIv[i] = 0x00;
506
+ }
507
+ else {
508
+ nextIv[i] = (nextIv[i] ?? 0) + 1;
509
+ break;
510
+ }
511
+ }
512
+ return nextIv;
513
+ }
514
+ encryptIfConfigured(mac, data) {
515
+ const normalizedMac = normalizeMAC(mac);
516
+ const config = this.encryptionConfig.get(normalizedMac);
517
+ if (!config) {
518
+ return data;
519
+ }
520
+ if (config.mode === 'gcm') {
521
+ const cipher = createCipheriv('aes-128-gcm', config.key, config.iv);
522
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
523
+ const tag = cipher.getAuthTag().subarray(0, 2);
524
+ this.encryptionConfig.set(normalizedMac, {
525
+ ...config,
526
+ iv: this.incrementIv(config.iv),
527
+ });
528
+ return Buffer.concat([encrypted, tag]);
529
+ }
530
+ const cipher = createCipheriv('aes-128-ctr', config.key, config.iv);
531
+ return Buffer.concat([cipher.update(data), cipher.final()]);
532
+ }
533
+ validateCommandResult(response, mac) {
534
+ if (!response || response.length === 0) {
535
+ throw new CommandFailedError(`Empty BLE response from ${mac}`, 'ble');
536
+ }
537
+ if (response.includes(0x07)) {
538
+ throw new CommandFailedError(`BLE command rejected by ${mac}: password required`, 'ble');
539
+ }
540
+ if (response.includes(0x09)) {
541
+ throw new CommandFailedError(`BLE command rejected by ${mac}: password incorrect`, 'ble');
542
+ }
543
+ const acknowledged = response.some(byte => byte === 0x01 || byte === 0x05 || byte === 0x06);
544
+ if (!acknowledged) {
545
+ throw new CommandFailedError(`Unexpected BLE response from ${mac}: ${response.toString('hex')}`, 'ble');
546
+ }
547
+ }
548
+ async sendCommand(mac, data, options = {}) {
549
+ const normalizedMac = normalizeMAC(mac);
550
+ const { expectResponse = true, validateResponse = true, responseTimeoutMs = 1200, } = options;
551
+ return this.withMacLock(normalizedMac, async () => {
552
+ const payload = this.encryptIfConfigured(normalizedMac, data);
553
+ await this.write(normalizedMac, payload);
554
+ if (!expectResponse && !validateResponse) {
555
+ return undefined;
556
+ }
557
+ const response = await withTimeout(this.read(normalizedMac), responseTimeoutMs, `BLE response timeout from ${normalizedMac}`);
558
+ if (validateResponse) {
559
+ this.validateCommandResult(response, normalizedMac);
560
+ }
561
+ return response;
562
+ });
563
+ }
564
+ async subscribeNotifications(mac, handler) {
565
+ const normalizedMac = normalizeMAC(mac);
566
+ if (!this.connections.has(normalizedMac)) {
567
+ await this.connect(normalizedMac);
568
+ }
569
+ const chars = this.characteristics.get(normalizedMac);
570
+ if (!chars?.notify) {
571
+ throw new CommandFailedError(`Notify characteristic not available for ${normalizedMac}`, 'ble');
572
+ }
573
+ if (!this.notificationHandlers.has(normalizedMac)) {
574
+ this.notificationHandlers.set(normalizedMac, new Set());
575
+ if (typeof chars.notify.on === 'function') {
576
+ chars.notify.on('data', (payload) => {
577
+ const handlers = this.notificationHandlers.get(normalizedMac);
578
+ if (!handlers) {
579
+ return;
580
+ }
581
+ for (const listener of handlers) {
582
+ listener(payload);
583
+ }
584
+ });
585
+ }
586
+ }
587
+ this.notificationHandlers.get(normalizedMac).add(handler);
588
+ if (typeof chars.notify.subscribe === 'function') {
589
+ await new Promise((resolve, reject) => {
590
+ chars.notify.subscribe((error) => {
591
+ if (error) {
592
+ reject(error);
593
+ }
594
+ else {
595
+ resolve();
596
+ }
597
+ });
598
+ });
599
+ }
600
+ }
601
+ unsubscribeNotifications(mac, handler) {
602
+ const normalizedMac = normalizeMAC(mac);
603
+ const handlers = this.notificationHandlers.get(normalizedMac);
604
+ if (!handlers) {
605
+ return;
606
+ }
607
+ handlers.delete(handler);
608
+ if (handlers.size === 0) {
609
+ this.notificationHandlers.delete(normalizedMac);
610
+ }
285
611
  }
286
612
  /**
287
613
  * Connect to a device
@@ -291,13 +617,25 @@ export class BLEConnection {
291
617
  const normalizedMac = normalizeMAC(mac);
292
618
  // Already connected?
293
619
  if (this.connections.has(normalizedMac)) {
620
+ this.clearDisconnectTimer(normalizedMac);
294
621
  this.logger.debug(`Already connected to ${mac}`);
295
622
  return;
296
623
  }
297
624
  this.logger.info(`Connecting to ${mac}`);
298
- // Find peripheral
625
+ // Find peripheral (by address or ID)
299
626
  const peripherals = await this.noble.peripherals || [];
300
- const peripheral = peripherals.find((p) => normalizeMAC(p.address) === normalizedMac);
627
+ let peripheral;
628
+ // Try to find by normalized MAC first
629
+ if (mac.startsWith('id:')) {
630
+ // ID-based lookup: extract the ID and find by peripheral.id
631
+ const bleId = mac.substring(3);
632
+ // Look through peripherals to find matching ID
633
+ peripheral = peripherals.find((p) => p.id === bleId);
634
+ }
635
+ else {
636
+ // MAC-based lookup
637
+ peripheral = peripherals.find((p) => p.address && normalizeMAC(p.address) === normalizedMac);
638
+ }
301
639
  if (!peripheral) {
302
640
  throw new DeviceNotFoundError(mac);
303
641
  }
@@ -313,9 +651,22 @@ export class BLEConnection {
313
651
  });
314
652
  }), BLE_CONNECT_TIMEOUT, `Connection to ${mac} timed out`);
315
653
  this.connections.set(normalizedMac, peripheral);
654
+ this.clearDisconnectTimer(normalizedMac);
316
655
  this.logger.info(`Connected to ${mac}`);
317
- // Discover characteristics
318
- await this.discoverCharacteristics(normalizedMac, peripheral);
656
+ // Discover characteristics (may throw)
657
+ try {
658
+ await this.discoverCharacteristics(normalizedMac, peripheral);
659
+ }
660
+ catch (error) {
661
+ // Clean up partial connection if characteristic discovery fails
662
+ this.connections.delete(normalizedMac);
663
+ this.characteristics.delete(normalizedMac);
664
+ // Best effort disconnect to avoid leaked active BLE links
665
+ await new Promise((resolve) => {
666
+ peripheral.disconnect(() => resolve());
667
+ });
668
+ throw error;
669
+ }
319
670
  }
320
671
  /**
321
672
  * Discover service characteristics
@@ -345,6 +696,9 @@ export class BLEConnection {
345
696
  async disconnect(mac) {
346
697
  const normalizedMac = normalizeMAC(mac);
347
698
  const peripheral = this.connections.get(normalizedMac);
699
+ this.clearDisconnectTimer(normalizedMac);
700
+ this.clearEncryption(normalizedMac);
701
+ this.notificationHandlers.delete(normalizedMac);
348
702
  if (!peripheral) {
349
703
  return;
350
704
  }
@@ -377,6 +731,7 @@ export class BLEConnection {
377
731
  reject(error);
378
732
  }
379
733
  else {
734
+ this.scheduleDisconnect(normalizedMac);
380
735
  resolve();
381
736
  }
382
737
  });
@@ -402,6 +757,7 @@ export class BLEConnection {
402
757
  reject(error);
403
758
  }
404
759
  else {
760
+ this.scheduleDisconnect(normalizedMac);
405
761
  resolve(data);
406
762
  }
407
763
  });