@switchbot/homebridge-switchbot 5.0.0-beta.109 → 5.0.0-beta.110
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/config.schema.json +4 -0
- package/dist/device-types.d.ts +18 -2
- package/dist/device-types.d.ts.map +1 -1
- package/dist/device-types.js +71 -0
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +1 -0
- package/dist/deviceFactory.js.map +1 -1
- package/dist/homebridge-ui/endpoints/config.d.ts +0 -3
- package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/config.js +40 -9
- package/dist/homebridge-ui/endpoints/config.js.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +59 -2
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +254 -0
- package/dist/homebridge-ui/public/index.html +23 -168
- package/dist/homebridge-ui/public/js/api.d.ts +4 -0
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +14 -0
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +17 -1
- package/dist/homebridge-ui/public/js/app.js +556 -44
- 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-delete.d.ts +1 -0
- package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices-delete.js +108 -1
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices-delete.ts +124 -1
- package/dist/homebridge-ui/public/js/devices.d.ts +1 -0
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +10 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +10 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +130 -4
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +158 -4
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +71 -26
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +74 -27
- package/dist/homebridge-ui/public/js/render.d.ts +57 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +286 -13
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +331 -11
- package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
- package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
- package/dist/homebridge-ui/utils/device-migration.js +111 -0
- package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
- package/dist/platform.js +2 -2
- package/dist/platform.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/src/device-types.ts +85 -0
- package/src/deviceFactory.ts +1 -0
- package/src/homebridge-ui/endpoints/config.ts +54 -10
- package/src/homebridge-ui/endpoints/devices.ts +62 -3
- package/src/homebridge-ui/public/css/styles.css +254 -0
- package/src/homebridge-ui/public/index.html +23 -168
- package/src/homebridge-ui/public/js/api.ts +17 -1
- package/src/homebridge-ui/public/js/app.ts +2 -1
- package/src/homebridge-ui/public/js/devices-delete.ts +124 -1
- package/src/homebridge-ui/public/js/devices.ts +10 -0
- package/src/homebridge-ui/public/js/discovery.ts +158 -4
- package/src/homebridge-ui/public/js/modals.ts +74 -27
- package/src/homebridge-ui/public/js/render.ts +331 -11
- package/src/homebridge-ui/utils/device-migration.ts +144 -0
- package/src/platform.ts +2 -2
- package/todo/COMPLETED.md +189 -0
- package/todo/README.md +59 -0
- package/todo/TODO.md +205 -0
- package/todo/mockups/filtering-sorting.svg +112 -0
- package/todo/mockups/modal-switchbot-theme.svg +66 -0
- package/todo/mockups/signal-strength-visualization.svg +94 -0
- package/TODO.md +0 -564
|
@@ -1,5 +1,126 @@
|
|
|
1
1
|
import type { DEVICE_TYPES } from './constants.js'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Get RSSI signal quality level and color based on dBm value
|
|
5
|
+
* @param rssi Signal strength in dBm (typically -30 to -90)
|
|
6
|
+
* @returns Object with quality level, color, and description
|
|
7
|
+
*/
|
|
8
|
+
export function getRssiSignalQuality(rssi: number | undefined): {
|
|
9
|
+
level: 'excellent' | 'good' | 'fair' | 'poor' | 'unknown'
|
|
10
|
+
color: string
|
|
11
|
+
bgColor: string
|
|
12
|
+
description: string
|
|
13
|
+
bars: number
|
|
14
|
+
} {
|
|
15
|
+
if (!rssi || rssi === 0) {
|
|
16
|
+
return {
|
|
17
|
+
level: 'unknown',
|
|
18
|
+
color: '#999',
|
|
19
|
+
bgColor: '#f5f5f5',
|
|
20
|
+
description: 'Signal strength unknown',
|
|
21
|
+
bars: 0,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const dbm = Math.floor(rssi)
|
|
26
|
+
|
|
27
|
+
if (dbm > -60) {
|
|
28
|
+
return {
|
|
29
|
+
level: 'excellent',
|
|
30
|
+
color: '#34a853',
|
|
31
|
+
bgColor: '#e8f5e9',
|
|
32
|
+
description: `Excellent (${dbm} dBm)`,
|
|
33
|
+
bars: 4,
|
|
34
|
+
}
|
|
35
|
+
} else if (dbm > -75) {
|
|
36
|
+
return {
|
|
37
|
+
level: 'good',
|
|
38
|
+
color: '#fbbc04',
|
|
39
|
+
bgColor: '#fffde7',
|
|
40
|
+
description: `Good (${dbm} dBm)`,
|
|
41
|
+
bars: 3,
|
|
42
|
+
}
|
|
43
|
+
} else if (dbm > -85) {
|
|
44
|
+
return {
|
|
45
|
+
level: 'fair',
|
|
46
|
+
color: '#ff9800',
|
|
47
|
+
bgColor: '#fff3e0',
|
|
48
|
+
description: `Fair (${dbm} dBm)`,
|
|
49
|
+
bars: 2,
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
return {
|
|
53
|
+
level: 'poor',
|
|
54
|
+
color: '#ea4335',
|
|
55
|
+
bgColor: '#ffebee',
|
|
56
|
+
description: `Poor (${dbm} dBm) - unreliable`,
|
|
57
|
+
bars: 1,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create visual signal strength indicator bars
|
|
64
|
+
* @param rssi Signal strength in dBm
|
|
65
|
+
* @returns HTML element showing filled bars
|
|
66
|
+
*/
|
|
67
|
+
export function renderSignalBars(rssi: number | undefined): HTMLElement {
|
|
68
|
+
const quality = getRssiSignalQuality(rssi)
|
|
69
|
+
|
|
70
|
+
const container = document.createElement('span')
|
|
71
|
+
container.style.display = 'inline-flex'
|
|
72
|
+
container.style.gap = '2px'
|
|
73
|
+
container.style.alignItems = 'center'
|
|
74
|
+
container.style.marginLeft = '8px'
|
|
75
|
+
container.style.fontSize = '12px'
|
|
76
|
+
|
|
77
|
+
// Create 4 bars
|
|
78
|
+
for (let i = 1; i <= 4; i++) {
|
|
79
|
+
const bar = document.createElement('span')
|
|
80
|
+
bar.style.height = `${i * 3}px`
|
|
81
|
+
bar.style.width = '3px'
|
|
82
|
+
bar.style.borderRadius = '1px'
|
|
83
|
+
bar.style.border = `1px solid ${quality.color}`
|
|
84
|
+
|
|
85
|
+
if (i <= quality.bars) {
|
|
86
|
+
bar.style.backgroundColor = quality.color
|
|
87
|
+
} else {
|
|
88
|
+
bar.style.backgroundColor = 'transparent'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
container.appendChild(bar)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add tooltip
|
|
95
|
+
container.title = quality.description
|
|
96
|
+
|
|
97
|
+
return container
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create signal quality badge with color
|
|
102
|
+
* @param rssi Signal strength in dBm
|
|
103
|
+
* @returns HTML element showing quality level
|
|
104
|
+
*/
|
|
105
|
+
export function renderSignalQualityBadge(rssi: number | undefined): HTMLElement {
|
|
106
|
+
const quality = getRssiSignalQuality(rssi)
|
|
107
|
+
|
|
108
|
+
const badge = document.createElement('span')
|
|
109
|
+
badge.textContent = quality.level.charAt(0).toUpperCase() + quality.level.slice(1)
|
|
110
|
+
badge.style.cssText = `
|
|
111
|
+
background: ${quality.color};
|
|
112
|
+
color: white;
|
|
113
|
+
padding: 2px 6px;
|
|
114
|
+
border-radius: 3px;
|
|
115
|
+
font-size: 10px;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
margin-left: 8px;
|
|
118
|
+
`
|
|
119
|
+
badge.title = quality.description
|
|
120
|
+
|
|
121
|
+
return badge
|
|
122
|
+
}
|
|
123
|
+
|
|
3
124
|
export function renderBadge(text: string, style: string): HTMLElement {
|
|
4
125
|
const badge = document.createElement('span')
|
|
5
126
|
badge.textContent = text
|
|
@@ -35,51 +156,80 @@ export function renderIRBadge(): HTMLElement {
|
|
|
35
156
|
|
|
36
157
|
export async function renderDiscoveredDevices(devices: any[]): Promise<HTMLElement> {
|
|
37
158
|
const ul = document.createElement('ul')
|
|
38
|
-
ul.style.maxHeight = '
|
|
159
|
+
ul.style.maxHeight = '400px'
|
|
39
160
|
ul.style.overflowY = 'auto'
|
|
161
|
+
ul.style.marginTop = '12px'
|
|
162
|
+
ul.style.padding = '0'
|
|
163
|
+
ul.style.listStyle = 'none'
|
|
40
164
|
|
|
41
165
|
const { addDeviceToConfig } = await import('./discovery.js')
|
|
42
166
|
|
|
43
167
|
for (const d of devices) {
|
|
44
168
|
const li = document.createElement('li')
|
|
45
169
|
li.className = 'device-item'
|
|
170
|
+
li.style.display = 'flex'
|
|
171
|
+
li.style.alignItems = 'center'
|
|
172
|
+
li.style.justifyContent = 'space-between'
|
|
173
|
+
li.style.padding = '10px 12px'
|
|
174
|
+
li.style.marginBottom = '6px'
|
|
175
|
+
li.style.borderRadius = '6px'
|
|
176
|
+
li.style.transition = 'all 0.2s ease'
|
|
46
177
|
|
|
47
178
|
const info = document.createElement('div')
|
|
48
179
|
info.style.flex = '1'
|
|
49
180
|
|
|
181
|
+
const nameContainer = document.createElement('div')
|
|
182
|
+
nameContainer.style.display = 'flex'
|
|
183
|
+
nameContainer.style.alignItems = 'center'
|
|
184
|
+
nameContainer.style.marginBottom = '4px'
|
|
185
|
+
nameContainer.style.flexWrap = 'wrap'
|
|
186
|
+
nameContainer.style.gap = '6px'
|
|
187
|
+
|
|
50
188
|
const name = document.createElement('div')
|
|
51
189
|
name.style.fontWeight = '500'
|
|
190
|
+
name.style.fontSize = '14px'
|
|
52
191
|
name.textContent = d.name || d.id
|
|
53
192
|
|
|
193
|
+
nameContainer.appendChild(name)
|
|
194
|
+
|
|
54
195
|
// Add connection type badge
|
|
55
196
|
if (d.connectionType) {
|
|
56
|
-
name.appendChild(document.createTextNode(' '))
|
|
57
197
|
const badge = renderConnectionBadge(d.connectionType)
|
|
58
|
-
if (badge) {
|
|
198
|
+
if (badge) { nameContainer.appendChild(badge) }
|
|
59
199
|
}
|
|
60
200
|
|
|
61
201
|
// Add IR badge if it's an IR device
|
|
62
202
|
if (d.isIR) {
|
|
63
|
-
|
|
64
|
-
|
|
203
|
+
nameContainer.appendChild(renderIRBadge())
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add signal strength visualization (only for BLE/wireless devices)
|
|
207
|
+
if (d.rssi !== undefined && d.rssi !== null && d.rssi !== 0) {
|
|
208
|
+
nameContainer.appendChild(renderSignalBars(d.rssi))
|
|
209
|
+
nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
|
|
65
210
|
}
|
|
66
211
|
|
|
67
212
|
const details = document.createElement('div')
|
|
68
|
-
details.style.fontSize = '
|
|
69
|
-
details.style.opacity = '0.
|
|
213
|
+
details.style.fontSize = '11px'
|
|
214
|
+
details.style.opacity = '0.7'
|
|
215
|
+
details.style.marginTop = '4px'
|
|
216
|
+
details.style.fontFamily = 'monospace'
|
|
70
217
|
|
|
71
218
|
let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
|
|
72
219
|
if (d.hubDeviceId) { detailsText += ` | Hub: ${d.hubDeviceId}` }
|
|
73
|
-
if (d.rssi) { detailsText += ` | Signal: ${d.rssi} dBm` }
|
|
74
220
|
if (d.address) { detailsText += ` | MAC: ${d.address}` }
|
|
75
221
|
details.textContent = detailsText
|
|
76
222
|
|
|
77
|
-
info.appendChild(
|
|
223
|
+
info.appendChild(nameContainer)
|
|
78
224
|
info.appendChild(details)
|
|
79
225
|
|
|
80
226
|
const addBtn = document.createElement('button')
|
|
81
227
|
addBtn.textContent = '+ Add'
|
|
82
228
|
addBtn.style.marginLeft = '8px'
|
|
229
|
+
addBtn.style.padding = '6px 14px'
|
|
230
|
+
addBtn.style.fontSize = '12px'
|
|
231
|
+
addBtn.style.whiteSpace = 'nowrap'
|
|
232
|
+
addBtn.style.flexShrink = '0'
|
|
83
233
|
addBtn.onclick = async () => {
|
|
84
234
|
await addDeviceToConfig(d)
|
|
85
235
|
}
|
|
@@ -92,21 +242,177 @@ export async function renderDiscoveredDevices(devices: any[]): Promise<HTMLEleme
|
|
|
92
242
|
return ul
|
|
93
243
|
}
|
|
94
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Filter devices by connection type and search query
|
|
247
|
+
* @param devices Discovered devices array
|
|
248
|
+
* @param connectionType Filter: 'all' | 'ble' | 'api' | 'both' | 'ir'
|
|
249
|
+
* @param searchQuery Search term to match against name/id/type
|
|
250
|
+
* @returns Filtered devices array
|
|
251
|
+
*/
|
|
252
|
+
export function filterDevices(
|
|
253
|
+
devices: any[],
|
|
254
|
+
connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir' = 'all',
|
|
255
|
+
searchQuery = '',
|
|
256
|
+
): any[] {
|
|
257
|
+
let filtered = [...devices]
|
|
258
|
+
|
|
259
|
+
// Filter by connection type
|
|
260
|
+
if (connectionType !== 'all') {
|
|
261
|
+
filtered = filtered.filter(d => {
|
|
262
|
+
if (connectionType === 'ir') {
|
|
263
|
+
return d.isIR === true
|
|
264
|
+
}
|
|
265
|
+
if (connectionType === 'ble') {
|
|
266
|
+
return d.connectionType === 'BLE' || d.connectionType?.includes('BLE')
|
|
267
|
+
}
|
|
268
|
+
if (connectionType === 'api') {
|
|
269
|
+
return d.connectionType === 'OpenAPI' || d.connectionType === 'API' || d.connectionType?.includes('API')
|
|
270
|
+
}
|
|
271
|
+
if (connectionType === 'both') {
|
|
272
|
+
return d.connectionType === 'Both' || d.connectionType?.includes('Both')
|
|
273
|
+
}
|
|
274
|
+
return true
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Filter by search query
|
|
279
|
+
if (searchQuery.trim()) {
|
|
280
|
+
const query = searchQuery.toLowerCase()
|
|
281
|
+
filtered = filtered.filter(d => {
|
|
282
|
+
const name = (d.name || '').toLowerCase()
|
|
283
|
+
const id = (d.id || '').toLowerCase()
|
|
284
|
+
const type = (d.type || '').toLowerCase()
|
|
285
|
+
const model = (d.model || '').toLowerCase()
|
|
286
|
+
return name.includes(query) || id.includes(query) || type.includes(query) || model.includes(query)
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return filtered
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Sort devices by specified criteria
|
|
295
|
+
* @param devices Devices array to sort
|
|
296
|
+
* @param sortBy Sort criterion: 'name' | 'signal' | 'type' | 'connection'
|
|
297
|
+
* @returns Sorted devices array
|
|
298
|
+
*/
|
|
299
|
+
export function sortDevices(
|
|
300
|
+
devices: any[],
|
|
301
|
+
sortBy: 'name' | 'signal' | 'type' | 'connection' = 'name',
|
|
302
|
+
): any[] {
|
|
303
|
+
const sorted = [...devices]
|
|
304
|
+
|
|
305
|
+
switch (sortBy) {
|
|
306
|
+
case 'signal': {
|
|
307
|
+
// Sort by RSSI descending (strongest signal first)
|
|
308
|
+
sorted.sort((a, b) => {
|
|
309
|
+
const aRssi = a.rssi || 0
|
|
310
|
+
const bRssi = b.rssi || 0
|
|
311
|
+
return bRssi - aRssi // Descending order (higher is stronger)
|
|
312
|
+
})
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
case 'type': {
|
|
316
|
+
// Sort by device type alphabetically
|
|
317
|
+
sorted.sort((a, b) => {
|
|
318
|
+
const aType = (a.type || '').localeCompare(b.type || '')
|
|
319
|
+
return aType
|
|
320
|
+
})
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
case 'connection': {
|
|
324
|
+
// Sort by connection type: Both > BLE > OpenAPI > Others
|
|
325
|
+
const connectionOrder: Record<string, number> = {
|
|
326
|
+
'Both': 0,
|
|
327
|
+
'BLE': 1,
|
|
328
|
+
'OpenAPI': 2,
|
|
329
|
+
'API': 2,
|
|
330
|
+
'Unknown': 3,
|
|
331
|
+
}
|
|
332
|
+
sorted.sort((a, b) => {
|
|
333
|
+
const aOrder = connectionOrder[a.connectionType || 'Unknown'] ?? 3
|
|
334
|
+
const bOrder = connectionOrder[b.connectionType || 'Unknown'] ?? 3
|
|
335
|
+
return aOrder - bOrder
|
|
336
|
+
})
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
case 'name':
|
|
340
|
+
default: {
|
|
341
|
+
// Sort by name alphabetically
|
|
342
|
+
sorted.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || ''))
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return sorted
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get filter/sort preferences from localStorage
|
|
352
|
+
* @returns Object with current filter and sort preferences
|
|
353
|
+
*/
|
|
354
|
+
export function getDiscoveryPreferences(): {
|
|
355
|
+
connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
|
|
356
|
+
sortBy: 'name' | 'signal' | 'type' | 'connection'
|
|
357
|
+
searchQuery: string
|
|
358
|
+
} {
|
|
359
|
+
try {
|
|
360
|
+
const stored = localStorage.getItem('discoveryPreferences')
|
|
361
|
+
if (stored) {
|
|
362
|
+
return JSON.parse(stored)
|
|
363
|
+
}
|
|
364
|
+
} catch (_e) {
|
|
365
|
+
// Ignore parse errors
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
connectionType: 'all',
|
|
370
|
+
sortBy: 'name',
|
|
371
|
+
searchQuery: '',
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Save filter/sort preferences to localStorage
|
|
377
|
+
* @param preferences Preferences object to save
|
|
378
|
+
*/
|
|
379
|
+
export function setDiscoveryPreferences(preferences: {
|
|
380
|
+
connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
|
|
381
|
+
sortBy: 'name' | 'signal' | 'type' | 'connection'
|
|
382
|
+
searchQuery: string
|
|
383
|
+
}): void {
|
|
384
|
+
try {
|
|
385
|
+
localStorage.setItem('discoveryPreferences', JSON.stringify(preferences))
|
|
386
|
+
} catch (_e) {
|
|
387
|
+
// Ignore storage errors
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
95
391
|
export function renderDeviceList(list: any[]): void {
|
|
96
392
|
const ul = document.getElementById('devices')
|
|
97
393
|
const status = document.getElementById('status')
|
|
394
|
+
const removeAllContainer = document.getElementById('removeAllContainer')
|
|
98
395
|
|
|
99
396
|
if (!ul || !status) { return }
|
|
100
397
|
|
|
101
398
|
if (!list.length) {
|
|
102
399
|
status.textContent = 'No devices found in config.'
|
|
103
400
|
ul.innerHTML = ''
|
|
401
|
+
// Hide remove all button when no devices
|
|
402
|
+
if (removeAllContainer) {
|
|
403
|
+
removeAllContainer.style.display = 'none'
|
|
404
|
+
}
|
|
104
405
|
return
|
|
105
406
|
}
|
|
106
407
|
|
|
107
408
|
status.textContent = `Found ${list.length} device(s)`
|
|
108
409
|
ul.innerHTML = ''
|
|
109
410
|
|
|
411
|
+
// Show remove all button when devices exist
|
|
412
|
+
if (removeAllContainer) {
|
|
413
|
+
removeAllContainer.style.display = 'block'
|
|
414
|
+
}
|
|
415
|
+
|
|
110
416
|
for (const d of list) {
|
|
111
417
|
const li = document.createElement('li')
|
|
112
418
|
li.className = 'device-item'
|
|
@@ -114,6 +420,12 @@ export function renderDeviceList(list: any[]): void {
|
|
|
114
420
|
const info = document.createElement('div')
|
|
115
421
|
info.style.flex = '1'
|
|
116
422
|
|
|
423
|
+
const nameContainer = document.createElement('div')
|
|
424
|
+
nameContainer.style.display = 'flex'
|
|
425
|
+
nameContainer.style.alignItems = 'center'
|
|
426
|
+
nameContainer.style.flexWrap = 'wrap'
|
|
427
|
+
nameContainer.style.gap = '6px'
|
|
428
|
+
|
|
117
429
|
const name = document.createElement('div')
|
|
118
430
|
name.style.fontWeight = '500'
|
|
119
431
|
name.textContent = d.name || d.id
|
|
@@ -124,6 +436,15 @@ export function renderDeviceList(list: any[]): void {
|
|
|
124
436
|
code.style.opacity = '0.75'
|
|
125
437
|
code.style.marginLeft = '8px'
|
|
126
438
|
|
|
439
|
+
nameContainer.appendChild(name)
|
|
440
|
+
nameContainer.appendChild(code)
|
|
441
|
+
|
|
442
|
+
// Add signal strength visualization if RSSI is available
|
|
443
|
+
if (d.rssi !== undefined && d.rssi !== null && d.rssi !== 0) {
|
|
444
|
+
nameContainer.appendChild(renderSignalBars(d.rssi))
|
|
445
|
+
nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
|
|
446
|
+
}
|
|
447
|
+
|
|
127
448
|
const meta = document.createElement('div')
|
|
128
449
|
meta.style.opacity = '0.75'
|
|
129
450
|
meta.style.marginTop = '4px'
|
|
@@ -134,8 +455,7 @@ export function renderDeviceList(list: any[]): void {
|
|
|
134
455
|
const roomText = d.room ? `room: ${d.room}` : ''
|
|
135
456
|
meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
|
|
136
457
|
|
|
137
|
-
info.appendChild(
|
|
138
|
-
info.appendChild(code)
|
|
458
|
+
info.appendChild(nameContainer)
|
|
139
459
|
info.appendChild(meta)
|
|
140
460
|
|
|
141
461
|
const buttons = document.createElement('div')
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface DeviceMigrationResult {
|
|
2
|
+
migrated: boolean;
|
|
3
|
+
originalType: string | undefined;
|
|
4
|
+
correctedType: string | null;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validate and optionally correct a device's configDeviceType
|
|
9
|
+
* @param device Device object from config
|
|
10
|
+
* @param autoCorrect Whether to auto-correct invalid types
|
|
11
|
+
* @returns Migration result with details
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateAndMigrateDeviceType(device: any, autoCorrect?: boolean): DeviceMigrationResult;
|
|
14
|
+
/**
|
|
15
|
+
* Validate all devices in config and optionally auto-correct invalid types
|
|
16
|
+
* @param config Configuration object containing devices array
|
|
17
|
+
* @param autoCorrect Whether to auto-correct invalid types
|
|
18
|
+
* @returns Array of migration results with statistics
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateAndMigrateConfig(config: any, autoCorrect?: boolean): {
|
|
21
|
+
results: DeviceMigrationResult[];
|
|
22
|
+
statistics: {
|
|
23
|
+
total: number;
|
|
24
|
+
valid: number;
|
|
25
|
+
corrected: number;
|
|
26
|
+
invalid: number;
|
|
27
|
+
};
|
|
28
|
+
warnings: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get list of all valid device types for error messages
|
|
32
|
+
* @returns Array of valid device types grouped by category
|
|
33
|
+
*/
|
|
34
|
+
export declare function getValidDeviceTypesList(): Record<string, readonly string[]>;
|
|
35
|
+
//# sourceMappingURL=device-migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-migration.d.ts","sourceRoot":"","sources":["../../../src/homebridge-ui/utils/device-migration.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,GAAG,EACX,WAAW,UAAQ,GAClB,qBAAqB,CA2CvB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,GAAG,EACX,WAAW,UAAQ,GAClB;IACD,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,UAAU,EAAE;QACV,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB,CAsDA;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAE3E"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { DEVICE_TYPES, isValidDeviceType, normalizeDeviceType } from '../../device-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate and optionally correct a device's configDeviceType
|
|
4
|
+
* @param device Device object from config
|
|
5
|
+
* @param autoCorrect Whether to auto-correct invalid types
|
|
6
|
+
* @returns Migration result with details
|
|
7
|
+
*/
|
|
8
|
+
export function validateAndMigrateDeviceType(device, autoCorrect = false) {
|
|
9
|
+
const originalType = device.configDeviceType;
|
|
10
|
+
const deviceId = device.deviceId || device.id || 'unknown';
|
|
11
|
+
// Check if type is valid
|
|
12
|
+
if (isValidDeviceType(originalType)) {
|
|
13
|
+
return {
|
|
14
|
+
migrated: false,
|
|
15
|
+
originalType,
|
|
16
|
+
correctedType: originalType,
|
|
17
|
+
message: `Device "${device.configDeviceName || deviceId}" type is valid`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Try to find a valid mapping
|
|
21
|
+
const correctedType = normalizeDeviceType(originalType);
|
|
22
|
+
if (!correctedType) {
|
|
23
|
+
return {
|
|
24
|
+
migrated: false,
|
|
25
|
+
originalType,
|
|
26
|
+
correctedType: null,
|
|
27
|
+
message: `Device "${device.configDeviceName || deviceId}" has invalid type "${originalType}" with no valid mapping`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Found a valid mapping
|
|
31
|
+
if (autoCorrect) {
|
|
32
|
+
device.configDeviceType = correctedType;
|
|
33
|
+
return {
|
|
34
|
+
migrated: true,
|
|
35
|
+
originalType,
|
|
36
|
+
correctedType,
|
|
37
|
+
message: `Device "${device.configDeviceName || deviceId}" type auto-corrected: "${originalType}" → "${correctedType}"`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
return {
|
|
42
|
+
migrated: false,
|
|
43
|
+
originalType,
|
|
44
|
+
correctedType,
|
|
45
|
+
message: `Device "${device.configDeviceName || deviceId}" requires migration: "${originalType}" → "${correctedType}"`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validate all devices in config and optionally auto-correct invalid types
|
|
51
|
+
* @param config Configuration object containing devices array
|
|
52
|
+
* @param autoCorrect Whether to auto-correct invalid types
|
|
53
|
+
* @returns Array of migration results with statistics
|
|
54
|
+
*/
|
|
55
|
+
export function validateAndMigrateConfig(config, autoCorrect = false) {
|
|
56
|
+
const results = [];
|
|
57
|
+
const warnings = [];
|
|
58
|
+
let validCount = 0;
|
|
59
|
+
let correctedCount = 0;
|
|
60
|
+
let invalidCount = 0;
|
|
61
|
+
if (!config?.devices || !Array.isArray(config.devices)) {
|
|
62
|
+
return {
|
|
63
|
+
results,
|
|
64
|
+
statistics: {
|
|
65
|
+
total: 0,
|
|
66
|
+
valid: 0,
|
|
67
|
+
corrected: 0,
|
|
68
|
+
invalid: 0,
|
|
69
|
+
},
|
|
70
|
+
warnings: ['No devices found in config'],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
for (const device of config.devices) {
|
|
74
|
+
const result = validateAndMigrateDeviceType(device, autoCorrect);
|
|
75
|
+
results.push(result);
|
|
76
|
+
if (!result.originalType || isValidDeviceType(result.originalType)) {
|
|
77
|
+
validCount++;
|
|
78
|
+
}
|
|
79
|
+
else if (result.migrated) {
|
|
80
|
+
correctedCount++;
|
|
81
|
+
}
|
|
82
|
+
else if (result.correctedType) {
|
|
83
|
+
warnings.push(`Device "${device.configDeviceName || device.deviceId}" needs correction: "${result.originalType}" → "${result.correctedType}"`);
|
|
84
|
+
invalidCount++;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
invalidCount++;
|
|
88
|
+
warnings.push(`Device "${device.configDeviceName || device.deviceId}" has INVALID type "${result.originalType}" with no mapping`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Validation complete - results and warnings are returned via return value
|
|
92
|
+
// Caller (config endpoint) will handle logging/reporting
|
|
93
|
+
return {
|
|
94
|
+
results,
|
|
95
|
+
statistics: {
|
|
96
|
+
total: config.devices.length,
|
|
97
|
+
valid: validCount,
|
|
98
|
+
corrected: correctedCount,
|
|
99
|
+
invalid: invalidCount,
|
|
100
|
+
},
|
|
101
|
+
warnings,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get list of all valid device types for error messages
|
|
106
|
+
* @returns Array of valid device types grouped by category
|
|
107
|
+
*/
|
|
108
|
+
export function getValidDeviceTypesList() {
|
|
109
|
+
return DEVICE_TYPES;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=device-migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-migration.js","sourceRoot":"","sources":["../../../src/homebridge-ui/utils/device-migration.ts"],"names":[],"mappings":"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"}
|
package/dist/platform.js
CHANGED
|
@@ -1123,7 +1123,7 @@ export class SwitchBotHAPPlatform {
|
|
|
1123
1123
|
let accessory = this.accessories.get(uuid);
|
|
1124
1124
|
// If not found by uuid, attempt to find by stored deviceId in accessory.context
|
|
1125
1125
|
if (!accessory) {
|
|
1126
|
-
for (const [, a] of
|
|
1126
|
+
for (const [, a] of this.accessories.entries()) {
|
|
1127
1127
|
try {
|
|
1128
1128
|
if (a && a.context && a.context.deviceId === d.id) {
|
|
1129
1129
|
accessory = a;
|
|
@@ -1484,7 +1484,7 @@ export class SwitchBotMatterPlatform {
|
|
|
1484
1484
|
const uuid = matterApi.uuid.generate(`${d.id}`);
|
|
1485
1485
|
// Try to find existing restored accessory by deviceId
|
|
1486
1486
|
let existing;
|
|
1487
|
-
for (const [, a] of
|
|
1487
|
+
for (const [, a] of this.accessories.entries()) {
|
|
1488
1488
|
try {
|
|
1489
1489
|
if (a && a.context && a.context.deviceId === d.id) {
|
|
1490
1490
|
existing = a;
|