@switchbot/homebridge-switchbot 5.0.0-beta.112 → 5.0.0-beta.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/MIGRATION.md +10 -0
  2. package/README.md +43 -0
  3. package/TODO.md +263 -0
  4. package/config.schema.json +27 -10
  5. package/dist/device-types.d.ts.map +1 -1
  6. package/dist/device-types.js +93 -63
  7. package/dist/device-types.js.map +1 -1
  8. package/dist/deviceFactory.d.ts.map +1 -1
  9. package/dist/deviceFactory.js +55 -33
  10. package/dist/deviceFactory.js.map +1 -1
  11. package/dist/devices/genericDevice.d.ts +12 -1
  12. package/dist/devices/genericDevice.d.ts.map +1 -1
  13. package/dist/devices/genericDevice.js +121 -1
  14. package/dist/devices/genericDevice.js.map +1 -1
  15. package/dist/errors.d.ts +5 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +51 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  20. package/dist/homebridge-ui/endpoints/devices.js +80 -1
  21. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  22. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  23. package/dist/homebridge-ui/endpoints/discovery.js +73 -41
  24. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  25. package/dist/homebridge-ui/public/css/styles.css +98 -0
  26. package/dist/homebridge-ui/public/index.html +57 -0
  27. package/dist/homebridge-ui/public/js/api.d.ts +24 -1
  28. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  29. package/dist/homebridge-ui/public/js/api.js +33 -2
  30. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  31. package/dist/homebridge-ui/public/js/api.ts +52 -3
  32. package/dist/homebridge-ui/public/js/app.js +1560 -104
  33. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  34. package/dist/homebridge-ui/public/js/app.ts +2 -1
  35. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  36. package/dist/homebridge-ui/public/js/devices.js +2 -0
  37. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  38. package/dist/homebridge-ui/public/js/devices.ts +13 -1
  39. package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
  40. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/discovery.js +1223 -126
  42. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/discovery.ts +1323 -145
  44. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  45. package/dist/homebridge-ui/public/js/modals.js +42 -0
  46. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  47. package/dist/homebridge-ui/public/js/modals.ts +50 -1
  48. package/dist/homebridge-ui/public/js/render.d.ts +3 -0
  49. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  50. package/dist/homebridge-ui/public/js/render.js +373 -1
  51. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  52. package/dist/homebridge-ui/public/js/render.ts +405 -2
  53. package/dist/platform.d.ts +1 -0
  54. package/dist/platform.d.ts.map +1 -1
  55. package/dist/platform.js +7 -3
  56. package/dist/platform.js.map +1 -1
  57. package/dist/switchbotClient.d.ts.map +1 -1
  58. package/dist/switchbotClient.js +51 -5
  59. package/dist/switchbotClient.js.map +1 -1
  60. package/docs/assets/highlight.css +9 -2
  61. package/docs/index.html +28 -2
  62. package/docs/variables/default.html +1 -1
  63. package/package.json +2 -2
  64. package/src/device-types.ts +95 -64
  65. package/src/deviceFactory.ts +55 -33
  66. package/src/devices/genericDevice.ts +113 -1
  67. package/src/errors.ts +43 -0
  68. package/src/homebridge-ui/endpoints/devices.ts +82 -1
  69. package/src/homebridge-ui/endpoints/discovery.ts +78 -44
  70. package/src/homebridge-ui/public/css/styles.css +98 -0
  71. package/src/homebridge-ui/public/index.html +57 -0
  72. package/src/homebridge-ui/public/js/api.ts +52 -3
  73. package/src/homebridge-ui/public/js/app.ts +2 -1
  74. package/src/homebridge-ui/public/js/devices.ts +13 -1
  75. package/src/homebridge-ui/public/js/discovery.ts +1323 -145
  76. package/src/homebridge-ui/public/js/modals.ts +50 -1
  77. package/src/homebridge-ui/public/js/render.ts +405 -2
  78. package/src/platform.ts +7 -3
  79. package/src/switchbotClient.ts +44 -9
  80. package/test/matter-integration.spec.ts +21 -0
  81. package/todo/COMPLETED.md +300 -0
  82. package/todo/TODO.md +80 -66
