@switchbot/homebridge-switchbot 5.0.0-beta.99 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/.changeset/config.json +14 -0
  2. package/.github/copilot-instructions.md +39 -0
  3. package/.github/workflows/ci.yml +4 -1
  4. package/.github/workflows/manual-e2e.yml +6 -3
  5. package/.github/workflows/release.yml +64 -15
  6. package/.github/workflows/stale.yml +2 -4
  7. package/.husky/pre-push +15 -0
  8. package/CHANGELOG.md +126 -134
  9. package/MIGRATION.md +16 -6
  10. package/README.md +84 -3
  11. package/TODO.md +263 -0
  12. package/config.schema.json +229 -36
  13. package/dist/SwitchBotHAPPlatform.d.ts +133 -0
  14. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
  15. package/dist/SwitchBotHAPPlatform.js +555 -0
  16. package/dist/SwitchBotHAPPlatform.js.map +1 -0
  17. package/dist/SwitchBotMatterPlatform.d.ts +141 -0
  18. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
  19. package/dist/SwitchBotMatterPlatform.js +536 -0
  20. package/dist/SwitchBotMatterPlatform.js.map +1 -0
  21. package/dist/device-types.d.ts +31 -0
  22. package/dist/device-types.d.ts.map +1 -0
  23. package/dist/device-types.js +246 -0
  24. package/dist/device-types.js.map +1 -0
  25. package/dist/deviceCommandMapper.d.ts +10 -0
  26. package/dist/deviceCommandMapper.d.ts.map +1 -0
  27. package/dist/deviceCommandMapper.js +319 -0
  28. package/dist/deviceCommandMapper.js.map +1 -0
  29. package/dist/deviceFactory.d.ts +3 -2
  30. package/dist/deviceFactory.d.ts.map +1 -1
  31. package/dist/deviceFactory.js +107 -29
  32. package/dist/deviceFactory.js.map +1 -1
  33. package/dist/devices/genericDevice.d.ts +59 -37
  34. package/dist/devices/genericDevice.d.ts.map +1 -1
  35. package/dist/devices/genericDevice.js +376 -78
  36. package/dist/devices/genericDevice.js.map +1 -1
  37. package/dist/errors.d.ts +38 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +32 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/homebridge-ui/device-types.js +246 -0
  42. package/dist/homebridge-ui/device-types.js.map +1 -0
  43. package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
  44. package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
  45. package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
  46. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  47. package/dist/homebridge-ui/endpoints/config.js +90 -0
  48. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  49. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  50. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  51. package/dist/homebridge-ui/endpoints/devices.js +144 -0
  52. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  53. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  54. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  55. package/dist/homebridge-ui/endpoints/discovery.js +219 -0
  56. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  57. package/dist/homebridge-ui/errors.js +32 -0
  58. package/dist/homebridge-ui/errors.js.map +1 -0
  59. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
  60. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
  61. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
  62. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
  63. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
  64. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
  65. package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
  66. package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
  67. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
  68. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
  69. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
  70. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
  71. package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
  72. package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
  73. package/dist/homebridge-ui/public/css/styles.css +483 -0
  74. package/dist/homebridge-ui/public/index.html +197 -621
  75. package/dist/homebridge-ui/public/js/advanced-settings.d.ts +3 -0
  76. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -0
  77. package/dist/homebridge-ui/public/js/advanced-settings.js +95 -0
  78. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -0
  79. package/dist/homebridge-ui/public/js/advanced-settings.ts +94 -0
  80. package/dist/homebridge-ui/public/js/api.d.ts +66 -0
  81. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  82. package/dist/homebridge-ui/public/js/api.js +295 -0
  83. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  84. package/dist/homebridge-ui/public/js/api.ts +355 -0
  85. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  86. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  87. package/dist/homebridge-ui/public/js/app.js +3722 -0
  88. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  89. package/dist/homebridge-ui/public/js/app.ts +22 -0
  90. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  91. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  92. package/dist/homebridge-ui/public/js/constants.js +2 -0
  93. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  94. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  95. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  96. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  97. package/dist/homebridge-ui/public/js/credentials.js +99 -0
  98. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  99. package/dist/homebridge-ui/public/js/credentials.ts +105 -0
  100. package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
  101. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  102. package/dist/homebridge-ui/public/js/devices-delete.js +199 -0
  103. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  104. package/dist/homebridge-ui/public/js/devices-delete.ts +227 -0
  105. package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
  106. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  107. package/dist/homebridge-ui/public/js/devices.js +98 -0
  108. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  109. package/dist/homebridge-ui/public/js/devices.ts +106 -0
  110. package/dist/homebridge-ui/public/js/discovery.d.ts +9 -0
  111. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  112. package/dist/homebridge-ui/public/js/discovery.js +1201 -0
  113. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  114. package/dist/homebridge-ui/public/js/discovery.ts +1335 -0
  115. package/dist/homebridge-ui/public/js/logger.d.ts +7 -0
  116. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  117. package/dist/homebridge-ui/public/js/logger.js +17 -0
  118. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  119. package/dist/homebridge-ui/public/js/logger.ts +17 -0
  120. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  121. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  122. package/dist/homebridge-ui/public/js/modal.js +35 -0
  123. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  124. package/dist/homebridge-ui/public/js/modal.ts +35 -0
  125. package/dist/homebridge-ui/public/js/modals.d.ts +15 -0
  126. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  127. package/dist/homebridge-ui/public/js/modals.js +675 -0
  128. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  129. package/dist/homebridge-ui/public/js/modals.ts +765 -0
  130. package/dist/homebridge-ui/public/js/render.d.ts +71 -0
  131. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  132. package/dist/homebridge-ui/public/js/render.js +960 -0
  133. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  134. package/dist/homebridge-ui/public/js/render.ts +1084 -0
  135. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  136. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  137. package/dist/homebridge-ui/public/js/toast.js +38 -0
  138. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  139. package/dist/homebridge-ui/public/js/toast.ts +44 -0
  140. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  141. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  142. package/dist/homebridge-ui/public/js/types.js +2 -0
  143. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  144. package/dist/homebridge-ui/public/js/types.ts +26 -0
  145. package/dist/homebridge-ui/server.d.ts +1 -3
  146. package/dist/homebridge-ui/server.d.ts.map +1 -1
  147. package/dist/homebridge-ui/server.js +8 -471
  148. package/dist/homebridge-ui/server.js.map +1 -1
  149. package/dist/homebridge-ui/settings.js +8 -0
  150. package/dist/homebridge-ui/settings.js.map +1 -0
  151. package/dist/homebridge-ui/switchbotClient.js +247 -0
  152. package/dist/homebridge-ui/switchbotClient.js.map +1 -0
  153. package/dist/homebridge-ui/utils/config-parser.d.ts +39 -0
  154. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  155. package/dist/homebridge-ui/utils/config-parser.js +108 -0
  156. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  157. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  158. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  159. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  160. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  161. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  162. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  163. package/dist/homebridge-ui/utils/logger.js +17 -0
  164. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  165. package/dist/index.d.ts +10 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +12 -2
  168. package/dist/index.js.map +1 -1
  169. package/dist/settings.d.ts +1 -0
  170. package/dist/settings.d.ts.map +1 -1
  171. package/dist/settings.js +1 -0
  172. package/dist/settings.js.map +1 -1
  173. package/dist/switchbotClient.d.ts +12 -10
  174. package/dist/switchbotClient.d.ts.map +1 -1
  175. package/dist/switchbotClient.js +156 -103
  176. package/dist/switchbotClient.js.map +1 -1
  177. package/dist/utils.d.ts +76 -1
  178. package/dist/utils.d.ts.map +1 -1
  179. package/dist/utils.js +1121 -4
  180. package/dist/utils.js.map +1 -1
  181. package/docs/assets/highlight.css +16 -2
  182. package/docs/assets/main.js +1 -1
  183. package/docs/index.html +82 -5
  184. package/docs/variables/default.html +3 -1
  185. package/eslint.config.js +9 -5
  186. package/nodemon.json +2 -2
  187. package/package.json +34 -21
  188. package/scripts/build-ui.js +37 -0
  189. package/scripts/free-dev-ports.mjs +105 -0
  190. package/scripts/generate-matter-maps.js +34 -17
  191. package/scripts/sync-device-types.mjs +31 -0
  192. package/src/SwitchBotHAPPlatform.ts +558 -0
  193. package/src/SwitchBotMatterPlatform.ts +538 -0
  194. package/src/device-types.js +246 -0
  195. package/src/device-types.js.map +1 -0
  196. package/src/device-types.ts +261 -0
  197. package/src/deviceCommandMapper.js +319 -0
  198. package/src/deviceCommandMapper.js.map +1 -0
  199. package/src/deviceCommandMapper.ts +333 -0
  200. package/src/deviceFactory.ts +125 -45
  201. package/src/devices/genericDevice.ts +411 -69
  202. package/src/errors.js +32 -0
  203. package/src/errors.js.map +1 -0
  204. package/src/errors.ts +35 -0
  205. package/src/homebridge-ui/endpoints/config.ts +110 -0
  206. package/src/homebridge-ui/endpoints/devices.ts +153 -0
  207. package/src/homebridge-ui/endpoints/discovery.ts +240 -0
  208. package/src/homebridge-ui/public/css/styles.css +483 -0
  209. package/src/homebridge-ui/public/index.html +197 -621
  210. package/src/homebridge-ui/public/js/advanced-settings.ts +94 -0
  211. package/src/homebridge-ui/public/js/api.ts +355 -0
  212. package/src/homebridge-ui/public/js/app.ts +22 -0
  213. package/src/homebridge-ui/public/js/constants.ts +1 -0
  214. package/src/homebridge-ui/public/js/credentials.ts +105 -0
  215. package/src/homebridge-ui/public/js/devices-delete.ts +227 -0
  216. package/src/homebridge-ui/public/js/devices.ts +106 -0
  217. package/src/homebridge-ui/public/js/discovery.ts +1335 -0
  218. package/src/homebridge-ui/public/js/logger.ts +17 -0
  219. package/src/homebridge-ui/public/js/modal.ts +35 -0
  220. package/src/homebridge-ui/public/js/modals.ts +765 -0
  221. package/src/homebridge-ui/public/js/render.ts +1084 -0
  222. package/src/homebridge-ui/public/js/toast.ts +44 -0
  223. package/src/homebridge-ui/public/js/types.ts +26 -0
  224. package/src/homebridge-ui/server.ts +9 -554
  225. package/src/homebridge-ui/utils/config-parser.ts +125 -0
  226. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  227. package/src/homebridge-ui/utils/logger.ts +17 -0
  228. package/src/index.ts +12 -2
  229. package/src/settings.js +8 -0
  230. package/src/settings.js.map +1 -0
  231. package/src/settings.ts +2 -0
  232. package/src/switchbotClient.js +247 -0
  233. package/src/switchbotClient.js.map +1 -0
  234. package/src/switchbotClient.ts +177 -114
  235. package/src/utils.ts +1133 -5
  236. package/test/client/switchbot-client-debounce.spec.ts +35 -0
  237. package/test/client/switchbot-client-openapi.spec.ts +19 -0
  238. package/test/client/switchbotClient.spec.ts +64 -0
  239. package/test/device/device-mapping.spec.ts +23 -0
  240. package/test/device/deviceBase.spec.ts +26 -0
  241. package/test/device/deviceFactory-edge.spec.ts +15 -0
  242. package/test/device/deviceFactory.spec.ts +33 -0
  243. package/test/device/fan-swing.spec.ts +34 -0
  244. package/test/device/genericDevice-blepoll.spec.ts +47 -0
  245. package/test/device/irdevice.spec.ts +9 -0
  246. package/test/device/lock-users.spec.ts +35 -0
  247. package/test/device/matter-descriptors.spec.ts +22 -0
  248. package/test/device/matter-device-state.spec.ts +37 -0
  249. package/test/e2e/run-e2e.spec.ts +18 -19
  250. package/test/errors/errors.spec.ts +10 -0
  251. package/test/helpers/matter-harness.ts +20 -9
  252. package/test/homebridge-ui/server.spec.ts +9 -0
  253. package/test/platform/accessory-restore.spec.ts +37 -0
  254. package/test/platform/matter-childbridge.spec.ts +34 -0
  255. package/test/platform/matter-integration.spec.ts +33 -0
  256. package/test/platform/platform-edge.spec.ts +73 -0
  257. package/test/platform/platform.integration.spec.ts +34 -0
  258. package/test/utils/utils-extra.spec.ts +10 -0
  259. package/test/utils/utils.spec.ts +53 -0
  260. package/todo/TODO.md +80 -0
  261. package/tsconfig.ui.json +11 -0
  262. package/.github/npm-version-script-esm.js +0 -97
  263. package/.github/workflows/beta-release.yml +0 -52
  264. package/dist/platform.d.ts +0 -35
  265. package/dist/platform.d.ts.map +0 -1
  266. package/dist/platform.js +0 -945
  267. package/dist/platform.js.map +0 -1
  268. package/src/platform.ts +0 -963
  269. package/test/accessory-restore.spec.ts +0 -73
  270. package/test/device-mapping.spec.ts +0 -37
  271. package/test/deviceFactory.spec.ts +0 -18
  272. package/test/fan-swing.spec.ts +0 -29
  273. package/test/lock-users.spec.ts +0 -44
  274. package/test/matter-childbridge.spec.ts +0 -55
  275. package/test/matter-descriptors.spec.ts +0 -97
  276. package/test/matter-device-state.spec.ts +0 -101
  277. package/test/matter-integration.spec.ts +0 -70
  278. package/test/platform.integration.spec.ts +0 -55
  279. package/test/switchbot-client-debounce.spec.ts +0 -131
  280. package/test/switchbot-client-openapi.spec.ts +0 -56
  281. package/test/switchbotClient.spec.ts +0 -10
  282. package/test/utils.spec.ts +0 -20
