@switchbot/homebridge-switchbot 5.0.0-beta.112 → 5.0.0-beta.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +10 -0
- package/README.md +43 -0
- package/TODO.md +263 -0
- package/config.schema.json +27 -10
- package/dist/device-types.d.ts.map +1 -1
- package/dist/device-types.js +93 -63
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -33
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +12 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +121 -1
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +80 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.js +73 -41
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +98 -0
- package/dist/homebridge-ui/public/index.html +57 -0
- package/dist/homebridge-ui/public/js/api.d.ts +24 -1
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +33 -2
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +52 -3
- package/dist/homebridge-ui/public/js/app.js +1503 -104
- package/dist/homebridge-ui/public/js/app.js.map +3 -3
- package/dist/homebridge-ui/public/js/app.ts +2 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +2 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +13 -1
- package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +1223 -126
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1323 -145
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +42 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +50 -1
- package/dist/homebridge-ui/public/js/render.d.ts +3 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +311 -1
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +341 -2
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +6 -2
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +51 -5
- package/dist/switchbotClient.js.map +1 -1
- package/docs/assets/highlight.css +9 -2
- package/docs/index.html +28 -2
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
- package/src/device-types.ts +95 -64
- package/src/deviceFactory.ts +55 -33
- package/src/devices/genericDevice.ts +113 -1
- package/src/errors.ts +43 -0
- package/src/homebridge-ui/endpoints/devices.ts +82 -1
- package/src/homebridge-ui/endpoints/discovery.ts +78 -44
- package/src/homebridge-ui/public/css/styles.css +98 -0
- package/src/homebridge-ui/public/index.html +57 -0
- package/src/homebridge-ui/public/js/api.ts +52 -3
- package/src/homebridge-ui/public/js/app.ts +2 -1
- package/src/homebridge-ui/public/js/devices.ts +13 -1
- package/src/homebridge-ui/public/js/discovery.ts +1323 -145
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +341 -2
- package/src/platform.ts +6 -2
- package/src/switchbotClient.ts +44 -9
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +300 -0
- package/todo/TODO.md +80 -66
|
@@ -10,9 +10,37 @@ import { uiLog } from '../utils/logger.js'
|
|
|
10
10
|
* Discovers SwitchBot devices via BLE and OpenAPI
|
|
11
11
|
*/
|
|
12
12
|
export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
|
|
13
|
-
server.onRequest('/
|
|
13
|
+
server.onRequest('/ble-status', async () => {
|
|
14
14
|
try {
|
|
15
15
|
const { platform } = await getSwitchBotPlatformConfig(server)
|
|
16
|
+
const token = getCredential(platform, 'openApiToken') || platform.token
|
|
17
|
+
const secret = getCredential(platform, 'openApiSecret') || platform.secret
|
|
18
|
+
const { SwitchBot } = await import('node-switchbot')
|
|
19
|
+
const switchbot = new SwitchBot({
|
|
20
|
+
token: token || undefined,
|
|
21
|
+
secret: secret || undefined,
|
|
22
|
+
enableBLE: true,
|
|
23
|
+
enableFallback: true,
|
|
24
|
+
enableRetry: true,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await switchbot.discover({ timeout: 1000 })
|
|
28
|
+
return { success: true, data: { available: true, message: 'adapter ready' } }
|
|
29
|
+
} catch (e) {
|
|
30
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
31
|
+
uiLog.warn(`GET /ble-status failed: ${message}`)
|
|
32
|
+
return { success: true, data: { available: false, message } }
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
server.onRequest('/discover', async (payload?: any) => {
|
|
37
|
+
try {
|
|
38
|
+
const { platform } = await getSwitchBotPlatformConfig(server)
|
|
39
|
+
const mode = String(payload?.mode || 'all').toLowerCase()
|
|
40
|
+
const runBle = mode === 'all' || mode === 'ble'
|
|
41
|
+
const runOpenApi = mode === 'all' || mode === 'openapi'
|
|
42
|
+
const bleScanDurationSeconds = Math.max(3, Math.min(15, Number(payload?.bleScanDurationSeconds || 5)))
|
|
43
|
+
const bleTimeoutSeconds = Math.max(3, Math.min(30, Number(payload?.bleTimeoutSeconds || 8)))
|
|
16
44
|
|
|
17
45
|
uiLog.debug(`GET /discover - Platform config keys: ${Object.keys(platform).join(', ')}`)
|
|
18
46
|
|
|
@@ -40,55 +68,57 @@ export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
|
|
|
40
68
|
const deviceMap = new Map<string, any>()
|
|
41
69
|
|
|
42
70
|
// 1. Try BLE discovery first (with timeout)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
if (runBle) {
|
|
72
|
+
uiLog.info('GET /discover - Starting BLE scan...')
|
|
73
|
+
try {
|
|
74
|
+
const bleTimeout = bleTimeoutSeconds * 1000
|
|
75
|
+
const bleDiscoveryPromise = switchbot.discover({ timeout: bleScanDurationSeconds * 1000 })
|
|
76
|
+
const bleDevices = await Promise.race([
|
|
77
|
+
bleDiscoveryPromise,
|
|
78
|
+
new Promise<any[]>(resolve => setTimeout(resolve, bleTimeout, [])),
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(bleDevices) && bleDevices.length > 0) {
|
|
82
|
+
uiLog.info(`GET /discover - Found ${bleDevices.length} BLE devices`)
|
|
83
|
+
for (const [index, d] of bleDevices.entries()) {
|
|
84
|
+
const info = typeof d?.getInfo === 'function' ? d.getInfo() : undefined
|
|
85
|
+
const id = d?.id
|
|
86
|
+
|| (typeof d?.getId === 'function' ? d.getId() : undefined)
|
|
87
|
+
|| info?.id
|
|
88
|
+
|
|
89
|
+
const mac = d?.mac
|
|
90
|
+
|| (typeof d?.getMAC === 'function' ? d.getMAC() : undefined)
|
|
91
|
+
|| info?.mac
|
|
92
|
+
|
|
93
|
+
if (!id) {
|
|
94
|
+
uiLog.warn(`GET /discover - BLE device at index ${index} has no id, skipping`)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const connectionTypes = Array.isArray(info?.connectionTypes) ? info.connectionTypes : []
|
|
99
|
+
const isHybrid = connectionTypes.includes('api')
|
|
63
100
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
deviceMap.set(id, {
|
|
102
|
+
id,
|
|
103
|
+
name: d?.name || (typeof d?.getName === 'function' ? d.getName() : undefined) || info?.name || d?.deviceName || id,
|
|
104
|
+
type: d?.deviceType || (typeof d?.getDeviceType === 'function' ? d.getDeviceType() : undefined) || info?.deviceType || d?.type || d?.model || 'unknown',
|
|
105
|
+
model: info?.model || d?.model || d?.deviceModel,
|
|
106
|
+
address: mac,
|
|
107
|
+
connectionType: isHybrid ? 'Both' : 'BLE',
|
|
108
|
+
rssi: info?.rssi || d?.rssi,
|
|
109
|
+
})
|
|
67
110
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const isHybrid = connectionTypes.includes('api')
|
|
71
|
-
|
|
72
|
-
deviceMap.set(id, {
|
|
73
|
-
id,
|
|
74
|
-
name: d?.name || (typeof d?.getName === 'function' ? d.getName() : undefined) || info?.name || d?.deviceName || id,
|
|
75
|
-
type: d?.deviceType || (typeof d?.getDeviceType === 'function' ? d.getDeviceType() : undefined) || info?.deviceType || d?.type || d?.model || 'unknown',
|
|
76
|
-
model: info?.model || d?.model || d?.deviceModel,
|
|
77
|
-
address: mac,
|
|
78
|
-
connectionType: isHybrid ? 'Both' : 'BLE',
|
|
79
|
-
rssi: info?.rssi || d?.rssi,
|
|
80
|
-
})
|
|
111
|
+
} else {
|
|
112
|
+
uiLog.info('GET /discover - No BLE devices found or scan timed out')
|
|
81
113
|
}
|
|
82
|
-
}
|
|
83
|
-
uiLog.
|
|
114
|
+
} catch (bleErr) {
|
|
115
|
+
uiLog.warn(`GET /discover - BLE discovery failed: ${bleErr instanceof Error ? bleErr.message : String(bleErr)}`)
|
|
116
|
+
// Continue with OpenAPI even if BLE fails
|
|
84
117
|
}
|
|
85
|
-
} catch (bleErr) {
|
|
86
|
-
uiLog.warn(`GET /discover - BLE discovery failed: ${bleErr instanceof Error ? bleErr.message : String(bleErr)}`)
|
|
87
|
-
// Continue with OpenAPI even if BLE fails
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
// 2. Get devices from OpenAPI (only if credentials are available)
|
|
91
|
-
if (hasOpenAPICredentials) {
|
|
121
|
+
if (runOpenApi && hasOpenAPICredentials) {
|
|
92
122
|
uiLog.info('GET /discover - Fetching devices from OpenAPI...')
|
|
93
123
|
try {
|
|
94
124
|
const apiClient = switchbot.getAPIClient()
|
|
@@ -157,7 +187,7 @@ export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
|
|
|
157
187
|
throw apiErr
|
|
158
188
|
}
|
|
159
189
|
}
|
|
160
|
-
} else {
|
|
190
|
+
} else if (runOpenApi) {
|
|
161
191
|
uiLog.info('GET /discover - Skipping OpenAPI discovery (no credentials configured)')
|
|
162
192
|
}
|
|
163
193
|
|
|
@@ -166,6 +196,10 @@ export function registerDiscoveryEndpoint(server: HomebridgePluginUiServer) {
|
|
|
166
196
|
uiLog.info(`GET /discover - Normalized BLE devices: ${normalizedBleDevices.length} (firstId: ${firstNormalizedBleId})`)
|
|
167
197
|
|
|
168
198
|
// Check if we found any devices
|
|
199
|
+
if (deviceMap.size === 0 && mode === 'openapi' && !hasOpenAPICredentials) {
|
|
200
|
+
return { success: true, data: [] }
|
|
201
|
+
}
|
|
202
|
+
|
|
169
203
|
if (deviceMap.size === 0) {
|
|
170
204
|
const errorMsg = hasOpenAPICredentials
|
|
171
205
|
? 'No devices found via BLE or OpenAPI. Make sure devices are powered on and in range.'
|
|
@@ -310,6 +310,92 @@ select:focus {
|
|
|
310
310
|
gap: 6px;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
.discover-phase-progress {
|
|
314
|
+
margin-bottom: 8px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.discovery-settings-row {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-wrap: wrap;
|
|
320
|
+
gap: 8px;
|
|
321
|
+
align-items: center;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.discover-phase-track {
|
|
325
|
+
width: 100%;
|
|
326
|
+
height: 5px;
|
|
327
|
+
border-radius: 999px;
|
|
328
|
+
background: rgba(148, 163, 184, 0.35);
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.discover-phase-fill {
|
|
333
|
+
width: 0%;
|
|
334
|
+
height: 100%;
|
|
335
|
+
border-radius: 999px;
|
|
336
|
+
background: var(--switchbot-red, #ef4444);
|
|
337
|
+
transition: width 0.25s ease;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.discover-phase-label {
|
|
341
|
+
margin-top: 3px;
|
|
342
|
+
font-size: 10px;
|
|
343
|
+
opacity: 0.75;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.discovery-groups {
|
|
347
|
+
display: block;
|
|
348
|
+
max-height: 400px;
|
|
349
|
+
overflow-y: auto;
|
|
350
|
+
margin-top: 10px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.discovery-group {
|
|
354
|
+
margin-top: 8px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.discovery-group:first-child {
|
|
358
|
+
margin-top: 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.discovery-group-header {
|
|
362
|
+
margin: 0 0 4px;
|
|
363
|
+
font-size: 11px;
|
|
364
|
+
font-weight: 600;
|
|
365
|
+
opacity: 0.85;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.discovery-group-header-btn {
|
|
369
|
+
width: 100%;
|
|
370
|
+
text-align: left;
|
|
371
|
+
margin: 0 0 4px;
|
|
372
|
+
padding: 2px 0;
|
|
373
|
+
border: 0;
|
|
374
|
+
background: transparent;
|
|
375
|
+
color: inherit;
|
|
376
|
+
font-size: 11px;
|
|
377
|
+
font-weight: 600;
|
|
378
|
+
opacity: 0.9;
|
|
379
|
+
cursor: pointer;
|
|
380
|
+
border-radius: 0;
|
|
381
|
+
box-shadow: none;
|
|
382
|
+
transform: none;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.discovery-group-header-btn:hover,
|
|
386
|
+
.discovery-group-header-btn:active {
|
|
387
|
+
background: transparent;
|
|
388
|
+
color: inherit;
|
|
389
|
+
box-shadow: none;
|
|
390
|
+
transform: none;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.discovery-group-empty {
|
|
394
|
+
font-size: 11px;
|
|
395
|
+
opacity: 0.75;
|
|
396
|
+
padding: 4px 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
313
399
|
#devices.device-grid,
|
|
314
400
|
#discoveredDevices.device-grid,
|
|
315
401
|
ul.device-grid {
|
|
@@ -347,3 +433,15 @@ ul.device-grid {
|
|
|
347
433
|
.card:hover {
|
|
348
434
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
349
435
|
}
|
|
436
|
+
@keyframes pulse {
|
|
437
|
+
0%, 100% {
|
|
438
|
+
opacity: 1;
|
|
439
|
+
}
|
|
440
|
+
50% {
|
|
441
|
+
opacity: 0.5;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.discovery-scanning-pulse {
|
|
446
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
447
|
+
}
|
|
@@ -42,10 +42,66 @@
|
|
|
42
42
|
Adds all discovered devices automatically; already configured devices are skipped.
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
|
+
<div class="discovery-settings-row" style="margin-bottom: 2px">
|
|
46
|
+
<label id="bleScanSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
47
|
+
<span>Scan:</span>
|
|
48
|
+
<select id="bleScanDurationSelect" style="font-size: 11px; padding: 2px 6px">
|
|
49
|
+
<option value="3">3s</option>
|
|
50
|
+
<option value="5" selected>5s</option>
|
|
51
|
+
<option value="10">10s</option>
|
|
52
|
+
<option value="15">15s</option>
|
|
53
|
+
</select>
|
|
54
|
+
</label>
|
|
55
|
+
<label id="bleTimeoutSetting" style="display: inline-flex; align-items: center; gap: 4px; font-size: 11px">
|
|
56
|
+
<span>Timeout:</span>
|
|
57
|
+
<input
|
|
58
|
+
id="bleTimeoutInput"
|
|
59
|
+
type="number"
|
|
60
|
+
min="3"
|
|
61
|
+
max="30"
|
|
62
|
+
step="1"
|
|
63
|
+
value="8"
|
|
64
|
+
style="width: 56px; font-size: 11px; padding: 2px 4px"
|
|
65
|
+
/>
|
|
66
|
+
<span>s</span>
|
|
67
|
+
</label>
|
|
68
|
+
<label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
|
|
69
|
+
<input type="checkbox" id="disableBleScanCheckbox" style="width: auto; margin: 0" />
|
|
70
|
+
<span>Disable BLE scan</span>
|
|
71
|
+
</label>
|
|
72
|
+
</div>
|
|
73
|
+
<div id="bluetoothStatus" style="margin-bottom: 4px; font-size: 10px; opacity: 0.8">Bluetooth: checking...</div>
|
|
74
|
+
<div class="discovery-settings-row" style="margin-bottom: 8px">
|
|
75
|
+
<div id="lastScannedStatus" style="font-size: 10px; opacity: 0.8; flex: 1 1 auto">Last scanned: never</div>
|
|
76
|
+
<label style="font-size: 11px">
|
|
77
|
+
Auto-refresh:
|
|
78
|
+
<select id="autoRefreshIntervalSelect" style="margin-left: 4px; font-size: 11px; padding: 2px 6px">
|
|
79
|
+
<option value="0" selected>Off</option>
|
|
80
|
+
<option value="30">30s</option>
|
|
81
|
+
<option value="60">1m</option>
|
|
82
|
+
<option value="300">5m</option>
|
|
83
|
+
</select>
|
|
84
|
+
</label>
|
|
85
|
+
<button id="refreshDiscoverBtn" class="secondary" style="padding: 4px 9px; font-size: 11px">Refresh</button>
|
|
86
|
+
</div>
|
|
45
87
|
<button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
|
|
46
88
|
🔍 Discover
|
|
47
89
|
</button>
|
|
90
|
+
<button
|
|
91
|
+
id="cancelDiscoverBtn"
|
|
92
|
+
style="margin-left: 6px; margin-bottom: 8px; display: none; padding: 6px 10px; font-size: 11px"
|
|
93
|
+
class="secondary"
|
|
94
|
+
>
|
|
95
|
+
Cancel
|
|
96
|
+
</button>
|
|
48
97
|
<div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
|
|
98
|
+
<div id="discoverDevicesFound" style="margin-bottom: 6px; font-size: 11px; display: none"></div>
|
|
99
|
+
<div id="discoverPhaseProgress" class="discover-phase-progress" style="display: none">
|
|
100
|
+
<div class="discover-phase-track">
|
|
101
|
+
<div id="discoverPhaseFill" class="discover-phase-fill"></div>
|
|
102
|
+
</div>
|
|
103
|
+
<div id="discoverPhaseLabel" class="discover-phase-label"></div>
|
|
104
|
+
</div>
|
|
49
105
|
<div id="discoveredList" style="display: none">
|
|
50
106
|
<h3 style="margin-top: 0; font-size: 12px; margin-bottom: 6px; font-weight: 600">Available Devices</h3>
|
|
51
107
|
<ul
|
|
@@ -59,6 +115,7 @@
|
|
|
59
115
|
<h2>Configured Devices</h2>
|
|
60
116
|
<div id="status">Loading…</div>
|
|
61
117
|
<ul id="devices"></ul>
|
|
118
|
+
<!-- BLE encryption key fields will be rendered dynamically per device in the UI (see app.ts/devices.js) -->
|
|
62
119
|
<div
|
|
63
120
|
id="removeAllContainer"
|
|
64
121
|
style="
|
|
@@ -100,9 +100,18 @@ export async function fetchDevices(): Promise<any[]> {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export
|
|
103
|
+
export type DiscoverRequestOptions = {
|
|
104
|
+
bleEnabled?: boolean
|
|
105
|
+
bleScanDurationSeconds?: number
|
|
106
|
+
bleTimeoutSeconds?: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function discoverDevices(
|
|
110
|
+
mode: 'all' | 'ble' | 'openapi' = 'all',
|
|
111
|
+
options?: DiscoverRequestOptions,
|
|
112
|
+
): Promise<any[]> {
|
|
104
113
|
try {
|
|
105
|
-
const resp = await homebridge.request('/discover', {})
|
|
114
|
+
const resp = await homebridge.request('/discover', { mode, ...options })
|
|
106
115
|
uiLog.info('Discover response:', resp)
|
|
107
116
|
|
|
108
117
|
if (!resp || resp.success === false) {
|
|
@@ -115,17 +124,57 @@ export async function discoverDevices(): Promise<any[]> {
|
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
127
|
+
export async function fetchBluetoothStatus(): Promise<{ available: boolean, message: string }> {
|
|
128
|
+
try {
|
|
129
|
+
const resp = await homebridge.request('/ble-status', {})
|
|
130
|
+
if (!resp || resp.success === false) {
|
|
131
|
+
return { available: false, message: 'Bluetooth status unavailable' }
|
|
132
|
+
}
|
|
133
|
+
return resp.data || { available: false, message: 'Bluetooth status unavailable' }
|
|
134
|
+
} catch (_e) {
|
|
135
|
+
return { available: false, message: 'Bluetooth status unavailable' }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function testDeviceConnection(payload: {
|
|
140
|
+
deviceId: string
|
|
141
|
+
connectionType?: string
|
|
142
|
+
address?: string
|
|
143
|
+
}): Promise<{
|
|
144
|
+
success: boolean
|
|
145
|
+
deviceId: string
|
|
146
|
+
method: string
|
|
147
|
+
latencyMs: number
|
|
148
|
+
message: string
|
|
149
|
+
state?: Record<string, any>
|
|
150
|
+
}> {
|
|
151
|
+
const resp = await homebridge.request('/test-connection', payload)
|
|
152
|
+
if (!resp || resp.success === false) {
|
|
153
|
+
throw new Error(resp?.data?.message || 'Connection test failed')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return resp.data || {
|
|
157
|
+
success: false,
|
|
158
|
+
deviceId: payload.deviceId,
|
|
159
|
+
method: 'Auto',
|
|
160
|
+
latencyMs: 0,
|
|
161
|
+
message: 'Connection test failed',
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
118
165
|
export async function addDevice(
|
|
119
166
|
deviceId: string,
|
|
120
167
|
name: string,
|
|
121
168
|
type: string,
|
|
122
|
-
options?: { address?: string, model?: string, rssi?: number },
|
|
169
|
+
options?: { address?: string, model?: string, rssi?: number, encryptionKey?: string, keyId?: string },
|
|
123
170
|
): Promise<any> {
|
|
124
171
|
try {
|
|
125
172
|
const payload: any = { deviceId, name, type }
|
|
126
173
|
if (options?.address) { payload.address = options.address }
|
|
127
174
|
if (options?.model) { payload.model = options.model }
|
|
128
175
|
if (options?.rssi !== undefined && options?.rssi !== null && options?.rssi !== 0) { payload.rssi = options.rssi }
|
|
176
|
+
if (options?.encryptionKey) { payload.encryptionKey = options.encryptionKey }
|
|
177
|
+
if (options?.keyId) { payload.keyId = options.keyId }
|
|
129
178
|
|
|
130
179
|
uiLog.info('Adding device to config:', payload)
|
|
131
180
|
const resp = await homebridge.request('/add-device', payload)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadCredentialStatus, saveCredentials } from './credentials.js'
|
|
2
2
|
import { loadConfiguredDevices, initRemoveAllButton } from './devices.js'
|
|
3
3
|
// Wire up global event handlers
|
|
4
|
-
import { discoverDevices } from './discovery.js';
|
|
4
|
+
import { discoverDevices, initializeDiscoverySettings } from './discovery.js';
|
|
5
5
|
|
|
6
6
|
(window as any).loadCredentialStatus = loadCredentialStatus
|
|
7
7
|
;(window as any).saveCredentials = saveCredentials
|
|
@@ -10,6 +10,7 @@ import { discoverDevices } from './discovery.js';
|
|
|
10
10
|
// Initialize on page load
|
|
11
11
|
async function init(): Promise<void> {
|
|
12
12
|
await loadCredentialStatus()
|
|
13
|
+
await initializeDiscoverySettings()
|
|
13
14
|
const list = await loadConfiguredDevices()
|
|
14
15
|
await initRemoveAllButton()
|
|
15
16
|
}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
|
|
1
2
|
import { addDevice, fetchDevices, syncParentPluginConfigFromDisk } from './api.js'
|
|
2
3
|
import { uiLog } from './logger.js'
|
|
3
4
|
import { hideBusyUi, showBusyUi } from './modal.js'
|
|
4
5
|
import { renderDeviceList } from './render.js'
|
|
5
6
|
import { toastError, toastInfo, toastSuccess, toastWarning } from './toast.js'
|
|
6
7
|
|
|
8
|
+
type ImportDiscoveredDeviceResult = {
|
|
9
|
+
configDeviceName: string
|
|
10
|
+
configDeviceType: string
|
|
11
|
+
address?: string
|
|
12
|
+
encryptionKey?: string
|
|
13
|
+
keyId?: string
|
|
14
|
+
} | null
|
|
15
|
+
|
|
7
16
|
export async function addDeviceToConfig(device: any, options: { refresh?: boolean, showStatus?: boolean } = {}): Promise<{ added: boolean }> {
|
|
8
17
|
const { refresh = true, showStatus = true } = options
|
|
9
18
|
|
|
10
19
|
try {
|
|
11
20
|
const { importDiscoveredDevice } = await import('./modals.js')
|
|
12
|
-
|
|
21
|
+
|
|
22
|
+
const importValues: ImportDiscoveredDeviceResult = await importDiscoveredDevice(device)
|
|
13
23
|
if (!importValues) {
|
|
14
24
|
return { added: false }
|
|
15
25
|
}
|
|
@@ -20,6 +30,8 @@ export async function addDeviceToConfig(device: any, options: { refresh?: boolea
|
|
|
20
30
|
address: importValues.address,
|
|
21
31
|
model: device.model,
|
|
22
32
|
rssi: device.rssi,
|
|
33
|
+
encryptionKey: importValues.encryptionKey,
|
|
34
|
+
keyId: importValues.keyId,
|
|
23
35
|
})
|
|
24
36
|
uiLog.info('Add device response:', resp)
|
|
25
37
|
|