@@ -176,9 +176,299 @@ 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
+
296
+ for (const row of rows) {
297
+ const line = document.createElement('div')
298
+ line.style.display = 'flex'
299
+ line.style.alignItems = 'center'
300
+ line.style.justifyContent = 'space-between'
301
+ line.style.gap = '8px'
302
+ line.style.padding = '2px 0'
303
+
304
+ const label = document.createElement('span')
305
+ label.style.fontWeight = '600'
306
+ label.style.minWidth = '110px'
307
+ label.textContent = `${row.label}:`
308
+
309
+ const valueWrap = document.createElement('span')
310
+ valueWrap.style.display = 'inline-flex'
311
+ valueWrap.style.alignItems = 'center'
312
+ valueWrap.style.gap = '6px'
313
+ valueWrap.style.flex = '1'
314
+ valueWrap.style.justifyContent = 'flex-end'
315
+ valueWrap.style.minWidth = '0'
316
+
317
+ const value = document.createElement('span')
318
+ value.style.fontFamily = 'monospace'
319
+ value.style.fontSize = '11px'
320
+ value.style.opacity = '0.9'
321
+ value.style.whiteSpace = 'normal'
322
+ value.style.overflowWrap = 'anywhere'
323
+ value.style.wordBreak = 'break-word'
324
+ value.style.textAlign = 'right'
325
+ value.textContent = row.value
326
+
327
+ valueWrap.appendChild(value)
328
+
329
+ if (row.copyable && row.value && row.value !== 'N/A') {
330
+ const copyBtn = document.createElement('button')
331
+ copyBtn.textContent = '📋'
332
+ copyBtn.title = `Copy ${row.label}`
333
+ copyBtn.style.padding = '2px 6px'
334
+ copyBtn.style.fontSize = '10px'
335
+ copyBtn.style.lineHeight = '1'
336
+ copyBtn.style.background = '#e5e7eb'
337
+ copyBtn.style.color = '#111827'
338
+
339
+ copyBtn.onclick = async () => {
340
+ try {
341
+ await navigator.clipboard.writeText(row.value)
342
+ copyBtn.textContent = '✓'
343
+ setTimeout(() => {
344
+ copyBtn.textContent = '📋'
345
+ }, 1200)
346
+ } catch (_e) {
347
+ copyBtn.textContent = '!'
348
+ setTimeout(() => {
349
+ copyBtn.textContent = '📋'
350
+ }, 1200)
351
+ }
352
+ }
353
+
354
+ valueWrap.appendChild(copyBtn)
355
+ }
356
+
357
+ line.appendChild(label)
358
+ line.appendChild(valueWrap)
359
+ details.appendChild(line)
360
+
361
+ // If this is the Battery row, add a sparkline chart below
362
+ if (row.label === 'Battery' && Array.isArray(batteryHistory) && batteryHistory.length > 1) {
363
+ const chart = document.createElement('div')
364
+ chart.style.margin = '2px 0 8px 0'
365
+ chart.style.width = '100%'
366
+ chart.style.height = '28px'
367
+ chart.style.display = 'flex'
368
+ // SVG sparkline
369
+ const w = 120, h = 24, pad = 2
370
+ const min = Math.min(...batteryHistory.map(b => b.value), 100)
371
+ const max = Math.max(...batteryHistory.map(b => b.value), 0)
372
+ const range = max - min || 1
373
+ const points = batteryHistory.map((b, i) => {
374
+ const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
375
+ const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
376
+ return `${x},${y}`
377
+ }).join(' ')
378
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
379
+ svg.setAttribute('width', String(w))
380
+ svg.setAttribute('height', String(h))
381
+ svg.setAttribute('viewBox', `0 0 ${w} ${h}`)
382
+ svg.style.display = 'block'
383
+ svg.style.background = '#f3f4f6'
384
+ svg.style.borderRadius = '3px'
385
+ svg.style.marginTop = '2px'
386
+ svg.style.boxShadow = '0 1px 2px #0001'
387
+ // Polyline for trend
388
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline')
389
+ polyline.setAttribute('points', points)
390
+ polyline.setAttribute('fill', 'none')
391
+ polyline.setAttribute('stroke', '#2563eb')
392
+ polyline.setAttribute('stroke-width', '2')
393
+ svg.appendChild(polyline)
394
+ // Dots for each point
395
+ batteryHistory.forEach((b, i) => {
396
+ const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
397
+ const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
398
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
399
+ circle.setAttribute('cx', String(x))
400
+ circle.setAttribute('cy', String(y))
401
+ circle.setAttribute('r', '2.5')
402
+ circle.setAttribute('fill', '#2563eb')
403
+ svg.appendChild(circle)
404
+ })
405
+ // Min/max labels
406
+ const minLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
407
+ minLabel.setAttribute('x', '2')
408
+ minLabel.setAttribute('y', String(h - 2))
409
+ minLabel.setAttribute('font-size', '9')
410
+ minLabel.setAttribute('fill', '#888')
411
+ minLabel.textContent = `${min}%`
412
+ svg.appendChild(minLabel)
413
+ const maxLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
414
+ maxLabel.setAttribute('x', String(w - 18))
415
+ maxLabel.setAttribute('y', '10')
416
+ maxLabel.setAttribute('font-size', '9')
417
+ maxLabel.setAttribute('fill', '#888')
418
+ maxLabel.textContent = `${max}%`
419
+ svg.appendChild(maxLabel)
420
+ chart.appendChild(svg)
421
+ details.appendChild(chart)
422
+ }
423
+ }
424
+
425
+ // --- Expose advanced/extra features dynamically ---
426
+ const featureKeys = [
427
+ 'airQuality', 'pm25', 'pm10', 'voc', 'co2', 'humidity', 'temperature',
428
+ 'preset', 'mode', 'presetMode', 'direction', 'calibration', 'multiCommand',
429
+ 'extendedInfo', 'segmentedControl', 'features', 'capabilities', 'state',
430
+ ]
431
+ const shown = new Set(rows.map(r => r.label.toLowerCase().replace(/\s/g, '')))
432
+ for (const key of featureKeys) {
433
+ if (device && device[key] !== undefined && !shown.has(key.toLowerCase())) {
434
+ const line = document.createElement('div')
435
+ line.style.display = 'flex'
436
+ line.style.alignItems = 'center'
437
+ line.style.justifyContent = 'space-between'
438
+ line.style.gap = '8px'
439
+ line.style.padding = '2px 0'
440
+
441
+ const label = document.createElement('span')
442
+ label.style.fontWeight = '600'
443
+ label.style.minWidth = '110px'
444
+ label.textContent = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()) + ':'
445
+
446
+ const value = document.createElement('span')
447
+ value.style.fontFamily = 'monospace'
448
+ value.style.fontSize = '11px'
449
+ value.style.opacity = '0.9'
450
+ value.style.whiteSpace = 'normal'
451
+ value.style.overflowWrap = 'anywhere'
452
+ value.style.wordBreak = 'break-word'
453
+ value.style.textAlign = 'right'
454
+ value.textContent = typeof device[key] === 'object' ? JSON.stringify(device[key]) : String(device[key])
455
+
456
+ line.appendChild(label)
457
+ line.appendChild(value)
458
+ details.appendChild(line)
459
+ }
460
+ }
461
+
462
+ return details
463
+ }
464
+
179
465
  export async function renderDiscoveredDevices(
180
466
  devices: any[],
181
- options: { configuredIds?: Set<string> } = {},
467
+ options: {
468
+ configuredIds?: Set<string>
469
+ selectedIds?: Set<string>
470
+ onToggleSelect?: (device: any, selected: boolean) => void
471
+ } = {},
182
472
  ): Promise<HTMLElement> {
183
473
  const ul = document.createElement('ul')
184
474
  ul.className = 'device-grid'
@@ -191,6 +481,8 @@ export async function renderDiscoveredDevices(
191
481
  const { addDeviceToConfig } = await import('./discovery.js')
192
482
  const { loadConfiguredDevices } = await import('./devices.js')
193
483
  const configuredIds = options.configuredIds ?? new Set<string>()
484
+ const selectedIds = options.selectedIds ?? new Set<string>()
485
+ const onToggleSelect = options.onToggleSelect
194
486
 
195
487
  for (const d of devices) {
196
488
  const deviceId = normalizeId(d.id)
@@ -224,8 +516,76 @@ export async function renderDiscoveredDevices(
224
516
  name.style.fontSize = '13px'
225
517
  name.textContent = d.name || d.id
226
518
 
519
+ const selectCheckbox = document.createElement('input')
520
+ selectCheckbox.type = 'checkbox'
521
+ selectCheckbox.style.width = 'auto'
522
+ selectCheckbox.style.margin = '0 2px 0 0'
523
+ selectCheckbox.checked = selectedIds.has(deviceId)
524
+ if (alreadyAdded) {
525
+ selectCheckbox.disabled = true
526
+ selectCheckbox.title = 'Already configured'
527
+ }
528
+ selectCheckbox.onchange = () => {
529
+ onToggleSelect?.(d, selectCheckbox.checked)
530
+ // Notify listeners (e.g., batch buttons) of selection change
531
+ window.dispatchEvent(new CustomEvent('discovery-selection-changed'))
532
+ }
533
+
534
+ nameContainer.appendChild(selectCheckbox)
535
+
536
+
227
537
  nameContainer.appendChild(name)
228
538
 
539
+ // Show firmware update available indicator if present
540
+ if (d.firmwareUpdateAvailable) {
541
+ const fwBadge = document.createElement('span');
542
+ fwBadge.textContent = 'Update Available';
543
+ fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
544
+ fwBadge.title = 'A firmware update is available for this device.';
545
+ nameContainer.appendChild(fwBadge);
546
+ }
547
+
548
+ // Show offline/unreachable indicator if device is offline
549
+ let offline = false;
550
+ let lastSeen = d.lastSeen || d.lastseen || d.updatedAt;
551
+ if (typeof d.offline === 'boolean') {
552
+ offline = d.offline;
553
+ } else if (lastSeen) {
554
+ try {
555
+ const last = new Date(lastSeen).getTime();
556
+ if (!isNaN(last) && Date.now() - last > 1000 * 60 * 60) { // 1 hour
557
+ offline = true;
558
+ }
559
+ } catch {}
560
+ }
561
+ if (offline) {
562
+ const offlineBadge = document.createElement('span');
563
+ offlineBadge.textContent = 'Offline';
564
+ offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
565
+ offlineBadge.title = 'Device is offline or unreachable.';
566
+ nameContainer.appendChild(offlineBadge);
567
+ }
568
+
569
+ const expandedDetails = document.createElement('div')
570
+ expandedDetails.style.display = 'none'
571
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d))
572
+
573
+ const expandBtn = document.createElement('button')
574
+ expandBtn.textContent = '▾'
575
+ expandBtn.title = 'Show details'
576
+ expandBtn.style.padding = '2px 6px'
577
+ expandBtn.style.fontSize = '11px'
578
+ expandBtn.style.marginLeft = '4px'
579
+ expandBtn.style.background = '#e5e7eb'
580
+ expandBtn.style.color = '#111827'
581
+ expandBtn.style.transition = 'transform 0.2s ease'
582
+ expandBtn.onclick = () => {
583
+ const isHidden = expandedDetails.style.display === 'none'
584
+ expandedDetails.style.display = isHidden ? 'block' : 'none'
585
+ expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
586
+ }
587
+ nameContainer.appendChild(expandBtn)
588
+
229
589
  const duplicateBadge = document.createElement('span')
230
590
  duplicateBadge.textContent = alreadyAdded ? '✓ Already Added' : '➕ New Device'
231
591
  duplicateBadge.style.cssText = alreadyAdded
@@ -250,6 +610,18 @@ export async function renderDiscoveredDevices(
250
610
  nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
251
611
  }
252
612
 
613
+ // Add battery warning indicator if battery < 20%
614
+ if (typeof d.battery === 'number' && d.battery < 20) {
615
+ const batteryWarn = document.createElement('span');
616
+ batteryWarn.textContent = `⚠️ ${d.battery}%`;
617
+ batteryWarn.style.cssText =
618
+ d.battery < 10
619
+ ? 'background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
620
+ : 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
621
+ batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low';
622
+ nameContainer.appendChild(batteryWarn);
623
+ }
624
+
253
625
  const details = document.createElement('div')
254
626
  details.style.fontSize = '10px'
255
627
  details.style.opacity = '0.7'
@@ -266,6 +638,7 @@ export async function renderDiscoveredDevices(
266
638
 
267
639
  info.appendChild(nameContainer)
268
640
  info.appendChild(details)
641
+ info.appendChild(expandedDetails)
269
642
 
270
643
  const addBtn = document.createElement('button')
271
644
  addBtn.textContent = alreadyAdded ? 'Already Added' : 'Add to Config'
@@ -312,6 +685,7 @@ export async function renderDiscoveredDevices(
312
685
  actions.style.gap = '5px'
313
686
  actions.appendChild(viewBtn)
314
687
  actions.appendChild(addBtn)
688
+ actions.appendChild(createConnectionTestControls(d))
315
689
  li.appendChild(actions)
316
690
  ul.appendChild(li)
317
691
  continue
@@ -327,6 +701,7 @@ export async function renderDiscoveredDevices(
327
701
  actions.style.marginTop = '2px'
328
702
  actions.style.gap = '5px'
329
703
  actions.appendChild(addBtn)
704
+ actions.appendChild(createConnectionTestControls(d))
330
705
 
331
706
  li.appendChild(info)
332
707
  li.appendChild(actions)
@@ -533,6 +908,25 @@ export function renderDeviceList(list: any[]): void {
533
908
  name.style.fontSize = '13px'
534
909
  name.textContent = d.name || d.id
535
910
 
911
+ const expandedDetails = document.createElement('div')
912
+ expandedDetails.style.display = 'none'
913
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d))
914
+
915
+ const expandBtn = document.createElement('button')
916
+ expandBtn.textContent = '▾'
917
+ expandBtn.title = 'Show details'
918
+ expandBtn.style.padding = '2px 6px'
919
+ expandBtn.style.fontSize = '11px'
920
+ expandBtn.style.marginLeft = '4px'
921
+ expandBtn.style.background = '#e5e7eb'
922
+ expandBtn.style.color = '#111827'
923
+ expandBtn.style.transition = 'transform 0.2s ease'
924
+ expandBtn.onclick = () => {
925
+ const isHidden = expandedDetails.style.display === 'none'
926
+ expandedDetails.style.display = isHidden ? 'block' : 'none'
927
+ expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
928
+ }
929
+
536
930
  const code = document.createElement('code')
537
931
  code.textContent = d.id
538
932
  code.style.fontSize = '10px'
@@ -543,7 +937,14 @@ export function renderDeviceList(list: any[]): void {
543
937
  code.style.wordBreak = 'break-word'
544
938
  code.style.maxWidth = '100%'
545
939
 
546
- nameContainer.appendChild(name)
940
+ const headerRow = document.createElement('div')
941
+ headerRow.style.display = 'inline-flex'
942
+ headerRow.style.alignItems = 'center'
943
+ headerRow.style.gap = '4px'
944
+ headerRow.appendChild(name)
945
+ headerRow.appendChild(expandBtn)
946
+
947
+ nameContainer.appendChild(headerRow)
547
948
  nameContainer.appendChild(code)
548
949
 
549
950
  // Add signal strength visualization if RSSI is available
@@ -564,6 +965,7 @@ export function renderDeviceList(list: any[]): void {
564
965
 
565
966
  info.appendChild(nameContainer)
566
967
  info.appendChild(meta)
968
+ info.appendChild(expandedDetails)
567
969
 
568
970
  const buttons = document.createElement('div')
569
971
  buttons.className = 'device-actions'
@@ -614,6 +1016,7 @@ export function renderDeviceList(list: any[]): void {
614
1016
 
615
1017
  buttons.appendChild(editBtn)
616
1018
  buttons.appendChild(copyBtn)
1019
+ buttons.appendChild(createConnectionTestControls(d))
617
1020
  buttons.appendChild(deleteBtn)
618
1021
 
619
1022
  li.appendChild(info)
@@ -1,5 +1,6 @@
1
1
  import type { SwitchBotPluginConfig } from './settings.js';
2
2
  import type { API, Logger, PlatformConfig } from 'homebridge';
3
+ export declare const DEVICE_MATTER_CLUSTERS: Record<string, any>;
3
4
  export declare class SwitchBotHAPPlatform {
4
5
  api: API | undefined;
5
6
  log: Logger;
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAq/B7D,qBAAa,oBAAoB;IAC/B,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IAEnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA2C1D,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,WAAW;IAoKX,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAYvC,wBAAwB,CAAC,CAAC,SAAS,EAAE,GAAG;CASzC;AAGD,qBAAa,uBAAuB;IAClC,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA8CpD,WAAW;IAwCjB,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAWvC,wBAAwB,CAAC,SAAS,EAAE,GAAG;IAWjC,yBAAyB;CAqHhC;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAkE7D,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CA2ItD,CAAA;AA0yBD,qBAAa,oBAAoB;IAC/B,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IAEnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA2C1D,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,WAAW;IAsKX,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAYvC,wBAAwB,CAAC,CAAC,SAAS,EAAE,GAAG;CASzC;AAGD,qBAAa,uBAAuB;IAClC,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,qBAAqB,CAAA;IAC7B,OAAO,EAAE,GAAG,EAAE,CAAK;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAE7B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,oBAAoB,CAA8B;gBAE9C,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA8CpD,WAAW;IAwCjB,OAAO,CAAC,aAAa;YAUP,qBAAqB;IAU7B,kBAAkB,CAAC,SAAS,EAAE,GAAG;IAWvC,wBAAwB,CAAC,SAAS,EAAE,GAAG;IAWjC,yBAAyB;CAqHhC;AAED,eAAe,oBAAoB,CAAA"}
package/dist/platform.js CHANGED
@@ -55,7 +55,7 @@ const DEVICE_MATTER_SUPPORTED = {
55
55
  // Default Matter cluster configurations by device type
56
56
  // Maps device types to their Matter cluster states (used when device doesn't provide clusters)
57
57
  // Note: wosweeper/curtain/plug variants are normalized before cluster lookup (see loadDevices)
58
- const DEVICE_MATTER_CLUSTERS = {
58
+ export const DEVICE_MATTER_CLUSTERS = {
59
59
  // Core devices - aligned with HAP service implementations
60
60
  bot: { onOff: { onOff: false } }, // Switch → OnOff
61
61
  vacuum: {
@@ -87,7 +87,7 @@ const DEVICE_MATTER_CLUSTERS = {
87
87
  }, // Switch in HAP, RobotVacuumCleaner in Matter
88
88
  curtain: {
89
89
  windowCovering: {
90
- // Use a lift-only/non-tilt covering type to satisfy Matter conformance rules.
90
+ // Use Rollershade (0) for lift-only/non-tilt covering to match test expectation.
91
91
  // Drapery (4) can fail validation in some stacks when TL (tilt) is not present.
92
92
  type: 0, // Rollershade
93
93
  },
@@ -122,7 +122,9 @@ const DEVICE_MATTER_CLUSTERS = {
122
122
  minLevel: 0,
123
123
  maxLevel: 254,
124
124
  },
125
- colorControl: {},
125
+ colorControl: {
126
+ colorMode: 0,
127
+ },
126
128
  }, // Lightbulb with color → OnOff + LevelControl + ColorControl
127
129
  lock: {
128
130
  doorLock: {
@@ -1078,6 +1080,8 @@ export class SwitchBotHAPPlatform {
1078
1080
  id: raw.deviceId ?? raw.id,
1079
1081
  name: raw.configDeviceName ?? raw.name,
1080
1082
  type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
1083
+ encryptionKey: raw.encryptionKey,
1084
+ keyId: raw.keyId,
1081
1085
  _raw: raw,
1082
1086
  };
1083
1087
  const type = normalizeTypeForMatter(d.type);