@switchbot/homebridge-switchbot 5.0.0-beta.133 → 5.0.0-beta.135
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 +0 -11
- package/dist/deviceFactory.d.ts +3 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +3 -18
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +1 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +67 -19
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/homebridge-ui/public/index.html +107 -124
- package/dist/homebridge-ui/public/js/api.d.ts +1 -4
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +1 -7
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +1 -8
- package/dist/homebridge-ui/public/js/app.js +201 -90
- package/dist/homebridge-ui/public/js/app.js.map +2 -2
- package/dist/homebridge-ui/public/js/app.ts +5 -6
- package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.js +1 -1
- package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
- package/dist/homebridge-ui/public/js/credentials.ts +7 -3
- package/dist/homebridge-ui/public/js/devices-delete.js +1 -1
- package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices-delete.ts +1 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +0 -1
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +0 -2
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +6 -3
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +9 -3
- package/dist/homebridge-ui/public/js/logger.d.ts +1 -0
- package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/logger.js +8 -3
- package/dist/homebridge-ui/public/js/logger.js.map +1 -1
- package/dist/homebridge-ui/public/js/logger.ts +12 -6
- package/dist/homebridge-ui/public/js/modals.d.ts +4 -0
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +152 -59
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +187 -60
- package/dist/homebridge-ui/public/js/render.d.ts +3 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +63 -24
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +102 -51
- package/dist/homebridge-ui/utils/logger.js +1 -1
- package/dist/homebridge-ui/utils/logger.js.map +1 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +38 -30
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts +5 -5
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +21 -12
- package/dist/switchbotClient.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/scripts/build-ui.js +4 -3
- package/scripts/generate-matter-maps.js +34 -17
- package/scripts/sync-device-types.mjs +4 -1
- package/src/deviceFactory.ts +3 -18
- package/src/devices/genericDevice.ts +69 -21
- package/src/homebridge-ui/public/index.html +107 -124
- package/src/homebridge-ui/public/js/api.ts +1 -8
- package/src/homebridge-ui/public/js/app.ts +5 -6
- package/src/homebridge-ui/public/js/credentials.ts +7 -3
- package/src/homebridge-ui/public/js/devices-delete.ts +1 -1
- package/src/homebridge-ui/public/js/devices.ts +0 -2
- package/src/homebridge-ui/public/js/discovery.ts +9 -3
- package/src/homebridge-ui/public/js/logger.ts +12 -6
- package/src/homebridge-ui/public/js/modals.ts +187 -60
- package/src/homebridge-ui/public/js/render.ts +102 -51
- package/src/homebridge-ui/utils/logger.ts +1 -1
- package/src/platform.ts +38 -29
- package/src/switchbotClient.ts +42 -27
- package/test/accessory-restore.spec.ts +9 -8
- package/test/device-mapping.spec.ts +4 -3
- package/test/deviceFactory.spec.ts +4 -3
- package/test/e2e/run-e2e.spec.ts +6 -10
- package/test/fan-swing.spec.ts +7 -6
- package/test/helpers/matter-harness.ts +18 -7
- package/test/lock-users.spec.ts +5 -5
- package/test/matter-childbridge.spec.ts +10 -7
- package/test/matter-descriptors.spec.ts +8 -8
- package/test/matter-device-state.spec.ts +52 -36
- package/test/matter-integration.spec.ts +53 -6
- package/test/platform.integration.spec.ts +7 -5
- package/test/switchbot-client-debounce.spec.ts +36 -40
- package/test/switchbot-client-openapi.spec.ts +22 -15
- package/test/switchbotClient.spec.ts +4 -3
- package/test/utils.spec.ts +2 -1
|
@@ -1,7 +1,15 @@
|
|
|
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<{
|
|
4
|
+
export async function importDiscoveredDevice(device: any): Promise<{
|
|
5
|
+
configDeviceName: string
|
|
6
|
+
configDeviceType: string
|
|
7
|
+
address?: string
|
|
8
|
+
connectionPreference?: string
|
|
9
|
+
room?: string
|
|
10
|
+
encryptionKey?: string
|
|
11
|
+
keyId?: string
|
|
12
|
+
} | null> {
|
|
5
13
|
return new Promise((resolve) => {
|
|
6
14
|
const div = document.createElement('div')
|
|
7
15
|
div.style.position = 'fixed'
|
|
@@ -91,6 +99,51 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
|
|
|
91
99
|
typeSelect.appendChild(optgroup)
|
|
92
100
|
})
|
|
93
101
|
|
|
102
|
+
// --- Connection Preference ---
|
|
103
|
+
const connectionPrefLabel = document.createElement('label')
|
|
104
|
+
connectionPrefLabel.textContent = 'Connection Preference'
|
|
105
|
+
connectionPrefLabel.style.display = 'block'
|
|
106
|
+
connectionPrefLabel.style.marginBottom = '6px'
|
|
107
|
+
connectionPrefLabel.style.fontWeight = '500'
|
|
108
|
+
connectionPrefLabel.style.fontSize = '12px'
|
|
109
|
+
connectionPrefLabel.style.color = '#6b7280'
|
|
110
|
+
|
|
111
|
+
const connectionPrefSelect = document.createElement('select')
|
|
112
|
+
connectionPrefSelect.style.width = '100%'
|
|
113
|
+
connectionPrefSelect.style.marginBottom = '12px'
|
|
114
|
+
connectionPrefSelect.style.padding = '8px 10px'
|
|
115
|
+
connectionPrefSelect.style.borderRadius = '6px'
|
|
116
|
+
connectionPrefSelect.style.fontSize = '14px'
|
|
117
|
+
connectionPrefSelect.style.boxSizing = 'border-box';
|
|
118
|
+
['auto', 'ble', 'openapi'].forEach((val) => {
|
|
119
|
+
const opt = document.createElement('option')
|
|
120
|
+
opt.value = val
|
|
121
|
+
opt.text = val.charAt(0).toUpperCase() + val.slice(1)
|
|
122
|
+
opt.selected = (device.connectionPreference || 'auto') === val
|
|
123
|
+
connectionPrefSelect.appendChild(opt)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// --- Room ---
|
|
127
|
+
const roomLabel = document.createElement('label')
|
|
128
|
+
roomLabel.textContent = 'Room'
|
|
129
|
+
roomLabel.style.display = 'block'
|
|
130
|
+
roomLabel.style.marginBottom = '6px'
|
|
131
|
+
roomLabel.style.fontWeight = '500'
|
|
132
|
+
roomLabel.style.fontSize = '12px'
|
|
133
|
+
roomLabel.style.color = '#6b7280'
|
|
134
|
+
|
|
135
|
+
const roomInput = document.createElement('input')
|
|
136
|
+
roomInput.type = 'text'
|
|
137
|
+
roomInput.value = device.room || ''
|
|
138
|
+
roomInput.placeholder = 'Optional room/location metadata'
|
|
139
|
+
roomInput.style.width = '100%'
|
|
140
|
+
roomInput.style.marginBottom = '12px'
|
|
141
|
+
roomInput.style.padding = '8px 10px'
|
|
142
|
+
roomInput.style.borderRadius = '6px'
|
|
143
|
+
roomInput.style.fontSize = '14px'
|
|
144
|
+
roomInput.style.boxSizing = 'border-box'
|
|
145
|
+
|
|
146
|
+
// --- BLE MAC Address ---
|
|
94
147
|
const macLabel = document.createElement('label')
|
|
95
148
|
macLabel.textContent = 'BLE MAC Address (optional)'
|
|
96
149
|
macLabel.style.display = 'block'
|
|
@@ -110,7 +163,7 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
|
|
|
110
163
|
macInput.style.fontSize = '14px'
|
|
111
164
|
macInput.style.boxSizing = 'border-box'
|
|
112
165
|
|
|
113
|
-
//
|
|
166
|
+
// --- Encryption Key ---
|
|
114
167
|
const encryptionKeyLabel = document.createElement('label')
|
|
115
168
|
encryptionKeyLabel.textContent = 'BLE Encryption Key (optional)'
|
|
116
169
|
encryptionKeyLabel.style.display = 'block'
|
|
@@ -130,7 +183,7 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
|
|
|
130
183
|
encryptionKeyInput.style.fontSize = '14px'
|
|
131
184
|
encryptionKeyInput.style.boxSizing = 'border-box'
|
|
132
185
|
|
|
133
|
-
//
|
|
186
|
+
// --- Key ID ---
|
|
134
187
|
const keyIdLabel = document.createElement('label')
|
|
135
188
|
keyIdLabel.textContent = 'BLE Key ID (optional)'
|
|
136
189
|
keyIdLabel.style.display = 'block'
|
|
@@ -171,7 +224,15 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
|
|
|
171
224
|
importBtn.style.padding = '8px 20px'
|
|
172
225
|
importBtn.style.fontSize = '13px'
|
|
173
226
|
|
|
174
|
-
const cleanup = (result: {
|
|
227
|
+
const cleanup = (result: {
|
|
228
|
+
configDeviceName: string
|
|
229
|
+
configDeviceType: string
|
|
230
|
+
address?: string
|
|
231
|
+
connectionPreference?: string
|
|
232
|
+
room?: string
|
|
233
|
+
encryptionKey?: string
|
|
234
|
+
keyId?: string
|
|
235
|
+
} | null) => {
|
|
175
236
|
div.remove()
|
|
176
237
|
resolve(result)
|
|
177
238
|
}
|
|
@@ -181,6 +242,8 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
|
|
|
181
242
|
configDeviceName: nameInput.value || device.name || device.id,
|
|
182
243
|
configDeviceType: typeSelect.value || device.type,
|
|
183
244
|
address: macInput.value || undefined,
|
|
245
|
+
connectionPreference: connectionPrefSelect.value || undefined,
|
|
246
|
+
room: roomInput.value || undefined,
|
|
184
247
|
encryptionKey: encryptionKeyInput.value || undefined,
|
|
185
248
|
keyId: keyIdInput.value || undefined,
|
|
186
249
|
})
|
|
@@ -197,6 +260,10 @@ export async function importDiscoveredDevice(device: any): Promise<{ configDevic
|
|
|
197
260
|
contentDiv.appendChild(nameInput)
|
|
198
261
|
contentDiv.appendChild(typeLabel)
|
|
199
262
|
contentDiv.appendChild(typeSelect)
|
|
263
|
+
contentDiv.appendChild(connectionPrefLabel)
|
|
264
|
+
contentDiv.appendChild(connectionPrefSelect)
|
|
265
|
+
contentDiv.appendChild(roomLabel)
|
|
266
|
+
contentDiv.appendChild(roomInput)
|
|
200
267
|
contentDiv.appendChild(macLabel)
|
|
201
268
|
contentDiv.appendChild(macInput)
|
|
202
269
|
contentDiv.appendChild(encryptionKeyLabel)
|
|
@@ -250,58 +317,6 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
250
317
|
typeSelect.appendChild(optgroup)
|
|
251
318
|
})
|
|
252
319
|
|
|
253
|
-
// --- Matter Type Advanced Field (only for window covering types) ---
|
|
254
|
-
const windowCoveringTypes = ['Roller Shade', 'Curtain', 'Curtain3', 'Blind Tilt']
|
|
255
|
-
const showMatterType = windowCoveringTypes.includes(device.configDeviceType || device.deviceType || device.type || '')
|
|
256
|
-
const matterTypeLabel = document.createElement('label')
|
|
257
|
-
matterTypeLabel.textContent = 'Matter Window Covering Type (Advanced)'
|
|
258
|
-
matterTypeLabel.style.display = showMatterType ? 'block' : 'none'
|
|
259
|
-
matterTypeLabel.style.marginBottom = '6px'
|
|
260
|
-
matterTypeLabel.style.fontWeight = '500'
|
|
261
|
-
matterTypeLabel.style.fontSize = '12px'
|
|
262
|
-
matterTypeLabel.style.color = '#6b7280'
|
|
263
|
-
matterTypeLabel.style.marginTop = '2px'
|
|
264
|
-
const matterTypes = [
|
|
265
|
-
{ value: '', label: 'Auto-detect (recommended)' },
|
|
266
|
-
{ value: 0, label: 'Roller Shade (0)' },
|
|
267
|
-
{ value: 1, label: 'Roman Shade (1)' },
|
|
268
|
-
{ value: 2, label: 'Balloon Shade (2)' },
|
|
269
|
-
{ value: 3, label: 'Woven Wood (3)' },
|
|
270
|
-
{ value: 4, label: 'Drapery (4)' },
|
|
271
|
-
{ value: 5, label: 'Awning (5)' },
|
|
272
|
-
{ value: 6, label: 'Shutter (6)' },
|
|
273
|
-
{ value: 7, label: 'Tilt Blind (7)' },
|
|
274
|
-
{ value: 8, label: 'Blind Tilt (8)' },
|
|
275
|
-
{ value: 9, label: 'Projector Screen (9)' },
|
|
276
|
-
{ value: 15, label: 'Other (15)' },
|
|
277
|
-
]
|
|
278
|
-
const matterTypeSelect = document.createElement('select')
|
|
279
|
-
matterTypeSelect.style.width = '100%'
|
|
280
|
-
matterTypeSelect.style.marginBottom = '12px'
|
|
281
|
-
matterTypeSelect.style.padding = '8px 10px'
|
|
282
|
-
matterTypeSelect.style.borderRadius = '6px'
|
|
283
|
-
matterTypeSelect.style.fontSize = '14px'
|
|
284
|
-
matterTypeSelect.style.boxSizing = 'border-box'
|
|
285
|
-
matterTypes.forEach((opt) => {
|
|
286
|
-
const option = document.createElement('option')
|
|
287
|
-
option.value = String(opt.value)
|
|
288
|
-
option.textContent = opt.label
|
|
289
|
-
if (device.matterType != null && String(opt.value) === String(device.matterType)) {
|
|
290
|
-
option.selected = true
|
|
291
|
-
}
|
|
292
|
-
matterTypeSelect.appendChild(option)
|
|
293
|
-
})
|
|
294
|
-
function updateMatterTypeVisibility() {
|
|
295
|
-
if (windowCoveringTypes.includes(typeSelect.value)) {
|
|
296
|
-
matterTypeLabel.style.display = 'block'
|
|
297
|
-
matterTypeSelect.style.display = 'block'
|
|
298
|
-
} else {
|
|
299
|
-
matterTypeLabel.style.display = 'none'
|
|
300
|
-
matterTypeSelect.style.display = 'none'
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
typeSelect.addEventListener('change', updateMatterTypeVisibility)
|
|
304
|
-
updateMatterTypeVisibility()
|
|
305
320
|
// Create modal dialog for editing
|
|
306
321
|
const div = document.createElement('div')
|
|
307
322
|
div.style.position = 'fixed'
|
|
@@ -402,6 +417,90 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
402
417
|
typeSelect.appendChild(optgroup)
|
|
403
418
|
})
|
|
404
419
|
|
|
420
|
+
// --- Connection Preference ---
|
|
421
|
+
const connectionPrefLabel = document.createElement('label')
|
|
422
|
+
connectionPrefLabel.textContent = 'Connection Preference'
|
|
423
|
+
connectionPrefLabel.style.display = 'block'
|
|
424
|
+
connectionPrefLabel.style.marginBottom = '6px'
|
|
425
|
+
connectionPrefLabel.style.fontWeight = '500'
|
|
426
|
+
connectionPrefLabel.style.fontSize = '12px'
|
|
427
|
+
connectionPrefLabel.style.color = '#6b7280'
|
|
428
|
+
|
|
429
|
+
const connectionPrefSelect = document.createElement('select')
|
|
430
|
+
connectionPrefSelect.style.width = '100%'
|
|
431
|
+
connectionPrefSelect.style.marginBottom = '12px'
|
|
432
|
+
connectionPrefSelect.style.padding = '8px 10px'
|
|
433
|
+
connectionPrefSelect.style.borderRadius = '6px'
|
|
434
|
+
connectionPrefSelect.style.fontSize = '14px'
|
|
435
|
+
connectionPrefSelect.style.boxSizing = 'border-box'
|
|
436
|
+
;['auto', 'ble', 'openapi'].forEach((val) => {
|
|
437
|
+
const opt = document.createElement('option')
|
|
438
|
+
opt.value = val
|
|
439
|
+
opt.text = val.charAt(0).toUpperCase() + val.slice(1)
|
|
440
|
+
opt.selected = (device.connectionPreference || 'auto') === val
|
|
441
|
+
connectionPrefSelect.appendChild(opt)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// --- Room ---
|
|
445
|
+
const roomLabel = document.createElement('label')
|
|
446
|
+
roomLabel.textContent = 'Room'
|
|
447
|
+
roomLabel.style.display = 'block'
|
|
448
|
+
roomLabel.style.marginBottom = '6px'
|
|
449
|
+
roomLabel.style.fontWeight = '500'
|
|
450
|
+
roomLabel.style.fontSize = '12px'
|
|
451
|
+
roomLabel.style.color = '#6b7280'
|
|
452
|
+
|
|
453
|
+
const roomInput = document.createElement('input')
|
|
454
|
+
roomInput.type = 'text'
|
|
455
|
+
roomInput.value = device.room || ''
|
|
456
|
+
roomInput.placeholder = 'Optional room/location metadata'
|
|
457
|
+
roomInput.style.width = '100%'
|
|
458
|
+
roomInput.style.marginBottom = '12px'
|
|
459
|
+
roomInput.style.padding = '8px 10px'
|
|
460
|
+
roomInput.style.borderRadius = '6px'
|
|
461
|
+
roomInput.style.fontSize = '14px'
|
|
462
|
+
roomInput.style.boxSizing = 'border-box'
|
|
463
|
+
|
|
464
|
+
// --- Encryption Key ---
|
|
465
|
+
const encryptionKeyLabel = document.createElement('label')
|
|
466
|
+
encryptionKeyLabel.textContent = 'BLE Encryption Key (optional)'
|
|
467
|
+
encryptionKeyLabel.style.display = 'block'
|
|
468
|
+
encryptionKeyLabel.style.marginBottom = '6px'
|
|
469
|
+
encryptionKeyLabel.style.fontWeight = '500'
|
|
470
|
+
encryptionKeyLabel.style.fontSize = '12px'
|
|
471
|
+
encryptionKeyLabel.style.color = '#6b7280'
|
|
472
|
+
|
|
473
|
+
const encryptionKeyInput = document.createElement('input')
|
|
474
|
+
encryptionKeyInput.type = 'password'
|
|
475
|
+
encryptionKeyInput.value = device.encryptionKey || ''
|
|
476
|
+
encryptionKeyInput.placeholder = 'Paste device BLE encryption key'
|
|
477
|
+
encryptionKeyInput.style.width = '100%'
|
|
478
|
+
encryptionKeyInput.style.marginBottom = '12px'
|
|
479
|
+
encryptionKeyInput.style.padding = '8px 10px'
|
|
480
|
+
encryptionKeyInput.style.borderRadius = '6px'
|
|
481
|
+
encryptionKeyInput.style.fontSize = '14px'
|
|
482
|
+
encryptionKeyInput.style.boxSizing = 'border-box'
|
|
483
|
+
|
|
484
|
+
// --- Key ID ---
|
|
485
|
+
const keyIdLabel = document.createElement('label')
|
|
486
|
+
keyIdLabel.textContent = 'BLE Key ID (optional)'
|
|
487
|
+
keyIdLabel.style.display = 'block'
|
|
488
|
+
keyIdLabel.style.marginBottom = '6px'
|
|
489
|
+
keyIdLabel.style.fontWeight = '500'
|
|
490
|
+
keyIdLabel.style.fontSize = '12px'
|
|
491
|
+
keyIdLabel.style.color = '#6b7280'
|
|
492
|
+
|
|
493
|
+
const keyIdInput = document.createElement('input')
|
|
494
|
+
keyIdInput.type = 'text'
|
|
495
|
+
keyIdInput.value = device.keyId || ''
|
|
496
|
+
keyIdInput.placeholder = 'e.g. ff'
|
|
497
|
+
keyIdInput.style.width = '100%'
|
|
498
|
+
keyIdInput.style.marginBottom = '12px'
|
|
499
|
+
keyIdInput.style.padding = '8px 10px'
|
|
500
|
+
keyIdInput.style.borderRadius = '6px'
|
|
501
|
+
keyIdInput.style.fontSize = '14px'
|
|
502
|
+
keyIdInput.style.boxSizing = 'border-box'
|
|
503
|
+
|
|
405
504
|
const errorMessage = document.createElement('div')
|
|
406
505
|
errorMessage.style.color = 'var(--switchbot-red, #ef4444)'
|
|
407
506
|
errorMessage.style.marginBottom = '12px'
|
|
@@ -442,10 +541,32 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
442
541
|
deviceId: device.id,
|
|
443
542
|
configDeviceName: nameInput.value || undefined,
|
|
444
543
|
configDeviceType: typeSelect.value,
|
|
445
|
-
|
|
544
|
+
connectionPreference: connectionPrefSelect.value,
|
|
545
|
+
room: roomInput.value || undefined,
|
|
546
|
+
encryptionKey: encryptionKeyInput.value || undefined,
|
|
547
|
+
keyId: keyIdInput.value || undefined,
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Only include defined properties in the options object
|
|
551
|
+
const options: any = {}
|
|
552
|
+
if (params.connectionPreference !== undefined) {
|
|
553
|
+
options.connectionPreference = params.connectionPreference
|
|
554
|
+
}
|
|
555
|
+
if (params.room !== undefined) {
|
|
556
|
+
options.room = params.room
|
|
557
|
+
}
|
|
558
|
+
if (params.encryptionKey !== undefined) {
|
|
559
|
+
options.encryptionKey = params.encryptionKey
|
|
560
|
+
}
|
|
561
|
+
if (params.keyId !== undefined) {
|
|
562
|
+
options.keyId = params.keyId
|
|
446
563
|
}
|
|
447
564
|
|
|
448
|
-
await updateDevice(
|
|
565
|
+
await updateDevice(
|
|
566
|
+
params.deviceId,
|
|
567
|
+
params.configDeviceName,
|
|
568
|
+
params.configDeviceType,
|
|
569
|
+
)
|
|
449
570
|
await syncParentPluginConfigFromDisk()
|
|
450
571
|
|
|
451
572
|
// Refresh device list
|
|
@@ -469,8 +590,14 @@ export async function editDevice(device: any): Promise<void> {
|
|
|
469
590
|
contentDiv.appendChild(apiTypeInput)
|
|
470
591
|
contentDiv.appendChild(typeLabel)
|
|
471
592
|
contentDiv.appendChild(typeSelect)
|
|
472
|
-
contentDiv.appendChild(
|
|
473
|
-
contentDiv.appendChild(
|
|
593
|
+
contentDiv.appendChild(connectionPrefLabel)
|
|
594
|
+
contentDiv.appendChild(connectionPrefSelect)
|
|
595
|
+
contentDiv.appendChild(roomLabel)
|
|
596
|
+
contentDiv.appendChild(roomInput)
|
|
597
|
+
contentDiv.appendChild(encryptionKeyLabel)
|
|
598
|
+
contentDiv.appendChild(encryptionKeyInput)
|
|
599
|
+
contentDiv.appendChild(keyIdLabel)
|
|
600
|
+
contentDiv.appendChild(keyIdInput)
|
|
474
601
|
contentDiv.appendChild(errorMessage)
|
|
475
602
|
contentDiv.appendChild(buttons)
|
|
476
603
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
// Move regexes to module scope to avoid re-compilation on every call
|
|
2
|
+
// import type { DEVICE_TYPES } from './constants.js' // Removed unused import
|
|
3
|
+
|
|
4
|
+
const SPACES_REGEX = /\s/g
|
|
5
|
+
const CAMELCASE_REGEX = /([A-Z])/g
|
|
6
|
+
const FIRST_CHAR_REGEX = /^./
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Get RSSI signal quality level and color based on dBm value
|
|
@@ -129,7 +134,9 @@ export function renderBadge(text: string, style: string): HTMLElement {
|
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
export function renderConnectionBadge(connectionType: string): HTMLElement | null {
|
|
132
|
-
if (!connectionType) {
|
|
137
|
+
if (!connectionType) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
133
140
|
|
|
134
141
|
const badge = renderBadge(connectionType, '')
|
|
135
142
|
|
|
@@ -265,17 +272,27 @@ export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
|
265
272
|
let batteryHistory: Array<{ value: number, ts: number }> = []
|
|
266
273
|
try {
|
|
267
274
|
const raw = localStorage.getItem(batteryHistoryKey)
|
|
268
|
-
if (raw)
|
|
269
|
-
|
|
275
|
+
if (raw) {
|
|
276
|
+
batteryHistory = JSON.parse(raw)
|
|
277
|
+
}
|
|
278
|
+
} catch (e) {
|
|
279
|
+
// Optionally log or handle error
|
|
280
|
+
}
|
|
270
281
|
const now = Date.now()
|
|
271
282
|
if (typeof device?.battery === 'number') {
|
|
272
283
|
// Only add if different from last or >1h since last
|
|
273
|
-
const last = batteryHistory
|
|
284
|
+
const last = batteryHistory.at(-1)
|
|
274
285
|
if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1000) {
|
|
275
286
|
batteryHistory.push({ value: device.battery, ts: now })
|
|
276
287
|
// Keep only last 30 entries (about a month if daily)
|
|
277
|
-
if (batteryHistory.length > 30)
|
|
278
|
-
|
|
288
|
+
if (batteryHistory.length > 30) {
|
|
289
|
+
batteryHistory = batteryHistory.slice(-30)
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory))
|
|
293
|
+
} catch (e) {
|
|
294
|
+
// Optionally log or handle error
|
|
295
|
+
}
|
|
279
296
|
}
|
|
280
297
|
}
|
|
281
298
|
|
|
@@ -291,8 +308,6 @@ export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
|
291
308
|
{ label: 'Last Seen', value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) },
|
|
292
309
|
]
|
|
293
310
|
|
|
294
|
-
|
|
295
|
-
|
|
296
311
|
for (const row of rows) {
|
|
297
312
|
const line = document.createElement('div')
|
|
298
313
|
line.style.display = 'flex'
|
|
@@ -366,7 +381,9 @@ export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
|
366
381
|
chart.style.height = '28px'
|
|
367
382
|
chart.style.display = 'flex'
|
|
368
383
|
// SVG sparkline
|
|
369
|
-
const w = 120
|
|
384
|
+
const w = 120
|
|
385
|
+
const h = 24
|
|
386
|
+
const pad = 2
|
|
370
387
|
const min = Math.min(...batteryHistory.map(b => b.value), 100)
|
|
371
388
|
const max = Math.max(...batteryHistory.map(b => b.value), 0)
|
|
372
389
|
const range = max - min || 1
|
|
@@ -424,11 +441,26 @@ export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
|
424
441
|
|
|
425
442
|
// --- Expose advanced/extra features dynamically ---
|
|
426
443
|
const featureKeys = [
|
|
427
|
-
'airQuality',
|
|
428
|
-
'
|
|
429
|
-
'
|
|
444
|
+
'airQuality',
|
|
445
|
+
'pm25',
|
|
446
|
+
'pm10',
|
|
447
|
+
'voc',
|
|
448
|
+
'co2',
|
|
449
|
+
'humidity',
|
|
450
|
+
'temperature',
|
|
451
|
+
'preset',
|
|
452
|
+
'mode',
|
|
453
|
+
'presetMode',
|
|
454
|
+
'direction',
|
|
455
|
+
'calibration',
|
|
456
|
+
'multiCommand',
|
|
457
|
+
'extendedInfo',
|
|
458
|
+
'segmentedControl',
|
|
459
|
+
'features',
|
|
460
|
+
'capabilities',
|
|
461
|
+
'state',
|
|
430
462
|
]
|
|
431
|
-
const shown = new Set(rows.map(r => r.label.toLowerCase().replace(
|
|
463
|
+
const shown = new Set(rows.map(r => r.label.toLowerCase().replace(SPACES_REGEX, '')))
|
|
432
464
|
for (const key of featureKeys) {
|
|
433
465
|
if (device && device[key] !== undefined && !shown.has(key.toLowerCase())) {
|
|
434
466
|
const line = document.createElement('div')
|
|
@@ -441,7 +473,7 @@ export function renderDeviceDetailsPanel(device: any): HTMLElement {
|
|
|
441
473
|
const label = document.createElement('span')
|
|
442
474
|
label.style.fontWeight = '600'
|
|
443
475
|
label.style.minWidth = '110px'
|
|
444
|
-
label.textContent = key.replace(
|
|
476
|
+
label.textContent = `${key.replace(CAMELCASE_REGEX, ' $1').replace(FIRST_CHAR_REGEX, s => s.toUpperCase())}:`
|
|
445
477
|
|
|
446
478
|
const value = document.createElement('span')
|
|
447
479
|
value.style.fontFamily = 'monospace'
|
|
@@ -533,37 +565,38 @@ export async function renderDiscoveredDevices(
|
|
|
533
565
|
|
|
534
566
|
nameContainer.appendChild(selectCheckbox)
|
|
535
567
|
|
|
536
|
-
|
|
537
568
|
nameContainer.appendChild(name)
|
|
538
569
|
|
|
539
570
|
// Show firmware update available indicator if present
|
|
540
571
|
if (d.firmwareUpdateAvailable) {
|
|
541
|
-
const fwBadge = document.createElement('span')
|
|
542
|
-
fwBadge.textContent = 'Update Available'
|
|
543
|
-
fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
544
|
-
fwBadge.title = 'A firmware update is available for this device.'
|
|
545
|
-
nameContainer.appendChild(fwBadge)
|
|
572
|
+
const fwBadge = document.createElement('span')
|
|
573
|
+
fwBadge.textContent = 'Update Available'
|
|
574
|
+
fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
575
|
+
fwBadge.title = 'A firmware update is available for this device.'
|
|
576
|
+
nameContainer.appendChild(fwBadge)
|
|
546
577
|
}
|
|
547
578
|
|
|
548
579
|
// Show offline/unreachable indicator if device is offline
|
|
549
|
-
let offline = false
|
|
550
|
-
|
|
580
|
+
let offline = false
|
|
581
|
+
const lastSeen = d.lastSeen || d.lastseen || d.updatedAt
|
|
551
582
|
if (typeof d.offline === 'boolean') {
|
|
552
|
-
offline = d.offline
|
|
583
|
+
offline = d.offline
|
|
553
584
|
} else if (lastSeen) {
|
|
554
585
|
try {
|
|
555
|
-
const last = new Date(lastSeen).getTime()
|
|
556
|
-
if (!isNaN(last)
|
|
557
|
-
|
|
586
|
+
const last = new Date(lastSeen).getTime()
|
|
587
|
+
if (!Number.isNaN(last)) {
|
|
588
|
+
if (Date.now() - last > 1000 * 60 * 60) { // 1 hour
|
|
589
|
+
offline = true
|
|
590
|
+
}
|
|
558
591
|
}
|
|
559
592
|
} catch {}
|
|
560
593
|
}
|
|
561
594
|
if (offline) {
|
|
562
|
-
const offlineBadge = document.createElement('span')
|
|
563
|
-
offlineBadge.textContent = 'Offline'
|
|
564
|
-
offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
565
|
-
offlineBadge.title = 'Device is offline or unreachable.'
|
|
566
|
-
nameContainer.appendChild(offlineBadge)
|
|
595
|
+
const offlineBadge = document.createElement('span')
|
|
596
|
+
offlineBadge.textContent = 'Offline'
|
|
597
|
+
offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
598
|
+
offlineBadge.title = 'Device is offline or unreachable.'
|
|
599
|
+
nameContainer.appendChild(offlineBadge)
|
|
567
600
|
}
|
|
568
601
|
|
|
569
602
|
const expandedDetails = document.createElement('div')
|
|
@@ -596,7 +629,9 @@ export async function renderDiscoveredDevices(
|
|
|
596
629
|
// Add connection type badge
|
|
597
630
|
if (d.connectionType) {
|
|
598
631
|
const badge = renderConnectionBadge(d.connectionType)
|
|
599
|
-
if (badge) {
|
|
632
|
+
if (badge) {
|
|
633
|
+
nameContainer.appendChild(badge)
|
|
634
|
+
}
|
|
600
635
|
}
|
|
601
636
|
|
|
602
637
|
// Add IR badge if it's an IR device
|
|
@@ -612,14 +647,14 @@ export async function renderDiscoveredDevices(
|
|
|
612
647
|
|
|
613
648
|
// Add battery warning indicator if battery < 20%
|
|
614
649
|
if (typeof d.battery === 'number' && d.battery < 20) {
|
|
615
|
-
const batteryWarn = document.createElement('span')
|
|
616
|
-
batteryWarn.textContent = `⚠️ ${d.battery}
|
|
617
|
-
batteryWarn.style.cssText
|
|
618
|
-
d.battery < 10
|
|
650
|
+
const batteryWarn = document.createElement('span')
|
|
651
|
+
batteryWarn.textContent = `⚠️ ${d.battery}%`
|
|
652
|
+
batteryWarn.style.cssText
|
|
653
|
+
= d.battery < 10
|
|
619
654
|
? 'background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
620
|
-
: 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
621
|
-
batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low'
|
|
622
|
-
nameContainer.appendChild(batteryWarn)
|
|
655
|
+
: 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
656
|
+
batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low'
|
|
657
|
+
nameContainer.appendChild(batteryWarn)
|
|
623
658
|
}
|
|
624
659
|
|
|
625
660
|
const details = document.createElement('div')
|
|
@@ -632,8 +667,12 @@ export async function renderDiscoveredDevices(
|
|
|
632
667
|
details.style.wordBreak = 'break-word'
|
|
633
668
|
|
|
634
669
|
let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
|
|
635
|
-
if (d.hubDeviceId) {
|
|
636
|
-
|
|
670
|
+
if (d.hubDeviceId) {
|
|
671
|
+
detailsText += ` | Hub: ${d.hubDeviceId}`
|
|
672
|
+
}
|
|
673
|
+
if (d.address) {
|
|
674
|
+
detailsText += ` | MAC: ${d.address}`
|
|
675
|
+
}
|
|
637
676
|
details.textContent = detailsText
|
|
638
677
|
|
|
639
678
|
info.appendChild(nameContainer)
|
|
@@ -727,7 +766,7 @@ export function filterDevices(
|
|
|
727
766
|
|
|
728
767
|
// Filter by connection type
|
|
729
768
|
if (connectionType !== 'all') {
|
|
730
|
-
filtered = filtered.filter(d => {
|
|
769
|
+
filtered = filtered.filter((d) => {
|
|
731
770
|
if (connectionType === 'ir') {
|
|
732
771
|
return d.isIR === true
|
|
733
772
|
}
|
|
@@ -747,7 +786,7 @@ export function filterDevices(
|
|
|
747
786
|
// Filter by search query
|
|
748
787
|
if (searchQuery.trim()) {
|
|
749
788
|
const query = searchQuery.toLowerCase()
|
|
750
|
-
filtered = filtered.filter(d => {
|
|
789
|
+
filtered = filtered.filter((d) => {
|
|
751
790
|
const name = (d.name || '').toLowerCase()
|
|
752
791
|
const id = (d.id || '').toLowerCase()
|
|
753
792
|
const type = (d.type || '').toLowerCase()
|
|
@@ -792,11 +831,11 @@ export function sortDevices(
|
|
|
792
831
|
case 'connection': {
|
|
793
832
|
// Sort by connection type: Both > BLE > OpenAPI > Others
|
|
794
833
|
const connectionOrder: Record<string, number> = {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
834
|
+
Both: 0,
|
|
835
|
+
BLE: 1,
|
|
836
|
+
OpenAPI: 2,
|
|
837
|
+
API: 2,
|
|
838
|
+
Unknown: 3,
|
|
800
839
|
}
|
|
801
840
|
sorted.sort((a, b) => {
|
|
802
841
|
const aOrder = connectionOrder[a.connectionType || 'Unknown'] ?? 3
|
|
@@ -844,6 +883,9 @@ export function getDiscoveryPreferences(): {
|
|
|
844
883
|
/**
|
|
845
884
|
* Save filter/sort preferences to localStorage
|
|
846
885
|
* @param preferences Preferences object to save
|
|
886
|
+
* @param preferences.connectionType Connection type filter
|
|
887
|
+
* @param preferences.sortBy Sort criterion
|
|
888
|
+
* @param preferences.searchQuery Search query string
|
|
847
889
|
*/
|
|
848
890
|
export function setDiscoveryPreferences(preferences: {
|
|
849
891
|
connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
|
|
@@ -862,7 +904,9 @@ export function renderDeviceList(list: any[]): void {
|
|
|
862
904
|
const status = document.getElementById('status')
|
|
863
905
|
const removeAllContainer = document.getElementById('removeAllContainer')
|
|
864
906
|
|
|
865
|
-
if (!ul || !status) {
|
|
907
|
+
if (!ul || !status) {
|
|
908
|
+
return
|
|
909
|
+
}
|
|
866
910
|
|
|
867
911
|
if (!list.length) {
|
|
868
912
|
status.textContent = 'No devices found in config.'
|
|
@@ -1000,7 +1044,12 @@ export function renderDeviceList(list: any[]): void {
|
|
|
1000
1044
|
copyBtn.classList.remove('success')
|
|
1001
1045
|
}, 1200)
|
|
1002
1046
|
} catch (e) {
|
|
1003
|
-
|
|
1047
|
+
copyBtn.textContent = 'Failed'
|
|
1048
|
+
copyBtn.classList.add('error')
|
|
1049
|
+
setTimeout(() => {
|
|
1050
|
+
copyBtn.textContent = 'Copy ID'
|
|
1051
|
+
copyBtn.classList.remove('error')
|
|
1052
|
+
}, 1200)
|
|
1004
1053
|
}
|
|
1005
1054
|
})
|
|
1006
1055
|
|
|
@@ -1023,4 +1072,6 @@ export function renderDeviceList(list: any[]): void {
|
|
|
1023
1072
|
li.appendChild(buttons)
|
|
1024
1073
|
ul.appendChild(li)
|
|
1025
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
// No return value needed for void function
|
|
1026
1077
|
}
|