@switchbot/homebridge-switchbot 5.0.0-beta.110 → 5.0.0-beta.112

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 (57) hide show
  1. package/.github/copilot-instructions.md +39 -0
  2. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  3. package/dist/homebridge-ui/endpoints/devices.js +10 -1
  4. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  5. package/dist/homebridge-ui/public/css/styles.css +111 -16
  6. package/dist/homebridge-ui/public/index.html +34 -60
  7. package/dist/homebridge-ui/public/js/api.d.ts +5 -1
  8. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  9. package/dist/homebridge-ui/public/js/api.js +13 -3
  10. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  11. package/dist/homebridge-ui/public/js/api.ts +13 -3
  12. package/dist/homebridge-ui/public/js/app.js +338 -42
  13. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  14. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  15. package/dist/homebridge-ui/public/js/devices.js +12 -3
  16. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  17. package/dist/homebridge-ui/public/js/devices.ts +13 -3
  18. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  19. package/dist/homebridge-ui/public/js/discovery.js +37 -3
  20. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  21. package/dist/homebridge-ui/public/js/discovery.ts +39 -2
  22. package/dist/homebridge-ui/public/js/modals.d.ts +5 -0
  23. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  24. package/dist/homebridge-ui/public/js/modals.js +146 -0
  25. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  26. package/dist/homebridge-ui/public/js/modals.ts +167 -0
  27. package/dist/homebridge-ui/public/js/render.d.ts +3 -1
  28. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  29. package/dist/homebridge-ui/public/js/render.js +135 -32
  30. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  31. package/dist/homebridge-ui/public/js/render.ts +145 -32
  32. package/dist/platform.d.ts.map +1 -1
  33. package/dist/platform.js +64 -109
  34. package/dist/platform.js.map +1 -1
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +2 -1
  37. package/dist/utils.js.map +1 -1
  38. package/docs/variables/default.html +1 -1
  39. package/package.json +9 -8
  40. package/src/homebridge-ui/endpoints/devices.ts +5 -2
  41. package/src/homebridge-ui/public/css/styles.css +111 -16
  42. package/src/homebridge-ui/public/index.html +34 -60
  43. package/src/homebridge-ui/public/js/api.ts +13 -3
  44. package/src/homebridge-ui/public/js/devices.ts +13 -3
  45. package/src/homebridge-ui/public/js/discovery.ts +39 -2
  46. package/src/homebridge-ui/public/js/modals.ts +167 -0
  47. package/src/homebridge-ui/public/js/render.ts +145 -32
  48. package/src/platform.ts +77 -109
  49. package/src/utils.ts +2 -1
  50. package/test/e2e/run-e2e.spec.ts +12 -10
  51. package/test/helpers/matter-harness.ts +2 -2
  52. package/test/matter-childbridge.spec.ts +5 -1
  53. package/test/matter-integration.spec.ts +41 -0
  54. package/test/utils.spec.ts +18 -2
  55. package/todo/COMPLETED.md +53 -0
  56. package/todo/TODO.md +13 -42
  57. package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
@@ -9,65 +9,45 @@
9
9
  <body>
10
10
  <h1>🤖 SwitchBot Configuration</h1>
11
11
 
12
- <div class="card">
12
+ <div class="card compact">
13
13
  <h2>API Credentials</h2>
14
- <p>
15
- Configure your SwitchBot API token and secret to enable device discovery
16
- and control.
17
- </p>
18
-
19
- <div class="form-group">
20
- <label for="token">API Token:</label>
21
- <input
22
- type="password"
23
- id="token"
24
- placeholder="Enter your SwitchBot API token"
25
- />
26
- <div class="status" id="tokenStatus"></div>
27
- </div>
28
-
29
- <div class="form-group">
30
- <label for="secret">API Secret:</label>
31
- <input
32
- type="password"
33
- id="secret"
34
- placeholder="Enter your SwitchBot API secret"
35
- />
36
- <div class="status" id="secretStatus"></div>
14
+ <div class="credentials-row">
15
+ <div class="form-group">
16
+ <label for="token">Token:</label>
17
+ <input type="password" id="token" placeholder="Enter token" />
18
+ <div class="status" id="tokenStatus"></div>
19
+ </div>
20
+ <div class="form-group">
21
+ <label for="secret">Secret:</label>
22
+ <input type="password" id="secret" placeholder="Enter secret" />
23
+ <div class="status" id="secretStatus"></div>
24
+ </div>
37
25
  </div>
