@switchbot/homebridge-switchbot 5.0.0-beta.153 → 5.0.0-beta.155

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 (102) hide show
  1. package/.github/workflows/release.yml +63 -15
  2. package/.github/workflows/stale.yml +2 -4
  3. package/CHANGELOG.md +21 -29
  4. package/MIGRATION.md +6 -6
  5. package/README.md +5 -3
  6. package/dist/device-types.js +7 -7
  7. package/dist/device-types.js.map +1 -1
  8. package/dist/deviceFactory.d.ts +1 -1
  9. package/dist/deviceFactory.d.ts.map +1 -1
  10. package/dist/deviceFactory.js +20 -20
  11. package/dist/deviceFactory.js.map +1 -1
  12. package/dist/homebridge-ui/device-types.js +246 -0
  13. package/dist/homebridge-ui/device-types.js.map +1 -0
  14. package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
  15. package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
  16. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  17. package/dist/homebridge-ui/endpoints/devices.js +64 -1
  18. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  19. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  20. package/dist/homebridge-ui/endpoints/discovery.js +5 -1
  21. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  22. package/dist/homebridge-ui/errors.js +32 -0
  23. package/dist/homebridge-ui/errors.js.map +1 -0
  24. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
  25. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
  26. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
  27. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
  28. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
  29. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
  30. package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
  31. package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
  32. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
  33. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
  34. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
  35. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
  36. package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
  37. package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
  38. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  39. package/dist/homebridge-ui/public/js/api.js +24 -11
  40. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  41. package/dist/homebridge-ui/public/js/api.ts +24 -12
  42. package/dist/homebridge-ui/public/js/app.js +117 -267
  43. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  44. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  45. package/dist/homebridge-ui/public/js/devices.js +2 -0
  46. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  47. package/dist/homebridge-ui/public/js/devices.ts +2 -0
  48. package/dist/homebridge-ui/public/js/discovery.d.ts +5 -0
  49. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  50. package/dist/homebridge-ui/public/js/discovery.js +79 -245
  51. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  52. package/dist/homebridge-ui/public/js/discovery.ts +88 -247
  53. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  54. package/dist/homebridge-ui/public/js/render.js +2 -1
  55. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  56. package/dist/homebridge-ui/public/js/render.ts +2 -1
  57. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -1
  58. package/dist/homebridge-ui/public/js/toast.js +15 -6
  59. package/dist/homebridge-ui/public/js/toast.js.map +1 -1
  60. package/dist/homebridge-ui/public/js/toast.ts +14 -7
  61. package/dist/homebridge-ui/settings.js +8 -0
  62. package/dist/homebridge-ui/settings.js.map +1 -0
  63. package/dist/homebridge-ui/switchbotClient.js +247 -0
  64. package/dist/homebridge-ui/switchbotClient.js.map +1 -0
  65. package/dist/homebridge-ui/utils/config-parser.d.ts +4 -0
  66. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  67. package/dist/homebridge-ui/utils/config-parser.js +21 -0
  68. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  69. package/dist/switchbotClient.d.ts +7 -1
  70. package/dist/switchbotClient.d.ts.map +1 -1
  71. package/dist/switchbotClient.js +82 -10
  72. package/dist/switchbotClient.js.map +1 -1
  73. package/docs/assets/main.js +1 -1
  74. package/docs/index.html +10 -4
  75. package/docs/variables/default.html +1 -1
  76. package/eslint.config.js +9 -10
  77. package/package.json +26 -24
  78. package/src/device-types.js +246 -0
  79. package/src/device-types.js.map +1 -0
  80. package/src/device-types.ts +7 -7
  81. package/src/deviceCommandMapper.js +319 -0
  82. package/src/deviceCommandMapper.js.map +1 -0
  83. package/src/deviceFactory.ts +22 -21
  84. package/src/errors.js +32 -0
  85. package/src/errors.js.map +1 -0
  86. package/src/homebridge-ui/endpoints/devices.ts +66 -1
  87. package/src/homebridge-ui/endpoints/discovery.ts +5 -1
  88. package/src/homebridge-ui/public/js/api.ts +24 -12
  89. package/src/homebridge-ui/public/js/devices.ts +2 -0
  90. package/src/homebridge-ui/public/js/discovery.ts +88 -247
  91. package/src/homebridge-ui/public/js/render.ts +2 -1
  92. package/src/homebridge-ui/public/js/toast.ts +14 -7
  93. package/src/homebridge-ui/utils/config-parser.ts +17 -0
  94. package/src/settings.js +8 -0
  95. package/src/settings.js.map +1 -0
  96. package/src/switchbotClient.js +247 -0
  97. package/src/switchbotClient.js.map +1 -0
  98. package/src/switchbotClient.ts +95 -10
  99. package/test/client/switchbotClient.spec.ts +42 -1
  100. package/test/e2e/run-e2e.spec.ts +1 -0
  101. package/tsconfig.ui.json +11 -0
  102. package/.github/workflows/beta-release.yml +0 -52
