@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
@@ -0,0 +1,765 @@
1
+ import { DEVICE_TYPES } from './constants.js'
2
+ import { uiLog } from './logger.js'
3
+
4
+ export type ImportDiscoveredDeviceResult = {
5
+ configDeviceName: string
6
+ configDeviceType: string
7
+ address?: string
8
+ connectionPreference?: string
9
+ room?: string
10
+ encryptionKey?: string
11
+ keyId?: string
12
+ refreshRate?: number
13
+ blePollingEnabled?: boolean
14
+ blePollIntervalMs?: number
15
+ } | null
16
+
17
+ export async function importDiscoveredDevice(device: any): Promise<ImportDiscoveredDeviceResult> {
18
+ // --- OpenAPI Polling Interval (refreshRate) ---
19
+ const openApiRefreshLabel = document.createElement('label')
20
+ openApiRefreshLabel.textContent = 'OpenAPI Polling Interval (seconds)'
21
+ openApiRefreshLabel.style.display = 'block'
22
+ openApiRefreshLabel.style.marginBottom = '6px'
23
+ openApiRefreshLabel.style.fontWeight = '500'
24
+ openApiRefreshLabel.style.fontSize = '12px'
25
+ openApiRefreshLabel.style.color = '#6b7280'
26
+ openApiRefreshLabel.title = 'How often to poll this device via OpenAPI for status (in seconds). Overrides platform value if set. Default: 300 (5 minutes). Minimum: 30.'
27
+
28
+ const openApiRefreshInput = document.createElement('input')
29
+ openApiRefreshInput.type = 'number'
30
+ openApiRefreshInput.value = device.refreshRate || 300
31
+ openApiRefreshInput.min = '30'
32
+ openApiRefreshInput.step = '1'
33
+ openApiRefreshInput.style.width = '100%'
34
+ openApiRefreshInput.style.marginBottom = '12px'
35
+ openApiRefreshInput.style.padding = '8px 10px'
36
+ openApiRefreshInput.style.borderRadius = '6px'
37
+ openApiRefreshInput.style.fontSize = '14px'
38
+ openApiRefreshInput.style.boxSizing = 'border-box'
39
+ return new Promise((resolve) => {
40
+ const div = document.createElement('div')
41
+ div.style.position = 'fixed'
42
+ div.style.top = '0'
43
+ div.style.left = '0'
44
+ div.style.width = '100%'
45
+ div.style.height = '100%'
46
+ div.style.background = 'rgba(0,0,0,0.7)'
47
+ div.style.display = 'flex'
48
+ div.style.alignItems = 'center'
49
+ div.style.justifyContent = 'center'
50
+ div.style.zIndex = '9999'
51
+
52
+ const modal = document.createElement('div')
53
+ modal.style.background = getComputedStyle(document.body).backgroundColor
54
+ modal.style.color = getComputedStyle(document.body).color
55
+ modal.style.padding = '0'
56
+ modal.style.borderRadius = '10px'
57
+ modal.style.minWidth = '440px'
58
+ modal.style.maxWidth = '90vw'
59
+ modal.style.boxShadow = '0 8px 32px rgba(0,0,0,0.35)'
60
+ modal.style.overflow = 'hidden'
61
+ modal.style.borderTop = '3px solid var(--switchbot-red, #ef4444)'
62
+
63
+ const title = document.createElement('h3')
64
+ title.textContent = 'Import Discovered Device'
65
+ title.style.marginTop = '0'
66
+ title.style.marginBottom = '16px'
67
+ title.style.padding = '20px 20px 0'
68
+ title.style.fontSize = '18px'
69
+ title.style.fontWeight = '600'
70
+ title.style.color = 'var(--switchbot-red, #ef4444)'
71
+ title.style.letterSpacing = '-0.02em'
72
+
73
+ const contentDiv = document.createElement('div')
74
+ contentDiv.style.padding = '0 20px 20px'
75
+
76
+ const nameLabel = document.createElement('label')
77
+ nameLabel.textContent = 'Device Name'
78
+ nameLabel.style.display = 'block'
79
+ nameLabel.style.marginBottom = '6px'
80
+ nameLabel.style.fontWeight = '500'
81
+ nameLabel.style.fontSize = '12px'
82
+ nameLabel.style.color = '#6b7280'
83
+
84
+ const nameInput = document.createElement('input')
85
+ nameInput.type = 'text'
86
+ // Never allow 'undefined' as a name
87
+ let safeName = device.name
88
+ if (!safeName || safeName === 'undefined') {
89
+ safeName = device.id || ''
90
+ }
91
+ nameInput.value = safeName
92
+ nameInput.style.width = '100%'
93
+ nameInput.style.marginBottom = '12px'
94
+ nameInput.style.padding = '8px 10px'
95
+ nameInput.style.borderRadius = '6px'
96
+ nameInput.style.fontSize = '14px'
97
+ nameInput.style.boxSizing = 'border-box'
98
+
99
+ // --- Device Type Select ---
100
+ const typeLabel = document.createElement('label')
101
+ typeLabel.textContent = 'Config Device Type'
102
+ typeLabel.style.display = 'block'
103
+ typeLabel.style.marginBottom = '6px'
104
+ typeLabel.style.fontWeight = '500'
105
+ typeLabel.style.fontSize = '12px'
106
+ typeLabel.style.color = '#6b7280'
107
+
108
+ const typeSelect = document.createElement('select')
109
+ typeSelect.style.width = '100%'
110
+ typeSelect.style.padding = '8px 10px'
111
+ typeSelect.style.marginBottom = '12px'
112
+ typeSelect.style.borderRadius = '6px'
113
+ typeSelect.style.fontSize = '14px'
114
+ typeSelect.style.background = getComputedStyle(nameInput).background
115
+ typeSelect.style.color = getComputedStyle(nameInput).color
116
+ typeSelect.style.border = getComputedStyle(nameInput).border
117
+ typeSelect.style.boxSizing = 'border-box'
118
+
119
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
120
+ const optgroup = document.createElement('optgroup')
121
+ optgroup.label = categoryName
122
+ DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
123
+ const opt = document.createElement('option')
124
+ opt.value = deviceType
125
+ opt.text = deviceType
126
+ const detectedType = (device.type || '').toLowerCase()
127
+ opt.selected = deviceType.toLowerCase() === detectedType
128
+ optgroup.appendChild(opt)
129
+ })
130
+ typeSelect.appendChild(optgroup)
131
+ })
132
+
133
+ // --- Connection Preference ---
134
+ const connectionPrefLabel = document.createElement('label')
135
+ connectionPrefLabel.textContent = 'Connection Preference'
136
+ connectionPrefLabel.style.display = 'block'
137
+ connectionPrefLabel.style.marginBottom = '6px'
138
+ connectionPrefLabel.style.fontWeight = '500'
139
+ connectionPrefLabel.style.fontSize = '12px'
140
+ connectionPrefLabel.style.color = '#6b7280'
141
+
142
+ const connectionPrefSelect = document.createElement('select')
143
+ connectionPrefSelect.style.width = '100%'
144
+ connectionPrefSelect.style.marginBottom = '12px'
145
+ connectionPrefSelect.style.padding = '8px 10px'
146
+ connectionPrefSelect.style.borderRadius = '6px'
147
+ connectionPrefSelect.style.fontSize = '14px'
148
+ connectionPrefSelect.style.boxSizing = 'border-box';
149
+ ['auto', 'ble', 'openapi'].forEach((val) => {
150
+ const opt = document.createElement('option')
151
+ opt.value = val
152
+ opt.text = val.charAt(0).toUpperCase() + val.slice(1)
153
+ opt.selected = (device.connectionPreference || 'auto') === val
154
+ connectionPrefSelect.appendChild(opt)
155
+ })
156
+
157
+ // --- Room ---
158
+ const roomLabel = document.createElement('label')
159
+ roomLabel.textContent = 'Room'
160
+ roomLabel.style.display = 'block'
161
+ roomLabel.style.marginBottom = '6px'
162
+ roomLabel.style.fontWeight = '500'
163
+ roomLabel.style.fontSize = '12px'
164
+ roomLabel.style.color = '#6b7280'
165
+
166
+ const roomInput = document.createElement('input')
167
+ roomInput.type = 'text'
168
+ roomInput.value = device.room || ''
169
+ roomInput.placeholder = 'Optional room/location metadata'
170
+ roomInput.style.width = '100%'
171
+ roomInput.style.marginBottom = '12px'
172
+ roomInput.style.padding = '8px 10px'
173
+ roomInput.style.borderRadius = '6px'
174
+ roomInput.style.fontSize = '14px'
175
+ roomInput.style.boxSizing = 'border-box'
176
+
177
+ // --- BLE MAC Address ---
178
+ const macLabel = document.createElement('label')
179
+ macLabel.textContent = 'BLE MAC Address (optional)'
180
+ macLabel.style.display = 'block'
181
+ macLabel.style.marginBottom = '6px'
182
+ macLabel.style.fontWeight = '500'
183
+ macLabel.style.fontSize = '12px'
184
+ macLabel.style.color = '#6b7280'
185
+
186
+ const macInput = document.createElement('input')
187
+ macInput.type = 'text'
188
+ macInput.value = device.address || ''
189
+ macInput.placeholder = 'AA:BB:CC:DD:EE:FF'
190
+ macInput.style.width = '100%'
191
+ macInput.style.marginBottom = '12px'
192
+ macInput.style.padding = '8px 10px'
193
+ macInput.style.borderRadius = '6px'
194
+ macInput.style.fontSize = '14px'
195
+ macInput.style.boxSizing = 'border-box'
196
+
197
+ // --- Encryption Key ---
198
+ const encryptionKeyLabel = document.createElement('label')
199
+ encryptionKeyLabel.textContent = 'BLE Encryption Key (optional)'
200
+ encryptionKeyLabel.style.display = 'block'
201
+ encryptionKeyLabel.style.marginBottom = '6px'
202
+ encryptionKeyLabel.style.fontWeight = '500'
203
+ encryptionKeyLabel.style.fontSize = '12px'
204
+ encryptionKeyLabel.style.color = '#6b7280'
205
+
206
+ const encryptionKeyInput = document.createElement('input')
207
+ encryptionKeyInput.type = 'password'
208
+ encryptionKeyInput.value = device.encryptionKey || ''
209
+ encryptionKeyInput.placeholder = 'Paste device BLE encryption key'
210
+ encryptionKeyInput.style.width = '100%'
211
+ encryptionKeyInput.style.marginBottom = '12px'
212
+ encryptionKeyInput.style.padding = '8px 10px'
213
+ encryptionKeyInput.style.borderRadius = '6px'
214
+ encryptionKeyInput.style.fontSize = '14px'
215
+ encryptionKeyInput.style.boxSizing = 'border-box'
216
+
217
+ // --- Key ID ---
218
+ const keyIdLabel = document.createElement('label')
219
+ keyIdLabel.textContent = 'BLE Key ID (optional)'
220
+ keyIdLabel.style.display = 'block'
221
+ keyIdLabel.style.marginBottom = '6px'
222
+ keyIdLabel.style.fontWeight = '500'
223
+ keyIdLabel.style.fontSize = '12px'
224
+ keyIdLabel.style.color = '#6b7280'
225
+
226
+ const keyIdInput = document.createElement('input')
227
+ keyIdInput.type = 'text'
228
+ keyIdInput.value = device.keyId || ''
229
+ keyIdInput.placeholder = 'e.g. ff'
230
+ keyIdInput.style.width = '100%'
231
+ keyIdInput.style.marginBottom = '12px'
232
+ keyIdInput.style.padding = '8px 10px'
233
+ keyIdInput.style.borderRadius = '6px'
234
+ keyIdInput.style.fontSize = '14px'
235
+ keyIdInput.style.boxSizing = 'border-box'
236
+
237
+ // --- BLE Polling Enabled ---
238
+ const blePollingEnabledLabel = document.createElement('label')
239
+ blePollingEnabledLabel.textContent = 'Enable BLE Polling Fallback'
240
+ blePollingEnabledLabel.style.display = 'block'
241
+ blePollingEnabledLabel.style.marginBottom = '6px'
242
+ blePollingEnabledLabel.style.fontWeight = '500'
243
+ blePollingEnabledLabel.style.fontSize = '12px'
244
+ blePollingEnabledLabel.style.color = '#6b7280'
245
+
246
+ const blePollingEnabledInput = document.createElement('input')
247
+ blePollingEnabledInput.type = 'checkbox'
248
+ blePollingEnabledInput.checked = device.blePollingEnabled !== false // default true
249
+ blePollingEnabledInput.style.marginRight = '8px'
250
+ blePollingEnabledInput.style.marginBottom = '12px'
251
+
252
+ // --- BLE Poll Interval ---
253
+ const blePollIntervalLabel = document.createElement('label')
254
+ blePollIntervalLabel.textContent = 'BLE Polling Interval (ms)'
255
+ blePollIntervalLabel.style.display = 'block'
256
+ blePollIntervalLabel.style.marginBottom = '6px'
257
+ blePollIntervalLabel.style.fontWeight = '500'
258
+ blePollIntervalLabel.style.fontSize = '12px'
259
+ blePollIntervalLabel.style.color = '#6b7280'
260
+
261
+ const blePollIntervalInput = document.createElement('input')
262
+ blePollIntervalInput.type = 'number'
263
+ blePollIntervalInput.value = device.blePollIntervalMs || 600000
264
+ blePollIntervalInput.min = '60000'
265
+ blePollIntervalInput.step = '1000'
266
+ blePollIntervalInput.style.width = '100%'
267
+ blePollIntervalInput.style.marginBottom = '12px'
268
+ blePollIntervalInput.style.padding = '8px 10px'
269
+ blePollIntervalInput.style.borderRadius = '6px'
270
+ blePollIntervalInput.style.fontSize = '14px'
271
+ blePollIntervalInput.style.boxSizing = 'border-box'
272
+
273
+ const buttons = document.createElement('div')
274
+ buttons.style.display = 'flex'
275
+ buttons.style.gap = '10px'
276
+ buttons.style.justifyContent = 'flex-end'
277
+ buttons.style.marginTop = '18px'
278
+ buttons.style.paddingTop = '18px'
279
+ buttons.style.borderTop = '1px solid rgba(0, 0, 0, 0.08)'
280
+
281
+ const cancelBtn = document.createElement('button')
282
+ cancelBtn.textContent = 'Cancel'
283
+ cancelBtn.className = 'secondary'
284
+ cancelBtn.style.background = '#6b7280'
285
+ cancelBtn.style.padding = '8px 16px'
286
+ cancelBtn.style.fontSize = '13px'
287
+
288
+ const importBtn = document.createElement('button')
289
+ importBtn.textContent = 'Add to Config'
290
+ importBtn.style.background = 'var(--switchbot-red, #ef4444)'
291
+ importBtn.style.padding = '8px 20px'
292
+ importBtn.style.fontSize = '13px'
293
+
294
+ const cleanup = (result: ImportDiscoveredDeviceResult) => {
295
+ div.remove()
296
+ resolve(result)
297
+ }
298
+
299
+ cancelBtn.onclick = () => cleanup(null)
300
+ importBtn.onclick = () => {
301
+ // Never allow 'undefined' as a name
302
+ let finalName = nameInput.value
303
+ if (!finalName || finalName === 'undefined') {
304
+ finalName = device.id || ''
305
+ }
306
+ cleanup({
307
+ configDeviceName: finalName,
308
+ configDeviceType: typeSelect.value || device.type,
309
+ address: macInput.value || undefined,
310
+ connectionPreference: connectionPrefSelect.value || undefined,
311
+ room: roomInput.value || undefined,
312
+ encryptionKey: encryptionKeyInput.value || undefined,
313
+ keyId: keyIdInput.value || undefined,
314
+ refreshRate: Number(openApiRefreshInput.value) || 300,
315
+ blePollingEnabled: blePollingEnabledInput.checked,
316
+ blePollIntervalMs: Number(blePollIntervalInput.value) || 600000,
317
+ })
318
+ }
319
+
320
+ div.addEventListener('click', (event) => {
321
+ if (event.target === div) {
322
+ cleanup(null)
323
+ }
324
+ })
325
+
326
+ buttons.appendChild(cancelBtn)
327
+ buttons.appendChild(importBtn)
328
+ contentDiv.appendChild(nameLabel)
329
+ contentDiv.appendChild(nameInput)
330
+ contentDiv.appendChild(typeLabel)
331
+ contentDiv.appendChild(typeSelect)
332
+ contentDiv.appendChild(connectionPrefLabel)
333
+ contentDiv.appendChild(connectionPrefSelect)
334
+ contentDiv.appendChild(roomLabel)
335
+ contentDiv.appendChild(roomInput)
336
+ contentDiv.appendChild(macLabel)
337
+ contentDiv.appendChild(macInput)
338
+ contentDiv.appendChild(encryptionKeyLabel)
339
+ contentDiv.appendChild(encryptionKeyInput)
340
+ contentDiv.appendChild(keyIdLabel)
341
+ contentDiv.appendChild(keyIdInput)
342
+
343
+ contentDiv.appendChild(openApiRefreshLabel)
344
+ contentDiv.appendChild(openApiRefreshInput)
345
+ contentDiv.appendChild(blePollingEnabledLabel)
346
+ contentDiv.appendChild(blePollingEnabledInput)
347
+ contentDiv.appendChild(blePollIntervalLabel)
348
+ contentDiv.appendChild(blePollIntervalInput)
349
+ contentDiv.appendChild(buttons)
350
+
351
+ modal.appendChild(title)
352
+ modal.appendChild(contentDiv)
353
+ div.appendChild(modal)
354
+ document.body.appendChild(div)
355
+ nameInput.focus()
356
+ })
357
+ }
358
+
359
+ export async function editDevice(device: any): Promise<void> {
360
+ // --- OpenAPI Polling Interval (refreshRate) ---
361
+ const openApiRefreshLabel = document.createElement('label')
362
+ openApiRefreshLabel.textContent = 'OpenAPI Polling Interval (seconds)'
363
+ openApiRefreshLabel.style.display = 'block'
364
+ openApiRefreshLabel.style.marginBottom = '6px'
365
+ openApiRefreshLabel.style.fontWeight = '500'
366
+ openApiRefreshLabel.style.fontSize = '12px'
367
+ openApiRefreshLabel.style.color = '#6b7280'
368
+ openApiRefreshLabel.title = 'How often to poll this device via OpenAPI for status (in seconds). Overrides platform value if set. Default: 300 (5 minutes). Minimum: 30.'
369
+
370
+ const openApiRefreshInput = document.createElement('input')
371
+ openApiRefreshInput.type = 'number'
372
+ openApiRefreshInput.value = device.refreshRate || 300
373
+ openApiRefreshInput.min = '30'
374
+ openApiRefreshInput.step = '1'
375
+ openApiRefreshInput.style.width = '100%'
376
+ openApiRefreshInput.style.marginBottom = '12px'
377
+ openApiRefreshInput.style.padding = '8px 10px'
378
+ openApiRefreshInput.style.borderRadius = '6px'
379
+ openApiRefreshInput.style.fontSize = '14px'
380
+ openApiRefreshInput.style.boxSizing = 'border-box'
381
+ // --- Device Type Select ---
382
+ // --- BLE Polling Enabled ---
383
+ const blePollingEnabledLabel = document.createElement('label')
384
+ blePollingEnabledLabel.textContent = 'Enable BLE Polling Fallback'
385
+ blePollingEnabledLabel.style.display = 'block'
386
+ blePollingEnabledLabel.style.marginBottom = '6px'
387
+ blePollingEnabledLabel.style.fontWeight = '500'
388
+ blePollingEnabledLabel.style.fontSize = '12px'
389
+ blePollingEnabledLabel.style.color = '#6b7280'
390
+
391
+ const blePollingEnabledInput = document.createElement('input')
392
+ blePollingEnabledInput.type = 'checkbox'
393
+ blePollingEnabledInput.checked = device.blePollingEnabled !== false // default true
394
+ blePollingEnabledInput.style.marginRight = '8px'
395
+ blePollingEnabledInput.style.marginBottom = '12px'
396
+
397
+ // --- BLE Poll Interval ---
398
+ const blePollIntervalLabel = document.createElement('label')
399
+ blePollIntervalLabel.textContent = 'BLE Polling Interval (ms)'
400
+ blePollIntervalLabel.style.display = 'block'
401
+ blePollIntervalLabel.style.marginBottom = '6px'
402
+ blePollIntervalLabel.style.fontWeight = '500'
403
+ blePollIntervalLabel.style.fontSize = '12px'
404
+ blePollIntervalLabel.style.color = '#6b7280'
405
+
406
+ const blePollIntervalInput = document.createElement('input')
407
+ blePollIntervalInput.type = 'number'
408
+ blePollIntervalInput.value = device.blePollIntervalMs || 600000
409
+ blePollIntervalInput.min = '60000'
410
+ blePollIntervalInput.step = '1000'
411
+ blePollIntervalInput.style.width = '100%'
412
+ blePollIntervalInput.style.marginBottom = '12px'
413
+ blePollIntervalInput.style.padding = '8px 10px'
414
+ blePollIntervalInput.style.borderRadius = '6px'
415
+ blePollIntervalInput.style.fontSize = '14px'
416
+ blePollIntervalInput.style.boxSizing = 'border-box'
417
+ const typeLabel = document.createElement('label')
418
+ typeLabel.textContent = 'Config Device Type'
419
+ typeLabel.style.display = 'block'
420
+ typeLabel.style.marginBottom = '6px'
421
+ typeLabel.style.fontWeight = '500'
422
+ typeLabel.style.fontSize = '12px'
423
+ typeLabel.style.color = '#6b7280'
424
+
425
+ const typeSelect = document.createElement('select')
426
+ typeSelect.style.width = '100%'
427
+ typeSelect.style.padding = '8px 10px'
428
+ typeSelect.style.marginBottom = '12px'
429
+ typeSelect.style.borderRadius = '6px'
430
+ typeSelect.style.fontSize = '14px'
431
+ typeSelect.style.background = getComputedStyle(document.body).backgroundColor
432
+ typeSelect.style.color = getComputedStyle(document.body).color
433
+ typeSelect.style.border = '1px solid #ccc'
434
+ typeSelect.style.boxSizing = 'border-box'
435
+
436
+ // Add option groups with all API device types
437
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
438
+ const optgroup = document.createElement('optgroup')
439
+ optgroup.label = categoryName
440
+ DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
441
+ const opt = document.createElement('option')
442
+ opt.value = deviceType
443
+ opt.text = deviceType
444
+ // Select current configDeviceType if set, otherwise match API deviceType
445
+ const currentType = device.configDeviceType || device.deviceType || device.type || ''
446
+ opt.selected = currentType === deviceType
447
+ optgroup.appendChild(opt)
448
+ })
449
+ typeSelect.appendChild(optgroup)
450
+ })
451
+
452
+ // Create modal dialog for editing
453
+ const div = document.createElement('div')
454
+ div.style.position = 'fixed'
455
+ div.style.top = '0'
456
+ div.style.left = '0'
457
+ div.style.width = '100%'
458
+ div.style.height = '100%'
459
+ div.style.background = 'rgba(0,0,0,0.7)'
460
+ div.style.display = 'flex'
461
+ div.style.alignItems = 'center'
462
+ div.style.justifyContent = 'center'
463
+ div.style.zIndex = '9999'
464
+
465
+ const modal = document.createElement('div')
466
+ modal.style.background = getComputedStyle(document.body).backgroundColor
467
+ modal.style.color = getComputedStyle(document.body).color
468
+ modal.style.padding = '0'
469
+ modal.style.borderRadius = '10px'
470
+ modal.style.minWidth = '440px'
471
+ modal.style.maxWidth = '90vw'
472
+ modal.style.boxShadow = '0 8px 32px rgba(0,0,0,0.35)'
473
+ modal.style.overflow = 'hidden'
474
+ modal.style.borderTop = '3px solid var(--switchbot-red, #ef4444)'
475
+
476
+ const title = document.createElement('h3')
477
+ title.textContent = 'Edit Device'
478
+ title.style.marginTop = '0'
479
+ title.style.marginBottom = '16px'
480
+ title.style.padding = '20px 20px 0'
481
+ title.style.fontSize = '18px'
482
+ title.style.fontWeight = '600'
483
+ title.style.color = 'var(--switchbot-red, #ef4444)'
484
+ title.style.letterSpacing = '-0.02em'
485
+
486
+ const contentDiv = document.createElement('div')
487
+ contentDiv.style.padding = '0 20px 20px'
488
+
489
+ const nameLabel = document.createElement('label')
490
+ nameLabel.textContent = 'Device Name'
491
+ nameLabel.style.display = 'block'
492
+ nameLabel.style.marginBottom = '6px'
493
+ nameLabel.style.fontWeight = '500'
494
+ nameLabel.style.fontSize = '12px'
495
+ nameLabel.style.color = '#6b7280'
496
+
497
+ const nameInput = document.createElement('input')
498
+ nameInput.type = 'text'
499
+ nameInput.value = device.name || device.id
500
+ nameInput.style.width = '100%'
501
+ nameInput.style.marginBottom = '12px'
502
+ nameInput.style.padding = '8px 10px'
503
+ nameInput.style.borderRadius = '6px'
504
+ nameInput.style.fontSize = '14px'
505
+ nameInput.style.boxSizing = 'border-box'
506
+ nameInput.style.transition = 'border-color 0.2s ease'
507
+
508
+ // Read-only API device type field
509
+ const apiTypeLabel = document.createElement('label')
510
+ apiTypeLabel.textContent = 'Device Type (API - Read Only)'
511
+ apiTypeLabel.style.display = 'block'
512
+ apiTypeLabel.style.marginBottom = '6px'
513
+ apiTypeLabel.style.fontWeight = '500'
514
+ apiTypeLabel.style.fontSize = '12px'
515
+ apiTypeLabel.style.color = '#6b7280'
516
+
517
+ const apiTypeInput = document.createElement('input')
518
+ apiTypeInput.type = 'text'
519
+ apiTypeInput.value = device.deviceType || device.type || 'Unknown'
520
+ apiTypeInput.readOnly = true
521
+ apiTypeInput.style.width = '100%'
522
+ apiTypeInput.style.marginBottom = '12px'
523
+ apiTypeInput.style.padding = '8px 10px'
524
+ apiTypeInput.style.borderRadius = '6px'
525
+ apiTypeInput.style.fontSize = '13px'
526
+ apiTypeInput.style.opacity = '0.6'
527
+ apiTypeInput.style.cursor = 'not-allowed'
528
+ apiTypeInput.style.boxSizing = 'border-box'
529
+ apiTypeInput.style.backgroundColor = '#f9fafb'
530
+
531
+ // Editable config device type dropdown
532
+
533
+ // Add option groups with all API device types
534
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
535
+ const optgroup = document.createElement('optgroup')
536
+ optgroup.label = categoryName
537
+
538
+ DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
539
+ const opt = document.createElement('option')
540
+ opt.value = deviceType
541
+ opt.text = deviceType
542
+
543
+ // Select current configDeviceType if set, otherwise match API deviceType
544
+ const currentType = device.configDeviceType || device.deviceType || device.type || ''
545
+ opt.selected = currentType === deviceType
546
+ optgroup.appendChild(opt)
547
+ })
548
+
549
+ typeSelect.appendChild(optgroup)
550
+ })
551
+
552
+ // --- Connection Preference ---
553
+ const connectionPrefLabel = document.createElement('label')
554
+ connectionPrefLabel.textContent = 'Connection Preference'
555
+ connectionPrefLabel.style.display = 'block'
556
+ connectionPrefLabel.style.marginBottom = '6px'
557
+ connectionPrefLabel.style.fontWeight = '500'
558
+ connectionPrefLabel.style.fontSize = '12px'
559
+ connectionPrefLabel.style.color = '#6b7280'
560
+
561
+ const connectionPrefSelect = document.createElement('select')
562
+ connectionPrefSelect.style.width = '100%'
563
+ connectionPrefSelect.style.marginBottom = '12px'
564
+ connectionPrefSelect.style.padding = '8px 10px'
565
+ connectionPrefSelect.style.borderRadius = '6px'
566
+ connectionPrefSelect.style.fontSize = '14px'
567
+ connectionPrefSelect.style.boxSizing = 'border-box'
568
+ ;['auto', 'ble', 'openapi'].forEach((val) => {
569
+ const opt = document.createElement('option')
570
+ opt.value = val
571
+ opt.text = val.charAt(0).toUpperCase() + val.slice(1)
572
+ opt.selected = (device.connectionPreference || 'auto') === val
573
+ connectionPrefSelect.appendChild(opt)
574
+ })
575
+
576
+ // --- Room ---
577
+ const roomLabel = document.createElement('label')
578
+ roomLabel.textContent = 'Room'
579
+ roomLabel.style.display = 'block'
580
+ roomLabel.style.marginBottom = '6px'
581
+ roomLabel.style.fontWeight = '500'
582
+ roomLabel.style.fontSize = '12px'
583
+ roomLabel.style.color = '#6b7280'
584
+
585
+ const roomInput = document.createElement('input')
586
+ roomInput.type = 'text'
587
+ roomInput.value = device.room || ''
588
+ roomInput.placeholder = 'Optional room/location metadata'
589
+ roomInput.style.width = '100%'
590
+ roomInput.style.marginBottom = '12px'
591
+ roomInput.style.padding = '8px 10px'
592
+ roomInput.style.borderRadius = '6px'
593
+ roomInput.style.fontSize = '14px'
594
+ roomInput.style.boxSizing = 'border-box'
595
+
596
+ // --- Encryption Key ---
597
+ const encryptionKeyLabel = document.createElement('label')
598
+ encryptionKeyLabel.textContent = 'BLE Encryption Key (optional)'
599
+ encryptionKeyLabel.style.display = 'block'
600
+ encryptionKeyLabel.style.marginBottom = '6px'
601
+ encryptionKeyLabel.style.fontWeight = '500'
602
+ encryptionKeyLabel.style.fontSize = '12px'
603
+ encryptionKeyLabel.style.color = '#6b7280'
604
+
605
+ const encryptionKeyInput = document.createElement('input')
606
+ encryptionKeyInput.type = 'password'
607
+ encryptionKeyInput.value = device.encryptionKey || ''
608
+ encryptionKeyInput.placeholder = 'Paste device BLE encryption key'
609
+ encryptionKeyInput.style.width = '100%'
610
+ encryptionKeyInput.style.marginBottom = '12px'
611
+ encryptionKeyInput.style.padding = '8px 10px'
612
+ encryptionKeyInput.style.borderRadius = '6px'
613
+ encryptionKeyInput.style.fontSize = '14px'
614
+ encryptionKeyInput.style.boxSizing = 'border-box'
615
+
616
+ // --- Key ID ---
617
+ const keyIdLabel = document.createElement('label')
618
+ keyIdLabel.textContent = 'BLE Key ID (optional)'
619
+ keyIdLabel.style.display = 'block'
620
+ keyIdLabel.style.marginBottom = '6px'
621
+ keyIdLabel.style.fontWeight = '500'
622
+ keyIdLabel.style.fontSize = '12px'
623
+ keyIdLabel.style.color = '#6b7280'
624
+
625
+ const keyIdInput = document.createElement('input')
626
+ keyIdInput.type = 'text'
627
+ keyIdInput.value = device.keyId || ''
628
+ keyIdInput.placeholder = 'e.g. ff'
629
+ keyIdInput.style.width = '100%'
630
+ keyIdInput.style.marginBottom = '12px'
631
+ keyIdInput.style.padding = '8px 10px'
632
+ keyIdInput.style.borderRadius = '6px'
633
+ keyIdInput.style.fontSize = '14px'
634
+ keyIdInput.style.boxSizing = 'border-box'
635
+
636
+ const errorMessage = document.createElement('div')
637
+ errorMessage.style.color = 'var(--switchbot-red, #ef4444)'
638
+ errorMessage.style.marginBottom = '12px'
639
+ errorMessage.style.fontSize = '12px'
640
+ errorMessage.style.display = 'none'
641
+ errorMessage.style.padding = '8px 10px'
642
+ errorMessage.style.background = 'var(--switchbot-red-light, #fee2e2)'
643
+ errorMessage.style.borderRadius = '6px'
644
+ errorMessage.style.fontWeight = '500'
645
+
646
+ const buttons = document.createElement('div')
647
+ buttons.style.display = 'flex'
648
+ buttons.style.gap = '10px'
649
+ buttons.style.justifyContent = 'flex-end'
650
+ buttons.style.marginTop = '18px'
651
+ buttons.style.paddingTop = '18px'
652
+ buttons.style.borderTop = '1px solid rgba(0, 0, 0, 0.08)'
653
+
654
+ const cancelBtn = document.createElement('button')
655
+ cancelBtn.textContent = 'Cancel'
656
+ cancelBtn.className = 'secondary'
657
+ cancelBtn.style.background = '#6b7280'
658
+ cancelBtn.style.padding = '8px 16px'
659
+ cancelBtn.style.fontSize = '13px'
660
+ cancelBtn.onclick = () => div.remove()
661
+
662
+ const saveBtn = document.createElement('button')
663
+ saveBtn.textContent = 'Save'
664
+ saveBtn.style.background = 'var(--switchbot-red, #ef4444)'
665
+ saveBtn.style.padding = '8px 20px'
666
+ saveBtn.style.fontSize = '13px'
667
+ saveBtn.onclick = async () => {
668
+ try {
669
+ const { updateDevice, syncParentPluginConfigFromDisk, fetchDevices } = await import('./api.js')
670
+ const { renderDeviceList } = await import('./render.js')
671
+
672
+ const params = {
673
+ deviceId: device.id,
674
+ configDeviceName: nameInput.value || undefined,
675
+ configDeviceType: typeSelect.value,
676
+ connectionPreference: connectionPrefSelect.value,
677
+ room: roomInput.value || undefined,
678
+ encryptionKey: encryptionKeyInput.value || undefined,
679
+ keyId: keyIdInput.value || undefined,
680
+ refreshRate: Number(openApiRefreshInput.value) || 300,
681
+ blePollingEnabled: blePollingEnabledInput.checked,
682
+ blePollIntervalMs: Number(blePollIntervalInput.value) || 600000,
683
+ }
684
+
685
+ // Only include defined properties in the options object
686
+ const options: any = {}
687
+ if (params.connectionPreference !== undefined) {
688
+ options.connectionPreference = params.connectionPreference
689
+ }
690
+ if (params.room !== undefined) {
691
+ options.room = params.room
692
+ }
693
+ if (params.encryptionKey !== undefined) {
694
+ options.encryptionKey = params.encryptionKey
695
+ }
696
+ if (params.keyId !== undefined) {
697
+ options.keyId = params.keyId
698
+ }
699
+ if (params.refreshRate !== undefined) {
700
+ options.refreshRate = params.refreshRate
701
+ }
702
+ if (params.blePollingEnabled !== undefined) {
703
+ options.blePollingEnabled = params.blePollingEnabled
704
+ }
705
+ if (params.blePollIntervalMs !== undefined) {
706
+ options.blePollIntervalMs = params.blePollIntervalMs
707
+ }
708
+
709
+ await updateDevice(
710
+ params.deviceId,
711
+ params.configDeviceName,
712
+ params.configDeviceType,
713
+ options,
714
+ )
715
+ await syncParentPluginConfigFromDisk()
716
+ contentDiv.appendChild(openApiRefreshLabel)
717
+ contentDiv.appendChild(openApiRefreshInput)
718
+
719
+ // Refresh device list
720
+ uiLog.info('[Edit Device] Refreshing device list after update')
721
+ const list = await fetchDevices()
722
+ renderDeviceList(list)
723
+ div.remove()
724
+ } catch (e) {
725
+ uiLog.error('Update error:', e)
726
+ errorMessage.textContent = `Error: ${e instanceof Error ? e.message : 'Failed to update device'}`
727
+ errorMessage.style.display = 'block'
728
+ }
729
+ }
730
+
731
+ buttons.appendChild(cancelBtn)
732
+ buttons.appendChild(saveBtn)
733
+
734
+ contentDiv.appendChild(nameLabel)
735
+ contentDiv.appendChild(nameInput)
736
+ contentDiv.appendChild(apiTypeLabel)
737
+ contentDiv.appendChild(apiTypeInput)
738
+ contentDiv.appendChild(typeLabel)
739
+ contentDiv.appendChild(typeSelect)
740
+ contentDiv.appendChild(connectionPrefLabel)
741
+ contentDiv.appendChild(connectionPrefSelect)
742
+ contentDiv.appendChild(roomLabel)
743
+ contentDiv.appendChild(roomInput)
744
+ contentDiv.appendChild(encryptionKeyLabel)
745
+ contentDiv.appendChild(encryptionKeyInput)
746
+ contentDiv.appendChild(keyIdLabel)
747
+ contentDiv.appendChild(keyIdInput)
748
+
749
+ // Insert BLE polling fields before error/buttons
750
+ contentDiv.appendChild(openApiRefreshLabel)
751
+ contentDiv.appendChild(openApiRefreshInput)
752
+ contentDiv.appendChild(blePollingEnabledLabel)
753
+ contentDiv.appendChild(blePollingEnabledInput)
754
+ contentDiv.appendChild(blePollIntervalLabel)
755
+ contentDiv.appendChild(blePollIntervalInput)
756
+ contentDiv.appendChild(errorMessage)
757
+ contentDiv.appendChild(buttons)
758
+
759
+ modal.appendChild(title)
760
+ modal.appendChild(contentDiv)
761
+
762
+ div.appendChild(modal)
763
+ document.body.appendChild(div)
764
+ nameInput.focus()
765
+ }