@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.
- package/TODO.md +564 -0
- package/config.schema.json +100 -25
- package/dist/device-types.d.ts +15 -0
- package/dist/device-types.d.ts.map +1 -0
- package/dist/device-types.js +145 -0
- package/dist/device-types.js.map +1 -0
- package/dist/deviceFactory.d.ts +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -17
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +1 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +4 -2
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/endpoints/config.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/config.js +73 -0
- package/dist/homebridge-ui/endpoints/config.js.map +1 -0
- package/dist/homebridge-ui/endpoints/credentials.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/credentials.js +65 -0
- package/dist/homebridge-ui/endpoints/credentials.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.js +285 -0
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/endpoints/discovery.js +171 -0
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/index.html +144 -624
- package/dist/homebridge-ui/public/js/api.d.ts +15 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/api.js +164 -0
- package/dist/homebridge-ui/public/js/api.js.map +1 -0
- package/dist/homebridge-ui/public/js/api.ts +189 -0
- package/dist/homebridge-ui/public/js/app.d.ts +2 -0
- package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/app.js +1069 -0
- package/dist/homebridge-ui/public/js/app.js.map +7 -0
- package/dist/homebridge-ui/public/js/app.ts +21 -0
- package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
- package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/constants.js +2 -0
- package/dist/homebridge-ui/public/js/constants.js.map +1 -0
- package/dist/homebridge-ui/public/js/constants.ts +1 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.js +96 -0
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
- package/dist/homebridge-ui/public/js/credentials.ts +100 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts +2 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.js +99 -0
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.ts +113 -0
- package/dist/homebridge-ui/public/js/devices.d.ts +8 -0
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/devices.js +71 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -0
- package/dist/homebridge-ui/public/js/devices.ts +78 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts +3 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.js +107 -0
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
- package/dist/homebridge-ui/public/js/discovery.ts +123 -0
- package/dist/homebridge-ui/public/js/logger.d.ts +6 -0
- package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/logger.js +12 -0
- package/dist/homebridge-ui/public/js/logger.js.map +1 -0
- package/dist/homebridge-ui/public/js/logger.ts +11 -0
- package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
- package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modal.js +26 -0
- package/dist/homebridge-ui/public/js/modal.js.map +1 -0
- package/dist/homebridge-ui/public/js/modal.ts +28 -0
- package/dist/homebridge-ui/public/js/modals.d.ts +2 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/modals.js +132 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -0
- package/dist/homebridge-ui/public/js/modals.ts +153 -0
- package/dist/homebridge-ui/public/js/render.d.ts +6 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/render.js +166 -0
- package/dist/homebridge-ui/public/js/render.js.map +1 -0
- package/dist/homebridge-ui/public/js/render.ts +190 -0
- package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
- package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/toast.js +29 -0
- package/dist/homebridge-ui/public/js/toast.js.map +1 -0
- package/dist/homebridge-ui/public/js/toast.ts +37 -0
- package/dist/homebridge-ui/public/js/types.d.ts +23 -0
- package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/types.js +2 -0
- package/dist/homebridge-ui/public/js/types.js.map +1 -0
- package/dist/homebridge-ui/public/js/types.ts +26 -0
- package/dist/homebridge-ui/server.d.ts +1 -3
- package/dist/homebridge-ui/server.d.ts.map +1 -1
- package/dist/homebridge-ui/server.js +10 -561
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/config-parser.js +87 -0
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
- package/dist/homebridge-ui/utils/logger.d.ts +7 -0
- package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/logger.js +17 -0
- package/dist/homebridge-ui/utils/logger.js.map +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +6 -2
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +2 -0
- package/dist/switchbotClient.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/eslint.config.js +5 -0
- package/nodemon.json +2 -2
- package/package.json +20 -10
- package/scripts/build-ui.js +36 -0
- package/scripts/free-dev-ports.mjs +105 -0
- package/scripts/sync-device-types.mjs +28 -0
- package/src/device-types.ts +145 -0
- package/src/deviceFactory.ts +71 -31
- package/src/devices/genericDevice.ts +5 -2
- package/src/homebridge-ui/endpoints/config.ts +79 -0
- package/src/homebridge-ui/endpoints/credentials.ts +74 -0
- package/src/homebridge-ui/endpoints/devices.ts +321 -0
- package/src/homebridge-ui/endpoints/discovery.ts +191 -0
- package/src/homebridge-ui/public/index.html +144 -624
- package/src/homebridge-ui/public/js/api.ts +189 -0
- package/src/homebridge-ui/public/js/app.ts +21 -0
- package/src/homebridge-ui/public/js/constants.ts +1 -0
- package/src/homebridge-ui/public/js/credentials.ts +100 -0
- package/src/homebridge-ui/public/js/devices-delete.ts +113 -0
- package/src/homebridge-ui/public/js/devices.ts +78 -0
- package/src/homebridge-ui/public/js/discovery.ts +123 -0
- package/src/homebridge-ui/public/js/logger.ts +11 -0
- package/src/homebridge-ui/public/js/modal.ts +28 -0
- package/src/homebridge-ui/public/js/modals.ts +153 -0
- package/src/homebridge-ui/public/js/render.ts +190 -0
- package/src/homebridge-ui/public/js/toast.ts +37 -0
- package/src/homebridge-ui/public/js/types.ts +26 -0
- package/src/homebridge-ui/server.ts +11 -647
- package/src/homebridge-ui/utils/config-parser.ts +108 -0
- package/src/homebridge-ui/utils/logger.ts +17 -0
- package/src/platform.ts +6 -2
- package/src/switchbotClient.ts +2 -0
- 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 {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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>
|
|
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
|
|
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
|
|
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>
|
|
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
|
|
129
|
-
|
|
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
|
|
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
|
|
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>
|
|
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>
|