@switchbot/homebridge-switchbot 5.1.0-beta.0 → 5.1.0-beta.10

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 (101) hide show
  1. package/.github/copilot-instructions.md +63 -0
  2. package/.github/scripts/normalize-changelog-header.cjs +86 -0
  3. package/.github/scripts/npm-version-script-esm-auto.js +201 -0
  4. package/.github/scripts/resolve-version-bump-esm.js +118 -0
  5. package/.github/workflows/release.yml +62 -13
  6. package/CHANGELOG.md +18 -0
  7. package/dist/{SwitchBotHAPPlatform.d.ts → Platform.HAP.d.ts} +2 -2
  8. package/dist/Platform.HAP.d.ts.map +1 -0
  9. package/dist/{SwitchBotHAPPlatform.js → Platform.HAP.js} +4 -2
  10. package/dist/Platform.HAP.js.map +1 -0
  11. package/dist/{SwitchBotMatterPlatform.d.ts → Platform.Matter.d.ts} +3 -2
  12. package/dist/Platform.Matter.d.ts.map +1 -0
  13. package/dist/{SwitchBotMatterPlatform.js → Platform.Matter.js} +24 -1
  14. package/dist/Platform.Matter.js.map +1 -0
  15. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  16. package/dist/homebridge-ui/endpoints/config.js +9 -2
  17. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  18. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +9 -2
  19. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
  20. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +8 -0
  21. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
  22. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +23 -0
  23. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +1 -0
  24. package/dist/homebridge-ui/public/css/styles.css +62 -2
  25. package/dist/homebridge-ui/public/index.html +61 -6
  26. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -1
  27. package/dist/homebridge-ui/public/js/advanced-settings.js +4 -3
  28. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -1
  29. package/dist/homebridge-ui/public/js/advanced-settings.ts +4 -3
  30. package/dist/homebridge-ui/public/js/api.d.ts +11 -0
  31. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  32. package/dist/homebridge-ui/public/js/api.js +44 -1
  33. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  34. package/dist/homebridge-ui/public/js/api.ts +46 -1
  35. package/dist/homebridge-ui/public/js/app.js +241 -182
  36. package/dist/homebridge-ui/public/js/app.js.map +4 -4
  37. package/dist/homebridge-ui/public/js/app.ts +47 -4
  38. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
  39. package/dist/homebridge-ui/public/js/credentials.js +11 -5
  40. package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
  41. package/dist/homebridge-ui/public/js/credentials.ts +11 -5
  42. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  43. package/dist/homebridge-ui/public/js/discovery.js +0 -36
  44. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  45. package/dist/homebridge-ui/public/js/discovery.ts +0 -37
  46. package/dist/homebridge-ui/public/js/legacy-view.d.ts +3 -0
  47. package/dist/homebridge-ui/public/js/legacy-view.d.ts.map +1 -0
  48. package/dist/homebridge-ui/public/js/legacy-view.js +152 -0
  49. package/dist/homebridge-ui/public/js/legacy-view.js.map +1 -0
  50. package/dist/homebridge-ui/public/js/legacy-view.ts +107 -0
  51. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  52. package/dist/homebridge-ui/public/js/render.js +38 -31
  53. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  54. package/dist/homebridge-ui/public/js/render.ts +39 -34
  55. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -1
  56. package/dist/homebridge-ui/public/js/toast.js +2 -3
  57. package/dist/homebridge-ui/public/js/toast.js.map +1 -1
  58. package/dist/homebridge-ui/public/js/toast.ts +2 -3
  59. package/dist/homebridge-ui/utils/config-parser.d.ts +8 -0
  60. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  61. package/dist/homebridge-ui/utils/config-parser.js +8 -0
  62. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  63. package/dist/homebridge-ui/utils/v4-detection.d.ts +12 -0
  64. package/dist/homebridge-ui/utils/v4-detection.d.ts.map +1 -0
  65. package/dist/homebridge-ui/utils/v4-detection.js +23 -0
  66. package/dist/homebridge-ui/utils/v4-detection.js.map +1 -0
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/docs/assets/icons.js +1 -1
  70. package/docs/assets/icons.svg +1 -1
  71. package/docs/assets/main.js +3 -3
  72. package/docs/variables/default.html +1 -1
  73. package/package.json +12 -18
  74. package/scripts/build-ui.js +30 -0
  75. package/src/{SwitchBotHAPPlatform.ts → Platform.HAP.ts} +5 -2
  76. package/src/{SwitchBotMatterPlatform.ts → Platform.Matter.ts} +28 -1
  77. package/src/homebridge-ui/endpoints/config.ts +10 -2
  78. package/src/homebridge-ui/public/css/styles.css +62 -2
  79. package/src/homebridge-ui/public/index.html +59 -4
  80. package/src/homebridge-ui/public/js/advanced-settings.ts +4 -3
  81. package/src/homebridge-ui/public/js/api.ts +46 -1
  82. package/src/homebridge-ui/public/js/app.ts +47 -4
  83. package/src/homebridge-ui/public/js/credentials.ts +11 -5
  84. package/src/homebridge-ui/public/js/discovery.ts +0 -37
  85. package/src/homebridge-ui/public/js/legacy-view.js +152 -0
  86. package/src/homebridge-ui/public/js/legacy-view.ts +107 -0
  87. package/src/homebridge-ui/public/js/render.ts +39 -34
  88. package/src/homebridge-ui/public/js/toast.ts +2 -3
  89. package/src/homebridge-ui/utils/config-parser.ts +9 -0
  90. package/src/homebridge-ui/utils/v4-detection.ts +23 -0
  91. package/src/index.ts +2 -2
  92. package/test/homebridge-ui/config-parser.spec.ts +61 -0
  93. package/test/platform/accessory-restore.spec.ts +2 -2
  94. package/test/platform/matter-childbridge.spec.ts +1 -1
  95. package/test/platform/matter-integration.spec.ts +51 -1
  96. package/test/platform/platform-edge.spec.ts +2 -2
  97. package/test/platform/platform.integration.spec.ts +1 -1
  98. package/dist/SwitchBotHAPPlatform.d.ts.map +0 -1
  99. package/dist/SwitchBotHAPPlatform.js.map +0 -1
  100. package/dist/SwitchBotMatterPlatform.d.ts.map +0 -1
  101. package/dist/SwitchBotMatterPlatform.js.map +0 -1