@@ -1,12 +1,189 @@
1
+ // Utility: Validate BLE response length before parsing
1
2
  /* eslint-disable style/max-statements-per-line, unused-imports/no-unused-vars */
3
+
4
+ /**
5
+ * Status Update Strategy for BLE and OpenAPI
6
+ *
7
+ * BLE (Bluetooth Low Energy):
8
+ * - Primary: Subscribes to device notifications for real-time state updates using _subscribeBLENotifications().
9
+ * - Fallback: (Recommended) Optionally, a low-frequency polling timer (e.g., every 5–10 minutes) can call getState() to recover from missed notifications or connection loss.
10
+ * - This ensures state stays in sync even if notifications are unreliable or the device reconnects.
11
+ * - Polling should be infrequent to avoid battery drain and BLE congestion.
12
+ *
13
+ * BLE Polling Options (config & per-device):
14
+ * - blePollingEnabled (boolean): Enable/disable BLE polling fallback (default: true).
15
+ * - blePollIntervalMs (integer): Polling interval in ms (default: 600000, min: 60000).
16
+ * - These can be set globally in config or overridden per device.
17
+ * - Setting a lower interval increases update frequency but may drain battery faster.
18
+ * - Setting a higher interval reduces battery impact but may delay state recovery.
19
+ *
20
+ * OpenAPI (Cloud):
21
+ * - Uses periodic polling to fetch device status at a configurable interval (default: 300 seconds, can be set per device or platform).
22
+ * - Platform supports batched refresh (matterBatchEnabled, matterBatchRefreshRate, etc.) and per-device refreshRate overrides.
23
+ * - Rate limiting:
24
+ * - Default daily limit: 10,000 OpenAPI requests (configurable via options.dailyApiLimit).
25
+ * - Reserve: 1,000 requests for user commands (options.dailyApiReserveForCommands).
26
+ * - When the remaining budget reaches the reserve, background polling/discovery pauses, but user commands and webhooks continue.
27
+ * - Counter resets at local or UTC midnight (options.dailyApiResetLocalMidnight).
28
+ *
29
+ * Best Practices:
30
+ * - BLE: Use notifications for instant updates, add periodic polling as a safety net.
31
+ * - OpenAPI: Tune polling intervals to balance freshness and rate limit budget.
32
+ * - Both: Document and expose polling intervals and rate limit settings in config.
33
+ *
34
+ * See README.md and docs for more details.
35
+ */
2
36
  import type { SwitchBotPluginConfig } from '../settings.js'
