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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/.changeset/config.json +14 -0
  2. package/.github/copilot-instructions.md +39 -0
  3. package/.github/workflows/ci.yml +4 -1
  4. package/.github/workflows/manual-e2e.yml +6 -3
  5. package/.github/workflows/release.yml +64 -15
  6. package/.github/workflows/stale.yml +2 -4
  7. package/.husky/pre-push +15 -0
  8. package/CHANGELOG.md +126 -134
  9. package/MIGRATION.md +16 -6
  10. package/README.md +84 -3
  11. package/TODO.md +263 -0
  12. package/config.schema.json +229 -36
  13. package/dist/SwitchBotHAPPlatform.d.ts +133 -0
  14. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
  15. package/dist/SwitchBotHAPPlatform.js +555 -0
  16. package/dist/SwitchBotHAPPlatform.js.map +1 -0
  17. package/dist/SwitchBotMatterPlatform.d.ts +141 -0
  18. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
  19. package/dist/SwitchBotMatterPlatform.js +536 -0
  20. package/dist/SwitchBotMatterPlatform.js.map +1 -0
  21. package/dist/device-types.d.ts +31 -0
  22. package/dist/device-types.d.ts.map +1 -0
  23. package/dist/device-types.js +246 -0
  24. package/dist/device-types.js.map +1 -0
  25. package/dist/deviceCommandMapper.d.ts +10 -0
  26. package/dist/deviceCommandMapper.d.ts.map +1 -0
  27. package/dist/deviceCommandMapper.js +319 -0
  28. package/dist/deviceCommandMapper.js.map +1 -0
  29. package/dist/deviceFactory.d.ts +3 -2
  30. package/dist/deviceFactory.d.ts.map +1 -1
  31. package/dist/deviceFactory.js +107 -29
  32. package/dist/deviceFactory.js.map +1 -1
  33. package/dist/devices/genericDevice.d.ts +59 -37
  34. package/dist/devices/genericDevice.d.ts.map +1 -1
  35. package/dist/devices/genericDevice.js +376 -78
  36. package/dist/devices/genericDevice.js.map +1 -1
  37. package/dist/errors.d.ts +38 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +32 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/homebridge-ui/device-types.js +246 -0
  42. package/dist/homebridge-ui/device-types.js.map +1 -0
  43. package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
  44. package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
  45. package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
  46. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  47. package/dist/homebridge-ui/endpoints/config.js +90 -0
  48. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  49. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  50. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  51. package/dist/homebridge-ui/endpoints/devices.js +144 -0
  52. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  53. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  54. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  55. package/dist/homebridge-ui/endpoints/discovery.js +219 -0
  56. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  57. package/dist/homebridge-ui/errors.js +32 -0
  58. package/dist/homebridge-ui/errors.js.map +1 -0
  59. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
  60. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
  61. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
  62. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
  63. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
  64. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
  65. package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
  66. package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
  67. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
  68. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
  69. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
  70. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
  71. package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
  72. package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
  73. package/dist/homebridge-ui/public/css/styles.css +483 -0
  74. package/dist/homebridge-ui/public/index.html +197 -621
  75. package/dist/homebridge-ui/public/js/advanced-settings.d.ts +3 -0
  76. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -0
  77. package/dist/homebridge-ui/public/js/advanced-settings.js +95 -0
  78. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -0
  79. package/dist/homebridge-ui/public/js/advanced-settings.ts +94 -0
  80. package/dist/homebridge-ui/public/js/api.d.ts +66 -0
  81. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  82. package/dist/homebridge-ui/public/js/api.js +295 -0
  83. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  84. package/dist/homebridge-ui/public/js/api.ts +355 -0
  85. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  86. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  87. package/dist/homebridge-ui/public/js/app.js +3722 -0
  88. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  89. package/dist/homebridge-ui/public/js/app.ts +22 -0
  90. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  91. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  92. package/dist/homebridge-ui/public/js/constants.js +2 -0
  93. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  94. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  95. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  96. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  97. package/dist/homebridge-ui/public/js/credentials.js +99 -0
  98. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  99. package/dist/homebridge-ui/public/js/credentials.ts +105 -0
  100. package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
  101. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  102. package/dist/homebridge-ui/public/js/devices-delete.js +199 -0
  103. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  104. package/dist/homebridge-ui/public/js/devices-delete.ts +227 -0
  105. package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
  106. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  107. package/dist/homebridge-ui/public/js/devices.js +98 -0
  108. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  109. package/dist/homebridge-ui/public/js/devices.ts +106 -0
  110. package/dist/homebridge-ui/public/js/discovery.d.ts +9 -0
  111. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  112. package/dist/homebridge-ui/public/js/discovery.js +1201 -0
  113. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  114. package/dist/homebridge-ui/public/js/discovery.ts +1335 -0
  115. package/dist/homebridge-ui/public/js/logger.d.ts +7 -0
  116. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  117. package/dist/homebridge-ui/public/js/logger.js +17 -0
  118. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  119. package/dist/homebridge-ui/public/js/logger.ts +17 -0
  120. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  121. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  122. package/dist/homebridge-ui/public/js/modal.js +35 -0
  123. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  124. package/dist/homebridge-ui/public/js/modal.ts +35 -0
  125. package/dist/homebridge-ui/public/js/modals.d.ts +15 -0
  126. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  127. package/dist/homebridge-ui/public/js/modals.js +675 -0
  128. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  129. package/dist/homebridge-ui/public/js/modals.ts +765 -0
  130. package/dist/homebridge-ui/public/js/render.d.ts +71 -0
  131. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  132. package/dist/homebridge-ui/public/js/render.js +960 -0
  133. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  134. package/dist/homebridge-ui/public/js/render.ts +1084 -0
  135. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  136. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  137. package/dist/homebridge-ui/public/js/toast.js +38 -0
  138. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  139. package/dist/homebridge-ui/public/js/toast.ts +44 -0
  140. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  141. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  142. package/dist/homebridge-ui/public/js/types.js +2 -0
  143. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  144. package/dist/homebridge-ui/public/js/types.ts +26 -0
  145. package/dist/homebridge-ui/server.d.ts +1 -3
  146. package/dist/homebridge-ui/server.d.ts.map +1 -1
  147. package/dist/homebridge-ui/server.js +8 -450
  148. package/dist/homebridge-ui/server.js.map +1 -1
  149. package/dist/homebridge-ui/settings.js +8 -0
  150. package/dist/homebridge-ui/settings.js.map +1 -0
  151. package/dist/homebridge-ui/switchbotClient.js +247 -0
  152. package/dist/homebridge-ui/switchbotClient.js.map +1 -0
  153. package/dist/homebridge-ui/utils/config-parser.d.ts +39 -0
  154. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  155. package/dist/homebridge-ui/utils/config-parser.js +108 -0
  156. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  157. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  158. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  159. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  160. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  161. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  162. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  163. package/dist/homebridge-ui/utils/logger.js +17 -0
  164. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  165. package/dist/index.d.ts +10 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +12 -2
  168. package/dist/index.js.map +1 -1
  169. package/dist/settings.d.ts +1 -0
  170. package/dist/settings.d.ts.map +1 -1
  171. package/dist/settings.js +1 -0
  172. package/dist/settings.js.map +1 -1
  173. package/dist/switchbotClient.d.ts +12 -10
  174. package/dist/switchbotClient.d.ts.map +1 -1
  175. package/dist/switchbotClient.js +156 -103
  176. package/dist/switchbotClient.js.map +1 -1
  177. package/dist/utils.d.ts +76 -1
  178. package/dist/utils.d.ts.map +1 -1
  179. package/dist/utils.js +1121 -4
  180. package/dist/utils.js.map +1 -1
  181. package/docs/assets/highlight.css +16 -2
  182. package/docs/assets/main.js +1 -1
  183. package/docs/index.html +82 -5
  184. package/docs/variables/default.html +3 -1
  185. package/eslint.config.js +9 -5
  186. package/nodemon.json +2 -2
  187. package/package.json +34 -21
  188. package/scripts/build-ui.js +37 -0
  189. package/scripts/free-dev-ports.mjs +105 -0
  190. package/scripts/generate-matter-maps.js +34 -17
  191. package/scripts/sync-device-types.mjs +31 -0
  192. package/src/SwitchBotHAPPlatform.ts +558 -0
  193. package/src/SwitchBotMatterPlatform.ts +538 -0
  194. package/src/device-types.js +246 -0
  195. package/src/device-types.js.map +1 -0
  196. package/src/device-types.ts +261 -0
  197. package/src/deviceCommandMapper.js +319 -0
  198. package/src/deviceCommandMapper.js.map +1 -0
  199. package/src/deviceCommandMapper.ts +333 -0
  200. package/src/deviceFactory.ts +125 -45
  201. package/src/devices/genericDevice.ts +411 -69
  202. package/src/errors.js +32 -0
  203. package/src/errors.js.map +1 -0
  204. package/src/errors.ts +35 -0
  205. package/src/homebridge-ui/endpoints/config.ts +110 -0
  206. package/src/homebridge-ui/endpoints/devices.ts +153 -0
  207. package/src/homebridge-ui/endpoints/discovery.ts +240 -0
  208. package/src/homebridge-ui/public/css/styles.css +483 -0
  209. package/src/homebridge-ui/public/index.html +197 -621
  210. package/src/homebridge-ui/public/js/advanced-settings.ts +94 -0
  211. package/src/homebridge-ui/public/js/api.ts +355 -0
  212. package/src/homebridge-ui/public/js/app.ts +22 -0
  213. package/src/homebridge-ui/public/js/constants.ts +1 -0
  214. package/src/homebridge-ui/public/js/credentials.ts +105 -0
  215. package/src/homebridge-ui/public/js/devices-delete.ts +227 -0
  216. package/src/homebridge-ui/public/js/devices.ts +106 -0
  217. package/src/homebridge-ui/public/js/discovery.ts +1335 -0
  218. package/src/homebridge-ui/public/js/logger.ts +17 -0
  219. package/src/homebridge-ui/public/js/modal.ts +35 -0
  220. package/src/homebridge-ui/public/js/modals.ts +765 -0
  221. package/src/homebridge-ui/public/js/render.ts +1084 -0
  222. package/src/homebridge-ui/public/js/toast.ts +44 -0
  223. package/src/homebridge-ui/public/js/types.ts +26 -0
  224. package/src/homebridge-ui/server.ts +9 -526
  225. package/src/homebridge-ui/utils/config-parser.ts +125 -0
  226. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  227. package/src/homebridge-ui/utils/logger.ts +17 -0
  228. package/src/index.ts +12 -2
  229. package/src/settings.js +8 -0
  230. package/src/settings.js.map +1 -0
  231. package/src/settings.ts +2 -0
  232. package/src/switchbotClient.js +247 -0
  233. package/src/switchbotClient.js.map +1 -0
  234. package/src/switchbotClient.ts +177 -114
  235. package/src/utils.ts +1133 -5
  236. package/test/client/switchbot-client-debounce.spec.ts +35 -0
  237. package/test/client/switchbot-client-openapi.spec.ts +19 -0
  238. package/test/client/switchbotClient.spec.ts +64 -0
  239. package/test/device/device-mapping.spec.ts +23 -0
  240. package/test/device/deviceBase.spec.ts +26 -0
  241. package/test/device/deviceFactory-edge.spec.ts +15 -0
  242. package/test/device/deviceFactory.spec.ts +33 -0
  243. package/test/device/fan-swing.spec.ts +34 -0
  244. package/test/device/genericDevice-blepoll.spec.ts +47 -0
  245. package/test/device/irdevice.spec.ts +9 -0
  246. package/test/device/lock-users.spec.ts +35 -0
  247. package/test/device/matter-descriptors.spec.ts +22 -0
  248. package/test/device/matter-device-state.spec.ts +37 -0
  249. package/test/e2e/run-e2e.spec.ts +18 -19
  250. package/test/errors/errors.spec.ts +10 -0
  251. package/test/helpers/matter-harness.ts +20 -9
  252. package/test/homebridge-ui/server.spec.ts +9 -0
  253. package/test/platform/accessory-restore.spec.ts +37 -0
  254. package/test/platform/matter-childbridge.spec.ts +34 -0
  255. package/test/platform/matter-integration.spec.ts +33 -0
  256. package/test/platform/platform-edge.spec.ts +73 -0
  257. package/test/platform/platform.integration.spec.ts +34 -0
  258. package/test/utils/utils-extra.spec.ts +10 -0
  259. package/test/utils/utils.spec.ts +53 -0
  260. package/todo/TODO.md +80 -0
  261. package/tsconfig.ui.json +11 -0
  262. package/.github/npm-version-script-esm.js +0 -97
  263. package/.github/workflows/beta-release.yml +0 -52
  264. package/dist/platform.d.ts +0 -35
  265. package/dist/platform.d.ts.map +0 -1
  266. package/dist/platform.js +0 -850
  267. package/dist/platform.js.map +0 -1
  268. package/src/platform.ts +0 -867
  269. package/test/accessory-restore.spec.ts +0 -73
  270. package/test/device-mapping.spec.ts +0 -37
  271. package/test/deviceFactory.spec.ts +0 -18
  272. package/test/fan-swing.spec.ts +0 -29
  273. package/test/lock-users.spec.ts +0 -44
  274. package/test/matter-childbridge.spec.ts +0 -55
  275. package/test/matter-descriptors.spec.ts +0 -97
  276. package/test/matter-device-state.spec.ts +0 -101
  277. package/test/matter-integration.spec.ts +0 -70
  278. package/test/platform.integration.spec.ts +0 -55
  279. package/test/switchbot-client-debounce.spec.ts +0 -131
  280. package/test/switchbot-client-openapi.spec.ts +0 -56
  281. package/test/switchbotClient.spec.ts +0 -10
  282. package/test/utils.spec.ts +0 -20