@@ -9,8 +9,57 @@
9
9
  </head>
10
10
 
11
11
  <body>
12
+
12
13
  <h1>🤖 SwitchBot Configuration</h1>
13
14
 
15
+ <!-- ...existing code... -->
16
+
17
+ <!-- Centered legacy view button at the bottom -->
18
+ <div class="centered-toggle-btn">
19
+ <button id="legacyViewBtn">Legacy View</button>
20
+ <button id="dynamicViewBtn" style="display: none;">Dynamic View</button>
21
+ </div>
22
+ <style>
23
+ .centered-toggle-btn {
24
+ display: flex;
25
+ flex-direction: row;
26
+ justify-content: center;
27
+ align-items: center;
28
+ margin-top: 32px;
29
+ margin-bottom: 12px;
30
+ gap: 12px;
31
+ }
32
+ #legacyViewBtn, #dynamicViewBtn {
33
+ font-size: 12px;
34
+ padding: 4px 10px;
35
+ }
36
+ </style>
37
+
38
+ <div id="dynamicUiContainer">
39
+
40
+ <!-- v4 config incompatibility warning – shown dynamically by app.js -->
41
+ <div id="v4ConfigWarning" class="v4-config-warning" style="display: none">
42
+ <strong>⚠️ Incompatible v4 Configuration Detected</strong>
43
+ <p>
44
+ Your config.json appears to be in the <strong>v4 format</strong> (using <code>credentials</code> and
45
+ <code>options.devices</code> fields). This format is <strong>not compatible with v5 or newer</strong> of the
46
+ SwitchBot plugin. Devices will not be recognised until the config is updated.
47
+ </p>
48
+ <p>
49
+ Please update your config to the v5 format:
50
+ </p>
51
+ <ul>
52
+ <li>Move <code>credentials.token</code> → <code>openApiToken</code> (top level)</li>
53
+ <li>Move <code>credentials.secret</code> → <code>openApiSecret</code> (top level)</li>
54
+ <li>Move <code>options.devices</code> → <code>devices</code> (top level)</li>
55
+ <li>Rename per-device <code>connectionType</code> → <code>connectionPreference</code></li>
56
+ <li>Remove the <code>credentials</code> and <code>options</code> wrapper objects</li>
57
+ </ul>
58
+ <p>
59
+ See the <a href="https://github.com/OpenWonderLabs/homebridge-switchbot/blob/latest/MIGRATION.md" target="_blank" rel="noopener">Migration Guide</a> for full details.
60
+ </p>
61
+ </div>
62
+
14
63
  <div class="card compact">
