@switchbot/homebridge-switchbot 5.0.0-beta.108 → 5.0.0-beta.109

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 (148) hide show
  1. package/TODO.md +564 -0
  2. package/config.schema.json +100 -25
  3. package/dist/device-types.d.ts +15 -0
  4. package/dist/device-types.d.ts.map +1 -0
  5. package/dist/device-types.js +145 -0
  6. package/dist/device-types.js.map +1 -0
  7. package/dist/deviceFactory.d.ts +1 -1
  8. package/dist/deviceFactory.d.ts.map +1 -1
  9. package/dist/deviceFactory.js +55 -17
  10. package/dist/deviceFactory.js.map +1 -1
  11. package/dist/devices/genericDevice.d.ts +1 -1
  12. package/dist/devices/genericDevice.d.ts.map +1 -1
  13. package/dist/devices/genericDevice.js +4 -2
  14. package/dist/devices/genericDevice.js.map +1 -1
  15. package/dist/homebridge-ui/endpoints/config.d.ts +6 -0
  16. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  17. package/dist/homebridge-ui/endpoints/config.js +73 -0
  18. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  19. package/dist/homebridge-ui/endpoints/credentials.d.ts +6 -0
  20. package/dist/homebridge-ui/endpoints/credentials.d.ts.map +1 -0
  21. package/dist/homebridge-ui/endpoints/credentials.js +65 -0
  22. package/dist/homebridge-ui/endpoints/credentials.js.map +1 -0
  23. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  24. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  25. package/dist/homebridge-ui/endpoints/devices.js +285 -0
  26. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  27. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  28. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  29. package/dist/homebridge-ui/endpoints/discovery.js +171 -0
  30. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  31. package/dist/homebridge-ui/public/index.html +144 -624
  32. package/dist/homebridge-ui/public/js/api.d.ts +15 -0
  33. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  34. package/dist/homebridge-ui/public/js/api.js +164 -0
  35. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  36. package/dist/homebridge-ui/public/js/api.ts +189 -0
  37. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  38. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  39. package/dist/homebridge-ui/public/js/app.js +1069 -0
  40. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  41. package/dist/homebridge-ui/public/js/app.ts +21 -0
  42. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  43. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  44. package/dist/homebridge-ui/public/js/constants.js +2 -0
  45. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  46. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  47. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  48. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  49. package/dist/homebridge-ui/public/js/credentials.js +96 -0
  50. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  51. package/dist/homebridge-ui/public/js/credentials.ts +100 -0
  52. package/dist/homebridge-ui/public/js/devices-delete.d.ts +2 -0
  53. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  54. package/dist/homebridge-ui/public/js/devices-delete.js +99 -0
  55. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  56. package/dist/homebridge-ui/public/js/devices-delete.ts +113 -0
  57. package/dist/homebridge-ui/public/js/devices.d.ts +8 -0
  58. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  59. package/dist/homebridge-ui/public/js/devices.js +71 -0
  60. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  61. package/dist/homebridge-ui/public/js/devices.ts +78 -0
  62. package/dist/homebridge-ui/public/js/discovery.d.ts +3 -0
  63. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  64. package/dist/homebridge-ui/public/js/discovery.js +107 -0
  65. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  66. package/dist/homebridge-ui/public/js/discovery.ts +123 -0
  67. package/dist/homebridge-ui/public/js/logger.d.ts +6 -0
  68. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  69. package/dist/homebridge-ui/public/js/logger.js +12 -0
  70. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  71. package/dist/homebridge-ui/public/js/logger.ts +11 -0
  72. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  73. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  74. package/dist/homebridge-ui/public/js/modal.js +26 -0
  75. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  76. package/dist/homebridge-ui/public/js/modal.ts +28 -0
  77. package/dist/homebridge-ui/public/js/modals.d.ts +2 -0
  78. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  79. package/dist/homebridge-ui/public/js/modals.js +132 -0
  80. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  81. package/dist/homebridge-ui/public/js/modals.ts +153 -0
  82. package/dist/homebridge-ui/public/js/render.d.ts +6 -0
  83. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  84. package/dist/homebridge-ui/public/js/render.js +166 -0
  85. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  86. package/dist/homebridge-ui/public/js/render.ts +190 -0
  87. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  88. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  89. package/dist/homebridge-ui/public/js/toast.js +29 -0
  90. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  91. package/dist/homebridge-ui/public/js/toast.ts +37 -0
  92. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  93. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  94. package/dist/homebridge-ui/public/js/types.js +2 -0
  95. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  96. package/dist/homebridge-ui/public/js/types.ts +26 -0
  97. package/dist/homebridge-ui/server.d.ts +1 -3
  98. package/dist/homebridge-ui/server.d.ts.map +1 -1
  99. package/dist/homebridge-ui/server.js +10 -561
  100. package/dist/homebridge-ui/server.js.map +1 -1
  101. package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
  102. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  103. package/dist/homebridge-ui/utils/config-parser.js +87 -0
  104. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  105. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  106. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  107. package/dist/homebridge-ui/utils/logger.js +17 -0
  108. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  109. package/dist/platform.d.ts.map +1 -1
  110. package/dist/platform.js +6 -2
  111. package/dist/platform.js.map +1 -1
  112. package/dist/switchbotClient.d.ts.map +1 -1
  113. package/dist/switchbotClient.js +2 -0
  114. package/dist/switchbotClient.js.map +1 -1
  115. package/docs/variables/default.html +1 -1
  116. package/eslint.config.js +5 -0
  117. package/nodemon.json +2 -2
  118. package/package.json +20 -10
  119. package/scripts/build-ui.js +36 -0
  120. package/scripts/free-dev-ports.mjs +105 -0
  121. package/scripts/sync-device-types.mjs +28 -0
  122. package/src/device-types.ts +145 -0
  123. package/src/deviceFactory.ts +71 -31
  124. package/src/devices/genericDevice.ts +5 -2
  125. package/src/homebridge-ui/endpoints/config.ts +79 -0
  126. package/src/homebridge-ui/endpoints/credentials.ts +74 -0
  127. package/src/homebridge-ui/endpoints/devices.ts +321 -0
  128. package/src/homebridge-ui/endpoints/discovery.ts +191 -0
  129. package/src/homebridge-ui/public/index.html +144 -624
  130. package/src/homebridge-ui/public/js/api.ts +189 -0
  131. package/src/homebridge-ui/public/js/app.ts +21 -0
  132. package/src/homebridge-ui/public/js/constants.ts +1 -0
  133. package/src/homebridge-ui/public/js/credentials.ts +100 -0
  134. package/src/homebridge-ui/public/js/devices-delete.ts +113 -0
  135. package/src/homebridge-ui/public/js/devices.ts +78 -0
  136. package/src/homebridge-ui/public/js/discovery.ts +123 -0
  137. package/src/homebridge-ui/public/js/logger.ts +11 -0
  138. package/src/homebridge-ui/public/js/modal.ts +28 -0
  139. package/src/homebridge-ui/public/js/modals.ts +153 -0
  140. package/src/homebridge-ui/public/js/render.ts +190 -0
  141. package/src/homebridge-ui/public/js/toast.ts +37 -0
  142. package/src/homebridge-ui/public/js/types.ts +26 -0
  143. package/src/homebridge-ui/server.ts +11 -647
  144. package/src/homebridge-ui/utils/config-parser.ts +108 -0
  145. package/src/homebridge-ui/utils/logger.ts +17 -0
  146. package/src/platform.ts +6 -2
  147. package/src/switchbotClient.ts +2 -0
  148. package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
