@switchbot/homebridge-switchbot 5.0.0-beta.111 → 5.0.0-beta.113
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/MIGRATION.md +10 -0
- package/README.md +43 -0
- package/TODO.md +263 -0
- package/config.schema.json +27 -10
- package/dist/device-types.d.ts.map +1 -1
- package/dist/device-types.js +93 -63
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -33
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +12 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +121 -1
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +80 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.js +73 -41
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +209 -16
- package/dist/homebridge-ui/public/index.html +87 -56
- package/dist/homebridge-ui/public/js/api.d.ts +24 -1
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +33 -2
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +52 -3
- package/dist/homebridge-ui/public/js/app.js +1604 -143
- package/dist/homebridge-ui/public/js/app.js.map +3 -3
- package/dist/homebridge-ui/public/js/app.ts +2 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +2 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +13 -1
- package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +1244 -124
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1344 -142
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +42 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +50 -1
- package/dist/homebridge-ui/public/js/render.d.ts +3 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +389 -36
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +421 -38
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +3 -4
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +51 -5
- package/dist/switchbotClient.js.map +1 -1
- package/docs/assets/highlight.css +9 -2
- package/docs/index.html +28 -2
- package/docs/variables/default.html +1 -1
- package/package.json +9 -8
- package/src/device-types.ts +95 -64
- package/src/deviceFactory.ts +55 -33
- package/src/devices/genericDevice.ts +113 -1
- package/src/errors.ts +43 -0
- package/src/homebridge-ui/endpoints/devices.ts +82 -1
- package/src/homebridge-ui/endpoints/discovery.ts +78 -44
- package/src/homebridge-ui/public/css/styles.css +209 -16
- package/src/homebridge-ui/public/index.html +87 -56
- package/src/homebridge-ui/public/js/api.ts +52 -3
- package/src/homebridge-ui/public/js/app.ts +2 -1
- package/src/homebridge-ui/public/js/devices.ts +13 -1
- package/src/homebridge-ui/public/js/discovery.ts +1344 -142
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +421 -38
- package/src/platform.ts +3 -4
- package/src/switchbotClient.ts +44 -9
- package/test/e2e/run-e2e.spec.ts +12 -10
- package/test/helpers/matter-harness.ts +2 -2
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +313 -0
- package/todo/TODO.md +80 -66
- package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
|
@@ -176,11 +176,238 @@ function scrollToConfiguredDevice(deviceId: string): void {
|
|
|
176
176
|
}, 1800)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
function createConnectionTestControls(device: any): HTMLElement {
|
|
180
|
+
const controls = document.createElement('div')
|
|
181
|
+
controls.style.display = 'inline-flex'
|
|
182
|
+
controls.style.alignItems = 'center'
|
|
183
|
+
controls.style.gap = '6px'
|
|
184
|
+
|
|
185
|
+
const button = document.createElement('button')
|
|
186
|
+
button.textContent = 'Test Connection'
|
|
187
|
+
button.className = 'secondary'
|
|
188
|
+
button.style.padding = '4px 9px'
|
|
189
|
+
button.style.fontSize = '11px'
|
|
190
|
+
|
|
191
|
+
const status = document.createElement('span')
|
|
192
|
+
status.style.fontSize = '10px'
|
|
193
|
+
status.style.opacity = '0.85'
|
|
194
|
+
status.style.whiteSpace = 'normal'
|
|
195
|
+
status.style.overflowWrap = 'anywhere'
|
|
196
|
+
|
|
197
|
+
button.onclick = async () => {
|
|
198
|
+
const startedAt = Date.now()
|
|
199
|
+
button.disabled = true
|
|
200
|
+
button.textContent = 'Testing...'
|
|
201
|
+
status.textContent = 'Checking...'
|
|
202
|
+
status.style.color = '#6b7280'
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const { testDeviceConnection } = await import('./api.js')
|
|
206
|
+
const result = await testDeviceConnection({
|
|
207
|
+
deviceId: String(device?.id || device?.deviceId || ''),
|
|
208
|
+
connectionType: device?.connectionType,
|
|
209
|
+
address: device?.address,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const measuredLatency = Number(result?.latencyMs) > 0
|
|
213
|
+
? Number(result.latencyMs)
|
|
214
|
+
: Date.now() - startedAt
|
|
215
|
+
|
|
216
|
+
if (result?.success) {
|
|
217
|
+
const method = result?.method || 'Auto'
|
|
218
|
+
status.textContent = `✓ ${method} · ${measuredLatency}ms`
|
|
219
|
+
status.style.color = '#16a34a'
|
|
220
|
+
} else {
|
|
221
|
+
const detail = result?.message ? ` · ${result.message}` : ''
|
|
222
|
+
status.textContent = `✗ Failed · ${measuredLatency}ms${detail}`
|
|
223
|
+
status.style.color = '#dc2626'
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
status.textContent = `✗ Failed · ${Date.now() - startedAt}ms`
|
|
227
|
+
status.style.color = '#dc2626'
|
|
228
|
+
} finally {
|
|
229
|
+
button.disabled = false
|
|
230
|
+
button.textContent = 'Test Connection'
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
controls.appendChild(button)
|
|
235
|
+
controls.appendChild(status)
|
|
236
|
+
return controls
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function formatLastSeen(value: any): string {
|
|
240
|
+
if (!value) {
|
|
241
|
+
return 'N/A'
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const date = new Date(value)
|
|
245
|
+
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString()
|
|
246
|
+
} catch (_e) {
|
|
247
|
+
return String(value)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
252
|
+
const details = document.createElement('div')
|
|
253
|
+
details.style.cssText = `
|
|
254
|
+
border-top: 1px solid #ddd;
|
|
255
|
+
padding: 8px;
|
|
256
|
+
background: #f9fafb;
|
|
257
|
+
border-radius: 4px;
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
margin-top: 4px;
|
|
260
|
+
`
|
|
261
|
+
|
|
262
|
+
// --- Battery history trending ---
|
|
263
|
+
// Persist battery readings in localStorage per device
|
|
264
|
+
const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`
|
|
265
|
+
let batteryHistory: Array<{ value: number, ts: number }> = []
|
|
266
|
+
try {
|
|
267
|
+
const raw = localStorage.getItem(batteryHistoryKey)
|
|
268
|
+
if (raw) batteryHistory = JSON.parse(raw)
|
|
269
|
+
} catch {}
|
|
270
|
+
const now = Date.now()
|
|
271
|
+
if (typeof device?.battery === 'number') {
|
|
272
|
+
// Only add if different from last or >1h since last
|
|
273
|
+
const last = batteryHistory[batteryHistory.length - 1]
|
|
274
|
+
if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1000) {
|
|
275
|
+
batteryHistory.push({ value: device.battery, ts: now })
|
|
276
|
+
// Keep only last 30 entries (about a month if daily)
|
|
277
|
+
if (batteryHistory.length > 30) batteryHistory = batteryHistory.slice(-30)
|
|
278
|
+
try { localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory)) } catch {}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const rows: Array<{ label: string, value: string, copyable?: boolean }> = [
|
|
283
|
+
{ label: 'Device ID', value: String(device?.id || device?.deviceId || 'N/A'), copyable: !!(device?.id || device?.deviceId) },
|
|
284
|
+
{ label: 'MAC Address', value: String(device?.address || 'N/A'), copyable: !!device?.address },
|
|
285
|
+
{ label: 'Device Type', value: String(device?.type || device?.configDeviceType || 'N/A') },
|
|
286
|
+
{ label: 'Model', value: String(device?.model || 'N/A') },
|
|
287
|
+
{ label: 'Hub ID', value: String(device?.hubDeviceId || 'N/A') },
|
|
288
|
+
{ label: 'Battery', value: device?.battery !== undefined && device?.battery !== null ? `${device.battery}%` : 'N/A' },
|
|
289
|
+
{ label: 'Firmware', value: String(device?.version || device?.firmware || 'N/A') },
|
|
290
|
+
{ label: 'Cloud Service', value: device?.enabled === false ? 'Disabled' : 'Enabled' },
|
|
291
|
+
{ label: 'Last Seen', value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) },
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
const line = document.createElement('div')
|
|
297
|
+
line.style.display = 'flex'
|
|
298
|
+
line.style.alignItems = 'center'
|
|
299
|
+
line.style.justifyContent = 'space-between'
|
|
300
|
+
line.style.gap = '8px'
|
|
301
|
+
line.style.padding = '2px 0'
|
|
302
|
+
|
|
303
|
+
const label = document.createElement('span')
|
|
304
|
+
label.style.fontWeight = '600'
|
|
305
|
+
label.style.minWidth = '110px'
|
|
306
|
+
label.textContent = `${row.label}:`
|
|
307
|
+
|
|
308
|
+
const valueWrap = document.createElement('span')
|
|
309
|
+
valueWrap.style.display = 'inline-flex'
|
|
310
|
+
valueWrap.style.alignItems = 'center'
|
|
311
|
+
valueWrap.style.gap = '6px'
|
|
312
|
+
valueWrap.style.flex = '1'
|
|
313
|
+
valueWrap.style.justifyContent = 'flex-end'
|
|
314
|
+
valueWrap.style.minWidth = '0'
|
|
315
|
+
|
|
316
|
+
const value = document.createElement('span')
|
|
317
|
+
value.style.fontFamily = 'monospace'
|
|
318
|
+
value.style.fontSize = '11px'
|
|
319
|
+
value.style.opacity = '0.9'
|
|
320
|
+
value.style.whiteSpace = 'normal'
|
|
321
|
+
value.style.overflowWrap = 'anywhere'
|
|
322
|
+
value.style.wordBreak = 'break-word'
|
|
323
|
+
value.style.textAlign = 'right'
|
|
324
|
+
value.textContent = row.value
|
|
325
|
+
|
|
326
|
+
valueWrap.appendChild(value)
|
|
327
|
+
|
|
328
|
+
if (row.copyable && row.value && row.value !== 'N/A') {
|
|
329
|
+
const copyBtn = document.createElement('button')
|
|
330
|
+
copyBtn.textContent = '📋'
|
|
331
|
+
copyBtn.title = `Copy ${row.label}`
|
|
332
|
+
copyBtn.style.padding = '2px 6px'
|
|
333
|
+
copyBtn.style.fontSize = '10px'
|
|
334
|
+
copyBtn.style.lineHeight = '1'
|
|
335
|
+
copyBtn.style.background = '#e5e7eb'
|
|
336
|
+
copyBtn.style.color = '#111827'
|
|
337
|
+
|
|
338
|
+
copyBtn.onclick = async () => {
|
|
339
|
+
try {
|
|
340
|
+
await navigator.clipboard.writeText(row.value)
|
|
341
|
+
copyBtn.textContent = '✓'
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
copyBtn.textContent = '📋'
|
|
344
|
+
}, 1200)
|
|
345
|
+
} catch (_e) {
|
|
346
|
+
copyBtn.textContent = '!'
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
copyBtn.textContent = '📋'
|
|
349
|
+
}, 1200)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
valueWrap.appendChild(copyBtn)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
line.appendChild(label)
|
|
357
|
+
line.appendChild(valueWrap)
|
|
358
|
+
details.appendChild(line)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// --- Expose advanced/extra features dynamically ---
|
|
362
|
+
const featureKeys = [
|
|
363
|
+
'airQuality', 'pm25', 'pm10', 'voc', 'co2', 'humidity', 'temperature',
|
|
364
|
+
'preset', 'mode', 'presetMode', 'direction', 'calibration', 'multiCommand',
|
|
365
|
+
'extendedInfo', 'segmentedControl', 'features', 'capabilities', 'state',
|
|
366
|
+
]
|
|
367
|
+
const shown = new Set(rows.map(r => r.label.toLowerCase().replace(/\s/g, '')))
|
|
368
|
+
for (const key of featureKeys) {
|
|
369
|
+
if (device && device[key] !== undefined && !shown.has(key.toLowerCase())) {
|
|
370
|
+
const line = document.createElement('div')
|
|
371
|
+
line.style.display = 'flex'
|
|
372
|
+
line.style.alignItems = 'center'
|
|
373
|
+
line.style.justifyContent = 'space-between'
|
|
374
|
+
line.style.gap = '8px'
|
|
375
|
+
line.style.padding = '2px 0'
|
|
376
|
+
|
|
377
|
+
const label = document.createElement('span')
|
|
378
|
+
label.style.fontWeight = '600'
|
|
379
|
+
label.style.minWidth = '110px'
|
|
380
|
+
label.textContent = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()) + ':'
|
|
381
|
+
|
|
382
|
+
const value = document.createElement('span')
|
|
383
|
+
value.style.fontFamily = 'monospace'
|
|
384
|
+
value.style.fontSize = '11px'
|
|
385
|
+
value.style.opacity = '0.9'
|
|
386
|
+
value.style.whiteSpace = 'normal'
|
|
387
|
+
value.style.overflowWrap = 'anywhere'
|
|
388
|
+
value.style.wordBreak = 'break-word'
|
|
389
|
+
value.style.textAlign = 'right'
|
|
390
|
+
value.textContent = typeof device[key] === 'object' ? JSON.stringify(device[key]) : String(device[key])
|
|
391
|
+
|
|
392
|
+
line.appendChild(label)
|
|
393
|
+
line.appendChild(value)
|
|
394
|
+
details.appendChild(line)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return details
|
|
399
|
+
}
|
|
400
|
+
|
|
179
401
|
export async function renderDiscoveredDevices(
|
|
180
402
|
devices: any[],
|
|
181
|
-
options: {
|
|
403
|
+
options: {
|
|
404
|
+
configuredIds?: Set<string>
|
|
405
|
+
selectedIds?: Set<string>
|
|
406
|
+
onToggleSelect?: (device: any, selected: boolean) => void
|
|
407
|
+
} = {},
|
|
182
408
|
): Promise<HTMLElement> {
|
|
183
409
|
const ul = document.createElement('ul')
|
|
410
|
+
ul.className = 'device-grid'
|
|
184
411
|
ul.style.maxHeight = '400px'
|
|
185
412
|
ul.style.overflowY = 'auto'
|
|
186
413
|
ul.style.marginTop = '12px'
|
|
@@ -190,6 +417,8 @@ export async function renderDiscoveredDevices(
|
|
|
190
417
|
const { addDeviceToConfig } = await import('./discovery.js')
|
|
191
418
|
const { loadConfiguredDevices } = await import('./devices.js')
|
|
192
419
|
const configuredIds = options.configuredIds ?? new Set<string>()
|
|
420
|
+
const selectedIds = options.selectedIds ?? new Set<string>()
|
|
421
|
+
const onToggleSelect = options.onToggleSelect
|
|
193
422
|
|
|
194
423
|
for (const d of devices) {
|
|
195
424
|
const deviceId = normalizeId(d.id)
|
|
@@ -198,35 +427,106 @@ export async function renderDiscoveredDevices(
|
|
|
198
427
|
const li = document.createElement('li')
|
|
199
428
|
li.className = 'device-item'
|
|
200
429
|
li.style.display = 'flex'
|
|
201
|
-
li.style.
|
|
202
|
-
li.style.
|
|
203
|
-
li.style.
|
|
204
|
-
li.style.
|
|
205
|
-
li.style.
|
|
430
|
+
li.style.flexDirection = 'column'
|
|
431
|
+
li.style.alignItems = 'stretch'
|
|
432
|
+
li.style.justifyContent = 'flex-start'
|
|
433
|
+
li.style.padding = '5px 8px'
|
|
434
|
+
li.style.marginBottom = '0'
|
|
435
|
+
li.style.borderRadius = '5px'
|
|
206
436
|
li.style.transition = 'all 0.2s ease'
|
|
207
437
|
|
|
208
438
|
const info = document.createElement('div')
|
|
209
|
-
info.style.flex = '1'
|
|
439
|
+
info.style.flex = '1 1 auto'
|
|
440
|
+
info.style.width = '100%'
|
|
441
|
+
info.style.minWidth = '0'
|
|
210
442
|
|
|
211
443
|
const nameContainer = document.createElement('div')
|
|
212
444
|
nameContainer.style.display = 'flex'
|
|
213
445
|
nameContainer.style.alignItems = 'center'
|
|
214
|
-
nameContainer.style.marginBottom = '
|
|
446
|
+
nameContainer.style.marginBottom = '0'
|
|
215
447
|
nameContainer.style.flexWrap = 'wrap'
|
|
216
|
-
nameContainer.style.gap = '
|
|
448
|
+
nameContainer.style.gap = '4px'
|
|
217
449
|
|
|
218
450
|
const name = document.createElement('div')
|
|
219
451
|
name.style.fontWeight = '500'
|
|
220
|
-
name.style.fontSize = '
|
|
452
|
+
name.style.fontSize = '13px'
|
|
221
453
|
name.textContent = d.name || d.id
|
|
222
454
|
|
|
455
|
+
const selectCheckbox = document.createElement('input')
|
|
456
|
+
selectCheckbox.type = 'checkbox'
|
|
457
|
+
selectCheckbox.style.width = 'auto'
|
|
458
|
+
selectCheckbox.style.margin = '0 2px 0 0'
|
|
459
|
+
selectCheckbox.checked = selectedIds.has(deviceId)
|
|
460
|
+
if (alreadyAdded) {
|
|
461
|
+
selectCheckbox.disabled = true
|
|
462
|
+
selectCheckbox.title = 'Already configured'
|
|
463
|
+
}
|
|
464
|
+
selectCheckbox.onchange = () => {
|
|
465
|
+
onToggleSelect?.(d, selectCheckbox.checked)
|
|
466
|
+
// Notify listeners (e.g., batch buttons) of selection change
|
|
467
|
+
window.dispatchEvent(new CustomEvent('discovery-selection-changed'))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
nameContainer.appendChild(selectCheckbox)
|
|
471
|
+
|
|
472
|
+
|
|
223
473
|
nameContainer.appendChild(name)
|
|
224
474
|
|
|
475
|
+
// Show firmware update available indicator if present
|
|
476
|
+
if (d.firmwareUpdateAvailable) {
|
|
477
|
+
const fwBadge = document.createElement('span');
|
|
478
|
+
fwBadge.textContent = 'Update Available';
|
|
479
|
+
fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
480
|
+
fwBadge.title = 'A firmware update is available for this device.';
|
|
481
|
+
nameContainer.appendChild(fwBadge);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Show offline/unreachable indicator if device is offline
|
|
485
|
+
let offline = false;
|
|
486
|
+
let lastSeen = d.lastSeen || d.lastseen || d.updatedAt;
|
|
487
|
+
if (typeof d.offline === 'boolean') {
|
|
488
|
+
offline = d.offline;
|
|
489
|
+
} else if (lastSeen) {
|
|
490
|
+
try {
|
|
491
|
+
const last = new Date(lastSeen).getTime();
|
|
492
|
+
if (!isNaN(last) && Date.now() - last > 1000 * 60 * 60) { // 1 hour
|
|
493
|
+
offline = true;
|
|
494
|
+
}
|
|
495
|
+
} catch {}
|
|
496
|
+
}
|
|
497
|
+
if (offline) {
|
|
498
|
+
const offlineBadge = document.createElement('span');
|
|
499
|
+
offlineBadge.textContent = 'Offline';
|
|
500
|
+
offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
501
|
+
offlineBadge.title = 'Device is offline or unreachable.';
|
|
502
|
+
nameContainer.appendChild(offlineBadge);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const expandedDetails = document.createElement('div')
|
|
506
|
+
expandedDetails.style.display = 'none'
|
|
507
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d))
|
|
508
|
+
|
|
509
|
+
const expandBtn = document.createElement('button')
|
|
510
|
+
expandBtn.textContent = '▾'
|
|
511
|
+
expandBtn.title = 'Show details'
|
|
512
|
+
expandBtn.style.padding = '2px 6px'
|
|
513
|
+
expandBtn.style.fontSize = '11px'
|
|
514
|
+
expandBtn.style.marginLeft = '4px'
|
|
515
|
+
expandBtn.style.background = '#e5e7eb'
|
|
516
|
+
expandBtn.style.color = '#111827'
|
|
517
|
+
expandBtn.style.transition = 'transform 0.2s ease'
|
|
518
|
+
expandBtn.onclick = () => {
|
|
519
|
+
const isHidden = expandedDetails.style.display === 'none'
|
|
520
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none'
|
|
521
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
|
|
522
|
+
}
|
|
523
|
+
nameContainer.appendChild(expandBtn)
|
|
524
|
+
|
|
225
525
|
const duplicateBadge = document.createElement('span')
|
|
226
526
|
duplicateBadge.textContent = alreadyAdded ? '✓ Already Added' : '➕ New Device'
|
|
227
527
|
duplicateBadge.style.cssText = alreadyAdded
|
|
228
|
-
? 'background: #16a34a; color: white; padding:
|
|
229
|
-
: 'background: #2563eb; color: white; padding:
|
|
528
|
+
? 'background: #16a34a; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
|
|
529
|
+
: 'background: #2563eb; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
|
|
230
530
|
nameContainer.appendChild(duplicateBadge)
|
|
231
531
|
|
|
232
532
|
// Add connection type badge
|
|
@@ -246,11 +546,26 @@ export async function renderDiscoveredDevices(
|
|
|
246
546
|
nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
|
|
247
547
|
}
|
|
248
548
|
|
|
549
|
+
// Add battery warning indicator if battery < 20%
|
|
550
|
+
if (typeof d.battery === 'number' && d.battery < 20) {
|
|
551
|
+
const batteryWarn = document.createElement('span');
|
|
552
|
+
batteryWarn.textContent = `⚠️ ${d.battery}%`;
|
|
553
|
+
batteryWarn.style.cssText =
|
|
554
|
+
d.battery < 10
|
|
555
|
+
? 'background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
556
|
+
: 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
557
|
+
batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low';
|
|
558
|
+
nameContainer.appendChild(batteryWarn);
|
|
559
|
+
}
|
|
560
|
+
|
|
249
561
|
const details = document.createElement('div')
|
|
250
|
-
details.style.fontSize = '
|
|
562
|
+
details.style.fontSize = '10px'
|
|
251
563
|
details.style.opacity = '0.7'
|
|
252
|
-
details.style.marginTop = '
|
|
564
|
+
details.style.marginTop = '0'
|
|
253
565
|
details.style.fontFamily = 'monospace'
|
|
566
|
+
details.style.whiteSpace = 'normal'
|
|
567
|
+
details.style.overflowWrap = 'anywhere'
|
|
568
|
+
details.style.wordBreak = 'break-word'
|
|
254
569
|
|
|
255
570
|
let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
|
|
256
571
|
if (d.hubDeviceId) { detailsText += ` | Hub: ${d.hubDeviceId}` }
|
|
@@ -259,12 +574,14 @@ export async function renderDiscoveredDevices(
|
|
|
259
574
|
|
|
260
575
|
info.appendChild(nameContainer)
|
|
261
576
|
info.appendChild(details)
|
|
577
|
+
info.appendChild(expandedDetails)
|
|
262
578
|
|
|
263
579
|
const addBtn = document.createElement('button')
|
|
264
580
|
addBtn.textContent = alreadyAdded ? 'Already Added' : 'Add to Config'
|
|
265
|
-
addBtn.style.marginLeft = '
|
|
266
|
-
addBtn.style.
|
|
267
|
-
addBtn.style.
|
|
581
|
+
addBtn.style.marginLeft = '0'
|
|
582
|
+
addBtn.style.marginTop = '2px'
|
|
583
|
+
addBtn.style.padding = '4px 9px'
|
|
584
|
+
addBtn.style.fontSize = '11px'
|
|
268
585
|
addBtn.style.whiteSpace = 'nowrap'
|
|
269
586
|
addBtn.style.flexShrink = '0'
|
|
270
587
|
addBtn.disabled = alreadyAdded
|
|
@@ -284,26 +601,46 @@ export async function renderDiscoveredDevices(
|
|
|
284
601
|
const viewBtn = document.createElement('button')
|
|
285
602
|
viewBtn.textContent = 'View in Config'
|
|
286
603
|
viewBtn.className = 'secondary'
|
|
287
|
-
viewBtn.style.marginLeft = '
|
|
288
|
-
viewBtn.style.padding = '
|
|
289
|
-
viewBtn.style.fontSize = '
|
|
604
|
+
viewBtn.style.marginLeft = '0'
|
|
605
|
+
viewBtn.style.padding = '4px 9px'
|
|
606
|
+
viewBtn.style.fontSize = '11px'
|
|
290
607
|
viewBtn.onclick = async () => {
|
|
291
608
|
await loadConfiguredDevices()
|
|
292
609
|
scrollToConfiguredDevice(d.id)
|
|
293
610
|
}
|
|
294
611
|
li.appendChild(info)
|
|
295
612
|
const actions = document.createElement('div')
|
|
613
|
+
actions.className = 'device-actions'
|
|
296
614
|
actions.style.display = 'flex'
|
|
297
615
|
actions.style.alignItems = 'center'
|
|
616
|
+
actions.style.flexWrap = 'wrap'
|
|
617
|
+
actions.style.justifyContent = 'flex-start'
|
|
618
|
+
actions.style.marginLeft = '0'
|
|
619
|
+
actions.style.width = '100%'
|
|
620
|
+
actions.style.marginTop = '2px'
|
|
621
|
+
actions.style.gap = '5px'
|
|
298
622
|
actions.appendChild(viewBtn)
|
|
299
623
|
actions.appendChild(addBtn)
|
|
624
|
+
actions.appendChild(createConnectionTestControls(d))
|
|
300
625
|
li.appendChild(actions)
|
|
301
626
|
ul.appendChild(li)
|
|
302
627
|
continue
|
|
303
628
|
}
|
|
304
629
|
|
|
630
|
+
const actions = document.createElement('div')
|
|
631
|
+
actions.className = 'device-actions'
|
|
632
|
+
actions.style.display = 'flex'
|
|
633
|
+
actions.style.flexWrap = 'wrap'
|
|
634
|
+
actions.style.justifyContent = 'flex-start'
|
|
635
|
+
actions.style.marginLeft = '0'
|
|
636
|
+
actions.style.width = '100%'
|
|
637
|
+
actions.style.marginTop = '2px'
|
|
638
|
+
actions.style.gap = '5px'
|
|
639
|
+
actions.appendChild(addBtn)
|
|
640
|
+
actions.appendChild(createConnectionTestControls(d))
|
|
641
|
+
|
|
305
642
|
li.appendChild(info)
|
|
306
|
-
li.appendChild(
|
|
643
|
+
li.appendChild(actions)
|
|
307
644
|
ul.appendChild(li)
|
|
308
645
|
}
|
|
309
646
|
|
|
@@ -474,6 +811,7 @@ export function renderDeviceList(list: any[]): void {
|
|
|
474
811
|
}
|
|
475
812
|
|
|
476
813
|
status.textContent = `Found ${list.length} device(s)`
|
|
814
|
+
ul.classList.add('device-grid')
|
|
477
815
|
ul.innerHTML = ''
|
|
478
816
|
|
|
479
817
|
// Show remove all button when devices exist
|
|
@@ -485,27 +823,64 @@ export function renderDeviceList(list: any[]): void {
|
|
|
485
823
|
const li = document.createElement('li')
|
|
486
824
|
li.className = 'device-item'
|
|
487
825
|
li.setAttribute('data-device-id', normalizeId(d.id))
|
|
826
|
+
li.style.display = 'flex'
|
|
827
|
+
li.style.flexDirection = 'column'
|
|
828
|
+
li.style.alignItems = 'stretch'
|
|
829
|
+
li.style.marginBottom = '0'
|
|
488
830
|
|
|
489
831
|
const info = document.createElement('div')
|
|
490
|
-
info.style.flex = '1'
|
|
832
|
+
info.style.flex = '1 1 auto'
|
|
833
|
+
info.style.width = '100%'
|
|
834
|
+
info.style.minWidth = '0'
|
|
491
835
|
|
|
492
836
|
const nameContainer = document.createElement('div')
|
|
493
837
|
nameContainer.style.display = 'flex'
|
|
494
|
-
nameContainer.style.
|
|
495
|
-
nameContainer.style.
|
|
496
|
-
nameContainer.style.gap = '
|
|
838
|
+
nameContainer.style.flexDirection = 'column'
|
|
839
|
+
nameContainer.style.alignItems = 'flex-start'
|
|
840
|
+
nameContainer.style.gap = '0'
|
|
497
841
|
|
|
498
842
|
const name = document.createElement('div')
|
|
499
843
|
name.style.fontWeight = '500'
|
|
844
|
+
name.style.fontSize = '13px'
|
|
500
845
|
name.textContent = d.name || d.id
|
|
501
846
|
|
|
847
|
+
const expandedDetails = document.createElement('div')
|
|
848
|
+
expandedDetails.style.display = 'none'
|
|
849
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d))
|
|
850
|
+
|
|
851
|
+
const expandBtn = document.createElement('button')
|
|
852
|
+
expandBtn.textContent = '▾'
|
|
853
|
+
expandBtn.title = 'Show details'
|
|
854
|
+
expandBtn.style.padding = '2px 6px'
|
|
855
|
+
expandBtn.style.fontSize = '11px'
|
|
856
|
+
expandBtn.style.marginLeft = '4px'
|
|
857
|
+
expandBtn.style.background = '#e5e7eb'
|
|
858
|
+
expandBtn.style.color = '#111827'
|
|
859
|
+
expandBtn.style.transition = 'transform 0.2s ease'
|
|
860
|
+
expandBtn.onclick = () => {
|
|
861
|
+
const isHidden = expandedDetails.style.display === 'none'
|
|
862
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none'
|
|
863
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
|
|
864
|
+
}
|
|
865
|
+
|
|
502
866
|
const code = document.createElement('code')
|
|
503
867
|
code.textContent = d.id
|
|
504
|
-
code.style.fontSize = '
|
|
868
|
+
code.style.fontSize = '10px'
|
|
505
869
|
code.style.opacity = '0.75'
|
|
506
|
-
code.style.marginLeft = '
|
|
507
|
-
|
|
508
|
-
|
|
870
|
+
code.style.marginLeft = '0'
|
|
871
|
+
code.style.whiteSpace = 'normal'
|
|
872
|
+
code.style.overflowWrap = 'anywhere'
|
|
873
|
+
code.style.wordBreak = 'break-word'
|
|
874
|
+
code.style.maxWidth = '100%'
|
|
875
|
+
|
|
876
|
+
const headerRow = document.createElement('div')
|
|
877
|
+
headerRow.style.display = 'inline-flex'
|
|
878
|
+
headerRow.style.alignItems = 'center'
|
|
879
|
+
headerRow.style.gap = '4px'
|
|
880
|
+
headerRow.appendChild(name)
|
|
881
|
+
headerRow.appendChild(expandBtn)
|
|
882
|
+
|
|
883
|
+
nameContainer.appendChild(headerRow)
|
|
509
884
|
nameContainer.appendChild(code)
|
|
510
885
|
|
|
511
886
|
// Add signal strength visualization if RSSI is available
|
|
@@ -516,8 +891,8 @@ export function renderDeviceList(list: any[]): void {
|
|
|
516
891
|
|
|
517
892
|
const meta = document.createElement('div')
|
|
518
893
|
meta.style.opacity = '0.75'
|
|
519
|
-
meta.style.marginTop = '
|
|
520
|
-
meta.style.fontSize = '
|
|
894
|
+
meta.style.marginTop = '0'
|
|
895
|
+
meta.style.fontSize = '11px'
|
|
521
896
|
|
|
522
897
|
const typeText = d.type ? `type: ${d.type}` : ''
|
|
523
898
|
const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
|
|
@@ -526,15 +901,22 @@ export function renderDeviceList(list: any[]): void {
|
|
|
526
901
|
|
|
527
902
|
info.appendChild(nameContainer)
|
|
528
903
|
info.appendChild(meta)
|
|
904
|
+
info.appendChild(expandedDetails)
|
|
529
905
|
|
|
530
906
|
const buttons = document.createElement('div')
|
|
907
|
+
buttons.className = 'device-actions'
|
|
531
908
|
buttons.style.display = 'flex'
|
|
532
|
-
buttons.style.
|
|
909
|
+
buttons.style.flexWrap = 'wrap'
|
|
910
|
+
buttons.style.justifyContent = 'flex-start'
|
|
911
|
+
buttons.style.marginLeft = '0'
|
|
912
|
+
buttons.style.width = '100%'
|
|
913
|
+
buttons.style.marginTop = '2px'
|
|
914
|
+
buttons.style.gap = '5px'
|
|
533
915
|
|
|
534
916
|
const editBtn = document.createElement('button')
|
|
535
917
|
editBtn.textContent = '✏️ Edit'
|
|
536
|
-
editBtn.style.padding = '
|
|
537
|
-
editBtn.style.fontSize = '
|
|
918
|
+
editBtn.style.padding = '4px 9px'
|
|
919
|
+
editBtn.style.fontSize = '11px'
|
|
538
920
|
editBtn.onclick = async () => {
|
|
539
921
|
const { editDevice } = await import('./modals.js')
|
|
540
922
|
await editDevice(d)
|
|
@@ -542,8 +924,8 @@ export function renderDeviceList(list: any[]): void {
|
|
|
542
924
|
|
|
543
925
|
const copyBtn = document.createElement('button')
|
|
544
926
|
copyBtn.textContent = 'Copy ID'
|
|
545
|
-
copyBtn.style.padding = '
|
|
546
|
-
copyBtn.style.fontSize = '
|
|
927
|
+
copyBtn.style.padding = '4px 9px'
|
|
928
|
+
copyBtn.style.fontSize = '11px'
|
|
547
929
|
copyBtn.addEventListener('click', async () => {
|
|
548
930
|
try {
|
|
549
931
|
await navigator.clipboard.writeText(d.id)
|
|
@@ -560,8 +942,8 @@ export function renderDeviceList(list: any[]): void {
|
|
|
560
942
|
|
|
561
943
|
const deleteBtn = document.createElement('button')
|
|
562
944
|
deleteBtn.textContent = '🗑️ Delete'
|
|
563
|
-
deleteBtn.style.padding = '
|
|
564
|
-
deleteBtn.style.fontSize = '
|
|
945
|
+
deleteBtn.style.padding = '4px 9px'
|
|
946
|
+
deleteBtn.style.fontSize = '11px'
|
|
565
947
|
deleteBtn.style.background = '#ef4444'
|
|
566
948
|
deleteBtn.onclick = async () => {
|
|
567
949
|
const { deleteDeviceFromConfig } = await import('./devices-delete.js')
|
|
@@ -570,6 +952,7 @@ export function renderDeviceList(list: any[]): void {
|
|
|
570
952
|
|
|
571
953
|
buttons.appendChild(editBtn)
|
|
572
954
|
buttons.appendChild(copyBtn)
|
|
955
|
+
buttons.appendChild(createConnectionTestControls(d))
|
|
573
956
|
buttons.appendChild(deleteBtn)
|
|
574
957
|
|
|
575
958
|
li.appendChild(info)
|
package/src/platform.ts
CHANGED
|
@@ -65,7 +65,7 @@ const DEVICE_MATTER_SUPPORTED: Record<string, boolean> = {
|
|
|
65
65
|
// Default Matter cluster configurations by device type
|
|
66
66
|
// Maps device types to their Matter cluster states (used when device doesn't provide clusters)
|
|
67
67
|
// Note: wosweeper/curtain/plug variants are normalized before cluster lookup (see loadDevices)
|
|
68
|
-
const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
|
|
68
|
+
export const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
|
|
69
69
|
// Core devices - aligned with HAP service implementations
|
|
70
70
|
bot: { onOff: { onOff: false } }, // Switch → OnOff
|
|
71
71
|
vacuum: {
|
|
@@ -133,9 +133,6 @@ const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
|
|
|
133
133
|
maxLevel: 254,
|
|
134
134
|
},
|
|
135
135
|
colorControl: {
|
|
136
|
-
currentHue: 0,
|
|
137
|
-
currentSaturation: 0,
|
|
138
|
-
colorTemperatureMireds: 400,
|
|
139
136
|
colorMode: 0,
|
|
140
137
|
},
|
|
141
138
|
}, // Lightbulb with color → OnOff + LevelControl + ColorControl
|
|
@@ -1099,6 +1096,8 @@ export class SwitchBotHAPPlatform {
|
|
|
1099
1096
|
id: raw.deviceId ?? raw.id,
|
|
1100
1097
|
name: raw.configDeviceName ?? raw.name,
|
|
1101
1098
|
type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
|
|
1099
|
+
encryptionKey: raw.encryptionKey,
|
|
1100
|
+
keyId: raw.keyId,
|
|
1102
1101
|
_raw: raw,
|
|
1103
1102
|
}
|
|
1104
1103
|
|