15
64
  <h2>API Credentials</h2>
16
65
  <div class="credentials-row">
@@ -81,10 +130,6 @@
81
130
  <button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
82
131
  🔍 Discover
83
132
  </button>
84
- <button id="cancelDiscoverBtn"
85
- style="margin-left: 6px; margin-bottom: 8px; display: none; padding: 6px 10px; font-size: 11px" class="secondary">
86
- Cancel
87
- </button>
88
133
  <div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
89
134
  <div id="discoverDevicesFound" style="margin-bottom: 6px; font-size: 11px; display: none"></div>
90
135
  <div id="discoverPhaseProgress" class="discover-phase-progress" style="display: none">
@@ -208,8 +253,18 @@
208
253
  </form>
209
254
  </div>
210
255
 
256
+ </div> <!-- end dynamicUiContainer -->
257
+
258
+ <div id="legacyViewContainer" style="display: none;">
259
+ <div class="card compact">
260
+ <h2>Legacy Config View</h2>
261
+ <div id="legacyConfigForm"></div>
262
+ </div>
263
+ </div>
264
+
211
265
  <script type="module" src="js/app.js"></script>
212
266
  <script type="module" src="js/advanced-settings.js"></script>
267
+ <script type="module" src="js/legacy-view.js"></script>
213
268
  </body>
214
269
 
215
270
  </html>
@@ -46,10 +46,11 @@ async function saveAdvancedSettings(): Promise<void> {
46
46
  if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
47
47
  throw new Error('No plugin config blocks returned from Homebridge')
48
48
  }
49
- // Find the SwitchBot config block
50
- const idx = pluginConfigBlocks.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
49
+ // Find or add the SwitchBot config block
50
+ let idx = pluginConfigBlocks.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
51
51
  if (idx === -1) {
52
- throw new Error('SwitchBot config not found')
52
+ pluginConfigBlocks.push({ platform: 'SwitchBot' })
53
+ idx = pluginConfigBlocks.length - 1
53
54
  }
54
55
  const config = pluginConfigBlocks[idx]
55
56
  // Update config values from UI
@@ -1,5 +1,6 @@
1
1
  // Fetch the list of configured devices from the Homebridge UI API
2
2
  import { isValidDeviceType, normalizeDeviceType } from '../../../device-types.js'
3
+ import { isV4Config } from '../../utils/v4-detection.js'
3
4
  import './types.js'
4
5
  import { uiLog } from './logger.js'
5
6
  import { toastError } from './toast.js'
@@ -22,6 +23,35 @@ export async function fetchDevices(): Promise<any[]> {
22
23
  }
23
24
  }
24
25
 
