@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
@@ -7,7 +7,7 @@ import {
7
7
  import { loadConfiguredDevices } from './devices.js'
8
8
  import { uiLog } from './logger.js'
9
9
  import { hideBusyUi, showBusyUi } from './modal.js'
10
- import { renderDiscoveredDevices } from './render.js'
10
+ import { renderDiscoveredDevices, filterDevices, sortDevices, getDiscoveryPreferences, setDiscoveryPreferences } from './render.js'
11
11
  import { toastError, toastInfo, toastSuccess, toastWarning } from './toast.js'
12
12
 
13
13
  export async function discoverDevices(): Promise<void> {
@@ -48,6 +48,9 @@ export async function discoverDevices(): Promise<void> {
48
48
  deviceId: d.id,
49
49
  name: d.name,
50
50
  type: d.type,
51
+ rssi: d.rssi,
52
+ address: d.address,
53
+ model: d.model,
51
54
  })),
52
55
  )
53
56
  uiLog.info('Bulk add response:', bulkResult)
@@ -100,10 +103,130 @@ export async function discoverDevices(): Promise<void> {
100
103
  }
101
104
 
102
105
  status.textContent = `Found ${devices.length} device(s)`
103
- const ul = await renderDiscoveredDevices(devices)
104
- document.getElementById('discoveredDevices')?.replaceWith(ul)
105
- ul.id = 'discoveredDevices'
106
+
107
+ // Create filter/sort controls
108
+ const controlsDiv = document.createElement('div')
109
+ controlsDiv.style.cssText = 'margin-bottom: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;'
110
+
111
+ // Load saved preferences
112
+ const preferences = getDiscoveryPreferences()
113
+
114
+ // Filter buttons
115
+ const filterLabel = document.createElement('label')
116
+ filterLabel.style.fontSize = '12px'
117
+ filterLabel.style.fontWeight = '500'
118
+ filterLabel.textContent = 'Filter:'
119
+
120
+ const filterGroup = document.createElement('div')
121
+ filterGroup.style.display = 'flex'
122
+ filterGroup.style.gap = '4px'
123
+
124
+ const filterOptions: Array<{ label: string, value: 'all' | 'ble' | 'api' | 'both' | 'ir' }> = [
125
+ { label: 'All', value: 'all' },
126
+ { label: 'BLE', value: 'ble' },
127
+ { label: 'API', value: 'api' },
128
+ { label: 'Both', value: 'both' },
129
+ { label: 'IR', value: 'ir' },
130
+ ]
131
+
132
+ for (const option of filterOptions) {
133
+ const btn = document.createElement('button')
134
+ btn.textContent = option.label
135
+ btn.style.padding = '4px 8px'
136
+ btn.style.fontSize = '11px'
137
+ btn.style.borderRadius = '3px'
138
+ btn.style.cursor = 'pointer'
139
+ btn.style.border = preferences.connectionType === option.value ? '2px solid #007AFF' : '1px solid #ccc'
140
+ btn.style.backgroundColor = preferences.connectionType === option.value ? '#f0f7ff' : '#fff'
141
+
142
+ btn.onclick = () => {
143
+ preferences.connectionType = option.value
144
+ setDiscoveryPreferences(preferences)
145
+ updateDiscoveryView(devices, preferences)
146
+ // Update button styles
147
+ for (const b of Array.from(filterGroup.querySelectorAll('button'))) {
148
+ ;(b as HTMLButtonElement).style.border = '1px solid #ccc'
149
+ ;(b as HTMLButtonElement).style.backgroundColor = '#fff'
150
+ }
151
+ btn.style.border = '2px solid #007AFF'
152
+ btn.style.backgroundColor = '#f0f7ff'
153
+ }
154
+
155
+ filterGroup.appendChild(btn)
156
+ }
157
+
158
+ // Sort dropdown
159
+ const sortLabel = document.createElement('label')
160
+ sortLabel.style.fontSize = '12px'
161
+ sortLabel.style.fontWeight = '500'
162
+ sortLabel.style.marginLeft = '8px'
163
+ sortLabel.textContent = 'Sort:'
164
+
165
+ const sortSelect = document.createElement('select')
166
+ sortSelect.style.fontSize = '11px'
167
+ sortSelect.style.padding = '4px 8px'
168
+ sortSelect.style.borderRadius = '3px'
169
+ sortSelect.value = preferences.sortBy
170
+
171
+ const sortOptions = [
172
+ { label: 'Name', value: 'name' },
173
+ { label: 'Signal Strength', value: 'signal' },
174
+ { label: 'Type', value: 'type' },
175
+ { label: 'Connection', value: 'connection' },
176
+ ]
177
+
178
+ for (const opt of sortOptions) {
179
+ const option = document.createElement('option')
180
+ option.value = opt.value
181
+ option.textContent = opt.label
182
+ sortSelect.appendChild(option)
183
+ }
184
+
185
+ sortSelect.onchange = () => {
186
+ preferences.sortBy = sortSelect.value as any
187
+ setDiscoveryPreferences(preferences)
188
+ updateDiscoveryView(devices, preferences)
189
+ }
190
+
191
+ // Search box
192
+ const searchInput = document.createElement('input')
193
+ searchInput.type = 'text'
194
+ searchInput.placeholder = 'Search by name, ID, or type...'
195
+ searchInput.style.fontSize = '11px'
196
+ searchInput.style.padding = '4px 8px'
197
+ searchInput.style.borderRadius = '3px'
198
+ searchInput.style.border = '1px solid #ccc'
199
+ searchInput.style.flex = '1'
200
+ searchInput.style.minWidth = '200px'
201
+ searchInput.style.maxWidth = '300px'
202
+ searchInput.value = preferences.searchQuery
203
+
204
+ let searchTimeout: NodeJS.Timeout
205
+ searchInput.oninput = () => {
206
+ clearTimeout(searchTimeout)
207
+ searchTimeout = setTimeout(() => {
208
+ preferences.searchQuery = searchInput.value
209
+ setDiscoveryPreferences(preferences)
210
+ updateDiscoveryView(devices, preferences)
211
+ }, 300) // Debounce search
212
+ }
213
+
214
+ // Assemble controls
215
+ controlsDiv.appendChild(filterLabel)
216
+ controlsDiv.appendChild(filterGroup)
217
+ controlsDiv.appendChild(sortLabel)
218
+ controlsDiv.appendChild(sortSelect)
219
+ controlsDiv.appendChild(searchInput)
220
+
221
+ // Clear list and add controls
222
+ list.innerHTML = ''
223
+ list.appendChild(controlsDiv)
224
+
225
+ // Display discovery list
106
226
  list.style.display = 'block'
