@switchbot/homebridge-switchbot 5.0.0-beta.107 → 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 -588
  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 -588
  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,627 +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
- const DEVICE_TYPES = [
483
- 'bot', 'curtain', 'fan', 'light', 'lightstrip', 'motion', 'contact',
484
- 'vacuum', 'lock', 'humidifier', 'temperature', 'relay', 'plug',
485
- 'wosweeper', 'blindtilt', 'curtain3', 'rollershade', 'meter',
486
- 'waterdetector', 'walletfinder', 'unknown'
487
- ]
488
-
489
- async function editDevice(device) {
490
- // Create modal dialog for editing
491
- const div = document.createElement('div')
492
- div.style.position = 'fixed'
493
- div.style.top = '0'
494
- div.style.left = '0'
495
- div.style.width = '100%'
496
- div.style.height = '100%'
497
- div.style.background = 'rgba(0,0,0,0.7)'
498
- div.style.display = 'flex'
499
- div.style.alignItems = 'center'
500
- div.style.justifyContent = 'center'
501
- div.style.zIndex = '9999'
502
-
503
- const modal = document.createElement('div')
504
- modal.style.background = getComputedStyle(document.body).backgroundColor
505
- modal.style.color = getComputedStyle(document.body).color
506
- modal.style.padding = '24px'
507
- modal.style.borderRadius = '8px'
508
- modal.style.minWidth = '400px'
509
- modal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.3)'
510
-
511
- const title = document.createElement('h3')
512
- title.textContent = 'Edit Device'
513
- title.style.marginTop = '0'
514
-
515
- const nameLabel = document.createElement('label')
516
- nameLabel.textContent = 'Device Name'
517
- nameLabel.style.display = 'block'
518
- nameLabel.style.marginBottom = '8px'
519
- nameLabel.style.fontWeight = '500'
520
-
521
- const nameInput = document.createElement('input')
522
- nameInput.type = 'text'
523
- nameInput.value = device.name || device.id
524
- nameInput.style.width = '100%'
525
- nameInput.style.marginBottom = '16px'
526
-
527
- const typeLabel = document.createElement('label')
528
- typeLabel.textContent = 'Device Type'
529
- typeLabel.style.display = 'block'
530
- typeLabel.style.marginBottom = '8px'
531
- typeLabel.style.fontWeight = '500'
532
-
533
- const typeSelect = document.createElement('select')
534
- typeSelect.style.width = '100%'
535
- typeSelect.style.padding = '8px'
536
- typeSelect.style.marginBottom = '16px'
537
- typeSelect.style.borderRadius = '4px'
538
- typeSelect.style.background = getComputedStyle(nameInput).background
539
- typeSelect.style.color = getComputedStyle(nameInput).color
540
- typeSelect.style.border = getComputedStyle(nameInput).border
541
-
542
- DEVICE_TYPES.forEach(t => {
543
- const opt = document.createElement('option')
544
- opt.value = t
545
- opt.text = t
546
- opt.selected = (device.type || 'unknown') === t
547
- typeSelect.appendChild(opt)
548
- })
549
-
550
- const buttons = document.createElement('div')
551
- buttons.style.display = 'flex'
552
- buttons.style.gap = '8px'
553
- buttons.style.justifyContent = 'flex-end'
554
-
555
- const cancelBtn = document.createElement('button')
556
- cancelBtn.textContent = 'Cancel'
557
- cancelBtn.style.background = '#666'
558
- cancelBtn.onclick = () => div.remove()
559
-
560
- const saveBtn = document.createElement('button')
561
- saveBtn.textContent = 'Save'
562
- saveBtn.onclick = async () => {
563
- try {
564
- const params = {
565
- deviceId: device.id,
566
- configDeviceName: nameInput.value || undefined,
567
- configDeviceType: typeSelect.value,
568
- }
569
- console.log('[Edit Device] Sending update request with params:', params)
570
-
571
- const resp = await homebridge.request('/update-device', params)
572
-
573
- console.log('[Edit Device] Update response:', resp)
574
-
575
- if (!resp || resp.success === false) {
576
- throw new Error(resp?.data?.message || 'Failed to update device')
577
- }
578
-
579
- await syncParentPluginConfigFromDisk()
580
-
581
- // Refresh device list
582
- console.log('[Edit Device] Refreshing device list after update')
583
- const list = await fetchDevices()
584
- render(list)
585
- div.remove()
586
- } catch (e) {
587
- console.error('Update error:', e)
588
- alert('Error: ' + (e?.message || 'Failed to update device'))
589
- }
590
- }
591
-
592
- buttons.appendChild(cancelBtn)
593
- buttons.appendChild(saveBtn)
594
-
595
- modal.appendChild(title)
596
- modal.appendChild(nameLabel)
597
- modal.appendChild(nameInput)
598
- modal.appendChild(typeLabel)
599
- modal.appendChild(typeSelect)
600
- modal.appendChild(buttons)
601
-
602
- div.appendChild(modal)
603
- document.body.appendChild(div)
604
- nameInput.focus()
605
- }
606
-
607
- function render(list) {
608
- const ul = document.getElementById('devices')
609
- const status = document.getElementById('status')
610
- if (!list.length) {
611
- status.textContent = 'No devices found in config.'
612
- ul.innerHTML = ''
613
- return
614
- }
615
- status.textContent = `Found ${list.length} device(s)`
616
- ul.innerHTML = ''
617
- for (const d of list) {
618
- const li = document.createElement('li')
619
- li.className = 'device-item'
620
-
621
- const info = document.createElement('div')
622
- info.style.flex = '1'
623
-
624
- const name = document.createElement('div')
625
- name.style.fontWeight = '500'
626
- name.textContent = d.name || d.id
627
-
628
- const code = document.createElement('code')
629
- code.textContent = d.id
630
- code.style.fontSize = '12px'
631
- code.style.opacity = '0.75'
632
- code.style.marginLeft = '8px'
633
-
634
- const meta = document.createElement('div')
635
- meta.style.opacity = '0.75'
636
- meta.style.marginTop = '4px'
637
- meta.style.fontSize = '12px'
638
- const typeText = d.type ? `type: ${d.type}` : ''
639
- const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
640
- const roomText = d.room ? `room: ${d.room}` : ''
641
- meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
642
-
643
- info.appendChild(name)
644
- info.appendChild(code)
645
- info.appendChild(meta)
646
-
647
- const buttons = document.createElement('div')
648
- buttons.style.display = 'flex'
649
- buttons.style.gap = '8px'
650
-
651
- const editBtn = document.createElement('button')
652
- editBtn.textContent = '✏️ Edit'
653
- editBtn.style.padding = '6px 12px'
654
- editBtn.style.fontSize = '12px'
655
- editBtn.onclick = async () => {
656
- await editDevice(d)
657
- }
658
-
659
- const copyBtn = document.createElement('button')
660
- copyBtn.textContent = 'Copy ID'
661
- copyBtn.style.padding = '6px 12px'
662
- copyBtn.style.fontSize = '12px'
663
- copyBtn.addEventListener('click', async () => {
664
- try {
665
- await navigator.clipboard.writeText(d.id)
666
- copyBtn.textContent = 'Copied'
667
- copyBtn.classList.add('success')
668
- setTimeout(() => {
669
- copyBtn.textContent = 'Copy ID'
670
- copyBtn.classList.remove('success')
671
- }, 1200)
672
- } catch (e) {
673
- alert('Failed to copy')
674
- }
675
- })
676
-
677
- const deleteBtn = document.createElement('button')
678
- deleteBtn.textContent = '🗑️ Delete'
679
- deleteBtn.style.padding = '6px 12px'
680
- deleteBtn.style.fontSize = '12px'
681
- deleteBtn.style.background = '#ef4444'
682
- deleteBtn.onclick = async () => {
683
- await deleteDevice(d.id, d.name || d.id)
684
- }
685
-
686
- buttons.appendChild(editBtn)
687
- buttons.appendChild(copyBtn)
688
- buttons.appendChild(deleteBtn)
689
-
690
- li.appendChild(info)
691
- li.appendChild(buttons)
692
- ul.appendChild(li)
693
- }
694
- }
695
-
696
- (async () => {
697
- await loadCredentialStatus()
698
- const list = await fetchDevices()
699
- render(list)
700
- })()
701
- </script>
257
+ <script type="module" src="js/app.js"></script>
702
258
  </body>
703
259
  </html>