@@ -1,3 +1,4 @@
1
+ // Extend the Window interface to include _discoverySelectedIds for type safety
1
2
  // Batch enable/disable helper (true module scope for UI access)
2
3
  import {
3
4
  addDevicesInBulk,
@@ -12,42 +13,9 @@ import { hideBusyUi, showBusyUi } from './modal.js'
12
13
  import { getDiscoveryPreferences, renderDiscoveredDevices, setDiscoveryPreferences } from './render.js'
13
14
  import { toastError, toastInfo, toastSuccess, toastWarning } from './toast.js'
14
15
 
15
- async function batchSetDeviceEnabled(selectedIds: Set<string>, enabled: boolean): Promise<void> {
16
- // Fetch current config
17
- const resp = await homebridge.request('/platform-config', {})
18
- if (!resp || resp.success === false || !resp.data) {
19
- throw new Error('Failed to load config')
20
- }
21
- const config = resp.data
22
- // Homebridge config may be an array of platforms, find SwitchBot
23
- const configArr = Array.isArray(config) ? config : [config]
24
- const platformIdx = configArr.findIndex((c: any) => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
25
- if (platformIdx === -1) {
26
- throw new Error('SwitchBot platform config not found')
27
- }
28
- const platformConfig = configArr[platformIdx]
29
- if (!Array.isArray(platformConfig.devices)) {
30
- throw new TypeError('No devices array in config')
31
- }
32
- let changed = false
33
- for (const dev of platformConfig.devices) {
34
- const id = String(dev.deviceId || dev.id || '').trim().toLowerCase()
35
- if (selectedIds.has(id)) {
36
- if (dev.enabled !== enabled) {
37
- dev.enabled = enabled
38
- changed = true
39
- }
40
- }
41
- }
42
- if (changed) {
43
- if (typeof homebridge.updatePluginConfig === 'function') {
44
- await homebridge.updatePluginConfig(configArr)
45
- } else {
46
- throw new TypeError('homebridge.updatePluginConfig is not available')
47
- }
48
- if (typeof homebridge.savePluginConfig === 'function') {
49
- await homebridge.savePluginConfig()
50
- }
16
+ declare global {
17
+ interface Window {
18
+ _discoverySelectedIds: Set<string>
51
19
  }
52
20
  }
53
21
 
@@ -378,9 +346,9 @@ function getDiscoveryGroupByPreference(): DiscoveryGroupBy {
378
346
  if (stored === 'hub' || stored === 'type') {
379
347
  return stored
380
348
  }
381
- return 'connection'
349
+ return 'type' // Default to Device Type grouping
382
350
  } catch (_e) {
383
- return 'connection'
351
+ return 'type'
384
352
  }
385
353
  }
386
354
 
@@ -502,12 +470,12 @@ export async function discoverDevices(): Promise<void> {
502
470
  const preferences = getDiscoveryPreferences()
503
471
  let groupBy: DiscoveryGroupBy = getDiscoveryGroupByPreference()
504
472
  let hideAdded = getDiscoveryHideAddedPreference()
505
- let controlsInitialized = false
506
473
  // Use persistent selection state across renders
507
- if (!(window as any)._discoverySelectedIds) {
508
- (window as any)._discoverySelectedIds = new Set<string>()
474
+ if (!window._discoverySelectedIds) {
475
+ window._discoverySelectedIds = new Set<string>()
509
476
  }
510
- const selectedIds: Set<string> = (window as any)._discoverySelectedIds
477
+ const selectedIds: Set<string> = window._discoverySelectedIds
478
+ let controlsInitialized = false
511
479
 
512
480
  // --- Real-time RSSI polling loop ---
513
481
  // (Moved inside main try block after bleSettings is defined)
@@ -515,14 +483,12 @@ export async function discoverDevices(): Promise<void> {
515
483
  // Batch enable/disable helper (moved to module scope for UI access)
516
484
  async function batchSetDeviceEnabled(selectedIds: Set<string>, enabled: boolean): Promise<void> {
517
485
  // Fetch current config
518
- const resp = await homebridge.request('/platform-config', {})
519
- if (!resp || resp.success === false || !resp.data) {
520
- throw new Error('Failed to load config')
521
- }
522
- const config = resp.data
523
- // Homebridge config may be an array of platforms, find SwitchBot
524
- const configArr = Array.isArray(config) ? config : [config]
525
- const platformIdx = configArr.findIndex((c: any) => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
486
+ // Fetch current config using Homebridge UI API
487
+ if (typeof homebridge.getPluginConfig !== 'function') {
488
+ throw new TypeError('homebridge.getPluginConfig is not available')
489
+ }
490
+ const configArr = await homebridge.getPluginConfig()
491
+ const platformIdx = Array.isArray(configArr) ? configArr.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot')) : -1
526
492
  if (platformIdx === -1) {
527
493
  throw new Error('SwitchBot platform config not found')
528
494
  }
@@ -553,14 +519,55 @@ export async function discoverDevices(): Promise<void> {
553
519
  }
554
520
 
555
521
  const ensureDiscoveryControls = async (): Promise<void> => {
522
+ // --- Select All / Deselect All controls ---
523
+ const selectAllBtn = document.createElement('button')
524
+ selectAllBtn.textContent = 'Select All'
525
+ selectAllBtn.style.fontSize = '13px'
526
+ selectAllBtn.style.padding = '6px 18px'
527
+ selectAllBtn.style.borderRadius = '6px'
528
+ selectAllBtn.style.background = '#f3f4f6'
529
+ selectAllBtn.style.color = '#1d4ed8'
530
+ selectAllBtn.style.border = '1px solid #d1d5db'
531
+ selectAllBtn.style.cursor = 'pointer'
532
+ selectAllBtn.style.marginRight = '8px'
533
+ selectAllBtn.onclick = () => {
534
+ // Add all visible device IDs to selectedIds
535
+ for (const d of discoveredDevices) {
536
+ selectedIds.add(normalizeId(d.id))
537
+ }
538
+ window.dispatchEvent(new Event('discovery-selection-changed'))
539
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds)
540
+ }
541
+
542
+ const deselectAllBtn = document.createElement('button')
543
+ deselectAllBtn.textContent = 'Deselect All'
544
+ deselectAllBtn.style.fontSize = '13px'
545
+ deselectAllBtn.style.padding = '6px 18px'
546
+ deselectAllBtn.style.borderRadius = '6px'
547
+ deselectAllBtn.style.background = '#f3f4f6'
548
+ deselectAllBtn.style.color = '#ef4444'
549
+ deselectAllBtn.style.border = '1px solid #d1d5db'
550
+ deselectAllBtn.style.cursor = 'pointer'
551
+ deselectAllBtn.onclick = () => {
552
+ // Remove all visible device IDs from selectedIds
553
+ for (const d of discoveredDevices) {
554
+ selectedIds.delete(normalizeId(d.id))
555
+ }
556
+ window.dispatchEvent(new Event('discovery-selection-changed'))
557
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds)
558
+ }
559
+
560
+ // Insert select/deselect all controls above the action buttons
561
+ const selectControlsRow = document.createElement('div')
562
+ selectControlsRow.style.display = 'flex'
563
+ selectControlsRow.style.gap = '10px'
564
+ selectControlsRow.style.margin = '0 0 10px 0'
565
+ selectControlsRow.appendChild(selectAllBtn)
566
+ selectControlsRow.appendChild(deselectAllBtn)
556
567
  if (controlsInitialized) {
557
568
  return
558
569
  }
559
- // Always use persistent selectedIds
560
- if (!(window as any)._discoverySelectedIds) {
561
- (window as any)._discoverySelectedIds = new Set<string>()
562
- }
563
- const selectedIds: Set<string> = (window as any)._discoverySelectedIds
570
+ // Always use persistent selectedIds (already defined in outer scope)
564
571
 
565
572
  const controlsDiv = document.createElement('div')
566
573
  controlsDiv.style.cssText = 'margin-bottom: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;'
@@ -643,17 +650,28 @@ export async function discoverDevices(): Promise<void> {
643
650
  void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds)
644
651
  }
645
652
 
646
- const groupLabel = document.createElement('label')
647
- groupLabel.style.fontSize = '12px'
648
- groupLabel.style.fontWeight = '500'
649
- groupLabel.style.marginLeft = '8px'
650
- groupLabel.textContent = 'Group:'
651
-
652
653
  const groupSelect = document.createElement('select')
653
654
  groupSelect.style.fontSize = '11px'
654
655
  groupSelect.style.padding = '4px 8px'
655
656
  groupSelect.style.borderRadius = '3px'
656
- groupSelect.value = groupBy
657
+ // Set default value to 'type' if no stored preference
658
+ if (!localStorage.getItem(DISCOVERY_GROUP_BY_KEY)) {
659
+ groupSelect.value = 'type'
660
+ } else {
661
+ groupSelect.value = groupBy
662
+ }
663
+
664
+ const groupLabel = document.createElement('label')
665
+ groupLabel.style.fontSize = '12px'
666
+ groupLabel.style.fontWeight = '500'
667
+ groupLabel.style.marginLeft = '8px'
668
+ // Set label text to match selected group
669
+ const groupLabelTextMap = {
670
+ connection: 'Connection',
671
+ hub: 'Hub',
672
+ type: 'Device Type',
673
+ }
674
+ groupLabel.textContent = `Group: ${groupLabelTextMap[groupSelect.value] || 'Connection'}`
657
675
 
658
676
  const groupOptions: Array<{ label: string, value: DiscoveryGroupBy }> = [
659
677
  { label: 'Connection', value: 'connection' },
@@ -671,6 +689,7 @@ export async function discoverDevices(): Promise<void> {
671
689
  groupSelect.onchange = () => {
672
690
  groupBy = groupSelect.value as DiscoveryGroupBy
673
691
  setDiscoveryGroupByPreference(groupBy)
692
+ groupLabel.textContent = `Group: ${groupLabelTextMap[groupSelect.value] || 'Connection'}`
674
693
  void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds)
675
694
  }
676
695
 
@@ -866,6 +885,7 @@ export async function discoverDevices(): Promise<void> {
866
885
 
867
886
  // Clear list and append controls in correct order
868
887
  list.innerHTML = ''
888
+ list.appendChild(selectControlsRow)
869
889
  list.appendChild(topActionRow)
870
890
  list.appendChild(controlsDiv)
871
891
 
@@ -1276,26 +1296,12 @@ async function updateDiscoveryView(
1276
1296
  }
1277
1297
  }
1278
1298
 
1279
- // --- Batch Import Controls ---
1280
- // Remove any duplicate 'Add Selected' buttons from legacy UI
1281
- // Robust NodeList iteration: classic for loop to avoid formatter and transpiler issues
1282
- const legacyAddSelectedBtns: HTMLButtonElement[] = []
1283
- const allBtns = document.querySelectorAll('button')
1284
- for (let i = 0; i < allBtns.length; i++) {
1285
- const btn = allBtns[i]
1286
- if (btn.textContent && btn.textContent.trim() === 'Add Selected') {
1287
- legacyAddSelectedBtns.push(btn)
1288
- }
1289
- }
1290
- for (let i = 0; i < legacyAddSelectedBtns.length; i++) {
1291
- legacyAddSelectedBtns[i].remove()
1292
- }
1293
-
1294
- // Listen for selection changes to update batch button states
1299
+ // Only update the enabled/disabled state of batch action buttons (created in ensureDiscoveryControls)
1295
1300
  function updateBatchButtonStates() {
1301
+ // These buttons are created in ensureDiscoveryControls and should have unique IDs
1296
1302
  const addSelectedBtn = document.getElementById('addSelectedBtn') as HTMLButtonElement | null
1297
- const enableSelectedBtn = document.querySelector('button')?.parentElement?.querySelector('button[aria-label="Enable Selected"]') as HTMLButtonElement | null
1298
- const disableSelectedBtn = document.querySelector('button')?.parentElement?.querySelector('button[aria-label="Disable Selected"]') as HTMLButtonElement | null
1303
+ const enableSelectedBtn = document.getElementById('enableSelectedBtn') as HTMLButtonElement | null
1304
+ const disableSelectedBtn = document.getElementById('disableSelectedBtn') as HTMLButtonElement | null
1299
1305
  const hasSelection = selectedIds.size > 0
1300
1306
  if (addSelectedBtn) {
1301
1307
  addSelectedBtn.disabled = !hasSelection
@@ -1309,173 +1315,8 @@ async function updateDiscoveryView(
1309
1315
  }
1310
1316
  window.removeEventListener('discovery-selection-changed', updateBatchButtonStates)
1311
1317
  window.addEventListener('discovery-selection-changed', updateBatchButtonStates)
1312
- // Add 'Add Selected to Config' button if not present, and align all red action buttons in a row
1313
- let batchControls = document.getElementById('batchImportControls')
1314
- if (!batchControls) {
1315
- batchControls = document.createElement('div')
1316
- batchControls.id = 'batchImportControls'
1317
- batchControls.style.display = 'flex'
1318
- batchControls.style.flexWrap = 'wrap'
1319
- batchControls.style.alignItems = 'center'
1320
- batchControls.style.gap = '18px'
1321
- batchControls.style.margin = '8px 0 18px 0'
1322
- batchControls.style.width = '100%'
1323
-
1324
- // Button container for horizontal alignment
1325
- const buttonRow = document.createElement('div')
1326
- buttonRow.style.display = 'flex'
1327
- buttonRow.style.flexWrap = 'nowrap'
1328
- buttonRow.style.alignItems = 'center'
1329
- buttonRow.style.justifyContent = 'center'
1330
- buttonRow.style.gap = '16px'
1331
- buttonRow.style.width = '100%'
1332
-
1333
- // Add Selected to Config button
1334
- const addSelectedBtn = document.createElement('button')
1335
- addSelectedBtn.id = 'addSelectedBtn'
1336
- addSelectedBtn.textContent = 'Add Selected to Config'
1337
- addSelectedBtn.disabled = selectedIds.size === 0
1338
- addSelectedBtn.style.fontWeight = '600'
1339
- // Shared style for all red action buttons
1340
- const redButtonStyle = {
1341
- width: '220px',
1342
- padding: '12px 0',
1343
- fontSize: '17px',
1344
- marginBottom: '0',
1345
- boxShadow: '0 2px 8px 0 rgba(220,38,38,0.10)',
1346
- borderRadius: '8px',
1347
- }
1348
- Object.assign(addSelectedBtn.style, redButtonStyle)
1349
- addSelectedBtn.style.background = '#ef4444'
1350
- addSelectedBtn.style.color = 'white'
1351
- addSelectedBtn.style.transition = 'background 0.2s, box-shadow 0.2s'
1352
- addSelectedBtn.onmouseenter = function () {
1353
- addSelectedBtn.style.background = '#dc2626'
1354
- }
1355
- addSelectedBtn.onmouseleave = function () {
1356
- addSelectedBtn.style.background = '#ef4444'
1357
- }
1358
- addSelectedBtn.onclick = async () => {
1359
- if (!selectedIds.size) {
1360
- return
1361
- }
1362
- addSelectedBtn.disabled = true
1363
- addSelectedBtn.textContent = 'Adding...'
1364
- try {
1365
- const selectedDevices = visibleDevices.filter(d => selectedIds.has(normalizeId(d.id)))
1366
- const bulkResult = await addDevicesInBulk(selectedDevices.map(d => ({
1367
- deviceId: d.id,
1368
- name: d.name,
1369
- type: d.type,
1370
- rssi: d.rssi,
1371
- address: d.address,
1372
- model: d.model,
1373
- })))
1374
- uiLog.info('Batch add response:', bulkResult)
1375
- if (!bulkResult || bulkResult.success === false) {
1376
- throw new Error(bulkResult?.data?.message || 'Batch add failed')
1377
- }
1378
- const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0
1379
- const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0
1380
- toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ''}`)
1381
- await loadConfiguredDevices()
1382
- selectedIds.clear()
1383
- addSelectedBtn.disabled = true
1384
- addSelectedBtn.textContent = 'Add Selected to Config'
1385
- await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds)
1386
- } catch (e) {
1387
- uiLog.error('Batch add error:', e)
1388
- toastError(e instanceof Error ? e.message : 'Failed to add devices')
1389
- addSelectedBtn.disabled = false
1390
- addSelectedBtn.textContent = 'Add Selected to Config'
1391
- }
1392
- }
1393
-
1394
- // Enable Selected button
1395
- const enableSelectedBtn = document.createElement('button')
1396
- enableSelectedBtn.textContent = 'Enable Selected'
1397
- enableSelectedBtn.style.fontWeight = '600'
1398
- Object.assign(enableSelectedBtn.style, redButtonStyle)
1399
- enableSelectedBtn.style.background = '#ef4444'
1400
- enableSelectedBtn.style.color = 'white'
1401
- enableSelectedBtn.style.transition = 'background 0.2s, box-shadow 0.2s'
1402
- enableSelectedBtn.onmouseenter = function () {
1403
- enableSelectedBtn.style.background = '#dc2626'
1404
- }
1405
- enableSelectedBtn.onmouseleave = function () {
1406
- enableSelectedBtn.style.background = '#ef4444'
1407
- }
1408
- enableSelectedBtn.disabled = selectedIds.size === 0
1409
- enableSelectedBtn.onclick = async () => {
1410
- if (!selectedIds.size) {
1411
- return
1412
- }
1413
- enableSelectedBtn.disabled = true
1414
- try {
1415
- showBusyUi()
1416
- await batchSetDeviceEnabled(selectedIds, true)
1417
- toastSuccess('Selected devices enabled')
1418
- await loadConfiguredDevices()
1419
- await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds)
1420
- } catch (e) {
1421
- uiLog.error('Batch enable error:', e)
1422
- toastError(e instanceof Error ? e.message : 'Failed to enable devices')
1423
- } finally {
1424
- hideBusyUi()
1425
- enableSelectedBtn.disabled = false
1426
- }
1427
- }
1428
-
1429
- // Disable Selected button
1430
- const disableSelectedBtn = document.createElement('button')
1431
- disableSelectedBtn.textContent = 'Disable Selected'
1432
- disableSelectedBtn.style.fontWeight = '600'
1433
- Object.assign(disableSelectedBtn.style, redButtonStyle)
1434
- disableSelectedBtn.style.background = '#ef4444'
1435
- disableSelectedBtn.style.color = 'white'
1436
- disableSelectedBtn.style.transition = 'background 0.2s, box-shadow 0.2s'
1437
- disableSelectedBtn.onmouseenter = function () {
1438
- disableSelectedBtn.style.background = '#dc2626'
1439
- }
1440
- disableSelectedBtn.onmouseleave = function () {
1441
- disableSelectedBtn.style.background = '#ef4444'
1442
- }
1443
- disableSelectedBtn.disabled = selectedIds.size === 0
1444
- disableSelectedBtn.onclick = async () => {
1445
- if (!selectedIds.size) {
1446
- return
1447
- }
1448
- disableSelectedBtn.disabled = true
1449
- try {
1450
- showBusyUi()
1451
- await batchSetDeviceEnabled(selectedIds, false)
1452
- toastSuccess('Selected devices disabled')
1453
- await loadConfiguredDevices()
1454
- await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds)
1455
- } catch (e) {
1456
- uiLog.error('Batch disable error:', e)
1457
- toastError(e instanceof Error ? e.message : 'Failed to disable devices')
1458
- } finally {
1459
- hideBusyUi()
1460
- disableSelectedBtn.disabled = false
1461
- }
1462
- }
1463
-
1464
- buttonRow.appendChild(addSelectedBtn)
1465
- buttonRow.appendChild(enableSelectedBtn)
1466
- buttonRow.appendChild(disableSelectedBtn)
1467
- batchControls.appendChild(buttonRow)
1468
- const listContainer = document.getElementById('discoveredList')
1469
- if (listContainer) {
1470
- listContainer.insertBefore(batchControls, listContainer.firstChild)
1471
- }
1472
- } else {
1473
- // Update button state if already present
1474
- const addSelectedBtn = document.getElementById('addSelectedBtn') as HTMLButtonElement | null
1475
- if (addSelectedBtn) {
1476
- addSelectedBtn.disabled = selectedIds.size === 0
1477
- }
1478
- }
1318
+ // Initial state update
1319
+ updateBatchButtonStates()
1479
1320
 
1480
1321
  // Update status with count
1481
1322
  const status = document.getElementById('discoverStatus')
@@ -295,6 +295,7 @@ export function renderDeviceDetailsPanel(device: any): HTMLElement {
295
295
  }
296
296
 
297
297
  const rows: Array<{ label: string, value: string, copyable?: boolean }> = [
298
+ { label: 'Name', value: String(device?.name || device?.configDeviceName || 'N/A') },
298
299
  { label: 'Device ID', value: String(device?.id || device?.deviceId || 'N/A'), copyable: !!(device?.id || device?.deviceId) },
299
300
  { label: 'MAC Address', value: String(device?.address || 'N/A'), copyable: !!device?.address },
300
301
  { label: 'Device Type', value: String(device?.type || device?.configDeviceType || 'N/A') },
@@ -1066,7 +1067,7 @@ export function renderDeviceList(list: any[]): void {
1066
1067
  deleteBtn.style.background = '#ef4444'
1067
1068
  deleteBtn.onclick = async () => {
1068
1069
  const { deleteDeviceFromConfig } = await import('./devices-delete.js')
1069
- await deleteDeviceFromConfig(d.id, d.name || d.id)
1070
+ await deleteDeviceFromConfig(d.id || d.deviceId, d.name || d.id || d.deviceId)
1070
1071
  }
1071
1072
 
1072
1073
  buttons.appendChild(editBtn)
@@ -7,16 +7,23 @@ function showToast(
7
7
  title = 'SwitchBot',
8
8
  ): void {
9
9
  try {
10
- const toast = homebridge?.toast
11
- const fn = toast?.[method]
12
- if (typeof fn === 'function') {
13
- fn(message, title)
14
- return
10
+ // Defensive: check for window and homebridge existence
11
+ const hb = typeof window !== 'undefined' ? (window as any).homebridge : undefined
12
+ const toast = hb && typeof hb.toast === 'object' ? hb.toast : undefined
13
+ const fn = toast && typeof toast[method] === 'function' ? toast[method] : undefined
14
+ if (fn) {
15
+ try {
16
+ fn(message, title)
17
+ return
18
+ } catch (err) {
19
+ uiLog.warn(`Toast ${method} threw:`, err)
20
+ }
15
21
  }
16
-
22
+ // Fallback: log to console
17
23
  uiLog.info(`[Toast:${method}] ${title} - ${message}`)
18
24
  } catch (e) {
19
- uiLog.warn(`Toast ${method} failed:`, e)
25
+ uiLog.warn(`Toast ${method} outer error:`, e)
26
+ uiLog.info(`[Toast:${method}] ${title} - ${message}`)
20
27
  }
21
28
  }
22
29
 
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Ensure required fields are present on the SwitchBot platform config
3
+ */
4
+ export function enforcePlatformConfigFields(platform: any): void {
5
+ if (!platform) return
6
+ if (!platform.platform) platform.platform = 'SwitchBot'
7
+ if (!platform.name) platform.name = 'SwitchBot'
8
+ if (!Array.isArray(platform.devices)) platform.devices = []
9
+ }
1
10
  import type { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
2
11
 
3
12
  import fs from 'node:fs/promises'
@@ -104,5 +113,13 @@ export async function getSwitchBotPlatformConfig(server: HomebridgePluginUiServe
104
113
  * Save the Homebridge config file
105
114
  */
106
115
  export async function saveConfig(cfgPath: string, cfg: any): Promise<void> {
116
+ // Defensive: enforce required fields on all SwitchBot platform blocks before saving
117
+ if (cfg && Array.isArray(cfg.platforms)) {
118
+ for (const p of cfg.platforms) {
119
+ if (p && (String(p.platform || '').toLowerCase() === 'switchbot' || String(p.name || '').toLowerCase() === 'switchbot')) {
120
+ enforcePlatformConfigFields(p)
121
+ }
122
+ }
123
+ }
107
124
  await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2), 'utf-8')
108
125
  }
@@ -0,0 +1,8 @@
1
+ export const PLUGIN_NAME = '@switchbot/homebridge-switchbot';
2
+ export const PLATFORM_NAME = 'SwitchBot';
3
+ export const DEFAULT_CONFIG = {
4
+ preferMatter: true,
5
+ enableMatter: true,
6
+ enableBLE: true,
7
+ };
8
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["settings.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,iCAAiC,CAAA;AAC5D,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAA;AAYxC,MAAM,CAAC,MAAM,cAAc,GAAmC;IAC5D,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,IAAI;IAClB,SAAS,EAAE,IAAI;CAChB,CAAA"}