@@ -77,663 +77,183 @@
77
77
  @media (prefers-color-scheme: dark) {
78
78
  }
79
79
 
80
- body { font-family: system-ui, -apple-system, Arial; padding: 16px }
81
- h1 { font-size: 24px; margin-top:0 }
82
- h2 { font-size: 16px; margin-top: 24px; margin-bottom: 12px; border-bottom: 1px solid; padding-bottom:8px }
83
- ul { padding-left: 0; list-style: none }
84
- li { margin: 8px 0; display:flex; gap:8px; align-items:center }
85
- button { padding: 8px 16px; background:#6366f1; color:#fff; border:none; border-radius:4px; cursor:pointer }
86
- button:hover { background:#4f46e5 }
87
- button.success { background:#10b981 }
88
- code { padding:4px 6px; border-radius:4px; font-family: monospace }
89
- .form-group { margin-bottom:16px }
90
- label { display:block; margin-bottom:6px; font-weight:500 }
91
- input { width:100%; max-width:400px; padding:8px; border: 1px solid; border-radius:4px; font-family:monospace }
92
- input:focus { outline:none }
93
- .status { font-size:14px; margin-top:4px }
94
- .status.ok { color:#10b981 }
95
- .error { color:#ef4444 }
96
- .success-msg { color:#10b981; margin-top:8px }
97
- .card { padding:16px; border-radius:8px; margin-bottom:16px }
98
- .device-item { padding:8px; border-radius:4px; margin-bottom:8px; display:flex; gap:8px; align-items:center; justify-content:space-between }
80
+ body {
81
+ font-family:
82
+ system-ui,
83
+ -apple-system,
84
+ Arial;
85
+ padding: 16px;
86
+ }
87
+ h1 {
88
+ font-size: 24px;
89
+ margin-top: 0;
90
+ }
91
+ h2 {
92
+ font-size: 16px;
93
+ margin-top: 24px;
94
+ margin-bottom: 12px;
95
+ border-bottom: 1px solid;
96
+ padding-bottom: 8px;
97
+ }
98
+ ul {
99
+ padding-left: 0;
100
+ list-style: none;
101
+ }
102
+ li {
103
+ margin: 8px 0;
104
+ display: flex;
105
+ gap: 8px;
106
+ align-items: center;
107
+ }
108
+ button {
109
+ padding: 8px 16px;
110
+ background: #6366f1;
111
+ color: #fff;
112
+ border: none;
113
+ border-radius: 4px;
114
+ cursor: pointer;
115
+ }
116
+ button:hover {
117
+ background: #4f46e5;
118
+ }
119
+ button.success {
120
+ background: #10b981;
121
+ }
122
+ code {
123
+ padding: 4px 6px;
124
+ border-radius: 4px;
125
+ font-family: monospace;
126
+ }
127
+ .form-group {
128
+ margin-bottom: 16px;
129
+ }
130
+ label {
131
+ display: block;
132
+ margin-bottom: 6px;
133
+ font-weight: 500;
134
+ }
135
+ input {
136
+ width: 100%;
137
+ max-width: 400px;
138
+ padding: 8px;
139
+ border: 1px solid;
140
+ border-radius: 4px;
141
+ font-family: monospace;
142
+ }
143
+ input:focus {
144
+ outline: none;
145
+ }
146
+ .status {
147
+ font-size: 14px;
148
+ margin-top: 4px;
149
+ }
150
+ .status.ok {
151
+ color: #10b981;
152
+ }
153
+ .error {
154
+ color: #ef4444;
155
+ }
156
+ .success-msg {
157
+ color: #10b981;
158
+ margin-top: 8px;
159
+ }
160
+ .card {
161
+ padding: 16px;
162
+ border-radius: 8px;
163
+ margin-bottom: 16px;
164
+ }
165
+ .device-item {
166
+ padding: 8px;
167
+ border-radius: 4px;
168
+ margin-bottom: 8px;
169
+ display: flex;
170
+ gap: 8px;
171
+ align-items: center;
172
+ justify-content: space-between;
173
+ }
99
174
  </style>
100
175
  </head>
101
176
  <body>
102
177
  <h1>🤖 SwitchBot Configuration</h1>
103
-
178
+
104
179
  <div class="card">
105
180
  <h2>API Credentials</h2>
106
- <p>Configure your SwitchBot API token and secret to enable device discovery and control.</p>
107
-
181
+ <p>
182
+ Configure your SwitchBot API token and secret to enable device discovery
183
+ and control.
184
+ </p>
185
+
108
186
  <div class="form-group">
109
187
  <label for="token">API Token:</label>
110
- <input type="password" id="token" placeholder="Enter your SwitchBot API token" />
188
+ <input
189
+ type="password"
190
+ id="token"
191
+ placeholder="Enter your SwitchBot API token"
192
+ />
111
193
  <div class="status" id="tokenStatus"></div>
112
194
  </div>
113
-
195
+
114
196
  <div class="form-group">
115
197
  <label for="secret">API Secret:</label>
116
- <input type="password" id="secret" placeholder="Enter your SwitchBot API secret" />
198
+ <input
199
+ type="password"
200
+ id="secret"
201
+ placeholder="Enter your SwitchBot API secret"
202
+ />
117
203
  <div class="status" id="secretStatus"></div>
118
204
  </div>
119
-
205
+
120
206
  <button id="saveBtn" onclick="saveCredentials()">Save Credentials</button>
121
207
  <div id="saveStatus"></div>
122
208
  </div>
123
-
209
+
124
210
  <div class="card">
125
211
  <h2>Discover Devices</h2>
126
- <p>Click "Discover" to find all devices available in your SwitchBot account and add them to your configuration.</p>
212
+ <p>
213
+ Click "Discover" to find all devices available in your SwitchBot account
214
+ and add them to your configuration.
215
+ </p>
127
216
  <div style="margin-bottom: 16px">
128
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer">
129
- <input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0">
217
+ <label
218
+ style="display: flex; align-items: center; gap: 8px; cursor: pointer"
219
+ >
220
+ <input
221
+ type="checkbox"
222
+ id="autoAddAllCheckbox"
223
+ style="width: auto; margin: 0"
224
+ />
130
225
  <span>Auto-add all discovered devices</span>
131
226
  </label>
132
227
  </div>
133
- <button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 16px">🔍 Discover Devices</button>
228
+ <button
229
+ id="discoverBtn"
230
+ onclick="discoverDevices()"
231
+ style="margin-bottom: 16px"
232
+ >
233
+ 🔍 Discover Devices
234
+ </button>
134
235
  <div id="discoverStatus" style="margin-bottom: 16px"></div>
135
236
  <div id="discoveredList" style="display: none">
136
237
  <h3 style="margin-top: 0">Available Devices</h3>
137
- <ul id="discoveredDevices" style="max-height: 300px; overflow-y: auto"></ul>
238
+ <ul
239
+ id="discoveredDevices"
240
+ style="max-height: 300px; overflow-y: auto"
241
+ ></ul>
138
242
  </div>
139
243
  </div>
140
-
244
+
141
245
  <div class="card">
142
246
  <h2>Configured Devices</h2>
143
- <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>
247
+ <p>
248
+ This page lists devices found in your Homebridge config for the
249
+ SwitchBot platform. Use the copy button to insert device IDs into the
250
+ plugin configuration. Connection preference (BLE/OpenAPI) is shown when
251
+ available.
252
+ </p>
144
253
  <div id="status">Loading…</div>
145
254
  <ul id="devices"></ul>
146
255
  </div>
147
256
 
148
- <script>
149
- async function loadCredentialStatus() {
150
- try {
151
- const resp = await homebridge.request('/credentials', {})
152
- console.log('Load credentials response:', resp)
153
-
154
- if (!resp || resp.success === false) {
155
- console.error('Failed to load credentials:', resp)
156
- return
157
- }
158
-
159
- const creds = resp.data || {}
160
- const tokenStatus = document.getElementById('tokenStatus')
161
- const secretStatus = document.getElementById('secretStatus')
162
-
163
- if (creds.hasToken) {
164
- tokenStatus.textContent = `✓ Configured (${creds.tokenLength} characters)`
165
- tokenStatus.classList.add('ok')
166
- } else {
167
- tokenStatus.textContent = 'Not configured'
168
- tokenStatus.classList.remove('ok')
169
- }
170
-
171
- if (creds.hasSecret) {
172
- secretStatus.textContent = `✓ Configured (${creds.secretLength} characters)`
173
- secretStatus.classList.add('ok')
174
- } else {
175
- secretStatus.textContent = 'Not configured'
176
- secretStatus.classList.remove('ok')
177
- }
178
- } catch (e) {
179
- console.error('Error loading credentials:', e)
180
- }
181
- }
182
-
183
- async function saveCredentials() {
184
- const token = document.getElementById('token').value
185
- const secret = document.getElementById('secret').value
186
- const saveStatus = document.getElementById('saveStatus')
187
- const saveBtn = document.getElementById('saveBtn')
188
-
189
- if (!token || !secret) {
190
- saveStatus.textContent = 'Please enter both token and secret'
191
- saveStatus.classList.add('error')
192
- return
193
- }
194
-
195
- try {
196
- saveBtn.disabled = true
197
- saveBtn.textContent = 'Saving...'
198
-
199
- console.log('Saving credentials...')
200
- const resp = await homebridge.request('/credentials', { token, secret })
201
- console.log('Save response:', resp)
202
-
203
- if (!resp || resp.success === false) {
204
- throw new Error(resp?.message || 'Save failed')
205
- }
206
-
207
- const result = resp.data || resp
208
- saveStatus.textContent = '✓ ' + (result.message || 'Credentials saved successfully')
209
- saveStatus.classList.remove('error')
210
- saveStatus.classList.add('success-msg')
211
-
212
- await syncParentPluginConfigFromDisk()
213
-
214
- // Clear inputs after successful save
215
- document.getElementById('token').value = ''
216
- document.getElementById('secret').value = ''
217
-
218
- // Reload status to verify save
219
- setTimeout(() => loadCredentialStatus(), 500)
220
-
221
- // Clear status message
222
- setTimeout(() => {
223
- saveStatus.textContent = ''
224
- saveStatus.classList.remove('success-msg')
225
- }, 3000)
226
- } catch (e) {
227
- console.error('Save error:', e)
228
- saveStatus.textContent = 'Error: ' + (e?.message || 'Failed to save')
229
- saveStatus.classList.add('error')
230
- } finally {
231
- saveBtn.disabled = false
232
- saveBtn.textContent = 'Save Credentials'
233
- }
234
- }
235
-
236
- function isSwitchBotPlatformConfig(block) {
237
- const platformName = String(block?.platform || block?.name || '').toLowerCase()
238
- return platformName === 'switchbot' || platformName === '@switchbot/homebridge-switchbot' || platformName.includes('switchbot')
239
- }
240
-
241
- async function syncParentPluginConfigFromDisk() {
242
- try {
243
- if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
244
- console.warn('[SwitchBot UI] Parent config sync API not available')
245
- return false
246
- }
247
-
248
- const diskResp = await homebridge.request('/platform-config', {})
249
- if (!diskResp || diskResp.success === false || !diskResp.data) {
250
- console.warn('[SwitchBot UI] Failed to fetch fresh platform config from disk')
251
- return false
252
- }
253
-
254
- const pluginConfigBlocks = await homebridge.getPluginConfig()
255
- if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
256
- console.warn('[SwitchBot UI] No plugin config blocks returned from Homebridge')
257
- return false
258
- }
259
-
260
- const index = pluginConfigBlocks.findIndex((block) => isSwitchBotPlatformConfig(block))
261
- if (index < 0) {
262
- console.warn('[SwitchBot UI] SwitchBot platform block not found in Homebridge plugin config')
263
- return false
264
- }
265
-
266
- pluginConfigBlocks[index] = diskResp.data
267
- await homebridge.updatePluginConfig(pluginConfigBlocks)
268
- return true
269
- } catch (e) {
270
- console.warn('[SwitchBot UI] Failed to sync parent plugin config cache:', e)
271
- return false
272
- }
273
- }
274
-
275
- async function fetchDevices() {
276
- try {
277
- const resp = await homebridge.request('/devices', {})
278
- if (!resp || resp.success === false) throw new Error(resp?.data?.message || 'request failed')
279
- return resp.data || []
280
- } catch (e) {
281
- console.error(e)
282
- return []
283
- }
284
- }
285
-
286
- async function discoverDevices() {
287
- const btn = document.getElementById('discoverBtn')
288
- const status = document.getElementById('discoverStatus')
289
- const list = document.getElementById('discoveredList')
290
- const ul = document.getElementById('discoveredDevices')
291
- const autoAddAll = document.getElementById('autoAddAllCheckbox').checked
292
-
293
- try {
294
- btn.disabled = true
295
- btn.textContent = '🔍 Discovering...'
296
- status.textContent = 'Searching SwitchBot account...'
297
- status.classList.remove('error')
298
-
299
- const resp = await homebridge.request('/discover', {})
300
- console.log('Discover response:', resp)
301
-
302
- if (!resp || resp.success === false) {
303
- throw new Error(resp?.data?.message || 'Discovery failed')
304
- }
305
-
306
- const devicesRaw = resp.data || []
307
- const devices = devicesRaw.filter((d, index, arr) =>
308
- !!d?.id && arr.findIndex((x) => x?.id === d.id) === index,
309
- )
310
-
311
- if (!devices.length) {
312
- status.textContent = 'No devices found in your SwitchBot account'
313
- list.style.display = 'none'
314
- return
315
- }
316
-
317
- // If auto-add is enabled, add all devices immediately using bulk endpoint
318
- if (autoAddAll) {
319
- status.textContent = `Auto-adding ${devices.length} device(s)...`
320
-
321
- try {
322
- const bulkResult = await homebridge.request('/add-devices', {
323
- devices: devices.map(d => ({
324
- deviceId: d.id,
325
- name: d.name,
326
- type: d.type,
327
- }))
328
- })
329
-
330
- console.log('Bulk add response:', bulkResult)
331
-
332
- if (!bulkResult || bulkResult.success === false) {
333
- throw new Error(bulkResult?.data?.message || 'Bulk add failed')
334
- }
335
-
336
- const addedCount = bulkResult?.data?.addedCount || 0
337
- const skippedCount = bulkResult?.data?.skippedCount || 0
338
- status.textContent = `✓ Added ${addedCount} device(s)` + (skippedCount > 0 ? ` (${skippedCount} skipped)` : '')
339
- status.classList.remove('error')
340
- list.style.display = 'none'
341
-
342
- // Sync parent Homebridge form cache to allow SAVE without full page reload
343
- if (addedCount > 0) {
344
- const synced = await syncParentPluginConfigFromDisk()
345
- status.textContent += synced
346
- ? ' - Homebridge form synced. You can click SAVE.'
347
- : ' - Sync pending: close/reopen settings before SAVE.'
348
- }
349
- } catch (e) {
350
- console.error('Bulk add error:', e)
351
- status.textContent = '✗ Error: ' + (e?.message || 'Failed to add devices')
352
- status.classList.add('error')
353
- }
354
-
355
- // Refresh the configured devices list
356
- await loadConfiguredDevices()
357
- return
358
- }
359
-
360
- status.textContent = `Found ${devices.length} device(s)`
361
- ul.innerHTML = ''
362
-
363
- for (const d of devices) {
364
- const li = document.createElement('li')
365
- li.className = 'device-item'
366
-
367
- const info = document.createElement('div')
368
- info.style.flex = '1'
369
- const name = document.createElement('div')
370
- name.style.fontWeight = '500'
371
- name.textContent = d.name || d.id
372
- const details = document.createElement('div')
373
- details.style.fontSize = '12px'
374
- details.style.opacity = '0.75'
375
- details.textContent = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
376
- info.appendChild(name)
377
- info.appendChild(details)
378
-
379
- const addBtn = document.createElement('button')
380
- addBtn.textContent = '+ Add'
381
- addBtn.style.marginLeft = '8px'
382
- addBtn.onclick = async () => {
383
- await addDeviceToConfig(d)
384
- }
385
-
386
- li.appendChild(info)
387
- li.appendChild(addBtn)
388
- ul.appendChild(li)
389
- }
390
-
391
- list.style.display = 'block'
392
- } catch (e) {
393
- console.error('Discovery error:', e)
394
- status.textContent = 'Error: ' + (e?.message || 'Discovery failed')
395
- status.classList.add('error')
396
- list.style.display = 'none'
397
- } finally {
398
- btn.disabled = false
399
- btn.textContent = '🔍 Discover Devices'
400
- }
401
- }
402
-
403
- async function addDeviceToConfig(device, options = {}) {
404
- const { refresh = true, showStatus = true } = options
405
- try {
406
- console.log('Adding device to config:', device)
407
-
408
- const resp = await homebridge.request('/add-device', {
409
- deviceId: device.id,
410
- name: device.name,
411
- type: device.type,
412
- })
413
-
414
- console.log('Add device response:', resp)
415
-
416
- if (!resp || resp.success === false) {
417
- throw new Error(resp?.data?.message || 'Failed to add device')
418
- }
419
-
420
- const alreadyExists = !!resp.data?.alreadyExists
421
- const message = resp.data?.message || (alreadyExists
422
- ? `Device "${device.name}" already in config`
423
- : `Device "${device.name}" added successfully!`)
424
-
425
- if (showStatus) {
426
- const status = document.getElementById('discoverStatus')
427
- status.textContent = (alreadyExists ? '• ' : '✓ ') + message
428
- status.classList.remove('error')
429
- status.classList.add('success-msg')
430
-
431
- // Sync parent Homebridge form cache to allow SAVE without full page reload
432
- if (!alreadyExists) {
433
- const synced = await syncParentPluginConfigFromDisk()
434
- status.textContent += synced
435
- ? ' - Homebridge form synced. You can click SAVE.'
436
- : ' - Sync pending: close/reopen settings before SAVE.'
437
- }
438
- }
439
-
440
- if (refresh) {
441
- await loadConfiguredDevices()
442
- }
443
-
444
- return { added: !alreadyExists }
445
- } catch (e) {
446
- console.error('Add device error:', e)
447
- const status = document.getElementById('discoverStatus')
448
- status.textContent = '✗ Error: ' + (e?.message || 'Failed to add device')
449
- status.classList.add('error')
450
- return { added: false }
451
- }
452
- }
453
-
454
- async function loadConfiguredDevices() {
455
- const list = await fetchDevices()
456
- render(list)
457
- }
458
-
459
- async function deleteDevice(deviceId, deviceName) {
460
- if (!confirm(`Are you sure you want to remove "${deviceName || deviceId}"?`)) {
461
- return
462
- }
463
-
464
- try {
465
- const resp = await homebridge.request('/delete-device', { deviceId })
466
-
467
- if (!resp || resp.success === false) {
468
- throw new Error(resp?.data?.message || 'Failed to delete device')
469
- }
470
-
471
- await syncParentPluginConfigFromDisk()
472
-
473
- // Refresh device list
474
- const list = await fetchDevices()
475
- render(list)
476
- } catch (e) {
477
- console.error('Delete error:', e)
478
- alert('Error: ' + (e?.message || 'Failed to delete device'))
479
- }
480
- }
481
-
482
- // Complete list of SwitchBot API device types (from official API documentation)
483
- const DEVICE_TYPES = {
484
- 'Window Coverings': ['Blind Tilt', 'Curtain', 'Curtain3', 'Roller Shade'],
485
- 'Locks & Access': ['Keypad', 'Keypad Touch', 'Keypad Vision', 'Keypad Vision Pro', 'Lock Lite', 'Smart Lock', 'Smart Lock Pro', 'Smart Lock Ultra', 'Video Doorbell'],
486
- 'Sensors': ['Contact Sensor', 'Motion Sensor', 'Presence Sensor', 'Water Detector'],
487
- 'Lighting': ['Candle Warmer Lamp', 'Ceiling Light', 'Ceiling Light Pro', 'Color Bulb', 'Floor Lamp', 'RGBIC Neon Rope Light', 'RGBIC Neon Wire Rope Light', 'RGBICWW Floor Lamp', 'RGBICWW Strip Light', 'Strip Light', 'Strip Light 3'],
488
- 'Climate Control': ['Air Purifier PM2.5', 'Air Purifier Table PM2.5', 'Air Purifier VOC', 'Air Purifier Table VOC', 'Battery Circulator Fan', 'Circulator Fan', 'Humidifier', 'Humidifier2', 'Meter', 'MeterPlus', 'MeterPro', 'MeterPro(CO2)', 'Smart Radiator Thermostat', 'Standing Circulator Fan', 'WoIOSensor'],
489
- 'Plugs & Switches': ['Garage Door Opener', 'Plug', 'Plug Mini (EU)', 'Plug Mini (JP)', 'Plug Mini (US)', 'Relay Switch 1', 'Relay Switch 1PM', 'Relay Switch 2PM'],
490
- 'Robot Vacuums': ['K10+', 'K10+ Pro', 'Robot Vacuum Cleaner K10+ Pro Combo', 'Robot Vacuum Cleaner K11+', 'Robot Vacuum Cleaner K20 Plus Pro', 'Robot Vacuum Cleaner S1', 'Robot Vacuum Cleaner S1 Plus', 'Robot Vacuum Cleaner S10', 'Robot Vacuum Cleaner S20'],
491
- 'Hubs': ['AI Hub', 'Hub', 'Hub 2', 'Hub 3', 'Hub Mini', 'Hub Plus'],
492
- 'Cameras': ['Indoor Cam', 'Pan/Tilt Cam', 'Pan/Tilt Cam 2K', 'Pan/Tilt Cam Plus 2K', 'Pan/Tilt Cam Plus 3K'],
493
- 'Other Devices': ['AI Art Frame', 'Bot', 'Home Climate Panel', 'Remote']
494
- }
495
-
496
- async function editDevice(device) {
497
- // Create modal dialog for editing
498
- const div = document.createElement('div')
499
- div.style.position = 'fixed'
500
- div.style.top = '0'
501
- div.style.left = '0'
502
- div.style.width = '100%'
503
- div.style.height = '100%'
504
- div.style.background = 'rgba(0,0,0,0.7)'
505
- div.style.display = 'flex'
506
- div.style.alignItems = 'center'
507
- div.style.justifyContent = 'center'
508
- div.style.zIndex = '9999'
509
-
510
- const modal = document.createElement('div')
511
- modal.style.background = getComputedStyle(document.body).backgroundColor
512
- modal.style.color = getComputedStyle(document.body).color
513
- modal.style.padding = '24px'
514
- modal.style.borderRadius = '8px'
515
- modal.style.minWidth = '400px'
516
- modal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.3)'
517
-
518
- const title = document.createElement('h3')
519
- title.textContent = 'Edit Device'
520
- title.style.marginTop = '0'
521
-
522
- const nameLabel = document.createElement('label')
523
- nameLabel.textContent = 'Device Name'
524
- nameLabel.style.display = 'block'
525
- nameLabel.style.marginBottom = '8px'
526
- nameLabel.style.fontWeight = '500'
527
-
528
- const nameInput = document.createElement('input')
529
- nameInput.type = 'text'
530
- nameInput.value = device.name || device.id
531
- nameInput.style.width = '100%'
532
- nameInput.style.marginBottom = '16px'
533
-
534
- // Read-only API device type field
535
- const apiTypeLabel = document.createElement('label')
536
- apiTypeLabel.textContent = 'Device Type (API - Read Only)'
537
- apiTypeLabel.style.display = 'block'
538
- apiTypeLabel.style.marginBottom = '8px'
539
- apiTypeLabel.style.fontWeight = '500'
540
-
541
- const apiTypeInput = document.createElement('input')
542
- apiTypeInput.type = 'text'
543
- apiTypeInput.value = device.deviceType || device.type || 'Unknown'
544
- apiTypeInput.readOnly = true
545
- apiTypeInput.style.width = '100%'
546
- apiTypeInput.style.marginBottom = '16px'
547
- apiTypeInput.style.opacity = '0.6'
548
- apiTypeInput.style.cursor = 'not-allowed'
549
-
550
- // Editable config device type dropdown
551
- const typeLabel = document.createElement('label')
552
- typeLabel.textContent = 'Config Device Type (Override)'
553
- typeLabel.style.display = 'block'
554
- typeLabel.style.marginBottom = '8px'
555
- typeLabel.style.fontWeight = '500'
556
-
557
- const typeSelect = document.createElement('select')
558
- typeSelect.style.width = '100%'
559
- typeSelect.style.padding = '8px'
560
- typeSelect.style.marginBottom = '16px'
561
- typeSelect.style.borderRadius = '4px'
562
- typeSelect.style.background = getComputedStyle(nameInput).background
563
- typeSelect.style.color = getComputedStyle(nameInput).color
564
- typeSelect.style.border = getComputedStyle(nameInput).border
565
-
566
- // Add option groups with all API device types
567
- Object.keys(DEVICE_TYPES).forEach(categoryName => {
568
- const optgroup = document.createElement('optgroup')
569
- optgroup.label = categoryName
570
-
571
- DEVICE_TYPES[categoryName].forEach(deviceType => {
572
- const opt = document.createElement('option')
573
- opt.value = deviceType
574
- opt.text = deviceType
575
- // Select current configDeviceType if set, otherwise match API deviceType
576
- const currentType = device.configDeviceType || device.deviceType || device.type || ''
577
- opt.selected = currentType === deviceType
578
- optgroup.appendChild(opt)
579
- })
580
-
581
- typeSelect.appendChild(optgroup)
582
- })
583
-
584
- const buttons = document.createElement('div')
585
- buttons.style.display = 'flex'
586
- buttons.style.gap = '8px'
587
- buttons.style.justifyContent = 'flex-end'
588
-
589
- const cancelBtn = document.createElement('button')
590
- cancelBtn.textContent = 'Cancel'
591
- cancelBtn.style.background = '#666'
592
- cancelBtn.onclick = () => div.remove()
593
-
594
- const saveBtn = document.createElement('button')
595
- saveBtn.textContent = 'Save'
596
- saveBtn.onclick = async () => {
597
- try {
598
- const params = {
599
- deviceId: device.id,
600
- configDeviceName: nameInput.value || undefined,
601
- configDeviceType: typeSelect.value,
602
- }
603
- console.log('[Edit Device] Sending update request with params:', params)
604
-
605
- const resp = await homebridge.request('/update-device', params)
606
-
607
- console.log('[Edit Device] Update response:', resp)
608
-
609
- if (!resp || resp.success === false) {
610
- throw new Error(resp?.data?.message || 'Failed to update device')
611
- }
612
-
613
- await syncParentPluginConfigFromDisk()
614
-
615
- // Refresh device list
616
- console.log('[Edit Device] Refreshing device list after update')
617
- const list = await fetchDevices()
618
- render(list)
619
- div.remove()
620
- } catch (e) {
621
- console.error('Update error:', e)
622
- alert('Error: ' + (e?.message || 'Failed to update device'))
623
- }
624
- }
625
-
626
- buttons.appendChild(cancelBtn)
627
- buttons.appendChild(saveBtn)
628
-
629
- modal.appendChild(title)
630
- modal.appendChild(nameLabel)
631
- modal.appendChild(nameInput)
632
- modal.appendChild(apiTypeLabel)
633
- modal.appendChild(apiTypeInput)
634
- modal.appendChild(typeLabel)
635
- modal.appendChild(typeSelect)
636
- modal.appendChild(buttons)
637
-
638
- div.appendChild(modal)
639
- document.body.appendChild(div)
640
- nameInput.focus()
641
- }
642
-
643
- function render(list) {
644
- const ul = document.getElementById('devices')
645
- const status = document.getElementById('status')
646
- if (!list.length) {
647
- status.textContent = 'No devices found in config.'
648
- ul.innerHTML = ''
649
- return
650
- }
651
- status.textContent = `Found ${list.length} device(s)`
652
- ul.innerHTML = ''
653
- for (const d of list) {
654
- const li = document.createElement('li')
655
- li.className = 'device-item'
656
-
657
- const info = document.createElement('div')
658
- info.style.flex = '1'
659
-
660
- const name = document.createElement('div')
661
- name.style.fontWeight = '500'
662
- name.textContent = d.name || d.id
663
-
664
- const code = document.createElement('code')
665
- code.textContent = d.id
666
- code.style.fontSize = '12px'
667
- code.style.opacity = '0.75'
668
- code.style.marginLeft = '8px'
669
-
670
- const meta = document.createElement('div')
671
- meta.style.opacity = '0.75'
672
- meta.style.marginTop = '4px'
673
- meta.style.fontSize = '12px'
674
- const typeText = d.type ? `type: ${d.type}` : ''
675
- const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
676
- const roomText = d.room ? `room: ${d.room}` : ''
677
- meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
678
-
679
- info.appendChild(name)
680
- info.appendChild(code)
681
- info.appendChild(meta)
682
-
683
- const buttons = document.createElement('div')
684
- buttons.style.display = 'flex'
685
- buttons.style.gap = '8px'
686
-
687
- const editBtn = document.createElement('button')
688
- editBtn.textContent = '✏️ Edit'
689
- editBtn.style.padding = '6px 12px'
690
- editBtn.style.fontSize = '12px'
691
- editBtn.onclick = async () => {
692
- await editDevice(d)
693
- }
694
-
695
- const copyBtn = document.createElement('button')
696
- copyBtn.textContent = 'Copy ID'
697
- copyBtn.style.padding = '6px 12px'
698
- copyBtn.style.fontSize = '12px'
699
- copyBtn.addEventListener('click', async () => {
700
- try {
701
- await navigator.clipboard.writeText(d.id)
702
- copyBtn.textContent = 'Copied'
703
- copyBtn.classList.add('success')
704
- setTimeout(() => {
705
- copyBtn.textContent = 'Copy ID'
706
- copyBtn.classList.remove('success')
707
- }, 1200)
708
- } catch (e) {
709
- alert('Failed to copy')
710
- }
711
- })
712
-
713
- const deleteBtn = document.createElement('button')
714
- deleteBtn.textContent = '🗑️ Delete'
715
- deleteBtn.style.padding = '6px 12px'
716
- deleteBtn.style.fontSize = '12px'
717
- deleteBtn.style.background = '#ef4444'
718
- deleteBtn.onclick = async () => {
719
- await deleteDevice(d.id, d.name || d.id)
720
- }
721
-
722
- buttons.appendChild(editBtn)
723
- buttons.appendChild(copyBtn)
724
- buttons.appendChild(deleteBtn)
725
-
726
- li.appendChild(info)
727
- li.appendChild(buttons)
728
- ul.appendChild(li)
729
- }
730
- }
731
-
732
- (async () => {
733
- await loadCredentialStatus()
734
- const list = await fetchDevices()
735
- render(list)
736
- })()
737
- </script>
257
+ <script type="module" src="js/app.js"></script>
738
258
  </body>
739
259
  </html>