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