@switchbot/homebridge-switchbot 5.0.0-beta.111 → 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 +209 -16
- package/dist/homebridge-ui/public/index.html +87 -56
- 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 +1604 -143
- 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 +1244 -124
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1344 -142
- 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 +389 -36
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +421 -38
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +3 -4
- 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 +9 -8
- 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 +209 -16
- package/src/homebridge-ui/public/index.html +87 -56
- 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 +1344 -142
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +421 -38
- package/src/platform.ts +3 -4
- package/src/switchbotClient.ts +44 -9
- package/test/e2e/run-e2e.spec.ts +12 -10
- package/test/helpers/matter-harness.ts +2 -2
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +313 -0
- package/todo/TODO.md +80 -66
- package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
|
@@ -3,6 +3,7 @@ import type { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
|
|
|
3
3
|
import { RequestError } from '@homebridge/plugin-ui-utils'
|
|
4
4
|
import fs from 'node:fs/promises'
|
|
5
5
|
|
|
6
|
+
import { SwitchBotClient } from '../../switchbotClient.js'
|
|
6
7
|
import { getAllDevices, getDeviceArrays, getSwitchBotPlatformConfig } from '../utils/config-parser.js'
|
|
7
8
|
import { uiLog } from '../utils/logger.js'
|
|
8
9
|
|
|
@@ -10,12 +11,90 @@ import { uiLog } from '../utils/logger.js'
|
|
|
10
11
|
* Register device CRUD endpoints
|
|
11
12
|
*/
|
|
12
13
|
export function registerDeviceEndpoints(server: HomebridgePluginUiServer) {
|
|
14
|
+
/**
|
|
15
|
+
* POST /test-connection - Test connectivity and basic read for a device
|
|
16
|
+
*/
|
|
17
|
+
server.onRequest('/test-connection', async (body: any) => {
|
|
18
|
+
let client: SwitchBotClient | null = null
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const deviceId = String(body?.deviceId || '').trim()
|
|
22
|
+
if (!deviceId) {
|
|
23
|
+
throw new Error('Device ID is required')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { platform } = await getSwitchBotPlatformConfig(server)
|
|
27
|
+
const allDevices = getAllDevices(platform)
|
|
28
|
+
const normalizedDeviceId = deviceId.toLowerCase()
|
|
29
|
+
const configuredDevice = allDevices.find((d: any) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId)
|
|
30
|
+
|
|
31
|
+
const startedAt = Date.now()
|
|
32
|
+
|
|
33
|
+
client = new SwitchBotClient({
|
|
34
|
+
...(platform as any),
|
|
35
|
+
logger: uiLog,
|
|
36
|
+
} as any)
|
|
37
|
+
|
|
38
|
+
await client.init()
|
|
39
|
+
const raw = await client.getDevice(deviceId)
|
|
40
|
+
const latencyMs = Date.now() - startedAt
|
|
41
|
+
|
|
42
|
+
const state = raw?.body ?? raw
|
|
43
|
+
const stateConnection = String(state?.connectionType || state?.source || body?.connectionType || '').toLowerCase()
|
|
44
|
+
const method = stateConnection.includes('ble')
|
|
45
|
+
? 'BLE'
|
|
46
|
+
: stateConnection.includes('api')
|
|
47
|
+
? 'OpenAPI'
|
|
48
|
+
: 'Auto'
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
data: {
|
|
53
|
+
success: true,
|
|
54
|
+
deviceId,
|
|
55
|
+
method,
|
|
56
|
+
latencyMs,
|
|
57
|
+
message: configuredDevice
|
|
58
|
+
? `Connected to "${configuredDevice.configDeviceName || configuredDevice.deviceId || deviceId}"`
|
|
59
|
+
: 'Connected successfully',
|
|
60
|
+
state: {
|
|
61
|
+
online: state?.online,
|
|
62
|
+
power: state?.power,
|
|
63
|
+
battery: state?.battery,
|
|
64
|
+
version: state?.version,
|
|
65
|
+
deviceType: state?.deviceType,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
const message = e instanceof Error ? e.message : String(e)
|
|
71
|
+
uiLog.warn(`POST /test-connection failed: ${message}`)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
data: {
|
|
76
|
+
success: false,
|
|
77
|
+
deviceId: String(body?.deviceId || ''),
|
|
78
|
+
method: 'Auto',
|
|
79
|
+
latencyMs: 0,
|
|
80
|
+
message,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
try {
|
|
85
|
+
await client?.destroy()
|
|
86
|
+
} catch (_e) {
|
|
87
|
+
// Ignore client shutdown errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
13
92
|
/**
|
|
14
93
|
* POST /add-device - Add a single device to config
|
|
15
94
|
*/
|
|
16
95
|
server.onRequest('/add-device', async (body: any) => {
|
|
17
96
|
try {
|
|
18
|
-
const { deviceId, name, type, address, model, rssi } = body
|
|
97
|
+
const { deviceId, name, type, address, model, rssi, encryptionKey, keyId } = body
|
|
19
98
|
|
|
20
99
|
if (!deviceId) {
|
|
21
100
|
throw new Error('Device ID is required')
|
|
@@ -55,6 +134,8 @@ export function registerDeviceEndpoints(server: HomebridgePluginUiServer) {
|
|
|
55
134
|
if (address) { newDevice.address = address }
|
|
56
135
|
if (model) { newDevice.model = model }
|
|
57
136
|
if (rssi !== undefined && rssi !== null && rssi !== 0) { newDevice.rssi = rssi }
|
|
137
|
+
if (encryptionKey) { newDevice.encryptionKey = encryptionKey }
|
|
138
|
+
if (keyId) { newDevice.keyId = keyId }
|
|
58
139
|
ref.push(newDevice)
|
|
59
140
|
addCount++
|
|
60
141
|
}
|
|
@@ -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.'
|
|
@@ -75,12 +75,13 @@ body {
|
|
|
75
75
|
system-ui,
|
|
76
76
|
-apple-system,
|
|
77
77
|
Arial;
|
|
78
|
-
padding:
|
|
78
|
+
padding: 10px;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
h1 {
|
|
82
|
-
font-size:
|
|
82
|
+
font-size: 18px;
|
|
83
83
|
margin-top: 0;
|
|
84
|
+
margin-bottom: 10px;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
h2 {
|
|
@@ -152,25 +153,25 @@ code {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
.form-group {
|
|
155
|
-
margin-bottom:
|
|
156
|
+
margin-bottom: 10px;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
label {
|
|
159
160
|
display: block;
|
|
160
|
-
margin-bottom:
|
|
161
|
+
margin-bottom: 4px;
|
|
161
162
|
font-weight: 500;
|
|
162
|
-
font-size:
|
|
163
|
+
font-size: 11px;
|
|
163
164
|
color: #6b7280;
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
input {
|
|
167
168
|
width: 100%;
|
|
168
169
|
max-width: 400px;
|
|
169
|
-
padding: 8px
|
|
170
|
+
padding: 6px 8px;
|
|
170
171
|
border: 1px solid #d1d5db;
|
|
171
172
|
border-radius: 6px;
|
|
172
173
|
font-family: system-ui, -apple-system, monospace;
|
|
173
|
-
font-size:
|
|
174
|
+
font-size: 12px;
|
|
174
175
|
transition: all 0.2s ease;
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -203,8 +204,8 @@ select:focus {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
.status {
|
|
206
|
-
font-size:
|
|
207
|
-
margin-top:
|
|
207
|
+
font-size: 11px;
|
|
208
|
+
margin-top: 3px;
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
.status.ok {
|
|
@@ -221,27 +222,207 @@ select:focus {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
.card {
|
|
224
|
-
padding:
|
|
225
|
+
padding: 12px;
|
|
225
226
|
border-radius: 8px;
|
|
226
|
-
margin-bottom:
|
|
227
|
+
margin-bottom: 10px;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
.
|
|
230
|
-
padding: 10px
|
|
231
|
-
|
|
230
|
+
.card.compact {
|
|
231
|
+
padding: 10px;
|
|
232
|
+
margin-bottom: 10px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.card.compact h2 {
|
|
236
|
+
margin-top: 0;
|
|
237
|
+
margin-bottom: 6px;
|
|
238
|
+
font-size: 13px;
|
|
239
|
+
font-weight: 600;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.card.compact .form-group {
|
|
232
243
|
margin-bottom: 6px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.card.compact label {
|
|
247
|
+
font-size: 11px;
|
|
248
|
+
margin-bottom: 2px;
|
|
249
|
+
display: block;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.card.compact input {
|
|
253
|
+
padding: 5px 8px;
|
|
254
|
+
font-size: 12px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.card.compact button {
|
|
258
|
+
padding: 6px 12px;
|
|
259
|
+
font-size: 12px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.card.compact .status {
|
|
263
|
+
font-size: 11px;
|
|
264
|
+
margin-top: 4px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.credentials-row {
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-wrap: wrap;
|
|
270
|
+
gap: 8px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.credentials-row .form-group {
|
|
274
|
+
flex: 1 1 100%;
|
|
275
|
+
min-width: 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@media (min-width: 700px) {
|
|
279
|
+
.credentials-row {
|
|
280
|
+
gap: 10px;
|
|
281
|
+
align-items: start;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.credentials-row .form-group {
|
|
285
|
+
flex: 1 1 calc(50% - 5px);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.device-item {
|
|
290
|
+
padding: 5px 8px;
|
|
291
|
+
border-radius: 5px;
|
|
292
|
+
margin-bottom: 0;
|
|
233
293
|
display: flex;
|
|
234
|
-
gap:
|
|
294
|
+
gap: 8px;
|
|
235
295
|
align-items: center;
|
|
236
296
|
justify-content: space-between;
|
|
237
297
|
transition: all 0.2s ease;
|
|
298
|
+
height: 100%;
|
|
299
|
+
min-width: 0;
|
|
238
300
|
}
|
|
239
301
|
|
|
240
302
|
.device-item:hover {
|
|
241
|
-
transform: translateY(-
|
|
303
|
+
transform: translateY(-0.5px);
|
|
242
304
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
243
305
|
}
|
|
244
306
|
|
|
307
|
+
.device-actions {
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-wrap: wrap;
|
|
310
|
+
gap: 6px;
|
|
311
|
+
}
|
|
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
|
+
|
|
399
|
+
#devices.device-grid,
|
|
400
|
+
#discoveredDevices.device-grid,
|
|
401
|
+
ul.device-grid {
|
|
402
|
+
display: grid;
|
|
403
|
+
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
|
404
|
+
gap: 6px;
|
|
405
|
+
padding-left: 0;
|
|
406
|
+
list-style: none;
|
|
407
|
+
margin: 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.device-item code {
|
|
411
|
+
display: inline-block;
|
|
412
|
+
max-width: 100%;
|
|
413
|
+
white-space: normal;
|
|
414
|
+
overflow-wrap: anywhere;
|
|
415
|
+
word-break: break-word;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@media (max-width: 900px) {
|
|
419
|
+
.device-actions {
|
|
420
|
+
width: 100%;
|
|
421
|
+
justify-content: flex-start !important;
|
|
422
|
+
margin-left: 0 !important;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
245
426
|
.card {
|
|
246
427
|
padding: 16px;
|
|
247
428
|
border-radius: 8px;
|
|
@@ -252,3 +433,15 @@ select:focus {
|
|
|
252
433
|
.card:hover {
|
|
253
434
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
254
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
|
+
}
|