@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.
Files changed (78) hide show
  1. package/config.schema.json +4 -0
  2. package/dist/device-types.d.ts +18 -2
  3. package/dist/device-types.d.ts.map +1 -1
  4. package/dist/device-types.js +71 -0
  5. package/dist/device-types.js.map +1 -1
  6. package/dist/deviceFactory.d.ts.map +1 -1
  7. package/dist/deviceFactory.js +1 -0
  8. package/dist/deviceFactory.js.map +1 -1
  9. package/dist/homebridge-ui/endpoints/config.d.ts +0 -3
  10. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  11. package/dist/homebridge-ui/endpoints/config.js +40 -9
  12. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  13. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  14. package/dist/homebridge-ui/endpoints/devices.js +59 -2
  15. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  16. package/dist/homebridge-ui/public/css/styles.css +254 -0
  17. package/dist/homebridge-ui/public/index.html +23 -168
  18. package/dist/homebridge-ui/public/js/api.d.ts +4 -0
  19. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  20. package/dist/homebridge-ui/public/js/api.js +14 -0
  21. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  22. package/dist/homebridge-ui/public/js/api.ts +17 -1
  23. package/dist/homebridge-ui/public/js/app.js +556 -44
  24. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  25. package/dist/homebridge-ui/public/js/app.ts +2 -1
  26. package/dist/homebridge-ui/public/js/devices-delete.d.ts +1 -0
  27. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -1
  28. package/dist/homebridge-ui/public/js/devices-delete.js +108 -1
  29. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
  30. package/dist/homebridge-ui/public/js/devices-delete.ts +124 -1
  31. package/dist/homebridge-ui/public/js/devices.d.ts +1 -0
  32. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  33. package/dist/homebridge-ui/public/js/devices.js +10 -0
  34. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  35. package/dist/homebridge-ui/public/js/devices.ts +10 -0
  36. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  37. package/dist/homebridge-ui/public/js/discovery.js +130 -4
  38. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  39. package/dist/homebridge-ui/public/js/discovery.ts +158 -4
  40. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/modals.js +71 -26
  42. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/modals.ts +74 -27
  44. package/dist/homebridge-ui/public/js/render.d.ts +57 -0
  45. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  46. package/dist/homebridge-ui/public/js/render.js +286 -13
  47. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  48. package/dist/homebridge-ui/public/js/render.ts +331 -11
  49. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  50. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  51. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  52. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  53. package/dist/platform.js +2 -2
  54. package/dist/platform.js.map +1 -1
  55. package/docs/variables/default.html +1 -1
  56. package/package.json +1 -1
  57. package/src/device-types.ts +85 -0
  58. package/src/deviceFactory.ts +1 -0
  59. package/src/homebridge-ui/endpoints/config.ts +54 -10
  60. package/src/homebridge-ui/endpoints/devices.ts +62 -3
  61. package/src/homebridge-ui/public/css/styles.css +254 -0
  62. package/src/homebridge-ui/public/index.html +23 -168
  63. package/src/homebridge-ui/public/js/api.ts +17 -1
  64. package/src/homebridge-ui/public/js/app.ts +2 -1
  65. package/src/homebridge-ui/public/js/devices-delete.ts +124 -1
  66. package/src/homebridge-ui/public/js/devices.ts +10 -0
  67. package/src/homebridge-ui/public/js/discovery.ts +158 -4
  68. package/src/homebridge-ui/public/js/modals.ts +74 -27
  69. package/src/homebridge-ui/public/js/render.ts +331 -11
  70. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  71. package/src/platform.ts +2 -2
  72. package/todo/COMPLETED.md +189 -0
  73. package/todo/README.md +59 -0
  74. package/todo/TODO.md +205 -0
  75. package/todo/mockups/filtering-sorting.svg +112 -0
  76. package/todo/mockups/modal-switchbot-theme.svg +66 -0
  77. package/todo/mockups/signal-strength-visualization.svg +94 -0
  78. 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 = '300px'
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) { name.appendChild(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
- name.appendChild(document.createTextNode(' '))
64
- name.appendChild(renderIRBadge())
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 = '12px'
69
- details.style.opacity = '0.75'
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(name)
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(name)
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,144 @@
1
+ import { DEVICE_TYPES, isValidDeviceType, normalizeDeviceType } from '../../device-types.js'
2
+
3
+ export interface DeviceMigrationResult {
4
+ migrated: boolean
5
+ originalType: string | undefined
6
+ correctedType: string | null
7
+ message: string
8
+ }
9
+
10
+ /**
11
+ * Validate and optionally correct a device's configDeviceType
12
+ * @param device Device object from config
13
+ * @param autoCorrect Whether to auto-correct invalid types
14
+ * @returns Migration result with details
15
+ */
16
+ export function validateAndMigrateDeviceType(
17
+ device: any,
18
+ autoCorrect = false,
19
+ ): DeviceMigrationResult {
20
+ const originalType = device.configDeviceType
21
+ const deviceId = device.deviceId || device.id || 'unknown'
22
+
23
+ // Check if type is valid
24
+ if (isValidDeviceType(originalType)) {
25
+ return {
26
+ migrated: false,
27
+ originalType,
28
+ correctedType: originalType,
29
+ message: `Device "${device.configDeviceName || deviceId}" type is valid`,
30
+ }
31
+ }
32
+
33
+ // Try to find a valid mapping
34
+ const correctedType = normalizeDeviceType(originalType)
35
+
36
+ if (!correctedType) {
37
+ return {
38
+ migrated: false,
39
+ originalType,
40
+ correctedType: null,
41
+ message: `Device "${device.configDeviceName || deviceId}" has invalid type "${originalType}" with no valid mapping`,
42
+ }
43
+ }
44
+
45
+ // Found a valid mapping
46
+ if (autoCorrect) {
47
+ device.configDeviceType = correctedType
48
+ return {
49
+ migrated: true,
50
+ originalType,
51
+ correctedType,
52
+ message: `Device "${device.configDeviceName || deviceId}" type auto-corrected: "${originalType}" → "${correctedType}"`,
53
+ }
54
+ } else {
55
+ return {
56
+ migrated: false,
57
+ originalType,
58
+ correctedType,
59
+ message: `Device "${device.configDeviceName || deviceId}" requires migration: "${originalType}" → "${correctedType}"`,
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Validate all devices in config and optionally auto-correct invalid types
66
+ * @param config Configuration object containing devices array
67
+ * @param autoCorrect Whether to auto-correct invalid types
68
+ * @returns Array of migration results with statistics
69
+ */
70
+ export function validateAndMigrateConfig(
71
+ config: any,
72
+ autoCorrect = false,
73
+ ): {
74
+ results: DeviceMigrationResult[]
75
+ statistics: {
76
+ total: number
77
+ valid: number
78
+ corrected: number
79
+ invalid: number
80
+ }
81
+ warnings: string[]
82
+ } {
83
+ const results: DeviceMigrationResult[] = []
84
+ const warnings: string[] = []
85
+ let validCount = 0
86
+ let correctedCount = 0
87
+ let invalidCount = 0
88
+
89
+ if (!config?.devices || !Array.isArray(config.devices)) {
90
+ return {
91
+ results,
92
+ statistics: {
93
+ total: 0,
94
+ valid: 0,
95
+ corrected: 0,
96
+ invalid: 0,
97
+ },
98
+ warnings: ['No devices found in config'],
99
+ }
100
+ }
101
+
102
+ for (const device of config.devices) {
103
+ const result = validateAndMigrateDeviceType(device, autoCorrect)
104
+ results.push(result)
105
+
106
+ if (!result.originalType || isValidDeviceType(result.originalType)) {
107
+ validCount++
108
+ } else if (result.migrated) {
109
+ correctedCount++
110
+ } else if (result.correctedType) {
111
+ warnings.push(
112
+ `Device "${device.configDeviceName || device.deviceId}" needs correction: "${result.originalType}" → "${result.correctedType}"`,
113
+ )
114
+ invalidCount++
115
+ } else {
116
+ invalidCount++
117
+ warnings.push(
118
+ `Device "${device.configDeviceName || device.deviceId}" has INVALID type "${result.originalType}" with no mapping`,
119
+ )
120
+ }
121
+ }
122
+
123
+ // Validation complete - results and warnings are returned via return value
124
+ // Caller (config endpoint) will handle logging/reporting
125
+
126
+ return {
127
+ results,
128
+ statistics: {
129
+ total: config.devices.length,
130
+ valid: validCount,
131
+ corrected: correctedCount,
132
+ invalid: invalidCount,
133
+ },
134
+ warnings,
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Get list of all valid device types for error messages
140
+ * @returns Array of valid device types grouped by category
141
+ */
142
+ export function getValidDeviceTypesList(): Record<string, readonly string[]> {
143
+ return DEVICE_TYPES
144
+ }
package/src/platform.ts CHANGED
@@ -1127,7 +1127,7 @@ export class SwitchBotHAPPlatform {
1127
1127
  let accessory: any = this.accessories.get(uuid)
1128
1128
  // If not found by uuid, attempt to find by stored deviceId in accessory.context
1129
1129
  if (!accessory) {
1130
- for (const [, a] of Array.from(this.accessories.entries())) {
1130
+ for (const [, a] of this.accessories.entries()) {
1131
1131
  try {
1132
1132
  if (a && a.context && a.context.deviceId === d.id) {
1133
1133
  accessory = a
@@ -1486,7 +1486,7 @@ export class SwitchBotMatterPlatform {
1486
1486
  const uuid = matterApi.uuid.generate(`${d.id}`)
1487
1487
  // Try to find existing restored accessory by deviceId
1488
1488
  let existing: any | undefined
1489
- for (const [, a] of Array.from(this.accessories.entries())) {
1489
+ for (const [, a] of this.accessories.entries()) {
1490
1490
  try {
1491
1491
  if (a && a.context && a.context.deviceId === d.id) {
1492
1492
  existing = a