@@ -1,639 +1,215 @@
1
1
  <!doctype html>
2
2
  <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>SwitchBot Plugin UI</title>
7
- <style>
8
- :root {
9
- color-scheme: light dark;
10
- }
11
3
 
12
- @media (prefers-color-scheme: light) {
13
- body {
14
- background: #f5f5f5;
15
- color: #1a1a1a;
16
- }
17
- input {
18
- background: #fff;
19
- border-color: #ddd;
20
- color: #1a1a1a;
21
- }
22
- input:focus {
23
- border-color: #6366f1;
24
- }
25
- .card {
26
- background: #fff;
27
- border: 1px solid #ddd;
28
- }
29
- code {
30
- background: #f0f0f0;
31
- color: #1a1a1a;
32
- }
33
- h2 {
34
- border-bottom-color: #ddd;
35
- }
36
- .status {
37
- color: #666;
38
- }
39
- }
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
7
+ <title>SwitchBot Plugin UI</title>
8
+ <link rel="stylesheet" href="css/styles.css" />
9
+ </head>
40
10
 
41
- @media (prefers-color-scheme: dark) {
42
- body {
43
- background: #1a1a1a;
44
- color: #fff;
45
- }
46
- input {
47
- background: #2a2a2a;
48
- border-color: #444;
49
- color: #fff;
50
- }
51
- .card {
52
- background: #2a2a2a;
53
- }
54
- code {
55
- background: #333;
56
- color: #fff;
57
- }
58
- h2 {
59
- border-bottom-color: #444;
60
- }
61
- .status {
62
- color: #888;
63
- }
64
- .device-item {
65
- background: #333;
66
- border: 1px solid #444;
67
- }
68
- }
11
+ <body>
12
+ <h1>🤖 SwitchBot Configuration</h1>
69
13
 