227
+
228
+ // Initial render with saved preferences
229
+ await updateDiscoveryView(devices, preferences)
107
230
  } catch (e) {
108
231
  uiLog.error('Discovery error:', e)
109
232
  status.textContent = `Error: ${e instanceof Error ? e.message : 'Discovery failed'}`
@@ -117,6 +240,37 @@ export async function discoverDevices(): Promise<void> {
117
240
  }
118
241
  }
119
242
 
243
+ /**
244
+ * Update discovery list with filtered and sorted devices
245
+ */
246
+ async function updateDiscoveryView(
247
+ allDevices: any[],
248
+ preferences: {
249
+ connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
250
+ sortBy: 'name' | 'signal' | 'type' | 'connection'
251
+ searchQuery: string
252
+ },
253
+ ): Promise<void> {
254
+ // Apply filters and sort
255
+ const filtered = filterDevices(allDevices, preferences.connectionType, preferences.searchQuery)
256
+ const sorted = sortDevices(filtered, preferences.sortBy)
257
+
258
+ // Render updated list
259
+ const ul = await renderDiscoveredDevices(sorted)
260
+ document.getElementById('discoveredDevices')?.replaceWith(ul)
261
+ ul.id = 'discoveredDevices'
262
+
263
+ // Update status with count
264
+ const status = document.getElementById('discoverStatus')
265
+ if (status) {
266
+ const totalCount = allDevices.length
267
+ const filteredCount = sorted.length
268
+ status.textContent = filteredCount === totalCount
269
+ ? `Found ${totalCount} device(s)`
270
+ : `Showing ${filteredCount} of ${totalCount} device(s)`
271
+ }
272
+ }
273
+
120
274
  export async function addDeviceToConfig(device: any): Promise<void> {
121
275
  const { addDeviceToConfig: addDevice } = await import('./devices.js')
122
276
  await addDevice(device)
@@ -18,58 +18,90 @@ export async function editDevice(device: any): Promise<void> {
18
18
  const modal = document.createElement('div')
19
19
  modal.style.background = getComputedStyle(document.body).backgroundColor
20
20
  modal.style.color = getComputedStyle(document.body).color
21
- modal.style.padding = '24px'
22
- modal.style.borderRadius = '8px'
23
- modal.style.minWidth = '400px'
24
- modal.style.boxShadow = '0 10px 40px rgba(0,0,0,0.3)'
21
+ modal.style.padding = '0'
22
+ modal.style.borderRadius = '10px'
23
+ modal.style.minWidth = '440px'
24
+ modal.style.maxWidth = '90vw'
25
+ modal.style.boxShadow = '0 8px 32px rgba(0,0,0,0.35)'
26
+ modal.style.overflow = 'hidden'
27
+ modal.style.borderTop = '3px solid var(--switchbot-red, #ef4444)'
25
28
 
26
29
  const title = document.createElement('h3')
27
30
  title.textContent = 'Edit Device'
28
31
  title.style.marginTop = '0'
32
+ title.style.marginBottom = '16px'
33
+ title.style.padding = '20px 20px 0'
34
+ title.style.fontSize = '18px'
35
+ title.style.fontWeight = '600'
36
+ title.style.color = 'var(--switchbot-red, #ef4444)'
37
+ title.style.letterSpacing = '-0.02em'
38
+
39
+ const contentDiv = document.createElement('div')
40
+ contentDiv.style.padding = '0 20px 20px'
29
41
 
30
42
  const nameLabel = document.createElement('label')
31
43
  nameLabel.textContent = 'Device Name'
32
44
  nameLabel.style.display = 'block'
33
- nameLabel.style.marginBottom = '8px'
45
+ nameLabel.style.marginBottom = '6px'
34
46
  nameLabel.style.fontWeight = '500'
47
+ nameLabel.style.fontSize = '12px'
48
+ nameLabel.style.color = '#6b7280'
35
49
 
36
50
  const nameInput = document.createElement('input')
37
51
  nameInput.type = 'text'
38
52
  nameInput.value = device.name || device.id
39
53
  nameInput.style.width = '100%'
40
- nameInput.style.marginBottom = '16px'
54
+ nameInput.style.marginBottom = '12px'
55
+ nameInput.style.padding = '8px 10px'
56
+ nameInput.style.borderRadius = '6px'
57
+ nameInput.style.fontSize = '14px'
58
+ nameInput.style.boxSizing = 'border-box'
59
+ nameInput.style.transition = 'border-color 0.2s ease'
41
60
 
42
61
  // Read-only API device type field
43
62
  const apiTypeLabel = document.createElement('label')
44
63
  apiTypeLabel.textContent = 'Device Type (API - Read Only)'
45
64
  apiTypeLabel.style.display = 'block'
46
- apiTypeLabel.style.marginBottom = '8px'
65
+ apiTypeLabel.style.marginBottom = '6px'
47
66
  apiTypeLabel.style.fontWeight = '500'
67
+ apiTypeLabel.style.fontSize = '12px'
68
+ apiTypeLabel.style.color = '#6b7280'
48
69
 
49
70
  const apiTypeInput = document.createElement('input')
50
71
  apiTypeInput.type = 'text'
51
72
  apiTypeInput.value = device.deviceType || device.type || 'Unknown'
52
73
  apiTypeInput.readOnly = true
53
74
  apiTypeInput.style.width = '100%'
54
- apiTypeInput.style.marginBottom = '16px'
75
+ apiTypeInput.style.marginBottom = '12px'
76
+ apiTypeInput.style.padding = '8px 10px'
77
+ apiTypeInput.style.borderRadius = '6px'
78
+ apiTypeInput.style.fontSize = '13px'
55
79
  apiTypeInput.style.opacity = '0.6'
56
80
  apiTypeInput.style.cursor = 'not-allowed'
81
+ apiTypeInput.style.boxSizing = 'border-box'
82
+ apiTypeInput.style.backgroundColor = '#f9fafb'
57
83
 
58
84
  // Editable config device type dropdown
59
85
  const typeLabel = document.createElement('label')
60
86
  typeLabel.textContent = 'Config Device Type (Override)'
61
87
  typeLabel.style.display = 'block'
62
- typeLabel.style.marginBottom = '8px'
88
+ typeLabel.style.marginBottom = '6px'
63
89
  typeLabel.style.fontWeight = '500'
90
+ typeLabel.style.fontSize = '12px'
91
+ typeLabel.style.color = '#6b7280'
64
92
 
65
93
  const typeSelect = document.createElement('select')
66
94
  typeSelect.style.width = '100%'
67
- typeSelect.style.padding = '8px'
68
- typeSelect.style.marginBottom = '16px'
69
- typeSelect.style.borderRadius = '4px'
95
+ typeSelect.style.padding = '8px 10px'
96
+ typeSelect.style.marginBottom = '12px'
97
+ typeSelect.style.borderRadius = '6px'
98
+ typeSelect.style.fontSize = '14px'
70
99
  typeSelect.style.background = getComputedStyle(nameInput).background
71
100
  typeSelect.style.color = getComputedStyle(nameInput).color
72
101
  typeSelect.style.border = getComputedStyle(nameInput).border
102
+ typeSelect.style.boxSizing = 'border-box'
103
+ typeSelect.style.cursor = 'pointer'
104
+ typeSelect.style.transition = 'border-color 0.2s ease'
73
105
 
74
106
  // Add option groups with all API device types
75
107
  Object.keys(DEVICE_TYPES).forEach((categoryName) => {
@@ -90,24 +122,37 @@ export async function editDevice(device: any): Promise<void> {
90
122
  typeSelect.appendChild(optgroup)
91
123
  })
92
124
 
93
- const buttons = document.createElement('div')
94
- buttons.style.display = 'flex'
95
- buttons.style.gap = '8px'
96
- buttons.style.justifyContent = 'flex-end'
97
-
98
125
  const errorMessage = document.createElement('div')
99
- errorMessage.style.color = '#ef4444'
126
+ errorMessage.style.color = 'var(--switchbot-red, #ef4444)'
100
127
  errorMessage.style.marginBottom = '12px'
101
128
  errorMessage.style.fontSize = '12px'
102
129
  errorMessage.style.display = 'none'
130
+ errorMessage.style.padding = '8px 10px'
131
+ errorMessage.style.background = 'var(--switchbot-red-light, #fee2e2)'
132
+ errorMessage.style.borderRadius = '6px'
133
+ errorMessage.style.fontWeight = '500'
134
+
135
+ const buttons = document.createElement('div')
136
+ buttons.style.display = 'flex'
137
+ buttons.style.gap = '10px'
138
+ buttons.style.justifyContent = 'flex-end'
139
+ buttons.style.marginTop = '18px'
140
+ buttons.style.paddingTop = '18px'
141
+ buttons.style.borderTop = '1px solid rgba(0, 0, 0, 0.08)'
103
142
 
104
143
  const cancelBtn = document.createElement('button')
105
144
  cancelBtn.textContent = 'Cancel'
106
- cancelBtn.style.background = '#666'
145
+ cancelBtn.className = 'secondary'
146
+ cancelBtn.style.background = '#6b7280'
147
+ cancelBtn.style.padding = '8px 16px'
148
+ cancelBtn.style.fontSize = '13px'
107
149
  cancelBtn.onclick = () => div.remove()
108
150
 
109
151
  const saveBtn = document.createElement('button')
110
152
  saveBtn.textContent = 'Save'
153
+ saveBtn.style.background = 'var(--switchbot-red, #ef4444)'
154
+ saveBtn.style.padding = '8px 20px'
155
+ saveBtn.style.fontSize = '13px'
111
156
  saveBtn.onclick = async () => {
112
157
  try {
113
158
  const { updateDevice, syncParentPluginConfigFromDisk, fetchDevices } = await import('./api.js')
@@ -137,15 +182,17 @@ export async function editDevice(device: any): Promise<void> {
137
182
  buttons.appendChild(cancelBtn)
138
183
  buttons.appendChild(saveBtn)
139
184
 
185
+ contentDiv.appendChild(nameLabel)
186
+ contentDiv.appendChild(nameInput)
187
+ contentDiv.appendChild(apiTypeLabel)
188
+ contentDiv.appendChild(apiTypeInput)
189
+ contentDiv.appendChild(typeLabel)
190
+ contentDiv.appendChild(typeSelect)
191
+ contentDiv.appendChild(errorMessage)
192
+ contentDiv.appendChild(buttons)
193
+
140
194
  modal.appendChild(title)
141
- modal.appendChild(nameLabel)
142
- modal.appendChild(nameInput)
143
- modal.appendChild(apiTypeLabel)
144
- modal.appendChild(apiTypeInput)
145
- modal.appendChild(typeLabel)
146
- modal.appendChild(typeSelect)
147
- modal.appendChild(errorMessage)
148
- modal.appendChild(buttons)
195
+ modal.appendChild(contentDiv)
149
196
 
150
197
  div.appendChild(modal)
151
198
  document.body.appendChild(div)