26
+ /**
27
+ * Detect whether the current plugin config is in the legacy v4 format.
28
+ *
29
+ * v4 configs stored credentials under a `credentials` sub-object and devices
30
+ * under `options.devices`. v5+ flattened these to `openApiToken`,
31
+ * `openApiSecret`, and a root-level `devices` array.
32
+ *
33
+ * Returns true when a v4 config block is found, false otherwise.
34
+ */
35
+ export async function detectV4Config(): Promise<boolean> {
36
+ try {
37
+ if (typeof homebridge.getPluginConfig !== 'function') {
38
+ return false
39
+ }
40
+ const configArr = await homebridge.getPluginConfig()
41
+ if (!Array.isArray(configArr)) {
42
+ return false
43
+ }
44
+ const config = configArr.find(isSwitchBotPlatformConfig)
45
+ if (!config || typeof config !== 'object') {
46
+ return false
47
+ }
48
+ return isV4Config(config)
49
+ } catch (e) {
50
+ uiLog.warn('Error detecting v4 config:', e)
51
+ return false
52
+ }
53
+ }
54
+
25
55
  /**
26
56
  * Validate and auto-correct device types in the config array before saving.
27
57
  * Returns an array of errors for devices that cannot be fixed.
@@ -270,7 +300,22 @@ export async function addDevicesInBulk(
270
300
  if (!resp || resp.success === false) {
271
301
  throw new Error(resp?.data?.message || 'Bulk add failed')
272
302
  }
273
- return resp.data || resp
303
+ return normalizeBulkAddDevicesResponse(resp)
304
+ }
305
+
306
+ export function normalizeBulkAddDevicesResponse(resp: any): any {
307
+ const payload = resp?.data && typeof resp.data === 'object' ? resp.data : resp
308
+ const addedCount = Number(payload?.addedCount ?? payload?.added ?? 0)
309
+ const skippedCount = Number(payload?.skippedCount ?? payload?.skipped ?? 0)
310
+ const updatedCount = Number(payload?.updatedCount ?? payload?.updated ?? 0)
311
+
312
+ return {
313
+ ...payload,
314
+ success: resp?.success ?? true,
315
+ addedCount,
316
+ skippedCount,
317
+ updatedCount,
318
+ }
274
319
  }
275
320
 
276
321
  export async function updateDevice(
@@ -1,13 +1,56 @@
1
+ import { detectV4Config } from './api.js'
1
2
  import { loadCredentialStatus, saveCredentials } from './credentials.js'
2
3
  import { initRemoveAllButton, loadConfiguredDevices } from './devices.js'
3
- import { discoverDevices, initializeDiscoverySettings } from './discovery.js';
4
+ import { discoverDevices, initializeDiscoverySettings } from './discovery.js'
4
5
 
5
- (window as any).loadCredentialStatus = loadCredentialStatus;
6
- (window as any).saveCredentials = saveCredentials;
7
- (window as any).discoverDevices = discoverDevices
6
+ // Ensure SwitchBot config block exists before any UI actions
7
+ async function ensureSwitchBotConfigBlock(): Promise<void> {
8
+ try {
9
+ if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
10
+ return
11
+ }
12
+ let configArr = await homebridge.getPluginConfig()
13
+ if (!Array.isArray(configArr)) {
14
+ configArr = []
15
+ }
16
+ const idx = configArr.findIndex(c => (c.platform || c.name || '').toLowerCase().includes('switchbot'))
17
+ if (idx === -1) {
18
+ configArr.push({ platform: 'SwitchBot' })
19
+ await homebridge.updatePluginConfig(configArr)
20
+ if (typeof homebridge.savePluginConfig === 'function') {
21
+ await homebridge.savePluginConfig()
22
+ }
23
+ }
24
+ } catch (_e) {
25
+ // Ignore errors
26
+ }
27
+ }
28
+
29
+ ;
30
+
31
+ (window as any).loadCredentialStatus = loadCredentialStatus
32
+ ;(window as any).saveCredentials = saveCredentials
33
+ ;(window as any).discoverDevices = discoverDevices
34
+
35
+ /**
36
+ * Show or hide the v4 config incompatibility warning banner.
37
+ */
38
+ async function checkV4Config(): Promise<void> {
39
+ const banner = document.getElementById('v4ConfigWarning')
40
+ if (!banner) {
41
+ return
42
+ }
43
+ const isV4 = await detectV4Config()
44
+ if (isV4) {
45
+ banner.style.display = 'block'
46
+ }
47
+ }
8
48
 
