@switchbot/homebridge-switchbot 5.0.0-beta.98 → 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 -450
  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 -526
  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 -850
  267. package/dist/platform.js.map +0 -1
  268. package/src/platform.ts +0 -867
  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
@@ -0,0 +1,94 @@
1
+ // advanced-settings.ts
2
+ // Handles loading and saving global OpenAPI polling/rate config in the Advanced Settings card
3
+
4
+ async function loadAdvancedSettings(): Promise<void> {
5
+ try {
6
+ if (typeof homebridge.getPluginConfig !== 'function') {
7
+ (document.getElementById('advancedSettingsStatus') as HTMLElement).textContent = 'Homebridge UI API not available.'
8
+ return
9
+ }
10
+ const pluginConfigBlocks = await homebridge.getPluginConfig()
11
+ if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
12
+ return
13
+ }
14
+ const config = pluginConfigBlocks.find(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
15
+ if (!config) {
16
+ return
17
+ }
18
+ (document.getElementById('openApiRefreshRate') as HTMLInputElement).value = String(config.openApiRefreshRate ?? 300)
19
+ ;(document.getElementById('matterBatchEnabled') as HTMLInputElement).checked = config.matterBatchEnabled !== false
20
+ ;(document.getElementById('matterBatchRefreshRate') as HTMLInputElement).value = String(config.matterBatchRefreshRate ?? 300)
21
+ ;(document.getElementById('dailyApiLimit') as HTMLInputElement).value = String(config.dailyApiLimit ?? 10000)
22
+ ;(document.getElementById('dailyApiReserveForCommands') as HTMLInputElement).value = String(config.dailyApiReserveForCommands ?? 1000)
23
+ ;(document.getElementById('dailyApiResetLocalMidnight') as HTMLInputElement).checked = !!config.dailyApiResetLocalMidnight
24
+ ;(document.getElementById('webhookOnlyOnReserve') as HTMLInputElement).checked = !!config.webhookOnlyOnReserve
25
+ ;(document.getElementById('matterBatchConcurrency') as HTMLInputElement).value = String(config.matterBatchConcurrency ?? 5)
26
+ ;(document.getElementById('matterBatchJitter') as HTMLInputElement).value = String(config.matterBatchJitter ?? 0)
27
+ ;(document.getElementById('enableMatter') as HTMLInputElement).checked = !!config.enableMatter
28
+ ;(document.getElementById('preferMatter') as HTMLInputElement).checked = !!config.preferMatter
29
+ ;(document.getElementById('enableBLE') as HTMLInputElement).checked = config.enableBLE !== false
30
+ ;(document.getElementById('blePollingEnabled') as HTMLInputElement).checked = config.blePollingEnabled !== false
31
+ ;(document.getElementById('blePollIntervalMs') as HTMLInputElement).value = String(config.blePollIntervalMs ?? 600000)
32
+ } catch (e) {
33
+ (document.getElementById('advancedSettingsStatus') as HTMLElement).textContent = 'Failed to load settings.'
34
+ }
35
+ }
36
+
37
+ async function saveAdvancedSettings(): Promise<void> {
38
+ const status = document.getElementById('advancedSettingsStatus') as HTMLElement
39
+ status.textContent = 'Saving...'
40
+ try {
41
+ if (typeof homebridge.getPluginConfig !== 'function') {
42
+ throw new TypeError('homebridge.getPluginConfig is not available')
43
+ }
44
+ // Get the full plugin config array
45
+ const pluginConfigBlocks = await homebridge.getPluginConfig()
46
+ if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
47
+ throw new Error('No plugin config blocks returned from Homebridge')
48
+ }
49
+ // Find the SwitchBot config block
50
+ const idx = pluginConfigBlocks.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
51
+ if (idx === -1) {
52
+ throw new Error('SwitchBot config not found')
53
+ }
54
+ const config = pluginConfigBlocks[idx]
55
+ // Update config values from UI
56
+ config.openApiRefreshRate = Number((document.getElementById('openApiRefreshRate') as HTMLInputElement).value) || 300
57
+ config.matterBatchEnabled = (document.getElementById('matterBatchEnabled') as HTMLInputElement).checked
58
+ config.matterBatchRefreshRate = Number((document.getElementById('matterBatchRefreshRate') as HTMLInputElement).value) || 300
59
+ config.dailyApiLimit = Number((document.getElementById('dailyApiLimit') as HTMLInputElement).value) || 10000
60
+ config.dailyApiReserveForCommands = Number((document.getElementById('dailyApiReserveForCommands') as HTMLInputElement).value) || 1000
61
+ config.dailyApiResetLocalMidnight = (document.getElementById('dailyApiResetLocalMidnight') as HTMLInputElement).checked
62
+ config.webhookOnlyOnReserve = (document.getElementById('webhookOnlyOnReserve') as HTMLInputElement).checked
63
+ config.matterBatchConcurrency = Number((document.getElementById('matterBatchConcurrency') as HTMLInputElement).value) || 5
64
+ config.matterBatchJitter = Number((document.getElementById('matterBatchJitter') as HTMLInputElement).value) || 0
65
+ config.enableMatter = (document.getElementById('enableMatter') as HTMLInputElement).checked
66
+ config.preferMatter = (document.getElementById('preferMatter') as HTMLInputElement).checked
67
+ config.enableBLE = (document.getElementById('enableBLE') as HTMLInputElement).checked
68
+ config.blePollingEnabled = (document.getElementById('blePollingEnabled') as HTMLInputElement).checked
69
+ config.blePollIntervalMs = Number((document.getElementById('blePollIntervalMs') as HTMLInputElement).value) || 600000
70
+
71
+ // Update config in memory and save to disk
72
+ if (typeof homebridge.updatePluginConfig === 'function') {
73
+ await homebridge.updatePluginConfig(pluginConfigBlocks)
74
+ } else {
75
+ throw new TypeError('homebridge.updatePluginConfig is not available')
76
+ }
77
+ if (typeof homebridge.savePluginConfig === 'function') {
78
+ await homebridge.savePluginConfig()
79
+ }
80
+ status.textContent = 'Settings saved!'
81
+ } catch (e: any) {
82
+ status.textContent = `Failed to save: ${e && e.message ? e.message : e}`
83
+ }
84
+ }
85
+
86
+ document.addEventListener('DOMContentLoaded', () => {
87
+ if (document.getElementById('advancedSettingsCard')) {
88
+ loadAdvancedSettings()
89
+ const btn = document.getElementById('saveAdvancedSettingsBtn')
90
+ if (btn) {
91
+ btn.onclick = saveAdvancedSettings
92
+ }
93
+ }
94
+ })
@@ -0,0 +1,355 @@
1
+ // Fetch the list of configured devices from the Homebridge UI API
2
+ import { isValidDeviceType, normalizeDeviceType } from '../../../device-types.js'
3
+ import './types.js'
4
+ import { uiLog } from './logger.js'
5
+ import { toastError } from './toast.js'
6
+
7
+ export async function fetchDevices(): Promise<any[]> {
8
+ try {
9
+ if (typeof homebridge.getPluginConfig !== 'function') {
10
+ throw new TypeError('Homebridge UI API not available')
11
+ }
12
+ const configArr = await homebridge.getPluginConfig()
13
+ const config = Array.isArray(configArr) && configArr.length > 0 ? configArr.find(isSwitchBotPlatformConfig) : null
14
+ if (!config || !Array.isArray(config.devices)) {
15
+ return []
16
+ }
17
+ return config.devices
18
+ } catch (e) {
19
+ const msg = e instanceof Error ? e.message : String(e)
20
+ uiLog.error('Error fetching devices:', msg)
21
+ return []
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Validate and auto-correct device types in the config array before saving.
27
+ * Returns an array of errors for devices that cannot be fixed.
28
+ */
29
+ export function validateAndFixDeviceTypes(devices: Array<{ deviceId: string, configDeviceName: string, configDeviceType: string }>) {
30
+ const errors: Array<{ deviceId: string, name: string, type: string }> = []
31
+ for (const d of devices) {
32
+ if (!isValidDeviceType(d.configDeviceType)) {
33
+ const fixed = normalizeDeviceType(d.configDeviceType)
34
+ if (fixed) {
35
+ d.configDeviceType = fixed
36
+ } else {
37
+ errors.push({
38
+ deviceId: d.deviceId,
39
+ name: d.configDeviceName,
40
+ type: d.configDeviceType,
41
+ })
42
+ }
43
+ }
44
+ }
45
+ return errors
46
+ }
47
+
48
+ // API wrapper functions for communicating with the Homebridge UI server
49
+
50
+ function isSwitchBotPlatformConfig(block: any): boolean {
51
+ const platformName = String(block?.platform || block?.name || '').toLowerCase()
52
+ return (
53
+ platformName === 'switchbot'
54
+ || platformName === '@switchbot/homebridge-switchbot'
55
+ || platformName.includes('switchbot')
56
+ )
57
+ }
58
+
59
+ export async function syncParentPluginConfigFromDisk(autoSave = false): Promise<boolean> {
60
+ try {
61
+ if (
62
+ typeof homebridge.getPluginConfig !== 'function'
63
+ || typeof homebridge.updatePluginConfig !== 'function'
64
+ ) {
65
+ uiLog.warn('Parent config sync API not available')
66
+ return false
67
+ }
68
+
69
+ // Use Homebridge UI API to fetch and update config
70
+ const pluginConfigBlocks = await homebridge.getPluginConfig()
71
+ if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
72
+ uiLog.warn('No plugin config blocks returned from Homebridge')
73
+ return false
74
+ }
75
+
76
+ const index = pluginConfigBlocks.findIndex(block => isSwitchBotPlatformConfig(block))
77
+ if (index < 0) {
78
+ uiLog.warn('SwitchBot platform block not found in Homebridge plugin config')
79
+ return false
80
+ }
81
+
82
+ // Validate and fix device types before saving
83
+ const errors = validateAndFixDeviceTypes(pluginConfigBlocks[index].devices || [])
84
+ if (errors.length > 0) {
85
+ toastError(`Invalid device types found: ${errors.map(e => `${e.name} (${e.type})`).join(', ')}`)
86
+ return false
87
+ }
88
+ // pluginConfigBlocks[index] is already up to date
89
+ await homebridge.updatePluginConfig(pluginConfigBlocks)
90
+
91
+ // Auto-save to disk if requested - prevents parent UI from overwriting with stale cache
92
+ if (autoSave && typeof homebridge.savePluginConfig === 'function') {
93
+ uiLog.info('Auto-saving config to disk...')
94
+ await homebridge.savePluginConfig()
95
+ uiLog.info('Config saved successfully')
96
+ }
97
+
98
+ return true
99
+ } catch (e) {
100
+ uiLog.warn('Failed to sync parent plugin config cache:', e)
101
+ return false
102
+ }
103
+ }
104
+
105
+ export async function fetchCredentialStatus(): Promise<any> {
106
+ try {
107
+ const resp = await homebridge.request('/credentials', {})
108
+ uiLog.info('Load credentials response:', resp)
109
+
110
+ if (!resp || resp.success === false) {
111
+ uiLog.error('Failed to load credentials:', resp)
112
+ return null
113
+ }
114
+
115
+ return resp.data || {}
116
+ } catch (e) {
117
+ uiLog.error('Error loading credentials:', e)
118
+ return null
119
+ }
120
+ }
121
+
122
+ export async function saveCredentials(token: string, secret: string): Promise<any> {
123
+ uiLog.info('Saving credentials...')
124
+ const resp = await homebridge.request('/credentials', { token, secret })
125
+ uiLog.info('Save response:', resp)
126
+ if (!resp || resp.success === false) {
127
+ throw new Error(resp?.message || 'Save failed')
128
+ }
129
+ return resp.data || resp
130
+ }
131
+
132
+ export interface DiscoverRequestOptions {
133
+ bleEnabled?: boolean
134
+ bleScanDurationSeconds?: number
135
+ bleTimeoutSeconds?: number
136
+ }
137
+
138
+ export async function discoverDevices(
139
+ mode: 'all' | 'ble' | 'openapi' = 'all',
140
+ options?: DiscoverRequestOptions,
141
+ ): Promise<any[]> {
142
+ const resp = await homebridge.request('/discover', { mode, ...options })
143
+ uiLog.info('Discover response:', resp)
144
+ if (!resp || resp.success === false) {
145
+ throw new Error(resp?.data?.message || 'Discovery failed')
146
+ }
147
+ return resp.data || []
148
+ }
149
+
150
+ export async function fetchBluetoothStatus(): Promise<{ available: boolean, message: string }> {
151
+ try {
152
+ const resp = await homebridge.request('/ble-status', {})
153
+ if (!resp || resp.success === false) {
154
+ return { available: false, message: 'Bluetooth status unavailable' }
155
+ }
156
+ return resp.data || { available: false, message: 'Bluetooth status unavailable' }
157
+ } catch (_e) {
158
+ return { available: false, message: 'Bluetooth status unavailable' }
159
+ }
160
+ }
161
+
162
+ export async function testDeviceConnection(payload: {
163
+ deviceId: string
164
+ connectionType?: string
165
+ address?: string
166
+ }): Promise<{
167
+ success: boolean
168
+ deviceId: string
169
+ method: string
170
+ latencyMs: number
171
+ message: string
172
+ state?: Record<string, any>
173
+ }> {
174
+ const resp = await homebridge.request('/test-connection', payload)
175
+ if (!resp || resp.success === false) {
176
+ throw new Error(resp?.data?.message || 'Connection test failed')
177
+ }
178
+
179
+ return resp.data || {
180
+ success: false,
181
+ deviceId: payload.deviceId,
182
+ method: 'Auto',
183
+ latencyMs: 0,
184
+ message: 'Connection test failed',
185
+ }
186
+ }
187
+
188
+ export async function addDevice(
189
+ deviceId: string,
190
+ name: string,
191
+ type: string,
192
+ options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string },
193
+ ): Promise<any> {
194
+ if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
195
+ throw new TypeError('Homebridge UI API not available')
196
+ }
197
+ const configArr = await homebridge.getPluginConfig()
198
+ const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1
199
+ if (idx === -1) {
200
+ throw new Error('SwitchBot config not found')
201
+ }
202
+ const config = configArr[idx]
203
+ if (!Array.isArray(config.devices)) {
204
+ config.devices = []
205
+ }
206
+ const normalizedDeviceId = String(deviceId).trim().toLowerCase()
207
+ const exists = config.devices.some((d: any) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId)
208
+ if (exists) {
209
+ return { alreadyExists: true, message: 'Device already in config' }
210
+ }
211
+ const newDevice: any = { deviceId, configDeviceName: name, configDeviceType: type }
212
+ if (options?.address) {
213
+ newDevice.address = options.address
214
+ }
215
+ if (options?.model) {
216
+ newDevice.model = options.model
217
+ }
218
+ if (options?.rssi !== undefined && options?.rssi !== null && options?.rssi !== 0) {
219
+ newDevice.rssi = options.rssi
220
+ }
221
+ if (options?.encryptionKey) {
222
+ newDevice.encryptionKey = options.encryptionKey
223
+ }
224
+ if (options?.keyId) {
225
+ newDevice.keyId = options.keyId
226
+ }
227
+ config.devices.push(newDevice)
228
+ await homebridge.updatePluginConfig(configArr)
229
+ if (typeof homebridge.savePluginConfig === 'function') {
230
+ await homebridge.savePluginConfig()
231
+ }
232
+ return { added: true, message: `Device "${name}" added successfully` }
233
+ }
234
+
235
+ export async function addDevicesInBulk(
236
+ devices: Array<{ deviceId: string, name: string, type: string, rssi?: number, address?: string, model?: string }>,
237
+ ): Promise<any> {
238
+ const resp = await homebridge.request('/add-devices', { devices })
239
+ uiLog.info('Bulk add response:', resp)
240
+ if (!resp || resp.success === false) {
241
+ throw new Error(resp?.data?.message || 'Bulk add failed')
242
+ }
243
+ return resp.data || resp
244
+ }
245
+
246
+ export async function updateDevice(
247
+ deviceId: string,
248
+ configDeviceName?: string,
249
+ configDeviceType?: string,
250
+ options?: {
251
+ refreshRate?: number
252
+ connectionPreference?: string
253
+ encryptionKey?: string
254
+ keyId?: string
255
+ room?: string
256
+ [key: string]: any
257
+ },
258
+ ): Promise<any> {
259
+ if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
260
+ throw new TypeError('Homebridge UI API not available')
261
+ }
262
+ const configArr = await homebridge.getPluginConfig()
263
+ const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1
264
+ if (idx === -1) {
265
+ throw new Error('SwitchBot config not found')
266
+ }
267
+ const config = configArr[idx]
268
+ if (!Array.isArray(config.devices)) {
269
+ throw new TypeError('No devices array in config')
270
+ }
271
+ const normalizedDeviceId = String(deviceId).trim().toLowerCase()
272
+ const device = config.devices.find((d: any) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId)
273
+ if (!device) {
274
+ throw new Error('Device not found in config')
275
+ }
276
+ if (configDeviceName) {
277
+ device.configDeviceName = configDeviceName
278
+ }
279
+ if (configDeviceType) {
280
+ device.configDeviceType = configDeviceType
281
+ }
282
+ if (options) {
283
+ Object.assign(device, options)
284
+ }
285
+ await homebridge.updatePluginConfig(configArr)
286
+ if (typeof homebridge.savePluginConfig === 'function') {
287
+ await homebridge.savePluginConfig()
288
+ }
289
+ return { updated: true, message: `Device updated successfully` }
290
+ }
291
+
292
+ export async function deleteDevice(deviceId: string): Promise<any> {
293
+ if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
294
+ throw new TypeError('Homebridge UI API not available')
295
+ }
296
+ const configArr = await homebridge.getPluginConfig()
297
+ const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1
298
+ if (idx === -1) {
299
+ throw new Error('SwitchBot config not found')
300
+ }
301
+ const config = configArr[idx]
302
+ if (!Array.isArray(config.devices)) {
303
+ throw new TypeError('No devices array in config')
304
+ }
305
+ const normalizedDeviceId = String(deviceId).trim().toLowerCase()
306
+ const before = config.devices.length
307
+ // Remove the target device
308
+ config.devices = config.devices.filter(d => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() !== normalizedDeviceId)
309
+ // Defensive: filter out any invalid device entries (missing required fields)
310
+ config.devices = config.devices.filter(d => d && typeof d === 'object' && d.deviceId && d.configDeviceType)
311
+ if (config.devices.length === before) {
312
+ throw new Error('Device not found in config')
313
+ }
314
+ await homebridge.updatePluginConfig(configArr)
315
+ if (typeof homebridge.savePluginConfig === 'function') {
316
+ await homebridge.savePluginConfig()
317
+ }
318
+ return { deleted: true, message: `Device removed from config` }
319
+ }
320
+
321
+ export async function deleteAllDevices(): Promise<any> {
322
+ if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
323
+ throw new TypeError('Homebridge UI API not available')
324
+ }
325
+ const configArr = await homebridge.getPluginConfig()
326
+ // Find or create the SwitchBot config block
327
+ let idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1
328
+ if (idx === -1) {
329
+ // If not found, create a new config block for SwitchBot
330
+ const newBlock = { platform: 'SwitchBot', devices: [] }
331
+ configArr.push(newBlock)
332
+ idx = configArr.length - 1
333
+ }
334
+ const config = configArr[idx]
335
+ // Always ensure devices is an array
336
+ if (!Array.isArray(config.devices)) {
337
+ config.devices = []
338
+ }
339
+ const deletedCount = config.devices.length
340
+ config.devices = []
341
+ // Defensive: ensure required fields for schema compliance
342
+ if (!config.platform) {
343
+ config.platform = 'SwitchBot'
344
+ }
345
+ // Ensure 'name' property is present (required by schema for Homebridge platform blocks)
346
+ if (!config.name) {
347
+ config.name = 'SwitchBot'
348
+ }
349
+ // Save updated config
350
+ await homebridge.updatePluginConfig(configArr)
351
+ if (typeof homebridge.savePluginConfig === 'function') {
352
+ await homebridge.savePluginConfig()
353
+ }
354
+ return { deleted: true, deletedCount, message: `Removed ${deletedCount} device(s) from config` }
355
+ }
@@ -0,0 +1,22 @@
1
+ import { loadCredentialStatus, saveCredentials } from './credentials.js'
2
+ import { initRemoveAllButton, loadConfiguredDevices } from './devices.js'
3
+ import { discoverDevices, initializeDiscoverySettings } from './discovery.js';
4
+
5
+ (window as any).loadCredentialStatus = loadCredentialStatus;
6
+ (window as any).saveCredentials = saveCredentials;
7
+ (window as any).discoverDevices = discoverDevices
8
+
9
+ // Initialize on page load
10
+ async function init(): Promise<void> {
11
+ await loadCredentialStatus()
12
+ await initializeDiscoverySettings()
13
+ await loadConfiguredDevices()
14
+ await initRemoveAllButton()
15
+ }
16
+
17
+ // Run initialization when DOM is ready
18
+ if (document.readyState === 'loading') {
19
+ document.addEventListener('DOMContentLoaded', init)
20
+ } else {
21
+ init()
22
+ }
@@ -0,0 +1 @@
1
+ export { DEVICE_TYPES } from '../../../device-types.js'
@@ -0,0 +1,105 @@
1
+ import { uiLog } from './logger.js'
2
+ import { hideBusyUi, showBusyUi } from './modal.js'
3
+ import { toastError, toastSuccess, toastWarning } from './toast.js'
4
+
5
+ export async function loadCredentialStatus(): Promise<void> {
6
+ try {
7
+ if (typeof homebridge.getPluginConfig !== 'function') {
8
+ uiLog.error('Homebridge UI API not available')
9
+ return
10
+ }
11
+ const configArr = await homebridge.getPluginConfig()
12
+ const config = Array.isArray(configArr) && configArr.length > 0 ? configArr[0] : {}
13
+ const token = config.openApiToken || ''
14
+ const secret = config.openApiSecret || ''
15
+
16
+ const tokenStatus = document.getElementById('tokenStatus')
17
+ const secretStatus = document.getElementById('secretStatus')
18
+ if (!tokenStatus || !secretStatus) {
19
+ return
20
+ }
21
+ if (token) {
22
+ tokenStatus.textContent = `✓ Configured (${token.length} characters)`
23
+ tokenStatus.classList.add('ok')
24
+ } else {
25
+ tokenStatus.textContent = 'Not configured'
26
+ tokenStatus.classList.remove('ok')
27
+ }
28
+ if (secret) {
29
+ secretStatus.textContent = `✓ Configured (${secret.length} characters)`
30
+ secretStatus.classList.add('ok')
31
+ } else {
32
+ secretStatus.textContent = 'Not configured'
33
+ secretStatus.classList.remove('ok')
34
+ }
35
+ } catch (e) {
36
+ uiLog.error('Error loading credentials:', e)
37
+ }
38
+ }
39
+
40
+ export async function saveCredentials(): Promise<void> {
41
+ const token = (document.getElementById('token') as HTMLInputElement)?.value
42
+ const secret = (document.getElementById('secret') as HTMLInputElement)?.value
43
+ const saveStatus = document.getElementById('saveStatus')
44
+ const saveBtn = document.getElementById('saveBtn') as HTMLButtonElement
45
+
46
+ if (!saveStatus || !saveBtn) {
47
+ return
48
+ }
49
+
50
+ if (!token || !secret) {
51
+ saveStatus.textContent = 'Please enter both token and secret'
52
+ saveStatus.classList.add('error')
53
+ toastWarning('Please enter both token and secret')
54
+ return
55
+ }
56
+
57
+ try {
58
+ showBusyUi()
59
+ saveBtn.disabled = true
60
+ saveBtn.textContent = 'Saving...'
61
+ uiLog.info('Saving credentials...')
62
+
63
+ if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
64
+ throw new TypeError('Homebridge UI API not available')
65
+ }
66
+ const configArr = await homebridge.getPluginConfig()
67
+ if (!Array.isArray(configArr) || configArr.length === 0) {
68
+ throw new Error('No plugin config found')
69
+ }
70
+ const config = configArr[0]
71
+ config.openApiToken = token
72
+ config.openApiSecret = secret
73
+ await homebridge.updatePluginConfig([config])
74
+ if (typeof homebridge.savePluginConfig === 'function') {
75
+ await homebridge.savePluginConfig()
76
+ }
77
+
78
+ saveStatus.textContent = `✓ Credentials saved successfully`
79
+ saveStatus.classList.remove('error')
80
+ saveStatus.classList.add('success-msg')
81
+ toastSuccess('Credentials saved successfully')
82
+
83
+ // Clear inputs after successful save
84
+ ;(document.getElementById('token') as HTMLInputElement).value = ''
85
+ ;(document.getElementById('secret') as HTMLInputElement).value = ''
86
+
87
+ // Reload status to verify save
88
+ setTimeout(loadCredentialStatus, 500)
89
+
90
+ // Clear status message
91
+ setTimeout(() => {
92
+ saveStatus.textContent = ''
93
+ saveStatus.classList.remove('success-msg')
94
+ }, 3000)
95
+ } catch (e) {
96
+ uiLog.error('Save error:', e)
97
+ saveStatus.textContent = `Error: ${e instanceof Error ? e.message : 'Failed to save'}`
98
+ saveStatus.classList.add('error')
99
+ toastError(e instanceof Error ? e.message : 'Failed to save credentials')
100
+ } finally {
101
+ hideBusyUi()
102
+ saveBtn.disabled = false
103
+ saveBtn.textContent = 'Save Credentials'
104
+ }
105
+ }