@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.
- package/.github/copilot-instructions.md +63 -0
- package/.github/scripts/normalize-changelog-header.cjs +86 -0
- package/.github/scripts/npm-version-script-esm-auto.js +201 -0
- package/.github/scripts/resolve-version-bump-esm.js +118 -0
- package/.github/workflows/release.yml +62 -13
- package/CHANGELOG.md +18 -0
- package/dist/{SwitchBotHAPPlatform.d.ts → Platform.HAP.d.ts} +2 -2
- package/dist/Platform.HAP.d.ts.map +1 -0
- package/dist/{SwitchBotHAPPlatform.js → Platform.HAP.js} +4 -2
- package/dist/Platform.HAP.js.map +1 -0
- package/dist/{SwitchBotMatterPlatform.d.ts → Platform.Matter.d.ts} +3 -2
- package/dist/Platform.Matter.d.ts.map +1 -0
- package/dist/{SwitchBotMatterPlatform.js → Platform.Matter.js} +24 -1
- package/dist/Platform.Matter.js.map +1 -0
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/config.js +9 -2
- package/dist/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +9 -2
- package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +8 -0
- package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +23 -0
- package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +1 -0
- package/dist/homebridge-ui/public/css/styles.css +62 -2
- package/dist/homebridge-ui/public/index.html +61 -6
- package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/advanced-settings.js +4 -3
- package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -1
- package/dist/homebridge-ui/public/js/advanced-settings.ts +4 -3
- package/dist/homebridge-ui/public/js/api.d.ts +11 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +44 -1
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +46 -1
- package/dist/homebridge-ui/public/js/app.js +241 -182
- package/dist/homebridge-ui/public/js/app.js.map +4 -4
- package/dist/homebridge-ui/public/js/app.ts +47 -4
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.js +11 -5
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.ts +11 -5
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +0 -36
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +0 -37
- package/dist/homebridge-ui/public/js/legacy-view.d.ts +3 -0
- package/dist/homebridge-ui/public/js/legacy-view.d.ts.map +1 -0
- package/dist/homebridge-ui/public/js/legacy-view.js +152 -0
- package/dist/homebridge-ui/public/js/legacy-view.js.map +1 -0
- package/dist/homebridge-ui/public/js/legacy-view.ts +107 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +38 -31
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +39 -34
- package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/toast.js +2 -3
- package/dist/homebridge-ui/public/js/toast.js.map +1 -1
- package/dist/homebridge-ui/public/js/toast.ts +2 -3
- package/dist/homebridge-ui/utils/config-parser.d.ts +8 -0
- package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
- package/dist/homebridge-ui/utils/config-parser.js +8 -0
- package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
- package/dist/homebridge-ui/utils/v4-detection.d.ts +12 -0
- package/dist/homebridge-ui/utils/v4-detection.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/v4-detection.js +23 -0
- package/dist/homebridge-ui/utils/v4-detection.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/assets/icons.js +1 -1
- package/docs/assets/icons.svg +1 -1
- package/docs/assets/main.js +3 -3
- package/docs/variables/default.html +1 -1
- package/package.json +12 -18
- package/scripts/build-ui.js +30 -0
- package/src/{SwitchBotHAPPlatform.ts → Platform.HAP.ts} +5 -2
- package/src/{SwitchBotMatterPlatform.ts → Platform.Matter.ts} +28 -1
- package/src/homebridge-ui/endpoints/config.ts +10 -2
- package/src/homebridge-ui/public/css/styles.css +62 -2
- package/src/homebridge-ui/public/index.html +59 -4
- package/src/homebridge-ui/public/js/advanced-settings.ts +4 -3
- package/src/homebridge-ui/public/js/api.ts +46 -1
- package/src/homebridge-ui/public/js/app.ts +47 -4
- package/src/homebridge-ui/public/js/credentials.ts +11 -5
- package/src/homebridge-ui/public/js/discovery.ts +0 -37
- package/src/homebridge-ui/public/js/legacy-view.js +152 -0
- package/src/homebridge-ui/public/js/legacy-view.ts +107 -0
- package/src/homebridge-ui/public/js/render.ts +39 -34
- package/src/homebridge-ui/public/js/toast.ts +2 -3
- package/src/homebridge-ui/utils/config-parser.ts +9 -0
- package/src/homebridge-ui/utils/v4-detection.ts +23 -0
- package/src/index.ts +2 -2
- package/test/homebridge-ui/config-parser.spec.ts +61 -0
- package/test/platform/accessory-restore.spec.ts +2 -2
- package/test/platform/matter-childbridge.spec.ts +1 -1
- package/test/platform/matter-integration.spec.ts +51 -1
- package/test/platform/platform-edge.spec.ts +2 -2
- package/test/platform/platform.integration.spec.ts +1 -1
- package/dist/SwitchBotHAPPlatform.d.ts.map +0 -1
- package/dist/SwitchBotHAPPlatform.js.map +0 -1
- package/dist/SwitchBotMatterPlatform.d.ts.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
67
|
-
if (!Array.isArray(configArr)
|
|
68
|
-
|
|
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[
|
|
76
|
+
const config = configArr[idx]
|
|
71
77
|
config.openApiToken = token
|
|
72
78
|
config.openApiSecret = secret
|
|
73
|
-
await homebridge.updatePluginConfig(
|
|
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)
|