38
-
39
- <button id="saveBtn" onclick="saveCredentials()">Save Credentials</button>
26
+ <button id="saveBtn" onclick="saveCredentials()">Save</button>
40
27
  <div id="saveStatus"></div>
41
28
  </div>
42
29
 
43
- <div class="card">
30
+ <div class="card compact">
44
31
  <h2>Discover Devices</h2>
45
- <p>
46
- Click "Discover" to find all devices available in your SwitchBot account
47
- and add them to your configuration.
48
- </p>
49
- <div style="margin-bottom: 16px">
50
- <label
51
- style="display: flex; align-items: center; gap: 8px; cursor: pointer"
52
- >
53
- <input
54
- type="checkbox"
55
- id="autoAddAllCheckbox"
56
- style="width: auto; margin: 0"
57
- />
58
- <span>Auto-add all discovered devices</span>
32
+ <div style="margin-bottom: 6px">
33
+ <label style="display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 11px">
34
+ <input type="checkbox" id="autoAddAllCheckbox" style="width: auto; margin: 0" />
35
+ <span>Auto-add all</span>
36
+ <span
37
+ title="When enabled, Discover immediately adds all found devices to your config. Existing devices are skipped automatically."
38
+ style="font-size: 11px; opacity: 0.7"
39
+ >ⓘ</span>
59
40
  </label>
41
+ <div style="margin-top: 2px; margin-left: 22px; font-size: 10px; opacity: 0.75; line-height: 1.25">
42
+ Adds all discovered devices automatically; already configured devices are skipped.
43
+ </div>
60
44
  </div>
61
- <button
62
- id="discoverBtn"
63
- onclick="discoverDevices()"
64
- style="margin-bottom: 16px"
65
- >
66
- 🔍 Discover Devices
45
+ <button id="discoverBtn" onclick="discoverDevices()" style="margin-bottom: 8px">
46
+ 🔍 Discover
67
47
  </button>
68
- <div id="discoverStatus" style="margin-bottom: 16px"></div>
48
+ <div id="discoverStatus" style="margin-bottom: 6px; font-size: 11px"></div>
69
49
  <div id="discoveredList" style="display: none">
70
- <h3 style="margin-top: 0">Available Devices</h3>
50
+ <h3 style="margin-top: 0; font-size: 12px; margin-bottom: 6px; font-weight: 600">Available Devices</h3>
71
51
  <ul
72
52
  id="discoveredDevices"
73
53
  style="max-height: 300px; overflow-y: auto"
@@ -75,22 +55,16 @@
75
55
  </div>
76
56
  </div>
77
57
 
78
- <div class="card">
58
+ <div class="card compact">
79
59
  <h2>Configured Devices</h2>
80
- <p>
81
- This page lists devices found in your Homebridge config for the
82
- SwitchBot platform. Use the copy button to insert device IDs into the
83
- plugin configuration. Connection preference (BLE/OpenAPI) is shown when
84
- available.
85
- </p>
86
60
  <div id="status">Loading…</div>
87
61
  <ul id="devices"></ul>
88
62
  <div
89
63
  id="removeAllContainer"
90
64
  style="
91
65
  display: none;
92
- margin-top: 16px;
93
- padding-top: 16px;
66
+ margin-top: 12px;
67
+ padding-top: 10px;
94
68
  border-top: 1px solid rgba(0, 0, 0, 0.1);
95
69
  "
96
70
  >
@@ -99,8 +73,8 @@
99
73
  style="
100
74
  background: #dc2626;
101
75
  width: 100%;
102
- padding: 10px 16px;
103
- font-size: 14px;
76
+ padding: 7px 12px;
77
+ font-size: 12px;
104
78
  font-weight: 500;
105
79
  "
106
80
  >
@@ -115,10 +115,20 @@ export async function discoverDevices(): Promise<any[]> {
115
115
  }
116
116
  }
117
117
 