70
- @media (prefers-color-scheme: light) {
71
- .device-item {
72
- background: #f9f9f9;
73
- border: 1px solid #e0e0e0;
74
- }
75
- }
76
-
77
- body { font-family: system-ui, -apple-system, Arial; padding: 16px }
78
- h1 { font-size: 24px; margin-top:0 }
79
- h2 { font-size: 16px; margin-top: 24px; margin-bottom: 12px; border-bottom: 1px solid; padding-bottom:8px }
80
- ul { padding-left: 0; list-style: none }
81
- li { margin: 8px 0; display:flex; gap:8px; align-items:center }
82
- button { padding: 8px 16px; background:#6366f1; color:#fff; border:none; border-radius:4px; cursor:pointer }
83
- button:hover { background:#4f46e5 }
84
- button.success { background:#10b981 }
85
- code { padding:4px 6px; border-radius:4px; font-family: monospace }
86
- .form-group { margin-bottom:16px }
87
- label { display:block; margin-bottom:6px; font-weight:500 }
88
- input { width:100%; max-width:400px; padding:8px; border: 1px solid; border-radius:4px; font-family:monospace }
89
- input:focus { outline:none }
90
- .status { font-size:14px; margin-top:4px }
91
- .status.ok { color:#10b981 }
92
- .error { color:#ef4444 }
93
- .success-msg { color:#10b981; margin-top:8px }
94
- .card { padding:16px; border-radius:8px; margin-bottom:16px }
95
- .device-item { padding:8px; border-radius:4px; margin-bottom:8px; display:flex; gap:8px; align-items:center; justify-content:space-between }
96
- </style>
97
- </head>
98
- <body>
99
- <h1>🤖 SwitchBot Configuration</h1>
100
-
101
- <div class="card">
102
- <h2>API Credentials</h2>
103
- <p>Configure your SwitchBot API token and secret to enable device discovery and control.</p>
104
-
14
+ <div class="card compact">
15
+ <h2>API Credentials</h2>
16
+ <div class="credentials-row">
105
17
  <div class="form-group">
106
- <label for="token">API Token:</label>
107
- <input type="password" id="token" placeholder="Enter your SwitchBot API token" />
18
+ <label for="token">Token:</label>
19
+ <input type="password" id="token" placeholder="Enter token" />
108
20
  <div class="status" id="tokenStatus"></div>
109
21
  </div>
110
-
111
22
  <div class="form-group">
112
- <label for="secret">API Secret:</label>
113
- <input type="password" id="secret" placeholder="Enter your SwitchBot API secret" />
23
+ <label for="secret">Secret:</label>
24
+ <input type="password" id="secret" placeholder="Enter secret" />
114
25
  <div class="status" id="secretStatus"></div>
115
26
  </div>
116
-
117
- <button id="saveBtn" onclick="saveCredentials()">Save Credentials</button>
118
- <div id="saveStatus"></div>
119
27
  </div>
120
-
121
- <div class="card">
122
- <h2>Discover Devices</h2>
123
- <p>Click "Discover" to find all devices available in your SwitchBot account and add them to your configuration.</p>
124
- <div style="margin-bottom: 16px">
125
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer">
126
- <input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0">
127
- <span>Auto-add all discovered devices</span>
128
- </label>
28
+ <button id="saveBtn" onclick="saveCredentials()">Save</button>
29
+ <div id="saveStatus"></div>
30
+ </div>
31
+
32
+ <div class="card compact">
33
+ <h2>Discover Devices</h2>
34
+ <div style="margin-bottom: 6px">
35
+ <label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
36
+ <input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0" />
37
+ <span>Auto-add all</span>
38
+ <span
39
+ title="When enabled, Discover immediately adds all found devices to your config. Existing devices are skipped automatically."
40
+ style="font-size: 11px; opacity: 0.7">ⓘ</span>
41
+ </label>
42
+ <div style="margin-top: 2px; margin-left: 22px; font-size: 10px; opacity: 0.75; line-height: 1.25">
43
+ Adds all discovered devices automatically; already configured devices are skipped.
129
44
  </div>
130
- <button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 16px">🔍 Discover Devices</button>
131
- <div id="discoverStatus" style="margin-bottom: 16px"></div>
132
- <div id="discoveredList" style="display: none">
133
- <h3 style="margin-top: 0">Available Devices</h3>
134
- <ul id="discoveredDevices" style="max-height: 300px; overflow-y: auto"></ul>
45
+ </div>
46
+ <div class="discovery-settings-row" style="margin-bottom: 2px">
47
+ <label id="bleScanSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
48
+ <span>Scan:</span>
49
+ <select id="bleScanDurationSelect" style="font-size: 11px; padding: 2px 6px">
50
+ <option value="3">3s</option>
51
+ <option value="5" selected>5s</option>
52
+ <option value="10">10s</option>
53
+ <option value="15">15s</option>
54
+ </select>
55
+ </label>
56
+ <label id="bleTimeoutSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
57
+ <span>Timeout:</span>
58
+ <input id="bleTimeoutInput" type="number" min="3" max="30" step="1" value="8"
59
+ style="width: 56px; font-size: 11px; padding: 2px 4px" />
60
+ <span>s</span>
61
+ </label>
62
+ <label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
63
+ <input type="checkbox" id="disableBleScanCheckbox" style="width: auto; margin: 0" />
64
+ <span>Disable BLE scan</span>
65
+ </label>
66
+ </div>
67
+ <div id="bluetoothStatus" style="margin-bottom: 4px; font-size: 10px; opacity: 0.8">Bluetooth: checking...</div>
68
+ <div class="discovery-settings-row" style="margin-bottom: 8px">
69
+ <div id="lastScannedStatus" style="font-size: 10px; opacity: 0.8; flex: 1 1 auto">Last scanned: never</div>
70
+ <label style="font-size: 11px">
71
+ Auto-refresh:
72
+ <select id="autoRefreshIntervalSelect" style="margin-left: 4px; font-size: 11px; padding: 2px 6px">
73
+ <option value="0" selected>Off</option>
74
+ <option value="30">30s</option>
75
+ <option value="60">1m</option>
76
+ <option value="300">5m</option>
77
+ </select>
78
+ </label>
79
+ <button id="refreshDiscoverBtn" class="secondary" style="padding: 4px 9px; font-size: 11px">Refresh</button>
80
+ </div>
81
+ <button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
82
+ 🔍 Discover
83
+ </button>
84
+ <button id="cancelDiscoverBtn"
85
+ style="margin-left: 6px; margin-bottom: 8px; display: none; padding: 6px 10px; font-size: 11px" class="secondary">
86
+ Cancel
87
+ </button>
88
+ <div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
89
+ <div id="discoverDevicesFound" style="margin-bottom: 6px; font-size: 11px; display: none"></div>
90
+ <div id="discoverPhaseProgress" class="discover-phase-progress" style="display: none">
91
+ <div class="discover-phase-track">
92
+ <div id="discoverPhaseFill" class="discover-phase-fill"></div>
135
93
  </div>
94
+ <div id="discoverPhaseLabel" class="discover-phase-label"></div>
136
95
  </div>
137
-
138
- <div class="card">
139
- <h2>Configured Devices</h2>
140
- <p>This page lists devices found in your Homebridge config for the SwitchBot platform. Use the copy button to insert device IDs into the plugin configuration. Connection preference (BLE/OpenAPI) is shown when available.</p>
141
- <div id="status">Loading…</div>
142
- <ul id="devices"></ul>
96
+ <div id="discoveredList">
97
+ <h3 style="margin-top: 0; font-size: 12px; margin-bottom: 6px; font-weight: 600">Available Devices</h3>
98
+ <ul id="discoveredDevices" style="max-height: 300px; overflow-y: auto"></ul>
143
99
  </div>
100
+ </div>
101
+
102
+ <div class="card compact">
103
+ <h2>Configured Devices</h2>
104
+ <div id="status">Loading…</div>
105
+ <ul id="devices"></ul>
106
+ <!-- BLE encryption key fields will be rendered dynamically per device in the UI (see app.ts/devices.js) -->
107
+ <div id="removeAllContainer" style="
108
+ display: none;
109
+ margin-top: 12px;
110
+ padding-top: 10px;
111
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
112
+ ">
113
+ <button id="removeAllBtn" style="
114
+ background: #dc2626;
115
+ width: 100%;
116
+ padding: 7px 12px;
117
+ font-size: 12px;
118
+ font-weight: 500;
119
+ ">
120
+ 🗑️ Remove All Devices
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+
126
+ <div class="card compact" id="advancedSettingsCard" style="margin-top: 24px;">
127
+ <h2>Advanced Settings</h2>
128
+ <form id="advancedSettingsForm">
129
+ <div class="form-group">
130
+ <label for="enableMatter">
131
+ <input type="checkbox" id="enableMatter" /> Enable Matter Support (Override)
132
+ </label>
133
+ <small>Manually enable Matter support. By default, Matter is auto-detected from the child bridge configuration. Only change this if you need to override the auto-detection.</small>
134
+ </div>
135
+ <div class="form-group">
136
+ <label for="preferMatter">
137
+ <input type="checkbox" id="preferMatter" /> Prefer Matter when available
138
+ </label>
139
+ <small>If enabled and Matter is available, devices will be presented as Matter accessories where supported (instead of HAP).</small>
140
+ </div>
141
+ <div class="form-group">
142
+ <label for="enableBLE">
143
+ <input type="checkbox" id="enableBLE" /> Enable BLE (Bluetooth)
144
+ </label>
145
+ <small>Enable or disable BLE (Bluetooth Low Energy) support. If disabled, only OpenAPI (cloud) will be used.</small>
146
+ </div>
147
+ <div class="form-group">
148
+ <label for="blePollingEnabled">
149
+ <input type="checkbox" id="blePollingEnabled" /> Enable BLE Polling Fallback
150
+ </label>
151
+ <small>If enabled, the plugin will periodically poll BLE devices for status as a safety net in addition to real-time notifications. This helps recover from missed notifications or connection loss. Recommended for reliability. Can be overridden per device.</small>
152
+ </div>
153
+ <div class="form-group">
154
+ <label for="blePollIntervalMs">BLE Polling Interval (ms)</label>
155
+ <input type="number" id="blePollIntervalMs" min="60000" max="3600000" step="1000" value="600000" />
156
+ <small>How often to poll BLE devices for status (in milliseconds). Default is 600000 (10 minutes). Set higher to reduce battery drain. Minimum 60000 (1 minute). Can be overridden per device.</small>
157
+ </div>
158
+ <div class="form-group">
159
+ <label for="openApiRefreshRate">OpenAPI Polling Interval (seconds)</label>
160
+ <input type="number" id="openApiRefreshRate" min="30" max="86400" step="1" value="300" />
161
+ <small>How often to poll devices via OpenAPI for status. Default: 300 (5 min). Min: 30. Can be overridden per device.</small>
162
+ </div>
163
+ <div class="form-group">
164
+ <label for="matterBatchEnabled">
165
+ <input type="checkbox" id="matterBatchEnabled" /> Enable Batched OpenAPI Polling
166
+ </label>
167
+ <small>Poll all OpenAPI devices in a single batch at the configured interval. Devices with per-device refreshRate are excluded from the batch.</small>
168
+ </div>
169
+ <div class="form-group">
170
+ <label for="matterBatchRefreshRate">OpenAPI Batch Polling Interval (seconds)</label>
171
+ <input type="number" id="matterBatchRefreshRate" min="30" max="86400" step="1" value="300" />
172
+ <small>Interval for batched OpenAPI polling. Falls back to OpenAPI Polling Interval if not set. Default: 300.</small>
173
+ </div>
174
+ <div class="form-group">
175
+ <label for="dailyApiLimit">OpenAPI Daily Request Limit</label>
176
+ <input type="number" id="dailyApiLimit" min="1000" max="100000" step="1" value="10000" />
177
+ <small>Maximum OpenAPI requests per day allowed by the plugin. Default: 10000.</small>
178
+ </div>
179
+ <div class="form-group">
180
+ <label for="dailyApiReserveForCommands">OpenAPI Reserve for Commands</label>
181
+ <input type="number" id="dailyApiReserveForCommands" min="0" max="10000" step="1" value="1000" />
182
+ <small>Requests reserved for user actions. When remaining budget reaches this value, background polling pauses. Default: 1000.</small>
183
+ </div>
184
+ <div class="form-group">
185
+ <label for="dailyApiResetLocalMidnight">
186
+ <input type="checkbox" id="dailyApiResetLocalMidnight" /> Reset OpenAPI Counter at Local Midnight
187
+ </label>
188
+ <small>If true, resets the daily OpenAPI request counter at local midnight. If false, resets at UTC midnight.</small>
189
+ </div>
190
+ <div class="form-group">
191
+ <label for="webhookOnlyOnReserve">
192
+ <input type="checkbox" id="webhookOnlyOnReserve" /> Only Allow Webhooks on Reserve
193
+ </label>
194
+ <small>When remaining OpenAPI budget reaches the reserve, only webhooks and user commands are allowed. Background polling/discovery pauses.</small>
195
+ </div>
196
+ <div class="form-group">
197
+ <label for="matterBatchConcurrency">OpenAPI Batch Concurrency</label>
198
+ <input type="number" id="matterBatchConcurrency" min="1" max="20" step="1" value="5" />
199
+ <small>Maximum number of parallel OpenAPI status calls during a batch. Default: 5.</small>
200
+ </div>
201
+ <div class="form-group">
202
+ <label for="matterBatchJitter">OpenAPI Batch Jitter (seconds)</label>
203
+ <input type="number" id="matterBatchJitter" min="0" max="300" step="1" value="0" />
204
+ <small>Random startup delay before the first batch to reduce synchronized spikes. Default: 0.</small>
205
+ </div>
206
+ <button type="button" id="saveAdvancedSettingsBtn">Save Advanced Settings</button>
207
+ <div id="advancedSettingsStatus" style="margin-top: 8px; font-size: 12px;"></div>
208
+ </form>
209
+ </div>
144
210
 
145
- <script>
146
- async function loadCredentialStatus() {
147
- try {
148
- const resp = await homebridge.request('/credentials', {})
149
- console.log('Load credentials response:', resp)
150
-
151
- if (!resp || resp.success === false) {
152
- console.error('Failed to load credentials:', resp)
153
- return
154
- }
155
-
156
- const creds = resp.data || {}
157
- const tokenStatus = document.getElementById('tokenStatus')
158
- const secretStatus = document.getElementById('secretStatus')
159
-
160
- if (creds.hasToken) {
161
- tokenStatus.textContent = `✓ Configured (${creds.tokenLength} characters)`
162
- tokenStatus.classList.add('ok')
163
- } else {
164
- tokenStatus.textContent = 'Not configured'
165
- tokenStatus.classList.remove('ok')
166
- }
167
-
168
- if (creds.hasSecret) {
169
- secretStatus.textContent = `✓ Configured (${creds.secretLength} characters)`
170
- secretStatus.classList.add('ok')
171
- } else {
172
- secretStatus.textContent = 'Not configured'
173
- secretStatus.classList.remove('ok')
174
- }
175
- } catch (e) {
176
- console.error('Error loading credentials:', e)
177
- }
178
- }
179
-
180
- async function saveCredentials() {
181
- const token = document.getElementById('token').value
182
- const secret = document.getElementById('secret').value
183
- const saveStatus = document.getElementById('saveStatus')
184
- const saveBtn = document.getElementById('saveBtn')
185
-
186
- if (!token || !secret) {
187
- saveStatus.textContent = 'Please enter both token and secret'
188
- saveStatus.classList.add('error')
189
- return
190
- }
191
-
192
- try {
193
- saveBtn.disabled = true
194
- saveBtn.textContent = 'Saving...'
195
-
196
- console.log('Saving credentials...')
197
- const resp = await homebridge.request('/credentials', { token, secret })
198
- console.log('Save response:', resp)
199
-
200
- if (!resp || resp.success === false) {
201
- throw new Error(resp?.message || 'Save failed')
202
- }
203
-
204
- const result = resp.data || resp
205
- saveStatus.textContent = '✓ ' + (result.message || 'Credentials saved successfully')
206
- saveStatus.classList.remove('error')
207
- saveStatus.classList.add('success-msg')
208
-
209
- // Clear inputs after successful save
210
- document.getElementById('token').value = ''
211
- document.getElementById('secret').value = ''
212
-
213
- // Reload status to verify save
214
- setTimeout(() => loadCredentialStatus(), 500)
215
-
216
- // Clear status message
217
- setTimeout(() => {
218
- saveStatus.textContent = ''
219
- saveStatus.classList.remove('success-msg')
220
- }, 3000)
221
- } catch (e) {
222
- console.error('Save error:', e)
223
- saveStatus.textContent = 'Error: ' + (e?.message || 'Failed to save')
224
- saveStatus.classList.add('error')
225
- } finally {
226
- saveBtn.disabled = false
227
- saveBtn.textContent = 'Save Credentials'
228
- }
229
- }
230
-
231
- async function fetchDevices() {
232
- try {
233
- const resp = await homebridge.request('/devices', {})
234
- if (!resp || resp.success === false) throw new Error(resp?.data?.message || 'request failed')
235
- return resp.data || []
236
- } catch (e) {
237
- console.error(e)
238
- return []
239
- }
240
- }
241
-
242
- async function discoverDevices() {
243
- const btn = document.getElementById('discoverBtn')
244
- const status = document.getElementById('discoverStatus')
245
- const list = document.getElementById('discoveredList')
246
- const ul = document.getElementById('discoveredDevices')
247
- const autoAddAll = document.getElementById('autoAddAllCheckbox').checked
248
-
249
- try {
250
- btn.disabled = true
251
- btn.textContent = '🔍 Discovering...'
252
- status.textContent = 'Searching SwitchBot account...'
253
- status.classList.remove('error')
254
-
255
- const resp = await homebridge.request('/discover', {})
256
- console.log('Discover response:', resp)
257
-
258
- if (!resp || resp.success === false) {
259
- throw new Error(resp?.data?.message || 'Discovery failed')
260
- }
261
-
262
- const devicesRaw = resp.data || []
263
- const devices = devicesRaw.filter((d, index, arr) =>
264
- !!d?.id && arr.findIndex((x) => x?.id === d.id) === index,
265
- )
266
-
267
- if (!devices.length) {
268
- status.textContent = 'No devices found in your SwitchBot account'
269
- list.style.display = 'none'
270
- return
271
- }
272
-
273
- // If auto-add is enabled, add all devices immediately using bulk endpoint
274
- if (autoAddAll) {
275
- status.textContent = `Auto-adding ${devices.length} device(s)...`
276
-
277
- try {
278
- const bulkResult = await homebridge.request('/add-devices', {
279
- devices: devices.map(d => ({
280
- deviceId: d.id,
281
- name: d.name,
282
- type: d.type,
283
- }))
284
- })
285
-
286
- console.log('Bulk add response:', bulkResult)
287
-
288
- if (!bulkResult || bulkResult.success === false) {
289
- throw new Error(bulkResult?.data?.message || 'Bulk add failed')
290
- }
291
-
292
- const addedCount = bulkResult?.data?.addedCount || 0
293
- const skippedCount = bulkResult?.data?.skippedCount || 0
294
- status.textContent = `✓ Added ${addedCount} device(s)` + (skippedCount > 0 ? ` (${skippedCount} skipped)` : '')
295
- status.classList.remove('error')
296
- list.style.display = 'none'
297
- } catch (e) {
298
- console.error('Bulk add error:', e)
299
- status.textContent = '✗ Error: ' + (e?.message || 'Failed to add devices')
300
- status.classList.add('error')
301
- }
302
-
303
- // Refresh the configured devices list
304
- await loadConfiguredDevices()
305
- return
306
- }
307
-
308
- status.textContent = `Found ${devices.length} device(s)`
309
- ul.innerHTML = ''
310
-
311
- for (const d of devices) {
312
- const li = document.createElement('li')
313
- li.className = 'device-item'
314
-
315
- const info = document.createElement('div')
316
- info.style.flex = '1'
317
- const name = document.createElement('div')
318
- name.style.fontWeight = '500'
319
- name.textContent = d.name || d.id
320
- const details = document.createElement('div')
321
- details.style.fontSize = '12px'
322
- details.style.opacity = '0.75'
323
- details.textContent = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
324
- info.appendChild(name)
325
- info.appendChild(details)
326
-
327
- const addBtn = document.createElement('button')
328
- addBtn.textContent = '+ Add'
329
- addBtn.style.marginLeft = '8px'
330
- addBtn.onclick = async () => {
331
- await addDeviceToConfig(d)
332
- }
333
-
334
- li.appendChild(info)
335
- li.appendChild(addBtn)
336
- ul.appendChild(li)
337
- }
338
-
339
- list.style.display = 'block'
340
- } catch (e) {
341
- console.error('Discovery error:', e)
342
- status.textContent = 'Error: ' + (e?.message || 'Discovery failed')
343
- status.classList.add('error')
344
- list.style.display = 'none'
345
- } finally {
346
- btn.disabled = false
347
- btn.textContent = '🔍 Discover Devices'
348
- }
349
- }
350
-
351
- async function addDeviceToConfig(device, options = {}) {
352
- const { refresh = true, showStatus = true } = options
353
- try {
354
- console.log('Adding device to config:', device)
355
-
356
- const resp = await homebridge.request('/add-device', {
357
- deviceId: device.id,
358
- name: device.name,
359
- type: device.type,
360
- })
361
-
362
- console.log('Add device response:', resp)
363
-
364
- if (!resp || resp.success === false) {
365
- throw new Error(resp?.data?.message || 'Failed to add device')
366
- }
367
-
368
- const alreadyExists = !!resp.data?.alreadyExists
369
- const message = resp.data?.message || (alreadyExists
370
- ? `Device "${device.name}" already in config`
371
- : `Device "${device.name}" added successfully!`)
372
-
373
- if (showStatus) {
374
- const status = document.getElementById('discoverStatus')
375
- status.textContent = (alreadyExists ? '• ' : '✓ ') + message
376
- status.classList.remove('error')
377
- status.classList.add('success-msg')
378
- }
379
-
380
- if (refresh) {
381
- await loadConfiguredDevices()
382
- }
383
-
384
- return { added: !alreadyExists }
385
- } catch (e) {
386
- console.error('Add device error:', e)
387
- const status = document.getElementById('discoverStatus')
388
- status.textContent = '✗ Error: ' + (e?.message || 'Failed to add device')
389
- status.classList.add('error')
390
- return { added: false }
391
- }
392
- }
393
-
394
- async function loadConfiguredDevices() {
395
- const list = await fetchDevices()
396
- render(list)
397
- }
398
-
399
- async function deleteDevice(deviceId, deviceName) {
400
- if (!confirm(`Are you sure you want to remove "${deviceName || deviceId}"?`)) {
401
- return
402
- }
403
-
404
- try {
405
- const resp = await homebridge.request('/delete-device', { deviceId })
406
-
407
- if (!resp || resp.success === false) {
408
- throw new Error(resp?.data?.message || 'Failed to delete device')
409
- }
410
-
411
- // Refresh device list
412
- const list = await fetchDevices()
413
- render(list)
414
- } catch (e) {
415
- console.error('Delete error:', e)
416
- alert('Error: ' + (e?.message || 'Failed to delete device'))
417
- }
418
- }
419
-
420
- const DEVICE_TYPES = [
421
- 'bot', 'curtain', 'fan', 'light', 'lightstrip', 'motion', 'contact',
422
- 'vacuum', 'lock', 'humidifier', 'temperature', 'relay', 'plug',
423
- 'wosweeper', 'blindtilt', 'curtain3', 'rollershade', 'meter',
424
- 'waterdetector', 'walletfinder', 'unknown'
425
- ]
426
-
427
- async function editDevice(device) {
428
- // Create modal dialog for editing
429
- const div = document.createElement('div')
430
- div.style.position = 'fixed'
431
- div.style.top = '0'
432
- div.style.left = '0'
433
- div.style.width = '100%'
434
- div.style.height = '100%'
435
- div.style.background = 'rgba(0,0,0,0.7)'
436
- div.style.display = 'flex'
437
- div.style.alignItems = 'center'
438
- div.style.justifyContent = 'center'
439
- div.style.zIndex = '9999'
440
-
441
- const modal = document.createElement('div')
442
- modal.style.background = getComputedStyle(document.body).backgroundColor
443
- modal.style.color = getComputedStyle(document.body).color
444
- modal.style.padding = '24px'
445
- modal.style.borderRadius = '8px'
446
- modal.style.minWidth = '400px'
447
- modal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.3)'
448
-
449
- const title = document.createElement('h3')
450
- title.textContent = 'Edit Device'
451
- title.style.marginTop = '0'
452
-
453
- const nameLabel = document.createElement('label')
454
- nameLabel.textContent = 'Device Name'
455
- nameLabel.style.display = 'block'
456
- nameLabel.style.marginBottom = '8px'
457
- nameLabel.style.fontWeight = '500'
458
-
459
- const nameInput = document.createElement('input')
460
- nameInput.type = 'text'
461
- nameInput.value = device.name || device.id
462
- nameInput.style.width = '100%'
463
- nameInput.style.marginBottom = '16px'
464
-
465
- const typeLabel = document.createElement('label')
466
- typeLabel.textContent = 'Device Type'
467
- typeLabel.style.display = 'block'
468
- typeLabel.style.marginBottom = '8px'
469
- typeLabel.style.fontWeight = '500'
470
-
471
- const typeSelect = document.createElement('select')
472
- typeSelect.style.width = '100%'
473
- typeSelect.style.padding = '8px'
474
- typeSelect.style.marginBottom = '16px'
475
- typeSelect.style.borderRadius = '4px'
476
- typeSelect.style.background = getComputedStyle(nameInput).background
477
- typeSelect.style.color = getComputedStyle(nameInput).color
478
- typeSelect.style.border = getComputedStyle(nameInput).border
479
-
480
- DEVICE_TYPES.forEach(t => {
481
- const opt = document.createElement('option')
482
- opt.value = t
483
- opt.text = t
484
- opt.selected = (device.type || 'unknown') === t
485
- typeSelect.appendChild(opt)
486
- })
487
-
488
- const buttons = document.createElement('div')
489
- buttons.style.display = 'flex'
490
- buttons.style.gap = '8px'
491
- buttons.style.justifyContent = 'flex-end'
492
-
493
- const cancelBtn = document.createElement('button')
494
- cancelBtn.textContent = 'Cancel'
495
- cancelBtn.style.background = '#666'
496
- cancelBtn.onclick = () => div.remove()
497
-
498
- const saveBtn = document.createElement('button')
499
- saveBtn.textContent = 'Save'
500
- saveBtn.onclick = async () => {
501
- try {
502
- const params = {
503
- deviceId: device.id,
504
- configDeviceName: nameInput.value || undefined,
505
- configDeviceType: typeSelect.value,
506
- }
507
- console.log('[Edit Device] Sending update request with params:', params)
508
-
509
- const resp = await homebridge.request('/update-device', params)
510
-
511
- console.log('[Edit Device] Update response:', resp)
512
-
513
- if (!resp || resp.success === false) {
514
- throw new Error(resp?.data?.message || 'Failed to update device')
515
- }
516
-
517
- // Refresh device list
518
- console.log('[Edit Device] Refreshing device list after update')
519
- const list = await fetchDevices()
520
- render(list)
521
- div.remove()
522
- } catch (e) {
523
- console.error('Update error:', e)
524
- alert('Error: ' + (e?.message || 'Failed to update device'))
525
- }
526
- }
527
-
528
- buttons.appendChild(cancelBtn)
529
- buttons.appendChild(saveBtn)
530
-
531
- modal.appendChild(title)
532
- modal.appendChild(nameLabel)
533
- modal.appendChild(nameInput)
534
- modal.appendChild(typeLabel)
535
- modal.appendChild(typeSelect)
536
- modal.appendChild(buttons)
537
-
538
- div.appendChild(modal)
539
- document.body.appendChild(div)
540
- nameInput.focus()
541
- }
542
-
543
- function render(list) {
544
- const ul = document.getElementById('devices')
545
- const status = document.getElementById('status')
546
- if (!list.length) {
547
- status.textContent = 'No devices found in config.'
548
- ul.innerHTML = ''
549
- return
550
- }
551
- status.textContent = `Found ${list.length} device(s)`
552
- ul.innerHTML = ''
553
- for (const d of list) {
554
- const li = document.createElement('li')
555
- li.className = 'device-item'
556
-
557
- const info = document.createElement('div')
558
- info.style.flex = '1'
559
-
560
- const name = document.createElement('div')
561
- name.style.fontWeight = '500'
562
- name.textContent = d.name || d.id
563
-
564
- const code = document.createElement('code')
565
- code.textContent = d.id
566
- code.style.fontSize = '12px'
567
- code.style.opacity = '0.75'
568
- code.style.marginLeft = '8px'
569
-
570
- const meta = document.createElement('div')
571
- meta.style.opacity = '0.75'
572
- meta.style.marginTop = '4px'
573
- meta.style.fontSize = '12px'
574
- const typeText = d.type ? `type: ${d.type}` : ''
575
- const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
576
- const roomText = d.room ? `room: ${d.room}` : ''
577
- meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
578
-
579
- info.appendChild(name)
580
- info.appendChild(code)
581
- info.appendChild(meta)
582
-
583
- const buttons = document.createElement('div')
584
- buttons.style.display = 'flex'
585
- buttons.style.gap = '8px'
586
-
587
- const editBtn = document.createElement('button')
588
- editBtn.textContent = '✏️ Edit'
589
- editBtn.style.padding = '6px 12px'
590
- editBtn.style.fontSize = '12px'
591
- editBtn.onclick = async () => {
592
- await editDevice(d)
593
- }
594
-
595
- const copyBtn = document.createElement('button')
596
- copyBtn.textContent = 'Copy ID'
597
- copyBtn.style.padding = '6px 12px'
598
- copyBtn.style.fontSize = '12px'
599
- copyBtn.addEventListener('click', async () => {
600
- try {
601
- await navigator.clipboard.writeText(d.id)
602
- copyBtn.textContent = 'Copied'
603
- copyBtn.classList.add('success')
604
- setTimeout(() => {
605
- copyBtn.textContent = 'Copy ID'
606
- copyBtn.classList.remove('success')
607
- }, 1200)
608
- } catch (e) {
609
- alert('Failed to copy')
610
- }
611
- })
612
-
613
- const deleteBtn = document.createElement('button')
614
- deleteBtn.textContent = '🗑️ Delete'
615
- deleteBtn.style.padding = '6px 12px'
616
- deleteBtn.style.fontSize = '12px'
617
- deleteBtn.style.background = '#ef4444'
618
- deleteBtn.onclick = async () => {
619
- await deleteDevice(d.id, d.name || d.id)
620
- }
621
-
622
- buttons.appendChild(editBtn)
623
- buttons.appendChild(copyBtn)
624
- buttons.appendChild(deleteBtn)
625
-
626
- li.appendChild(info)
627
- li.appendChild(buttons)
628
- ul.appendChild(li)
629
- }
630
- }
211
+ <script type="module" src="js/app.js"></script>
212
+ <script type="module" src="js/advanced-settings.js"></script>
213
+ </body>
631
214
 
632
- (async () => {
633
- await loadCredentialStatus()
634
- const list = await fetchDevices()
635
- render(list)
636
- })()
637
- </script>
638
- </body>
639
- </html>
215
+ </html>