3
37
 
38
+ import { Buffer } from 'node:buffer'
39
+
4
40
  import { MATTER_ATTRIBUTE_IDS, MATTER_CLUSTER_IDS } from '../utils.js'
5
41
  import { DeviceBase } from './deviceBase.js'
6
42
 
43
+ function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = '', log: import('homebridge').Logger): boolean {
44
+ if (!buf || typeof buf.length !== 'number' || buf.length !== expected) {
45
+ log.warn(`[BLE] Invalid response length${context ? ` for ${context}` : ''}: expected ${expected}, got ${buf?.length}`)
46
+ return false
47
+ }
48
+ return true
49
+ }
50
+
51
+ // BLE notification handling: per-command notification futures and unsolicited notification logging
52
+ const BLE_NOTIFICATION_HANDLERS = new Map<string, (payload: Buffer) => void>()
53
+
54
+ // Module-scope regex pattern to avoid recompilation
55
+ const HEX_COLOR_REGEX = /^#?[0-9A-F]{6}$/i
56
+
7
57
  export class GenericDevice extends DeviceBase {
58
+ protected log: import('homebridge').Logger
59
+ private _blePollTimer: NodeJS.Timeout | null = null
60
+ private _blePollIntervalMs: number
61
+ private _blePollingEnabled: boolean
62
+
8
63
  constructor(opts: any, cfg: SwitchBotPluginConfig) {
9
64
  super(opts, cfg)
65
+ // Require logger from opts or cfg
66
+ this.log = opts?.log || cfg?.log
67
+ if (!this.log) {
68
+ throw new Error('Device requires a logger (Homebridge logger) in opts or cfg')
69
+ }
70
+ // If BLE encryptionKey/keyId provided, set on node-switchbot device instance if possible
71
+ if (opts.encryptionKey && this.client && typeof this.client.devices?.get === 'function') {
72
+ try {
73
+ const dev = this.client.devices.get(opts.id)
74
+ if (dev && typeof dev.setKey === 'function') {
75
+ dev.setKey({
76
+ encryptionKey: opts.encryptionKey,
77
+ keyId: opts.keyId || undefined,
78
+ })
79
+ }
80
+ } catch (e) {
81
+ // ignore if device not found or setKey not available
82
+ }
83
+ }
84
+ // BLE polling config: allow override via opts.blePollingEnabled/blePollIntervalMs or cfg.blePollingEnabled/blePollIntervalMs
85
+ this._blePollingEnabled = opts?.blePollingEnabled ?? cfg?.blePollingEnabled ?? true
86
+ let pollMs = opts?.blePollIntervalMs ?? cfg?.blePollIntervalMs ?? 10 * 60 * 1000 // default: 10 min
87
+ if (typeof pollMs !== 'number' || Number.isNaN(pollMs) || pollMs < 60000) {
88
+ this.log.warn(`[BLE] Invalid blePollIntervalMs (${pollMs}), using minimum 60000ms`)
89
+ pollMs = 60000
90
+ }
91
+ this._blePollIntervalMs = pollMs
92
+ // Subscribe to BLE notifications if supported (node-switchbot v4+)
93
+ this._subscribeBLENotifications()
94
+ // Start BLE polling fallback if enabled
95
+ if (this._blePollingEnabled) {
96
+ this._startBlePolling()
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Start periodic BLE polling as a fallback to notifications.
102
+ */
103
+ private _startBlePolling() {
104
+ if (this._blePollTimer) {
105
+ clearInterval(this._blePollTimer)
106
+ }
107
+ this._blePollTimer = setInterval(async () => {
108
+ try {
109
+ this.log.debug(`[BLE] Polling getState() for device ${this.opts.id}`)
110
+ await this.getState()
111
+ } catch (e) {
112
+ this.log.debug(`[BLE] Polling getState() failed for device ${this.opts.id}:`, (e as Error)?.message)
113
+ }
114
+ }, this._blePollIntervalMs)
115
+ }
116
+
117
+ /**
118
+ * Clean up BLE polling timer on destroy.
119
+ */
120
+ async destroy(): Promise<void> {
121
+ if (this._blePollTimer) {
122
+ clearInterval(this._blePollTimer)
123
+ this._blePollTimer = null
124
+ }
125
+ // Only call super.destroy if DeviceBase.prototype.destroy is a function and not this method itself
126
+ const baseProto = Object.getPrototypeOf(GenericDevice.prototype)
127
+ if (typeof baseProto.destroy === 'function' && baseProto.destroy !== GenericDevice.prototype.destroy) {
128
+ await super.destroy()
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Subscribe to BLE notifications for this device (if supported by node-switchbot)
134
+ * Logs unsolicited notifications and enables per-command notification futures.
135
+ */
136
+ private async _subscribeBLENotifications() {
137
+ if (!this.client || typeof this.client.devices?.get !== 'function') { return }
138
+ const dev = this.client.devices.get(this.opts.id)
139
+ if (!dev || typeof dev.mac !== 'string' || !dev.mac) { return }
140
+ // Only subscribe once per device
141
+ if (BLE_NOTIFICATION_HANDLERS.has(dev.mac)) { return }
142
+ if (typeof dev.subscribeNotifications === 'function') {
143
+ const handler = (payload: Buffer) => {
144
+ // If a per-command notification future is waiting, let node-switchbot handle it
145
+ // Otherwise, log unsolicited notification
146
+ if (payload && payload.length > 0) {
147
+ // Unsolicited notification logging
148
+ // (node-switchbot will resolve per-command futures internally)
149
+ this.log.debug(`[BLE] Unsolicited notification from ${dev.mac}: ${payload.toString('hex')}`)
150
+ }
151
+ }
152
+ try {
153
+ // Subscribe and remember handler for possible cleanup
154
+ await dev.subscribeNotifications(handler)
155
+ BLE_NOTIFICATION_HANDLERS.set(dev.mac, handler)
156
+ } catch (e) {
157
+ // ignore if subscription fails
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Await a BLE notification for this device (for advanced use in subclasses)
164
+ * Returns the notification payload or throws on timeout.
165
+ */
166
+ protected async _awaitBLENotification(timeoutMs = 5000): Promise<Buffer> {
167
+ if (!this.client || typeof this.client.devices?.get !== 'function') { throw new Error('No BLE client/device') }
168
+ const dev = this.client.devices.get(this.opts.id)
169
+ if (!dev || typeof dev.mac !== 'string' || !dev.mac) { throw new Error('No BLE MAC for device') }
170
+ if (typeof dev.bleConnection?.sendCommand !== 'function') { throw new TypeError('BLE connection does not support sendCommand') }
171
+ // This is a low-level utility; in most cases, node-switchbot handles notification futures for commands
172
+ // Here, we expose a direct await for advanced use
173
+ return new Promise<Buffer>((resolve, reject) => {
174
+ let timer: NodeJS.Timeout | undefined
175
+ const handler = (payload: Buffer) => {
176
+ clearTimeout(timer)
177
+ dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
178
+ resolve(payload)
179
+ }
180
+ dev.bleConnection?.subscribeNotifications(dev.mac, handler).then(() => {
181
+ timer = setTimeout(() => {
182
+ dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
183
+ reject(new Error('BLE notification timeout'))
184
+ }, timeoutMs)
185
+ }).catch(reject)
186
+ })
10
187
  }
11
188
 
12
189
  async getState(): Promise<any> {
@@ -14,6 +191,13 @@ export class GenericDevice extends DeviceBase {
14
191
  if (this.client && typeof this.client.getDevice === 'function') {
15
192
  try {
16
193
  const raw = await this.client.getDevice(this.opts.id)
194
+ // If this is a BLE buffer/array, validate length (common BLE status: 12 bytes, but may vary by device)
195
+ if (raw && (raw instanceof Buffer || Array.isArray(raw) || raw instanceof Uint8Array)) {
196
+ // Default to 12, override per device if needed
197
+ if (!validateBLEResponseLength(raw, 12, this.opts.type, this.log)) {
198
+ return { id: this.opts.id, type: this.opts.type, error: 'invalid_ble_response_length', raw }
199
+ }
200
+ }
17
201
  // Normalize common response shapes
18
202
  try {
19
203
  const device = raw?.body ?? raw
@@ -127,62 +311,163 @@ export class GenericDevice extends DeviceBase {
127
311
  // platform can construct a Matter accessory representation when
128
312
  // Homebridge Matter APIs are available. Device subclasses may override
129
313
  // this to provide Matter-specific clusters/attributes if desired.
130
- createMatterAccessory(api: any): any {
131
- const hapDesc = this.createHAPAccessory(api)
132
-
314
+ async createMatterAccessory(api: any): Promise<any> {
315
+ // Dynamically detect features from getState()
316
+ const state = await this.getState()
133
317
  const clusters: any[] = []
134
- const mapCharacteristic = (charName: string) => {
135
- switch (charName) {
136
- case 'On':
137
- return { attr: 'onOff', get: async () => { const s = await this.getState(); return !!(s && (s.on === true || s.state === 'on' || s.power === 'on')) }, set: async (v: any) => this.setState({ on: !!v }) }
138
- case 'Brightness':
139
- return { attr: 'brightness', get: async () => { const s = await this.getState(); return typeof s.brightness === 'number' ? s.brightness : 100 }, set: async (v: any) => this.setState({ brightness: Number(v) }) }
140
- case 'Hue':
141
- return { attr: 'colorHue', get: async () => { const s = await this.getState(); return typeof s.hue === 'number' ? s.hue : 0 }, set: async (v: any) => this.setState({ hue: Number(v) }) }
142
- case 'Saturation':
143
- return { attr: 'colorSaturation', get: async () => { const s = await this.getState(); return typeof s.saturation === 'number' ? s.saturation : 0 }, set: async (v: any) => this.setState({ saturation: Number(v) }) }
144
- case 'ColorTemperature':
145
- return { attr: 'colorTemperature', get: async () => {
146
- const s = await this.getState(); if (typeof s.colorTemperature === 'number') { return s.colorTemperature } if (typeof s.kelvin === 'number') { return Math.round(1000000 / s.kelvin) } return 400
147
- }, set: async (v: any) => this.setState({ colorTemperature: Number(v) }) }
148
- case 'RotationSpeed':
149
- return { attr: 'rotationSpeed', get: async () => { const s = await this.getState(); return typeof s.speed === 'number' ? s.speed : 0 }, set: async (v: any) => this.setState({ speed: Number(v) }) }
150
- case 'TargetPosition':
151
- case 'CurrentPosition':
152
- return { attr: 'position', get: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, set: async (v: any) => this.setState({ position: Number(v) }) }
153
- case 'LockCurrentState':
154
- case 'LockTargetState':
155
- return { attr: 'locked', get: async () => { const s = await this.getState(); return !!(s && s.locked) }, set: async (v: any) => this.setState({ locked: !!v }) }
156
- case 'MotionDetected':
157
- return { attr: 'motionDetected', get: async () => { const s = await this.getState(); return !!(s && s.motion === true) }, set: undefined }
158
- case 'ContactSensorState':
159
- return { attr: 'contact', get: async () => { const s = await this.getState(); return s && s.open ? 1 : 0 }, set: undefined }
160
- default:
161
- return null
162
- }
318
+
319
+ // On/Off (Switch/Plug/Generic)
320
+ if ('on' in state || 'power' in state || 'state' in state) {
321
+ clusters.push({
322
+ type: 'OnOff',
323
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
324
+ attributes: {
325
+ onOff: { read: async () => !!(await this.getState()).on || (await this.getState()).power === 'on' || (await this.getState()).state === 'on', write: async (v: any) => this.setState({ on: !!v }) },
326
+ [MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => !!(await this.getState()).on || (await this.getState()).power === 'on' || (await this.getState()).state === 'on', write: async (v: any) => this.setState({ on: !!v }) },
327
+ },
328
+ })
163
329
  }
164
330
 
165
- for (const s of (hapDesc.services || [])) {
166
- const clusterType = (() => {
167
- switch ((s.type || '').toLowerCase()) {
168
- case 'lightbulb': return 'OnOff/LevelControl/ColorControl'
169
- case 'fan': return 'FanControl'
170
- case 'windowscovering': return 'Shade'
171
- case 'motionsensor': return 'OccupancySensing'
172
- case 'contactsensor': return 'DoorState'
173
- case 'lockmechanism': return 'DoorLock'
174
- case 'switch': return 'OnOff'
175
- default: return s.type || 'Generic'
176
- }
177
- })()
331
+ // Brightness (Light)
332
+ if ('brightness' in state) {
333
+ clusters.push({
334
+ type: 'LevelControl',
335
+ clusterId: MATTER_CLUSTER_IDS.LevelControl,
336
+ attributes: {
337
+ currentLevel: { read: async () => (await this.getState()).brightness ?? 100, write: async (v: any) => this.setState({ brightness: Number(v) }) },
338
+ [MATTER_ATTRIBUTE_IDS.LevelControl.CurrentLevel]: { read: async () => (await this.getState()).brightness ?? 100, write: async (v: any) => this.setState({ brightness: Number(v) }) },
339
+ },
340
+ })
341
+ }
178
342
 
179
- const attributes: Record<string, any> = {}
180
- for (const [charName] of Object.entries(s.characteristics || {})) {
181
- const mapped = mapCharacteristic(charName)
182
- if (mapped) { attributes[mapped.attr] = { read: typeof mapped.get === 'function' ? mapped.get : undefined, write: typeof mapped.set === 'function' ? mapped.set : undefined } }
183
- }
343
+ // Color (Light)
344
+ if ('hue' in state && 'saturation' in state) {
345
+ clusters.push({
346
+ type: 'ColorControl',
347
+ clusterId: MATTER_CLUSTER_IDS.ColorControl,
348
+ attributes: {
349
+ colorMode: { read: async () => 0 },
350
+ colorHue: { read: async () => (await this.getState()).hue ?? 0, write: async (v: any) => this.setState({ hue: Number(v) }) },
351
+ [MATTER_ATTRIBUTE_IDS.ColorControl.CurrentHue]: { read: async () => (await this.getState()).hue ?? 0, write: async (v: any) => this.setState({ hue: Number(v) }) },
352
+ colorSaturation: { read: async () => (await this.getState()).saturation ?? 0, write: async (v: any) => this.setState({ saturation: Number(v) }) },
353
+ [MATTER_ATTRIBUTE_IDS.ColorControl.CurrentSaturation]: { read: async () => (await this.getState()).saturation ?? 0, write: async (v: any) => this.setState({ saturation: Number(v) }) },
354
+ ...(typeof state.colorTemperature === 'number' || typeof state.kelvin === 'number'
355
+ ? {
356
+ colorTemperature: { read: async () => typeof (await this.getState()).colorTemperature === 'number' ? (await this.getState()).colorTemperature : Math.round(1000000 / ((await this.getState()).kelvin ?? 2500)), write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
357
+ [MATTER_ATTRIBUTE_IDS.ColorControl.ColorTemperatureMireds]: { read: async () => typeof (await this.getState()).colorTemperature === 'number' ? (await this.getState()).colorTemperature : Math.round(1000000 / ((await this.getState()).kelvin ?? 2500)), write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
358
+ }
359
+ : {}),
360
+ },
361
+ })
362
+ }
363
+
364
+ // Temperature sensor
365
+ if ('temperature' in state) {
366
+ clusters.push({
367
+ type: 'TemperatureMeasurement',
368
+ // No clusterId, not present in MATTER_CLUSTER_IDS
369
+ attributes: {
370
+ measuredValue: { read: async () => (await this.getState()).temperature ?? 0 },
371
+ },
372
+ })
373
+ }
374
+
375
+ // Humidity sensor
376
+ if ('humidity' in state) {
377
+ clusters.push({
378
+ type: 'RelativeHumidityMeasurement',
379
+ clusterId: MATTER_CLUSTER_IDS.RelativeHumidityMeasurement,
380
+ attributes: {
381
+ measuredValue: { read: async () => (await this.getState()).humidity ?? 0 },
382
+ },
383
+ })
384
+ }
385
+
386
+ // CO2 sensor
387
+ if ('CO2' in state) {
388
+ clusters.push({
389
+ type: 'AirQuality',
390
+ attributes: {
391
+ CO2: { read: async () => (await this.getState()).CO2 ?? 0 },
392
+ },
393
+ })
394
+ }
395
+
396
+ // Lock
397
+ if ('lockState' in state || 'locked' in state) {
398
+ clusters.push({
399
+ type: 'DoorLock',
400
+ clusterId: MATTER_CLUSTER_IDS.DoorLock,
401
+ attributes: {
402
+ lockState: { read: async () => (await this.getState()).lockState ?? (await this.getState()).locked ? 1 : 0, write: async (v: any) => this.setState({ locked: !!v }) },
403
+ },
404
+ })
405
+ }
406
+
407
+ // Motion sensor
408
+ if ('moveDetected' in state || 'motion' in state) {
409
+ clusters.push({
410
+ type: 'OccupancySensing',
411
+ // No clusterId, not present in MATTER_CLUSTER_IDS
412
+ attributes: {
413
+ occupancy: { read: async () => (await this.getState()).moveDetected === true || (await this.getState()).motion === true ? 1 : 0 },
414
+ },
415
+ })
416
+ }
417
+
418
+ // Contact sensor
419
+ if ('openState' in state || 'contact' in state || 'open' in state) {
420
+ clusters.push({
421
+ type: 'BooleanState',
422
+ // No clusterId, not present in MATTER_CLUSTER_IDS
423
+ attributes: {
424
+ stateValue: { read: async () => (await this.getState()).openState === 'open' || (await this.getState()).open === true ? 1 : 0 },
425
+ },
426
+ })
427
+ }
428
+
429
+ // Leak sensor
430
+ if ('leak' in state || 'status' in state) {
431
+ clusters.push({
432
+ type: 'LeakSensor',
433
+ attributes: {
434
+ leakDetected: { read: async () => (await this.getState()).leak === true || (await this.getState()).status === 1 ? 1 : 0 },
435
+ },
436
+ })
437
+ }
438
+
439
+ // Energy monitoring (Plug)
440
+ if ('voltage' in state || 'power' in state || 'electricCurrent' in state) {
441
+ clusters.push({
442
+ type: 'ElectricalMeasurement',
443
+ // No clusterId, not present in MATTER_CLUSTER_IDS
444
+ attributes: {
445
+ voltage: { read: async () => (await this.getState()).voltage ?? 0 },
446
+ power: { read: async () => (await this.getState()).power ?? 0 },
447
+ electricCurrent: { read: async () => (await this.getState()).electricCurrent ?? 0 },
448
+ },
449
+ })
450
+ }
451
+
452
+ // Fan
453
+ if ('speed' in state || 'fanSpeed' in state) {
454
+ clusters.push({
455
+ type: 'FanControl',
456
+ clusterId: MATTER_CLUSTER_IDS.FanControl,
457
+ attributes: {
458
+ speedCurrent: { read: async () => (await this.getState()).speed ?? (await this.getState()).fanSpeed ?? 0, write: async (v: any) => this.setState({ speed: Number(v) }) },
459
+ },
460
+ })
461
+ }
184
462
 
185
- clusters.push({ type: clusterType, attributes })
463
+ // Vacuum
464
+ if ('workingStatus' in state) {
465
+ clusters.push({
466
+ type: 'RobotVacuumCleaner',
467
+ attributes: {
468
+ workingStatus: { read: async () => (await this.getState()).workingStatus ?? 'StandBy' },
469
+ },
470
+ })
186
471
  }
187
472
 
188
473
  return {
@@ -225,25 +510,80 @@ export class CurtainDevice extends GenericDevice {
225
510
  }
226
511
  }
227
512
 
228
- // Matter-specific descriptor for Curtain (Shade cluster)
229
- createMatterAccessory(api: any): any {
513
+ // Matter-specific descriptor for Curtain (WindowCovering cluster) with new attributes
514
+ async createMatterAccessory(api: any): Promise<any> {
515
+ // Get current state for dynamic attributes
516
+ const state = await this.getState()
517
+ // Compose attributes for Matter WindowCovering cluster
518
+ const attributes: Record<string, any> = {
519
+ currentPositionLiftPercent100ths: {
520
+ read: async () => {
521
+ const s = await this.getState()
522
+ return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
523
+ },
524
+ write: undefined,
525
+ },
526
+ targetPositionLiftPercent100ths: {
527
+ read: async () => {
528
+ const s = await this.getState()
529
+ return typeof s.position === 'number' ? Math.round(s.position * 100) : 0
530
+ },
531
+ write: async (v: any) => this.setState({ position: Math.round(Number(v) / 100) }),
532
+ },
533
+ operationalStatus: {
534
+ read: async () => state.operationalStatus ?? { global: 0, lift: 0, tilt: 0 },
535
+ write: undefined,
536
+ },
537
+ endProductType: {
538
+ read: async () => state.endProductType ?? 0,
539
+ write: undefined,
540
+ },
541
+ configStatus: {
542
+ read: async () => state.configStatus ?? {
543
+ operational: true,
544
+ onlineReserved: true,
545
+ liftMovementReversed: false,
546
+ liftPositionAware: true,
547
+ tiltPositionAware: false,
548
+ liftEncoderControlled: true,
549
+ tiltEncoderControlled: false,
550
+ },
551
+ write: undefined,
552
+ },
553
+ }
554
+ // If tilt is supported, add tilt attributes
555
+ if (typeof state.tilt === 'number') {
556
+ attributes.currentPositionTiltPercent100ths = {
557
+ read: async () => {
558
+ const s = await this.getState()
559
+ return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
560
+ },
561
+ write: undefined,
562
+ }
563
+ attributes.targetPositionTiltPercent100ths = {
564
+ read: async () => {
565
+ const s = await this.getState()
566
+ return typeof s.tilt === 'number' ? Math.round(s.tilt * 100) : 0
567
+ },
568
+ write: async (v: any) => this.setState({ tilt: Math.round(Number(v) / 100) }),
569
+ }
570
+ }
571
+ const windowCoveringCluster = {
572
+ type: 'WindowCovering',
573
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
574
+ attributes,
575
+ }
576
+ // Provide both array and named property for clusters for compatibility with test expectations
577
+ const clustersArr: any[] = [windowCoveringCluster]
578
+ const clusters: any = [...clustersArr]
579
+ // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
580
+ const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
581
+ clusters.windowCovering = foundWC || null
230
582
  return {
231
583
  id: this.opts.id,
232
584
  name: this.opts.name ?? this.opts.type,
233
585
  protocol: 'matter',
234
- clusters: [
235
- {
236
- // Shade cluster
237
- type: 'Shade',
238
- clusterId: MATTER_CLUSTER_IDS.WindowCovering,
239
- attributes: {
240
- currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
241
- [MATTER_ATTRIBUTE_IDS.WindowCovering.CurrentPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
242
- targetPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
243
- [MATTER_ATTRIBUTE_IDS.WindowCovering.TargetPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
244
- },
245
- },
246
- ],
586
+ clusters,
247
587
  }
248
588
  }
249
589
  }
@@ -384,7 +724,7 @@ export class LightDevice extends GenericDevice {
384
724
  if (s && typeof s.hue === 'number') { return s.hue }
385
725
  // try HSV from color hex
386
726
  const hex = s?.color || s?.colorHex || s?.colour
387
- if (typeof hex === 'string' && /^#?[0-9A-F]{6}$/i.test(hex)) {
727
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
388
728
  const h = (() => {
389
729
  const hsl = (h: number, s: number, l: number) => ({ h, s, l })
390
730
  // convert hex -> rgb -> hsv
@@ -420,7 +760,7 @@ export class LightDevice extends GenericDevice {
420
760
  if (s && typeof s.saturation === 'number') { return s.saturation }
421
761
  // if color hex is available, derive saturation from rgb
422
762
  const hex = s?.color || s?.colorHex || s?.colour
423
- if (typeof hex === 'string' && /^#?[0-9A-F]{6}$/i.test(hex)) {
763
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
424
764
  const cleaned = hex.replace('#', '')
425
765
  const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
426
766
  const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
@@ -537,6 +877,8 @@ export class LightDevice extends GenericDevice {
537
877
  type: 'ColorControl',
538
878
  clusterId: MATTER_CLUSTER_IDS.ColorControl,
539
879
  attributes: {
880
+ // Required colorMode attribute for Matter conformance (0 = currentHueAndSaturation)
881
+ colorMode: { read: async () => 0 },
540
882
  colorHue: { read: async () => { const s = await this.getState(); return typeof s.hue === 'number' ? s.hue : 0 }, write: async (v: any) => this.setState({ hue: Number(v) }) },
541
883
  [MATTER_ATTRIBUTE_IDS.ColorControl.CurrentHue]: { read: async () => { const s = await this.getState(); return typeof s.hue === 'number' ? s.hue : 0 }, write: async (v: any) => this.setState({ hue: Number(v) }) },
542
884
  colorSaturation: { read: async () => { const s = await this.getState(); return typeof s.saturation === 'number' ? s.saturation : 0 }, write: async (v: any) => this.setState({ saturation: Number(v) }) },
@@ -844,7 +1186,7 @@ export class HumidifierDevice extends GenericDevice {
844
1186
 
845
1187
  // Provide Matter descriptor for humidifier (humidity and on/off)
846
1188
  export class HumidifierMatterDevice extends HumidifierDevice {
847
- createMatterAccessory(api: any) {
1189
+ async createMatterAccessory(api: any): Promise<any> {
848
1190
  return {
849
1191
  id: this.opts.id,
850
1192
  name: this.opts.name ?? this.opts.type,
package/src/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ // Error classes for node-switchbot v4 compatibility
2
+ // Always use local fallback classes for compatibility; upstream error classes are not guaranteed to exist
3
+ const SwitchbotOperationError = class extends Error {
4
+ code;
5
+ cause;
6
+ constructor(message, code, cause) {
7
+ super(message);
8
+ this.name = 'SwitchbotOperationError';
9
+ this.code = code;
10
+ this.cause = cause;
11
+ }
12
+ };
13
+ const SwitchbotAuthenticationError = class extends Error {
14
+ code;
15
+ cause;
16
+ constructor(message, code, cause) {
17
+ super(message);
18
+ this.name = 'SwitchbotAuthenticationError';
19
+ this.code = code;
20
+ this.cause = cause;
21
+ }
22
+ };
23
+ const CharacteristicMissingError = class extends Error {
24
+ characteristic;
25
+ constructor(message, characteristic) {
26
+ super(message);
27
+ this.name = 'CharacteristicMissingError';
28
+ this.characteristic = characteristic;
29
+ }
30
+ };
31
+ export { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError };
32
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,0GAA0G;AAE1G,MAAM,uBAAuB,GAAG,KAAM,SAAQ,KAAK;IACjD,IAAI,CAAS;IACb,KAAK,CAAQ;IACb,YAAY,OAAe,EAAE,IAAa,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAA;QACrC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF,CAAA;AAED,MAAM,4BAA4B,GAAG,KAAM,SAAQ,KAAK;IACtD,IAAI,CAAS;IACb,KAAK,CAAQ;IACb,YAAY,OAAe,EAAE,IAAa,EAAE,KAAa;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAA;QAC1C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF,CAAA;AAED,MAAM,0BAA0B,GAAG,KAAM,SAAQ,KAAK;IACpD,cAAc,CAAQ;IACtB,YAAY,OAAe,EAAE,cAAsB;QACjD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAA;QACxC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAA;IACtC,CAAC;CACF,CAAA;AAED,OAAO,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,CAAA"}
package/src/errors.ts ADDED
@@ -0,0 +1,35 @@
1
+ // Error classes for node-switchbot v4 compatibility
2
+ // Always use local fallback classes for compatibility; upstream error classes are not guaranteed to exist
3
+
4
+ const SwitchbotOperationError = class extends Error {
5
+ code?: string
6
+ cause?: Error
7
+ constructor(message: string, code?: string, cause?: Error) {
8
+ super(message)
9
+ this.name = 'SwitchbotOperationError'
10
+ this.code = code
11
+ this.cause = cause
12
+ }
13
+ }
14
+
15
+ const SwitchbotAuthenticationError = class extends Error {
16
+ code?: string
17
+ cause?: Error
18
+ constructor(message: string, code?: string, cause?: Error) {
19
+ super(message)
20
+ this.name = 'SwitchbotAuthenticationError'
21
+ this.code = code
22
+ this.cause = cause
23
+ }
24
+ }
25
+
26
+ const CharacteristicMissingError = class extends Error {
27
+ characteristic: string
28
+ constructor(message: string, characteristic: string) {
29
+ super(message)
30
+ this.name = 'CharacteristicMissingError'
31
+ this.characteristic = characteristic
32
+ }
33
+ }
34
+
35
+ export { CharacteristicMissingError, SwitchbotAuthenticationError, SwitchbotOperationError }