118
- export async function addDevice(deviceId: string, name: string, type: string): Promise<any> {
118
+ export async function addDevice(
119
+ deviceId: string,
120
+ name: string,
121
+ type: string,
122
+ options?: { address?: string, model?: string, rssi?: number },
123
+ ): Promise<any> {
119
124
  try {
120
- uiLog.info('Adding device to config:', { deviceId, name, type })
121
- const resp = await homebridge.request('/add-device', { deviceId, name, type })
125
+ const payload: any = { deviceId, name, type }
126
+ if (options?.address) { payload.address = options.address }
127
+ if (options?.model) { payload.model = options.model }
128
+ if (options?.rssi !== undefined && options?.rssi !== null && options?.rssi !== 0) { payload.rssi = options.rssi }
129
+
130
+ uiLog.info('Adding device to config:', payload)
131
+ const resp = await homebridge.request('/add-device', payload)
122
132
  uiLog.info('Add device response:', resp)
123
133
 
124
134
  if (!resp || resp.success === false) {
@@ -8,9 +8,19 @@ export async function addDeviceToConfig(device: any, options: { refresh?: boolea
8
8
  const { refresh = true, showStatus = true } = options
9
9
 
10
10
  try {
11
+ const { importDiscoveredDevice } = await import('./modals.js')
12
+ const importValues = await importDiscoveredDevice(device)
13
+ if (!importValues) {
14
+ return { added: false }
15
+ }
16
+
11
17
  showBusyUi()
12
18
  uiLog.info('Adding device to config:', device)
13
- const resp = await addDevice(device.id, device.name, device.type)
19
+ const resp = await addDevice(device.id, importValues.configDeviceName, importValues.configDeviceType, {
20
+ address: importValues.address,
21
+ model: device.model,
22
+ rssi: device.rssi,
23
+ })
14
24
  uiLog.info('Add device response:', resp)
15
25
 
16
26
  if (!resp || resp.success === false) {
@@ -21,8 +31,8 @@ export async function addDeviceToConfig(device: any, options: { refresh?: boolea
21
31
  const message
22
32
  = resp?.message
23
33
  || (alreadyExists
24
- ? `Device "${device.name}" already in config`
25
- : `Device "${device.name}" added successfully!`)
34
+ ? `Device "${importValues.configDeviceName}" already in config`
35
+ : `Device "${importValues.configDeviceName}" added successfully!`)
26
36
 
27
37
  if (alreadyExists) {
28
38
  toastInfo(message)
@@ -10,6 +10,10 @@ import { hideBusyUi, showBusyUi } from './modal.js'
10
10
  import { renderDiscoveredDevices, filterDevices, sortDevices, getDiscoveryPreferences, setDiscoveryPreferences } from './render.js'
11
11
  import { toastError, toastInfo, toastSuccess, toastWarning } from './toast.js'
12
12
 
13
+ function normalizeId(value: any): string {
14
+ return String(value ?? '').trim().toLowerCase()
15
+ }
16
+
13
17
  export async function discoverDevices(): Promise<void> {
14
18
  const btn = document.getElementById('discoverBtn') as HTMLButtonElement
15
19
  const status = document.getElementById('discoverStatus')
@@ -138,6 +142,7 @@ export async function discoverDevices(): Promise<void> {
138
142
  btn.style.cursor = 'pointer'
139
143
  btn.style.border = preferences.connectionType === option.value ? '2px solid #007AFF' : '1px solid #ccc'
140
144
  btn.style.backgroundColor = preferences.connectionType === option.value ? '#f0f7ff' : '#fff'
145
+ btn.style.color = preferences.connectionType === option.value ? '#1d4ed8' : '#374151'
141
146
 
142
147
  btn.onclick = () => {
143
148
  preferences.connectionType = option.value
@@ -147,9 +152,11 @@ export async function discoverDevices(): Promise<void> {
147
152
  for (const b of Array.from(filterGroup.querySelectorAll('button'))) {
148
153
  ;(b as HTMLButtonElement).style.border = '1px solid #ccc'
149
154
  ;(b as HTMLButtonElement).style.backgroundColor = '#fff'
155
+ ;(b as HTMLButtonElement).style.color = '#374151'
150
156
  }
151
157
  btn.style.border = '2px solid #007AFF'
152
158
  btn.style.backgroundColor = '#f0f7ff'
159
+ btn.style.color = '#1d4ed8'
153
160
  }
154
161
 
155
162
  filterGroup.appendChild(btn)
@@ -222,6 +229,19 @@ export async function discoverDevices(): Promise<void> {
222
229
  list.innerHTML = ''
223
230
  list.appendChild(controlsDiv)
224
231
 
232
+ // Create or get the device list container
233
+ let deviceListContainer = document.getElementById('discoveredDevices')
234
+ if (!deviceListContainer) {
235
+ deviceListContainer = document.createElement('ul')
236
+ deviceListContainer.id = 'discoveredDevices'
237
+ deviceListContainer.style.maxHeight = '400px'
238
+ deviceListContainer.style.overflowY = 'auto'
239
+ deviceListContainer.style.marginTop = '12px'
240
+ deviceListContainer.style.padding = '0'
241
+ deviceListContainer.style.listStyle = 'none'
242
+ list.appendChild(deviceListContainer)
243
+ }
244
+
225
245
  // Display discovery list
226
246
  list.style.display = 'block'
227
247
 
@@ -255,9 +275,26 @@ async function updateDiscoveryView(
255
275
  const filtered = filterDevices(allDevices, preferences.connectionType, preferences.searchQuery)
256
276
  const sorted = sortDevices(filtered, preferences.sortBy)
257
277
 
278
+ // Build configured device lookup for duplicate detection
279
+ const configured = await fetchDevices()
280
+ const configuredIds = new Set((configured || []).map(d => normalizeId(d?.id || d?.deviceId)))
281
+
258
282
  // Render updated list
259
- const ul = await renderDiscoveredDevices(sorted)
260
- document.getElementById('discoveredDevices')?.replaceWith(ul)
283
+ const ul = await renderDiscoveredDevices(sorted, {
284
+ configuredIds,
285
+ })
286
+
287
+ // Replace or append the rendered list
288
+ const existingList = document.getElementById('discoveredDevices')
289
+ if (existingList && existingList.parentNode) {
290
+ existingList.replaceWith(ul)
291
+ } else {
292
+ // Fallback: append to discovery list container
293
+ const listContainer = document.getElementById('discoveredList')
294
+ if (listContainer) {
295
+ listContainer.appendChild(ul)
296
+ }
297
+ }
261
298
  ul.id = 'discoveredDevices'
262
299
 
263
300
  // Update status with count
@@ -1,6 +1,173 @@
1
1
  import { DEVICE_TYPES } from './constants.js'
2
2
  import { uiLog } from './logger.js'
3
3
 
4
+ export async function importDiscoveredDevice(device: any): Promise<{ configDeviceName: string, configDeviceType: string, address?: string } | null> {
5
+ return new Promise((resolve) => {
6
+ const div = document.createElement('div')
7
+ div.style.position = 'fixed'
8
+ div.style.top = '0'
9
+ div.style.left = '0'
10
+ div.style.width = '100%'
11
+ div.style.height = '100%'
12
+ div.style.background = 'rgba(0,0,0,0.7)'
13
+ div.style.display = 'flex'
14
+ div.style.alignItems = 'center'
15
+ div.style.justifyContent = 'center'
16
+ div.style.zIndex = '9999'
17
+
18
+ const modal = document.createElement('div')
19
+ modal.style.background = getComputedStyle(document.body).backgroundColor
20
+ modal.style.color = getComputedStyle(document.body).color
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)'
28
+
29
+ const title = document.createElement('h3')
30
+ title.textContent = 'Import Discovered Device'
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'
41
+
42
+ const nameLabel = document.createElement('label')
43
+ nameLabel.textContent = 'Device Name'
44
+ nameLabel.style.display = 'block'
45
+ nameLabel.style.marginBottom = '6px'
46
+ nameLabel.style.fontWeight = '500'
47
+ nameLabel.style.fontSize = '12px'
48
+ nameLabel.style.color = '#6b7280'
49
+
50
+ const nameInput = document.createElement('input')
51
+ nameInput.type = 'text'
52
+ nameInput.value = device.name || device.id || ''
53
+ nameInput.style.width = '100%'
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
+
60
+ const typeLabel = document.createElement('label')
61
+ typeLabel.textContent = 'Config Device Type'
62
+ typeLabel.style.display = 'block'
63
+ typeLabel.style.marginBottom = '6px'
64
+ typeLabel.style.fontWeight = '500'
65
+ typeLabel.style.fontSize = '12px'
66
+ typeLabel.style.color = '#6b7280'
67
+
68
+ const typeSelect = document.createElement('select')
69
+ typeSelect.style.width = '100%'
70
+ typeSelect.style.padding = '8px 10px'
71
+ typeSelect.style.marginBottom = '12px'
72
+ typeSelect.style.borderRadius = '6px'
73
+ typeSelect.style.fontSize = '14px'
74
+ typeSelect.style.background = getComputedStyle(nameInput).background
75
+ typeSelect.style.color = getComputedStyle(nameInput).color
76
+ typeSelect.style.border = getComputedStyle(nameInput).border
77
+ typeSelect.style.boxSizing = 'border-box'
78
+
79
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
80
+ const optgroup = document.createElement('optgroup')
81
+ optgroup.label = categoryName
82
+
83
+ DEVICE_TYPES[categoryName as keyof typeof DEVICE_TYPES].forEach((deviceType) => {
84
+ const opt = document.createElement('option')
85
+ opt.value = deviceType
86
+ opt.text = deviceType
87
+ const detectedType = (device.type || '').toLowerCase()
88
+ opt.selected = deviceType.toLowerCase() === detectedType
89
+ optgroup.appendChild(opt)
90
+ })
91
+
92
+ typeSelect.appendChild(optgroup)
93
+ })
94
+
95
+ const macLabel = document.createElement('label')
96
+ macLabel.textContent = 'BLE MAC Address (optional)'
97
+ macLabel.style.display = 'block'
98
+ macLabel.style.marginBottom = '6px'
99
+ macLabel.style.fontWeight = '500'
100
+ macLabel.style.fontSize = '12px'
101
+ macLabel.style.color = '#6b7280'
102
+
103
+ const macInput = document.createElement('input')
104
+ macInput.type = 'text'
105
+ macInput.value = device.address || ''
106
+ macInput.placeholder = 'AA:BB:CC:DD:EE:FF'
107
+ macInput.style.width = '100%'
108
+ macInput.style.marginBottom = '12px'
109
+ macInput.style.padding = '8px 10px'
110
+ macInput.style.borderRadius = '6px'
111
+ macInput.style.fontSize = '14px'
112
+ macInput.style.boxSizing = 'border-box'
113
+
114
+ const buttons = document.createElement('div')
115
+ buttons.style.display = 'flex'
116
+ buttons.style.gap = '10px'
117
+ buttons.style.justifyContent = 'flex-end'
118
+ buttons.style.marginTop = '18px'
119
+ buttons.style.paddingTop = '18px'
120
+ buttons.style.borderTop = '1px solid rgba(0, 0, 0, 0.08)'
121
+
122
+ const cancelBtn = document.createElement('button')
123
+ cancelBtn.textContent = 'Cancel'
124
+ cancelBtn.className = 'secondary'
125
+ cancelBtn.style.background = '#6b7280'
126
+ cancelBtn.style.padding = '8px 16px'
127
+ cancelBtn.style.fontSize = '13px'
128
+
129
+ const importBtn = document.createElement('button')
130
+ importBtn.textContent = 'Add to Config'
131
+ importBtn.style.background = 'var(--switchbot-red, #ef4444)'
132
+ importBtn.style.padding = '8px 20px'
133
+ importBtn.style.fontSize = '13px'
134
+
135
+ const cleanup = (result: { configDeviceName: string, configDeviceType: string, address?: string } | null) => {
136
+ div.remove()
137
+ resolve(result)
138
+ }
139
+
140
+ cancelBtn.onclick = () => cleanup(null)
141
+ importBtn.onclick = () => cleanup({
142
+ configDeviceName: nameInput.value || device.name || device.id,
143
+ configDeviceType: typeSelect.value || device.type,
144
+ address: macInput.value || undefined,
145
+ })
146
+
147
+ div.addEventListener('click', (event) => {
148
+ if (event.target === div) {
149
+ cleanup(null)
150
+ }
151
+ })
152
+
153
+ buttons.appendChild(cancelBtn)
154
+ buttons.appendChild(importBtn)
155
+ contentDiv.appendChild(nameLabel)
156
+ contentDiv.appendChild(nameInput)
157
+ contentDiv.appendChild(typeLabel)
158
+ contentDiv.appendChild(typeSelect)
159
+ contentDiv.appendChild(macLabel)
160
+ contentDiv.appendChild(macInput)
161
+ contentDiv.appendChild(buttons)
162
+
163
+ modal.appendChild(title)
164
+ modal.appendChild(contentDiv)
165
+ div.appendChild(modal)
166
+ document.body.appendChild(div)
167
+ nameInput.focus()
168
+ })
169
+ }
170
+
4
171
  export async function editDevice(device: any): Promise<void> {
5
172
  // Create modal dialog for editing
6
173
  const div = document.createElement('div')