@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.
Files changed (85) hide show
  1. package/MIGRATION.md +10 -0
  2. package/README.md +43 -0
  3. package/TODO.md +263 -0
  4. package/config.schema.json +27 -10
  5. package/dist/device-types.d.ts.map +1 -1
  6. package/dist/device-types.js +93 -63
  7. package/dist/device-types.js.map +1 -1
  8. package/dist/deviceFactory.d.ts.map +1 -1
  9. package/dist/deviceFactory.js +55 -33
  10. package/dist/deviceFactory.js.map +1 -1
  11. package/dist/devices/genericDevice.d.ts +12 -1
  12. package/dist/devices/genericDevice.d.ts.map +1 -1
  13. package/dist/devices/genericDevice.js +121 -1
  14. package/dist/devices/genericDevice.js.map +1 -1
  15. package/dist/errors.d.ts +5 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +51 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  20. package/dist/homebridge-ui/endpoints/devices.js +80 -1
  21. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  22. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  23. package/dist/homebridge-ui/endpoints/discovery.js +73 -41
  24. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  25. package/dist/homebridge-ui/public/css/styles.css +209 -16
  26. package/dist/homebridge-ui/public/index.html +87 -56
  27. package/dist/homebridge-ui/public/js/api.d.ts +24 -1
  28. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  29. package/dist/homebridge-ui/public/js/api.js +33 -2
  30. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  31. package/dist/homebridge-ui/public/js/api.ts +52 -3
  32. package/dist/homebridge-ui/public/js/app.js +1604 -143
  33. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  34. package/dist/homebridge-ui/public/js/app.ts +2 -1
  35. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  36. package/dist/homebridge-ui/public/js/devices.js +2 -0
  37. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  38. package/dist/homebridge-ui/public/js/devices.ts +13 -1
  39. package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
  40. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/discovery.js +1244 -124
  42. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/discovery.ts +1344 -142
  44. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  45. package/dist/homebridge-ui/public/js/modals.js +42 -0
  46. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  47. package/dist/homebridge-ui/public/js/modals.ts +50 -1
  48. package/dist/homebridge-ui/public/js/render.d.ts +3 -0
  49. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  50. package/dist/homebridge-ui/public/js/render.js +389 -36
  51. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  52. package/dist/homebridge-ui/public/js/render.ts +421 -38
  53. package/dist/platform.d.ts +1 -0
  54. package/dist/platform.d.ts.map +1 -1
  55. package/dist/platform.js +3 -4
  56. package/dist/platform.js.map +1 -1
  57. package/dist/switchbotClient.d.ts.map +1 -1
  58. package/dist/switchbotClient.js +51 -5
  59. package/dist/switchbotClient.js.map +1 -1
  60. package/docs/assets/highlight.css +9 -2
  61. package/docs/index.html +28 -2
  62. package/docs/variables/default.html +1 -1
  63. package/package.json +9 -8
  64. package/src/device-types.ts +95 -64
  65. package/src/deviceFactory.ts +55 -33
  66. package/src/devices/genericDevice.ts +113 -1
  67. package/src/errors.ts +43 -0
  68. package/src/homebridge-ui/endpoints/devices.ts +82 -1
  69. package/src/homebridge-ui/endpoints/discovery.ts +78 -44
  70. package/src/homebridge-ui/public/css/styles.css +209 -16
  71. package/src/homebridge-ui/public/index.html +87 -56
  72. package/src/homebridge-ui/public/js/api.ts +52 -3
  73. package/src/homebridge-ui/public/js/app.ts +2 -1
  74. package/src/homebridge-ui/public/js/devices.ts +13 -1
  75. package/src/homebridge-ui/public/js/discovery.ts +1344 -142
  76. package/src/homebridge-ui/public/js/modals.ts +50 -1
  77. package/src/homebridge-ui/public/js/render.ts +421 -38
  78. package/src/platform.ts +3 -4
  79. package/src/switchbotClient.ts +44 -9
  80. package/test/e2e/run-e2e.spec.ts +12 -10
  81. package/test/helpers/matter-harness.ts +2 -2
  82. package/test/matter-integration.spec.ts +21 -0
  83. package/todo/COMPLETED.md +313 -0
  84. package/todo/TODO.md +80 -66
  85. 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('/discover', async () => {
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
- uiLog.info('GET /discover - Starting BLE scan...')
44
- try {
45
- const bleTimeout = 8000 // 8 second timeout for BLE scan
46
- const bleDiscoveryPromise = switchbot.discover({ timeout: 5000 }) // 5 second scan timeout
47
- const bleDevices = await Promise.race([
48
- bleDiscoveryPromise,
49
- new Promise<any[]>(resolve => setTimeout(resolve, bleTimeout, [])),
50
- ])
51
-
52
- if (Array.isArray(bleDevices) && bleDevices.length > 0) {
53
- uiLog.info(`GET /discover - Found ${bleDevices.length} BLE devices`)
54
- for (const [index, d] of bleDevices.entries()) {
55
- const info = typeof d?.getInfo === 'function' ? d.getInfo() : undefined
56
- const id = d?.id
57
- || (typeof d?.getId === 'function' ? d.getId() : undefined)
58
- || info?.id
59
-
60
- const mac = d?.mac
61
- || (typeof d?.getMAC === 'function' ? d.getMAC() : undefined)
62
- || info?.mac
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
- if (!id) {
65
- uiLog.warn(`GET /discover - BLE device at index ${index} has no id, skipping`)
66
- continue
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
- const connectionTypes = Array.isArray(info?.connectionTypes) ? info.connectionTypes : []
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
- } else {
83
- uiLog.info('GET /discover - No BLE devices found or scan timed out')
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: 16px;
78
+ padding: 10px;
79
79
  }
80
80
 
81
81
  h1 {
82
- font-size: 24px;
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: 16px;
156
+ margin-bottom: 10px;
156
157
  }
157
158
 
158
159
  label {
159
160
  display: block;
160
- margin-bottom: 6px;
161
+ margin-bottom: 4px;
161
162
  font-weight: 500;
162
- font-size: 12px;
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 10px;
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: 14px;
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: 14px;
207
- margin-top: 4px;
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: 16px;
225
+ padding: 12px;
225
226
  border-radius: 8px;
226
- margin-bottom: 16px;
227
+ margin-bottom: 10px;
227
228
  }
228
229
 
229
- .device-item {
230
- padding: 10px 12px;
231
- border-radius: 6px;
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: 10px;
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(-1px);
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
+ }