@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.
- 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 -588
- 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 -588
- 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,627 +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
|
-
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>
|