9
49
  // Initialize on page load
50
+
10
51
  async function init(): Promise<void> {
52
+ await ensureSwitchBotConfigBlock()
53
+ await checkV4Config()
11
54
  await loadCredentialStatus()
12
55
  await initializeDiscoverySettings()
13
56
  await loadConfiguredDevices()
@@ -63,14 +63,20 @@ export async function saveCredentials(): Promise<void> {
63
63
  if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
64
64
  throw new TypeError('Homebridge UI API not available')
65
65
  }
66
- const configArr = await homebridge.getPluginConfig()
67
- if (!Array.isArray(configArr) || configArr.length === 0) {
68
- throw new Error('No plugin config found')
66
+ let configArr = await homebridge.getPluginConfig()
67
+ if (!Array.isArray(configArr)) {
68
+ configArr = []
69
+ }
70
+ let idx = configArr.findIndex(c => (c.platform || c.name || '').toLowerCase() === 'switchbot')
71
+ if (idx === -1) {
72
+ // Add new SwitchBot platform config
73
+ configArr.push({ platform: 'SwitchBot' })
74
+ idx = configArr.length - 1
69
75
  }
70
- const config = configArr[0]
76
+ const config = configArr[idx]
71
77
  config.openApiToken = token
72
78
  config.openApiSecret = secret
73
- await homebridge.updatePluginConfig([config])
79
+ await homebridge.updatePluginConfig(configArr)
74
80
  if (typeof homebridge.savePluginConfig === 'function') {
75
81
  await homebridge.savePluginConfig()
76
82
  }
@@ -394,7 +394,6 @@ function setDiscoveryGroupExpanded(groupKey: string, expanded: boolean): void {
394
394
 
395
395
  export async function discoverDevices(): Promise<void> {
396
396
  const btn = document.getElementById('discoverBtn') as HTMLButtonElement
397
- const cancelBtn = document.getElementById('cancelDiscoverBtn') as HTMLButtonElement | null
398
397
  const status = document.getElementById('discoverStatus')
399
398
  const phaseProgress = document.getElementById('discoverPhaseProgress') as HTMLElement | null
400
399
  const phaseFill = document.getElementById('discoverPhaseFill') as HTMLElement | null
@@ -424,7 +423,6 @@ export async function discoverDevices(): Promise<void> {
424
423
  const startedAt = Date.now()
425
424
  let phaseStartedAt = startedAt
426
425
  let phase = 'Preparing discovery...'
427
- let cancelled = false
428
426
 
429
427
  // --- Real-time RSSI polling additions ---
430
428
  // (bleScanDurationSeconds is now only used in bleSettings below)
@@ -936,32 +934,8 @@ export async function discoverDevices(): Promise<void> {
936
934
  devicesFoundDisplay.classList.remove('discovery-scanning-pulse')
937
935
  }
938
936
 
939
- if (cancelBtn) {
940
- cancelBtn.style.display = 'inline-block'
941
- cancelBtn.disabled = false
942
- cancelBtn.onclick = () => {
943
- cancelled = true
944
- const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000))
945
- status.textContent = `Discovery cancelled (${totalSeconds}s total)`
946
- if (phaseProgress) {
947
- phaseProgress.style.display = 'none'
948
- }
949
- if (devicesFoundDisplay) {
950
- devicesFoundDisplay.style.display = 'none'
951
- devicesFoundDisplay.classList.remove('discovery-scanning-pulse')
952
- }
953
- cancelBtn.style.display = 'none'
954
- btn.disabled = false
955
- btn.textContent = '🔍 Discover Devices'
956
- hideBusyUi()
957
- }
958
- }
959
-
960
937
  if (bleSettings.bleEnabled) {
961
938
  const bleDevicesRaw = await apiDiscoverDevices('ble', bleSettings)
962
- if (cancelled) {
963
- return
964
- }
965
939
 
966
940
  discoveredDevices = dedupeById(bleDevicesRaw)
967
941
  uiLog.info('BLE discover response:', bleDevicesRaw)
@@ -988,9 +962,6 @@ export async function discoverDevices(): Promise<void> {
988
962
 
989
963
  try {
990
964
  const openApiDevicesRaw = await apiDiscoverDevices('openapi')
991
- if (cancelled) {
992
- return
993
- }
994
965
 
995
966
  uiLog.info('OpenAPI discover response:', openApiDevicesRaw)
996
967
  discoveredDevices = mergeDiscoveredDevices(discoveredDevices, openApiDevicesRaw)
@@ -1096,9 +1067,6 @@ export async function discoverDevices(): Promise<void> {
1096
1067
  setDiscoveryCache(discoveredDevices)
1097
1068
  updateLastScannedStatus()
1098
1069
  } catch (e) {
1099
- if (cancelled) {
1100
- return
1101
- }
1102
1070
  uiLog.error('Discovery error:', e)
1103
1071
  status.textContent = `Error: ${e instanceof Error ? e.message : 'Discovery failed'}`
1104
1072
  status.classList.add('error')
@@ -1123,11 +1091,6 @@ export async function discoverDevices(): Promise<void> {
1123
1091
  }
1124
1092
  btn.disabled = false
1125
1093
  btn.textContent = '🔍 Discover Devices'
1126
- if (cancelBtn) {
1127
- cancelBtn.disabled = false
1128
- cancelBtn.style.display = 'none'
1129
- cancelBtn.onclick = null
1130
- }
1131
1094
  }
1132
1095
  }
1133
1096
 
@@ -0,0 +1,152 @@
1
+ // --- Inlined legacy view logic to avoid ES module import/export issues ---
2
+
3
+ let legacySchema = null
4
+ async function loadLegacySchema() {
5
+ if (legacySchema) {
6
+ return legacySchema
7
+ }
8
+ const resp = await fetch('config.schema.json')
9
+ if (!resp.ok) {
10
+ throw new Error('Failed to load config.schema.json')
11
+ }
12
+ legacySchema = await resp.json()
13
+ return legacySchema
14
+ }
15
+
16
+ async function renderLegacyView() {
17
+ const container = document.getElementById('legacyConfigForm')
18
+ if (!container) {
19
+ const legacyContainer = document.getElementById('legacyViewContainer')
20
+ if (legacyContainer) {
21
+ legacyContainer.innerHTML = '<div style="color:red;font-weight:bold">[SwitchBot UI] Error: legacyConfigForm container not found</div>'
22
+ }
23
+ console.error('[SwitchBot UI] Error: legacyConfigForm container not found')
24
+ return
25
+ }
26
+ container.innerHTML = ''
27
+ const schema = await loadLegacySchema()
28
+ const { properties } = schema.schema
29
+ const layout = schema.layout || []
30
+ const form = document.createElement('form')
31
+ for (const section of layout) {
32
+ const fieldset = document.createElement('fieldset')
33
+ if (section.title) {
34
+ const legend = document.createElement('legend')
35
+ legend.textContent = section.title
36
+ fieldset.appendChild(legend)
37
+ }
38
+ for (const key of section.items) {
39
+ const prop = properties[key]
40
+ if (!prop) {
41
+ continue
42
+ }
43
+ if (prop.type === 'array' && prop.items && prop.items.properties) {
44
+ // Render array of devices as a table
45
+ const table = document.createElement('table')
46
+ table.style.marginBottom = '12px'
47
+ const thead = document.createElement('thead')
48
+ const headerRow = document.createElement('tr')
49
+ for (const colKey of Object.keys(prop.items.properties)) {
50
+ const th = document.createElement('th')
51
+ th.textContent = prop.items.properties[colKey].title || colKey
52
+ headerRow.appendChild(th)
53
+ }
54
+ thead.appendChild(headerRow)
55
+ table.appendChild(thead)
56
+ // No data rows (read-only legacy view)
57
+ fieldset.appendChild(table)
58
+ continue
59
+ }
60
+ const label = document.createElement('label')
61
+ label.textContent = prop.title || key
62
+ label.style.display = 'block'
63
+ let input
64
+ if (prop.enum) {
65
+ const select = document.createElement('select')
66
+ select.name = key
67
+ for (const opt of prop.enum) {
68
+ const option = document.createElement('option')
69
+ option.value = opt
70
+ option.textContent = opt
71
+ select.appendChild(option)
72
+ }
73
+ input = select
74
+ } else if (prop.type === 'boolean') {
75
+ const checkbox = document.createElement('input')
76
+ checkbox.type = 'checkbox'
77
+ checkbox.name = key
78
+ if (prop.default) {
79
+ checkbox.checked = true
80
+ }
81
+ input = checkbox
82
+ } else {
83
+ const text = document.createElement('input')
84
+ text.name = key
85
+ text.type = prop.format === 'password' ? 'password' : 'text'
86
+ if (prop.default) {
87
+ text.value = prop.default
88
+ }
89
+ input = text
90
+ }
91
+ label.appendChild(input)
92
+ fieldset.appendChild(label)
93
+ }
94
+ form.appendChild(fieldset)
95
+ }
96
+ container.appendChild(form)
97
+ }
98
+
99
+ async function enableLegacyView() {
100
+ document.getElementById('dynamicUiContainer').style.display = 'none'
101
+ document.getElementById('legacyViewContainer').style.display = 'block'
102
+ await renderLegacyView()
103
+ }
104
+
105
+ function disableLegacyView() {
106
+ document.getElementById('dynamicUiContainer').style.display = 'block'
107
+ document.getElementById('legacyViewContainer').style.display = 'none'
108
+ }
109
+
110
+ // Toggle button visibility and view containers
111
+ function updateViewButtons() {
112
+ const legacyBtn = document.getElementById('legacyViewBtn')
113
+ const dynamicBtn = document.getElementById('dynamicViewBtn')
114
+ const legacyContainer = document.getElementById('legacyViewContainer')
115
+ if (legacyContainer && legacyContainer.style.display === 'block') {
116
+ if (legacyBtn) {
117
+ legacyBtn.style.display = 'none'
118
+ }
119
+ if (dynamicBtn) {
120
+ dynamicBtn.style.display = 'inline-block'
121
+ }
122
+ } else {
123
+ if (legacyBtn) {
124
+ legacyBtn.style.display = 'inline-block'
125
+ }
126
+ if (dynamicBtn) {
127
+ dynamicBtn.style.display = 'none'
128
+ }
129
+ }
130
+ }
131
+
132
+ document.addEventListener('DOMContentLoaded', () => {
133
+ const legacyBtn = document.getElementById('legacyViewBtn')
134
+ const dynamicBtn = document.getElementById('dynamicViewBtn')
135
+ if (legacyBtn) {
136
+ legacyBtn.addEventListener('click', () => {
137
+ // eslint-disable-next-line no-console
138
+ console.log('[SwitchBot UI] Legacy View button clicked')
139
+ enableLegacyView()
140
+ updateViewButtons()
141
+ })
142
+ }
143
+ if (dynamicBtn) {
144
+ dynamicBtn.addEventListener('click', () => {
145
+ // eslint-disable-next-line no-console
146
+ console.log('[SwitchBot UI] Dynamic View button clicked')
147
+ disableLegacyView()
148
+ updateViewButtons()
149
+ })
150
+ }
151
+ updateViewButtons()
152
+ })
@@ -0,0 +1,107 @@
1
+ // legacy-view.ts
2
+ // Renders the config UI from config.schema.json and toggles between legacy and dynamic UI
3
+
4
+ import schema from '../../../../config.schema.json'
5
+
6
+ function renderLegacyView() {
7
+ const container = document.getElementById('legacyConfigForm')
8
+ if (!container) {
9
+ const legacyContainer = document.getElementById('legacyViewContainer')
10
+ if (legacyContainer) {
11
+ legacyContainer.innerHTML = '<div style="color:red;font-weight:bold">[SwitchBot UI] Error: legacyConfigForm container not found</div>'
12
+ }
13
+ console.error('[SwitchBot UI] Error: legacyConfigForm container not found')
14
+ return
15
+ }
16
+ container.innerHTML = ''
17
+ const { properties } = schema.schema
18
+ const layout = schema.layout || []
19
+ const form = document.createElement('form')
20
+ for (const section of layout) {
21
+ const fieldset = document.createElement('fieldset')
22
+ if (section.title) {
23
+ const legend = document.createElement('legend')
24
+ legend.textContent = section.title
25
+ fieldset.appendChild(legend)
26
+ }
27
+ for (const key of section.items) {
28
+ const prop = properties[key]
29
+ if (!prop) {
30
+ continue
31
+ }
32
+ if (prop.type === 'array' && prop.items && prop.items.properties) {
33
+ // Render array of devices as a table
34
+ const table = document.createElement('table')
35
+ table.style.marginBottom = '12px'
36
+ const thead = document.createElement('thead')
37
+ const headerRow = document.createElement('tr')
38
+ for (const colKey of Object.keys(prop.items.properties)) {
39
+ const th = document.createElement('th')
40
+ th.textContent = prop.items.properties[colKey].title || colKey
41
+ headerRow.appendChild(th)
42
+ }
43
+ thead.appendChild(headerRow)
44
+ table.appendChild(thead)
45
+ // No data rows (read-only legacy view)
46
+ fieldset.appendChild(table)
47
+ continue
48
+ }
49
+ const label = document.createElement('label')
50
+ label.textContent = prop.title || key
51
+ label.style.display = 'block'
52
+ let input: HTMLElement
53
+ if (prop.enum) {
54
+ const select = document.createElement('select')
55
+ select.name = key
56
+ for (const opt of prop.enum) {
57
+ const option = document.createElement('option')
58
+ option.value = opt
59
+ option.textContent = opt
60
+ select.appendChild(option)
61
+ }
62
+ input = select
63
+ } else if (prop.type === 'boolean') {
64
+ const checkbox = document.createElement('input')
65
+ checkbox.type = 'checkbox'
66
+ checkbox.name = key
67
+ if (prop.default) {
68
+ checkbox.checked = true
69
+ }
70
+ input = checkbox
71
+ } else {
72
+ const text = document.createElement('input')
73
+ text.name = key
74
+ text.type = prop.format === 'password' ? 'password' : 'text'
75
+ if (prop.default) {
76
+ text.value = prop.default
77
+ }
78
+ input = text
79
+ }
80
+ label.appendChild(input)
81
+ fieldset.appendChild(label)
82
+ }
83
+ form.appendChild(fieldset)
84
+ }
85
+ container.appendChild(form)
86
+ }
87
+
88
+ export function enableLegacyView() {
89
+ document.getElementById('dynamicUiContainer')!.style.display = 'none'
90
+ document.getElementById('legacyViewContainer')!.style.display = 'block'
91
+ renderLegacyView()
92
+ }
93
+
94
+ // DEBUG: assign to window for browser access
95
+ // eslint-disable-next-line ts/ban-ts-comment
96
+ // @ts-ignore
97
+ window.enableLegacyView = enableLegacyView
98
+ // eslint-disable-next-line ts/ban-ts-comment
99
+ // @ts-ignore
100
+ window.disableLegacyView = disableLegacyView
101
+
102
+ export function disableLegacyView() {
103
+ document.getElementById('dynamicUiContainer')!.style.display = 'block'
104
+ document.getElementById('legacyViewContainer')!.style.display = 'none'
105
+ }
106
+
107
+ // (window assignment removed; handled in .js entry point)