@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,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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../src/homebridge-ui/public/js/app.ts"],"names":[],"mappings":""}