@switchbot/homebridge-switchbot 5.0.0-beta.112 → 5.0.0-beta.113
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/MIGRATION.md +10 -0
- package/README.md +43 -0
- package/TODO.md +263 -0
- package/config.schema.json +27 -10
- package/dist/device-types.d.ts.map +1 -1
- package/dist/device-types.js +93 -63
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -33
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +12 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +121 -1
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +80 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.js +73 -41
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +98 -0
- package/dist/homebridge-ui/public/index.html +57 -0
- package/dist/homebridge-ui/public/js/api.d.ts +24 -1
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +33 -2
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +52 -3
- package/dist/homebridge-ui/public/js/app.js +1503 -104
- package/dist/homebridge-ui/public/js/app.js.map +3 -3
- package/dist/homebridge-ui/public/js/app.ts +2 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +2 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +13 -1
- package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +1223 -126
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1323 -145
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +42 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +50 -1
- 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 +311 -1
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +341 -2
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +6 -2
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +51 -5
- package/dist/switchbotClient.js.map +1 -1
- package/docs/assets/highlight.css +9 -2
- package/docs/index.html +28 -2
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
- package/src/device-types.ts +95 -64
- package/src/deviceFactory.ts +55 -33
- package/src/devices/genericDevice.ts +113 -1
- package/src/errors.ts +43 -0
- package/src/homebridge-ui/endpoints/devices.ts +82 -1
- package/src/homebridge-ui/endpoints/discovery.ts +78 -44
- package/src/homebridge-ui/public/css/styles.css +98 -0
- package/src/homebridge-ui/public/index.html +57 -0
- package/src/homebridge-ui/public/js/api.ts +52 -3
- package/src/homebridge-ui/public/js/app.ts +2 -1
- package/src/homebridge-ui/public/js/devices.ts +13 -1
- package/src/homebridge-ui/public/js/discovery.ts +1323 -145
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +341 -2
- package/src/platform.ts +6 -2
- package/src/switchbotClient.ts +44 -9
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +300 -0
- package/todo/TODO.md +80 -66
|
@@ -42,10 +42,12 @@ __export(api_exports, {
|
|
|
42
42
|
deleteAllDevices: () => deleteAllDevices,
|
|
43
43
|
deleteDevice: () => deleteDevice,
|
|
44
44
|
discoverDevices: () => discoverDevices,
|
|
45
|
+
fetchBluetoothStatus: () => fetchBluetoothStatus,
|
|
45
46
|
fetchCredentialStatus: () => fetchCredentialStatus,
|
|
46
47
|
fetchDevices: () => fetchDevices,
|
|
47
48
|
saveCredentials: () => saveCredentials,
|
|
48
49
|
syncParentPluginConfigFromDisk: () => syncParentPluginConfigFromDisk,
|
|
50
|
+
testDeviceConnection: () => testDeviceConnection,
|
|
49
51
|
updateDevice: () => updateDevice
|
|
50
52
|
});
|
|
51
53
|
function isSwitchBotPlatformConfig(block) {
|
|
@@ -125,9 +127,9 @@ async function fetchDevices() {
|
|
|
125
127
|
return [];
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
|
-
async function discoverDevices() {
|
|
130
|
+
async function discoverDevices(mode = "all", options) {
|
|
129
131
|
try {
|
|
130
|
-
const resp = await homebridge.request("/discover", {});
|
|
132
|
+
const resp = await homebridge.request("/discover", { mode, ...options });
|
|
131
133
|
uiLog.info("Discover response:", resp);
|
|
132
134
|
if (!resp || resp.success === false) {
|
|
133
135
|
throw new Error(resp?.data?.message || "Discovery failed");
|
|
@@ -137,6 +139,30 @@ async function discoverDevices() {
|
|
|
137
139
|
throw e;
|
|
138
140
|
}
|
|
139
141
|
}
|
|
142
|
+
async function fetchBluetoothStatus() {
|
|
143
|
+
try {
|
|
144
|
+
const resp = await homebridge.request("/ble-status", {});
|
|
145
|
+
if (!resp || resp.success === false) {
|
|
146
|
+
return { available: false, message: "Bluetooth status unavailable" };
|
|
147
|
+
}
|
|
148
|
+
return resp.data || { available: false, message: "Bluetooth status unavailable" };
|
|
149
|
+
} catch (_e) {
|
|
150
|
+
return { available: false, message: "Bluetooth status unavailable" };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function testDeviceConnection(payload) {
|
|
154
|
+
const resp = await homebridge.request("/test-connection", payload);
|
|
155
|
+
if (!resp || resp.success === false) {
|
|
156
|
+
throw new Error(resp?.data?.message || "Connection test failed");
|
|
157
|
+
}
|
|
158
|
+
return resp.data || {
|
|
159
|
+
success: false,
|
|
160
|
+
deviceId: payload.deviceId,
|
|
161
|
+
method: "Auto",
|
|
162
|
+
latencyMs: 0,
|
|
163
|
+
message: "Connection test failed"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
140
166
|
async function addDevice(deviceId, name, type, options) {
|
|
141
167
|
try {
|
|
142
168
|
const payload = { deviceId, name, type };
|
|
@@ -149,6 +175,12 @@ async function addDevice(deviceId, name, type, options) {
|
|
|
149
175
|
if (options?.rssi !== void 0 && options?.rssi !== null && options?.rssi !== 0) {
|
|
150
176
|
payload.rssi = options.rssi;
|
|
151
177
|
}
|
|
178
|
+
if (options?.encryptionKey) {
|
|
179
|
+
payload.encryptionKey = options.encryptionKey;
|
|
180
|
+
}
|
|
181
|
+
if (options?.keyId) {
|
|
182
|
+
payload.keyId = options.keyId;
|
|
183
|
+
}
|
|
152
184
|
uiLog.info("Adding device to config:", payload);
|
|
153
185
|
const resp = await homebridge.request("/add-device", payload);
|
|
154
186
|
uiLog.info("Add device response:", resp);
|
|
@@ -292,83 +324,466 @@ var init_toast = __esm({
|
|
|
292
324
|
var discovery_exports = {};
|
|
293
325
|
__export(discovery_exports, {
|
|
294
326
|
addDeviceToConfig: () => addDeviceToConfig,
|
|
295
|
-
discoverDevices: () => discoverDevices2
|
|
327
|
+
discoverDevices: () => discoverDevices2,
|
|
328
|
+
initializeDiscoverySettings: () => initializeDiscoverySettings
|
|
296
329
|
});
|
|
330
|
+
async function batchSetDeviceEnabled(selectedIds, enabled) {
|
|
331
|
+
const resp = await homebridge.request("/platform-config", {});
|
|
332
|
+
if (!resp || resp.success === false || !resp.data) {
|
|
333
|
+
throw new Error("Failed to load config");
|
|
334
|
+
}
|
|
335
|
+
const config = resp.data;
|
|
336
|
+
const configArr = Array.isArray(config) ? config : [config];
|
|
337
|
+
const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
|
|
338
|
+
if (platformIdx === -1) {
|
|
339
|
+
throw new Error("SwitchBot platform config not found");
|
|
340
|
+
}
|
|
341
|
+
const platformConfig = configArr[platformIdx];
|
|
342
|
+
if (!Array.isArray(platformConfig.devices)) {
|
|
343
|
+
throw new TypeError("No devices array in config");
|
|
344
|
+
}
|
|
345
|
+
let changed = false;
|
|
346
|
+
for (const dev of platformConfig.devices) {
|
|
347
|
+
const id = String(dev.deviceId || dev.id || "").trim().toLowerCase();
|
|
348
|
+
if (selectedIds.has(id)) {
|
|
349
|
+
if (dev.enabled !== enabled) {
|
|
350
|
+
dev.enabled = enabled;
|
|
351
|
+
changed = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (changed) {
|
|
356
|
+
if (typeof homebridge.updatePluginConfig === "function") {
|
|
357
|
+
await homebridge.updatePluginConfig(configArr);
|
|
358
|
+
} else {
|
|
359
|
+
throw new TypeError("homebridge.updatePluginConfig is not available");
|
|
360
|
+
}
|
|
361
|
+
if (typeof homebridge.savePluginConfig === "function") {
|
|
362
|
+
await homebridge.savePluginConfig();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
297
366
|
function normalizeId(value) {
|
|
298
367
|
return String(value ?? "").trim().toLowerCase();
|
|
299
368
|
}
|
|
369
|
+
function dedupeById(devices) {
|
|
370
|
+
return devices.filter((d, index, arr) => !!d?.id && arr.findIndex((x) => x?.id === d.id) === index);
|
|
371
|
+
}
|
|
372
|
+
function mergeDiscoveredDevices(existingDevices, incomingDevices) {
|
|
373
|
+
const deviceMap = /* @__PURE__ */ new Map();
|
|
374
|
+
for (const d of dedupeById(existingDevices)) {
|
|
375
|
+
deviceMap.set(d.id, { ...d });
|
|
376
|
+
}
|
|
377
|
+
for (const d of dedupeById(incomingDevices)) {
|
|
378
|
+
const current = deviceMap.get(d.id);
|
|
379
|
+
if (current) {
|
|
380
|
+
const nextConnectionType = current.connectionType === d.connectionType ? current.connectionType : "Both";
|
|
381
|
+
deviceMap.set(d.id, {
|
|
382
|
+
...current,
|
|
383
|
+
...d,
|
|
384
|
+
connectionType: nextConnectionType
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
deviceMap.set(d.id, { ...d });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return [...deviceMap.values()];
|
|
391
|
+
}
|
|
392
|
+
function setDiscoveryCache(devices) {
|
|
393
|
+
try {
|
|
394
|
+
const payload = { timestamp: Date.now(), devices };
|
|
395
|
+
localStorage.setItem(DISCOVERY_CACHE_KEY, JSON.stringify(payload));
|
|
396
|
+
} catch (_e) {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function clearDiscoveryCache() {
|
|
400
|
+
try {
|
|
401
|
+
localStorage.removeItem(DISCOVERY_CACHE_KEY);
|
|
402
|
+
} catch (_e) {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function getDiscoveryCache(validOnly = true) {
|
|
406
|
+
try {
|
|
407
|
+
const stored = localStorage.getItem(DISCOVERY_CACHE_KEY);
|
|
408
|
+
if (!stored) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const payload = JSON.parse(stored);
|
|
412
|
+
if (!payload || !Array.isArray(payload.devices) || typeof payload.timestamp !== "number") {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const age = Date.now() - payload.timestamp;
|
|
416
|
+
if (validOnly && age > DISCOVERY_CACHE_TTL_MS) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return payload;
|
|
420
|
+
} catch (_e) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function getDiscoveryAutoRefreshSeconds() {
|
|
425
|
+
try {
|
|
426
|
+
const stored = localStorage.getItem(DISCOVERY_AUTO_REFRESH_KEY);
|
|
427
|
+
const value = Number(stored || 0);
|
|
428
|
+
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
429
|
+
} catch (_e) {
|
|
430
|
+
return 0;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function setDiscoveryAutoRefreshSeconds(value) {
|
|
434
|
+
try {
|
|
435
|
+
localStorage.setItem(DISCOVERY_AUTO_REFRESH_KEY, String(Math.max(0, value)));
|
|
436
|
+
} catch (_e) {
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function getDiscoveryHideAddedPreference() {
|
|
440
|
+
try {
|
|
441
|
+
return localStorage.getItem(DISCOVERY_HIDE_ADDED_KEY) === "true";
|
|
442
|
+
} catch (_e) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function setDiscoveryHideAddedPreference(value) {
|
|
447
|
+
try {
|
|
448
|
+
localStorage.setItem(DISCOVERY_HIDE_ADDED_KEY, String(value));
|
|
449
|
+
} catch (_e) {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function formatElapsedShort(ms) {
|
|
453
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
|
|
454
|
+
if (totalSeconds < 60) {
|
|
455
|
+
return `${totalSeconds}s ago`;
|
|
456
|
+
}
|
|
457
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
458
|
+
if (minutes < 60) {
|
|
459
|
+
return `${minutes}m ago`;
|
|
460
|
+
}
|
|
461
|
+
const hours = Math.floor(minutes / 60);
|
|
462
|
+
if (hours < 24) {
|
|
463
|
+
return `${hours}h ago`;
|
|
464
|
+
}
|
|
465
|
+
const days = Math.floor(hours / 24);
|
|
466
|
+
return `${days}d ago`;
|
|
467
|
+
}
|
|
468
|
+
function updateLastScannedStatus() {
|
|
469
|
+
const lastScannedStatus = document.getElementById("lastScannedStatus");
|
|
470
|
+
if (!lastScannedStatus) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const cache = getDiscoveryCache(false);
|
|
474
|
+
if (!cache) {
|
|
475
|
+
lastScannedStatus.textContent = "Last scanned: never";
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const ageMs = Date.now() - cache.timestamp;
|
|
479
|
+
const timestampText = new Date(cache.timestamp).toLocaleString();
|
|
480
|
+
const stale = ageMs > DISCOVERY_CACHE_TTL_MS;
|
|
481
|
+
lastScannedStatus.textContent = stale ? `Last scanned: ${formatElapsedShort(ageMs)} (${timestampText}, cache expired)` : `Last scanned: ${formatElapsedShort(ageMs)} (${timestampText})`;
|
|
482
|
+
}
|
|
483
|
+
async function renderCachedDiscoveryResults() {
|
|
484
|
+
const cache = getDiscoveryCache(true);
|
|
485
|
+
const list = document.getElementById("discoveredList");
|
|
486
|
+
if (!cache || !list || !cache.devices.length) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
list.style.display = "block";
|
|
490
|
+
if (!window._discoverySelectedIds) {
|
|
491
|
+
window._discoverySelectedIds = /* @__PURE__ */ new Set();
|
|
492
|
+
}
|
|
493
|
+
await updateDiscoveryView(
|
|
494
|
+
cache.devices,
|
|
495
|
+
getDiscoveryPreferences(),
|
|
496
|
+
getDiscoveryGroupByPreference(),
|
|
497
|
+
getDiscoveryHideAddedPreference(),
|
|
498
|
+
window._discoverySelectedIds
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
function getDiscoveryBleSettings() {
|
|
502
|
+
try {
|
|
503
|
+
const stored = localStorage.getItem(DISCOVERY_BLE_SETTINGS_KEY);
|
|
504
|
+
if (!stored) {
|
|
505
|
+
return { bleEnabled: true, bleScanDurationSeconds: 5, bleTimeoutSeconds: 8 };
|
|
506
|
+
}
|
|
507
|
+
const parsed = JSON.parse(stored);
|
|
508
|
+
return {
|
|
509
|
+
bleEnabled: parsed?.bleEnabled !== false,
|
|
510
|
+
bleScanDurationSeconds: Math.max(3, Math.min(15, Number(parsed?.bleScanDurationSeconds || 5))),
|
|
511
|
+
bleTimeoutSeconds: Math.max(3, Math.min(30, Number(parsed?.bleTimeoutSeconds || 8)))
|
|
512
|
+
};
|
|
513
|
+
} catch (_e) {
|
|
514
|
+
return { bleEnabled: true, bleScanDurationSeconds: 5, bleTimeoutSeconds: 8 };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function setDiscoveryBleSettings(settings) {
|
|
518
|
+
try {
|
|
519
|
+
localStorage.setItem(DISCOVERY_BLE_SETTINGS_KEY, JSON.stringify(settings));
|
|
520
|
+
} catch (_e) {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function initializeDiscoverySettings() {
|
|
524
|
+
const scanSelect = document.getElementById("bleScanDurationSelect");
|
|
525
|
+
const timeoutInput = document.getElementById("bleTimeoutInput");
|
|
526
|
+
const disableBleCheckbox = document.getElementById("disableBleScanCheckbox");
|
|
527
|
+
const scanSetting = document.getElementById("bleScanSetting");
|
|
528
|
+
const timeoutSetting = document.getElementById("bleTimeoutSetting");
|
|
529
|
+
const bluetoothStatus = document.getElementById("bluetoothStatus");
|
|
530
|
+
const autoRefreshSelect = document.getElementById("autoRefreshIntervalSelect");
|
|
531
|
+
const refreshBtn = document.getElementById("refreshDiscoverBtn");
|
|
532
|
+
const current = getDiscoveryBleSettings();
|
|
533
|
+
if (scanSelect) {
|
|
534
|
+
scanSelect.value = String(current.bleScanDurationSeconds);
|
|
535
|
+
}
|
|
536
|
+
if (timeoutInput) {
|
|
537
|
+
timeoutInput.value = String(current.bleTimeoutSeconds);
|
|
538
|
+
}
|
|
539
|
+
if (disableBleCheckbox) {
|
|
540
|
+
disableBleCheckbox.checked = !current.bleEnabled;
|
|
541
|
+
}
|
|
542
|
+
if (autoRefreshSelect) {
|
|
543
|
+
autoRefreshSelect.value = String(getDiscoveryAutoRefreshSeconds());
|
|
544
|
+
}
|
|
545
|
+
const updateBleSettingVisibility = () => {
|
|
546
|
+
const disabled = !!disableBleCheckbox?.checked;
|
|
547
|
+
if (scanSetting) {
|
|
548
|
+
scanSetting.style.display = disabled ? "none" : "inline-flex";
|
|
549
|
+
}
|
|
550
|
+
if (timeoutSetting) {
|
|
551
|
+
timeoutSetting.style.display = disabled ? "none" : "inline-flex";
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const persistFromControls = () => {
|
|
555
|
+
const next = {
|
|
556
|
+
bleEnabled: !(disableBleCheckbox?.checked ?? false),
|
|
557
|
+
bleScanDurationSeconds: Math.max(3, Math.min(15, Number(scanSelect?.value || 5))),
|
|
558
|
+
bleTimeoutSeconds: Math.max(3, Math.min(30, Number(timeoutInput?.value || 8)))
|
|
559
|
+
};
|
|
560
|
+
setDiscoveryBleSettings(next);
|
|
561
|
+
};
|
|
562
|
+
scanSelect?.addEventListener("change", persistFromControls);
|
|
563
|
+
timeoutInput?.addEventListener("change", persistFromControls);
|
|
564
|
+
disableBleCheckbox?.addEventListener("change", () => {
|
|
565
|
+
persistFromControls();
|
|
566
|
+
updateBleSettingVisibility();
|
|
567
|
+
});
|
|
568
|
+
updateBleSettingVisibility();
|
|
569
|
+
if (bluetoothStatus) {
|
|
570
|
+
const status = await fetchBluetoothStatus();
|
|
571
|
+
bluetoothStatus.textContent = status.available ? `Bluetooth: available (${status.message})` : `Bluetooth: unavailable (${status.message})`;
|
|
572
|
+
}
|
|
573
|
+
updateLastScannedStatus();
|
|
574
|
+
if (discoveryLastScannedTimer) {
|
|
575
|
+
clearInterval(discoveryLastScannedTimer);
|
|
576
|
+
}
|
|
577
|
+
discoveryLastScannedTimer = setInterval(updateLastScannedStatus, 15e3);
|
|
578
|
+
refreshBtn?.addEventListener("click", () => {
|
|
579
|
+
void discoverDevices2();
|
|
580
|
+
});
|
|
581
|
+
const applyAutoRefresh = () => {
|
|
582
|
+
const seconds = Math.max(0, Number(autoRefreshSelect?.value || 0));
|
|
583
|
+
setDiscoveryAutoRefreshSeconds(seconds);
|
|
584
|
+
if (discoveryAutoRefreshTimer) {
|
|
585
|
+
clearInterval(discoveryAutoRefreshTimer);
|
|
586
|
+
discoveryAutoRefreshTimer = null;
|
|
587
|
+
}
|
|
588
|
+
if (seconds > 0) {
|
|
589
|
+
discoveryAutoRefreshTimer = setInterval(() => {
|
|
590
|
+
const discoverBtn = document.getElementById("discoverBtn");
|
|
591
|
+
if (!discoverBtn || discoverBtn.disabled || document.hidden) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
void discoverDevices2();
|
|
595
|
+
}, seconds * 1e3);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
autoRefreshSelect?.addEventListener("change", applyAutoRefresh);
|
|
599
|
+
applyAutoRefresh();
|
|
600
|
+
await renderCachedDiscoveryResults();
|
|
601
|
+
}
|
|
602
|
+
function getDiscoveryGroupByPreference() {
|
|
603
|
+
try {
|
|
604
|
+
const stored = localStorage.getItem(DISCOVERY_GROUP_BY_KEY);
|
|
605
|
+
if (stored === "hub" || stored === "type") {
|
|
606
|
+
return stored;
|
|
607
|
+
}
|
|
608
|
+
return "connection";
|
|
609
|
+
} catch (_e) {
|
|
610
|
+
return "connection";
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function setDiscoveryGroupByPreference(groupBy) {
|
|
614
|
+
try {
|
|
615
|
+
localStorage.setItem(DISCOVERY_GROUP_BY_KEY, groupBy);
|
|
616
|
+
} catch (_e) {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function getDiscoveryGroupExpandedState() {
|
|
620
|
+
try {
|
|
621
|
+
const stored = localStorage.getItem(DISCOVERY_GROUP_EXPANDED_KEY);
|
|
622
|
+
if (!stored) {
|
|
623
|
+
return {};
|
|
624
|
+
}
|
|
625
|
+
const parsed = JSON.parse(stored);
|
|
626
|
+
return typeof parsed === "object" && parsed ? parsed : {};
|
|
627
|
+
} catch (_e) {
|
|
628
|
+
return {};
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function setDiscoveryGroupExpandedState(state) {
|
|
632
|
+
try {
|
|
633
|
+
localStorage.setItem(DISCOVERY_GROUP_EXPANDED_KEY, JSON.stringify(state));
|
|
634
|
+
} catch (_e) {
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function isDiscoveryGroupExpanded(groupKey) {
|
|
638
|
+
const state = getDiscoveryGroupExpandedState();
|
|
639
|
+
return state[groupKey] !== false;
|
|
640
|
+
}
|
|
641
|
+
function setDiscoveryGroupExpanded(groupKey, expanded) {
|
|
642
|
+
const state = getDiscoveryGroupExpandedState();
|
|
643
|
+
state[groupKey] = expanded;
|
|
644
|
+
setDiscoveryGroupExpandedState(state);
|
|
645
|
+
}
|
|
300
646
|
async function discoverDevices2() {
|
|
301
647
|
const btn = document.getElementById("discoverBtn");
|
|
648
|
+
const cancelBtn = document.getElementById("cancelDiscoverBtn");
|
|
302
649
|
const status = document.getElementById("discoverStatus");
|
|
650
|
+
const phaseProgress = document.getElementById("discoverPhaseProgress");
|
|
651
|
+
const phaseFill = document.getElementById("discoverPhaseFill");
|
|
652
|
+
const phaseLabel = document.getElementById("discoverPhaseLabel");
|
|
303
653
|
const list = document.getElementById("discoveredList");
|
|
304
654
|
const autoAddAll = document.getElementById("autoAddAllCheckbox")?.checked;
|
|
655
|
+
const scanSelect = document.getElementById("bleScanDurationSelect");
|
|
656
|
+
const timeoutInput = document.getElementById("bleTimeoutInput");
|
|
657
|
+
const disableBleCheckbox = document.getElementById("disableBleScanCheckbox");
|
|
305
658
|
if (!btn || !status || !list) {
|
|
306
659
|
return;
|
|
307
660
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
661
|
+
const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
662
|
+
let spinnerIndex = 0;
|
|
663
|
+
const startedAt = Date.now();
|
|
664
|
+
let phaseStartedAt = startedAt;
|
|
665
|
+
let phase = "Preparing discovery...";
|
|
666
|
+
let cancelled = false;
|
|
667
|
+
const bleScanDurationSeconds = scanSelect ? Number(scanSelect.value) : 5;
|
|
668
|
+
const pollIntervalMs = 1e3;
|
|
669
|
+
const scanEndTime = Date.now() + bleScanDurationSeconds * 1e3;
|
|
670
|
+
const setPhase = (nextPhase) => {
|
|
671
|
+
phase = nextPhase;
|
|
672
|
+
phaseStartedAt = Date.now();
|
|
673
|
+
};
|
|
674
|
+
const getPhasePercent = (phaseName) => {
|
|
675
|
+
if (phaseName.includes("Scanning BLE")) {
|
|
676
|
+
return 35;
|
|
322
677
|
}
|
|
323
|
-
if (
|
|
324
|
-
|
|
678
|
+
if (phaseName.includes("Fetching OpenAPI")) {
|
|
679
|
+
return 75;
|
|
680
|
+
}
|
|
681
|
+
if (phaseName.includes("Complete")) {
|
|
682
|
+
return 100;
|
|
683
|
+
}
|
|
684
|
+
return 10;
|
|
685
|
+
};
|
|
686
|
+
const renderProgress = () => {
|
|
687
|
+
const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
|
|
688
|
+
const phaseSeconds = Math.max(0, Math.floor((Date.now() - phaseStartedAt) / 1e3));
|
|
689
|
+
const frame = spinnerFrames[spinnerIndex % spinnerFrames.length];
|
|
690
|
+
spinnerIndex += 1;
|
|
691
|
+
status.textContent = `${frame} ${phase} (${phaseSeconds}s, ${totalSeconds}s total)`;
|
|
692
|
+
if (phaseProgress) {
|
|
693
|
+
phaseProgress.style.display = "block";
|
|
694
|
+
}
|
|
695
|
+
if (phaseFill) {
|
|
696
|
+
phaseFill.style.width = `${getPhasePercent(phase)}%`;
|
|
697
|
+
}
|
|
698
|
+
if (phaseLabel) {
|
|
699
|
+
phaseLabel.textContent = phase;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
const progressTimer = setInterval(renderProgress, 250);
|
|
703
|
+
let discoveredDevices = [];
|
|
704
|
+
const preferences = getDiscoveryPreferences();
|
|
705
|
+
let groupBy = getDiscoveryGroupByPreference();
|
|
706
|
+
let hideAdded = getDiscoveryHideAddedPreference();
|
|
707
|
+
let controlsInitialized = false;
|
|
708
|
+
if (!window._discoverySelectedIds) {
|
|
709
|
+
window._discoverySelectedIds = /* @__PURE__ */ new Set();
|
|
710
|
+
}
|
|
711
|
+
const selectedIds = window._discoverySelectedIds;
|
|
712
|
+
setPhase("Scanning BLE (real-time RSSI)");
|
|
713
|
+
let pollTimer = null;
|
|
714
|
+
const pollDiscovery = async () => {
|
|
715
|
+
if (cancelled || Date.now() > scanEndTime) {
|
|
716
|
+
if (pollTimer) {
|
|
717
|
+
clearInterval(pollTimer);
|
|
718
|
+
}
|
|
719
|
+
setPhase("Scan Complete");
|
|
325
720
|
try {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
type: d.type,
|
|
331
|
-
rssi: d.rssi,
|
|
332
|
-
address: d.address,
|
|
333
|
-
model: d.model
|
|
334
|
-
}))
|
|
335
|
-
);
|
|
336
|
-
uiLog.info("Bulk add response:", bulkResult);
|
|
337
|
-
if (!bulkResult || bulkResult.success === false) {
|
|
338
|
-
throw new Error(bulkResult?.data?.message || "Bulk add failed");
|
|
339
|
-
}
|
|
340
|
-
const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
|
|
341
|
-
const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
|
|
342
|
-
status.textContent = `\u2713 Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`;
|
|
343
|
-
if (addedCount > 0) {
|
|
344
|
-
toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
|
|
345
|
-
} else if (skippedCount > 0) {
|
|
346
|
-
toastWarning(`No new devices were added (${skippedCount} skipped)`);
|
|
347
|
-
}
|
|
348
|
-
status.classList.remove("error");
|
|
349
|
-
list.style.display = "none";
|
|
350
|
-
if (devices.length > 0) {
|
|
351
|
-
const synced = await syncParentPluginConfigFromDisk(true);
|
|
352
|
-
status.textContent += synced ? " - Config saved automatically." : " - Warning: config may not persist until you close/reopen settings.";
|
|
353
|
-
if (synced) {
|
|
354
|
-
toastSuccess("Configuration synced and saved automatically");
|
|
355
|
-
} else {
|
|
356
|
-
toastWarning("Configuration sync failed; close and reopen settings before Save");
|
|
357
|
-
}
|
|
358
|
-
}
|
|
721
|
+
const devices = await discoverDevices("ble");
|
|
722
|
+
discoveredDevices = mergeDiscoveredDevices(discoveredDevices, devices);
|
|
723
|
+
setDiscoveryCache(discoveredDevices);
|
|
724
|
+
await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
|
|
359
725
|
} catch (e) {
|
|
360
|
-
uiLog.error("
|
|
361
|
-
status.textContent = `\u2717 Error: ${e instanceof Error ? e.message : "Failed to add devices"}`;
|
|
362
|
-
status.classList.add("error");
|
|
363
|
-
toastError(e instanceof Error ? e.message : "Failed to add devices");
|
|
726
|
+
uiLog.error("Final BLE poll failed:", e);
|
|
364
727
|
}
|
|
365
|
-
await loadConfiguredDevices();
|
|
366
728
|
return;
|
|
367
729
|
}
|
|
368
|
-
|
|
730
|
+
try {
|
|
731
|
+
const devices = await discoverDevices("ble");
|
|
732
|
+
discoveredDevices = mergeDiscoveredDevices(discoveredDevices, devices);
|
|
733
|
+
setDiscoveryCache(discoveredDevices);
|
|
734
|
+
await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
|
|
735
|
+
} catch (e) {
|
|
736
|
+
uiLog.error("BLE poll failed:", e);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
pollTimer = setInterval(pollDiscovery, pollIntervalMs);
|
|
740
|
+
pollDiscovery();
|
|
741
|
+
async function batchSetDeviceEnabled2(selectedIds2, enabled) {
|
|
742
|
+
const resp = await homebridge.request("/platform-config", {});
|
|
743
|
+
if (!resp || resp.success === false || !resp.data) {
|
|
744
|
+
throw new Error("Failed to load config");
|
|
745
|
+
}
|
|
746
|
+
const config = resp.data;
|
|
747
|
+
const configArr = Array.isArray(config) ? config : [config];
|
|
748
|
+
const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
|
|
749
|
+
if (platformIdx === -1) {
|
|
750
|
+
throw new Error("SwitchBot platform config not found");
|
|
751
|
+
}
|
|
752
|
+
const platformConfig = configArr[platformIdx];
|
|
753
|
+
if (!Array.isArray(platformConfig.devices)) {
|
|
754
|
+
throw new TypeError("No devices array in config");
|
|
755
|
+
}
|
|
756
|
+
let changed = false;
|
|
757
|
+
for (const dev of platformConfig.devices) {
|
|
758
|
+
const id = String(dev.deviceId || dev.id || "").trim().toLowerCase();
|
|
759
|
+
if (selectedIds2.has(id)) {
|
|
760
|
+
if (dev.enabled !== enabled) {
|
|
761
|
+
dev.enabled = enabled;
|
|
762
|
+
changed = true;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (changed) {
|
|
767
|
+
if (typeof homebridge.updatePluginConfig === "function") {
|
|
768
|
+
await homebridge.updatePluginConfig(configArr);
|
|
769
|
+
} else {
|
|
770
|
+
throw new TypeError("homebridge.updatePluginConfig is not available");
|
|
771
|
+
}
|
|
772
|
+
if (typeof homebridge.savePluginConfig === "function") {
|
|
773
|
+
await homebridge.savePluginConfig();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const ensureDiscoveryControls = async () => {
|
|
778
|
+
if (controlsInitialized) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (!window._discoverySelectedIds) {
|
|
782
|
+
window._discoverySelectedIds = /* @__PURE__ */ new Set();
|
|
783
|
+
}
|
|
784
|
+
const selectedIds2 = window._discoverySelectedIds;
|
|
369
785
|
const controlsDiv = document.createElement("div");
|
|
370
786
|
controlsDiv.style.cssText = "margin-bottom: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;";
|
|
371
|
-
const preferences = getDiscoveryPreferences();
|
|
372
787
|
const filterLabel = document.createElement("label");
|
|
373
788
|
filterLabel.style.fontSize = "12px";
|
|
374
789
|
filterLabel.style.fontWeight = "500";
|
|
@@ -384,30 +799,29 @@ async function discoverDevices2() {
|
|
|
384
799
|
{ label: "IR", value: "ir" }
|
|
385
800
|
];
|
|
386
801
|
for (const option of filterOptions) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
802
|
+
const filterBtn = document.createElement("button");
|
|
803
|
+
filterBtn.textContent = option.label;
|
|
804
|
+
filterBtn.style.padding = "4px 8px";
|
|
805
|
+
filterBtn.style.fontSize = "11px";
|
|
806
|
+
filterBtn.style.borderRadius = "3px";
|
|
807
|
+
filterBtn.style.cursor = "pointer";
|
|
808
|
+
filterBtn.style.border = preferences.connectionType === option.value ? "2px solid #007AFF" : "1px solid #ccc";
|
|
809
|
+
filterBtn.style.backgroundColor = preferences.connectionType === option.value ? "#f0f7ff" : "#fff";
|
|
810
|
+
filterBtn.style.color = preferences.connectionType === option.value ? "#1d4ed8" : "#374151";
|
|
811
|
+
filterBtn.onclick = () => {
|
|
397
812
|
preferences.connectionType = option.value;
|
|
398
813
|
setDiscoveryPreferences(preferences);
|
|
399
|
-
updateDiscoveryView(
|
|
400
|
-
|
|
401
|
-
;
|
|
814
|
+
void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
815
|
+
Array.prototype.forEach.call(filterGroup.querySelectorAll("button"), (b) => {
|
|
402
816
|
b.style.border = "1px solid #ccc";
|
|
403
817
|
b.style.backgroundColor = "#fff";
|
|
404
818
|
b.style.color = "#374151";
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
819
|
+
});
|
|
820
|
+
filterBtn.style.border = "2px solid #007AFF";
|
|
821
|
+
filterBtn.style.backgroundColor = "#f0f7ff";
|
|
822
|
+
filterBtn.style.color = "#1d4ed8";
|
|
409
823
|
};
|
|
410
|
-
filterGroup.appendChild(
|
|
824
|
+
filterGroup.appendChild(filterBtn);
|
|
411
825
|
}
|
|
412
826
|
const sortLabel = document.createElement("label");
|
|
413
827
|
sortLabel.style.fontSize = "12px";
|
|
@@ -426,26 +840,73 @@ async function discoverDevices2() {
|
|
|
426
840
|
{ label: "Connection", value: "connection" }
|
|
427
841
|
];
|
|
428
842
|
for (const opt of sortOptions) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
sortSelect.appendChild(
|
|
843
|
+
const sortOption = document.createElement("option");
|
|
844
|
+
sortOption.value = opt.value;
|
|
845
|
+
sortOption.textContent = opt.label;
|
|
846
|
+
sortSelect.appendChild(sortOption);
|
|
433
847
|
}
|
|
434
848
|
sortSelect.onchange = () => {
|
|
435
849
|
preferences.sortBy = sortSelect.value;
|
|
436
850
|
setDiscoveryPreferences(preferences);
|
|
437
|
-
updateDiscoveryView(
|
|
851
|
+
void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
852
|
+
};
|
|
853
|
+
const groupLabel = document.createElement("label");
|
|
854
|
+
groupLabel.style.fontSize = "12px";
|
|
855
|
+
groupLabel.style.fontWeight = "500";
|
|
856
|
+
groupLabel.style.marginLeft = "8px";
|
|
857
|
+
groupLabel.textContent = "Group:";
|
|
858
|
+
const groupSelect = document.createElement("select");
|
|
859
|
+
groupSelect.style.fontSize = "11px";
|
|
860
|
+
groupSelect.style.padding = "4px 8px";
|
|
861
|
+
groupSelect.style.borderRadius = "3px";
|
|
862
|
+
groupSelect.value = groupBy;
|
|
863
|
+
const groupOptions = [
|
|
864
|
+
{ label: "Connection", value: "connection" },
|
|
865
|
+
{ label: "Hub", value: "hub" },
|
|
866
|
+
{ label: "Device Type", value: "type" }
|
|
867
|
+
];
|
|
868
|
+
for (const opt of groupOptions) {
|
|
869
|
+
const groupOption = document.createElement("option");
|
|
870
|
+
groupOption.value = opt.value;
|
|
871
|
+
groupOption.textContent = opt.label;
|
|
872
|
+
groupSelect.appendChild(groupOption);
|
|
873
|
+
}
|
|
874
|
+
groupSelect.onchange = () => {
|
|
875
|
+
groupBy = groupSelect.value;
|
|
876
|
+
setDiscoveryGroupByPreference(groupBy);
|
|
877
|
+
void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
878
|
+
};
|
|
879
|
+
const hideAddedLabel = document.createElement("label");
|
|
880
|
+
hideAddedLabel.style.display = "inline-flex";
|
|
881
|
+
hideAddedLabel.style.alignItems = "center";
|
|
882
|
+
hideAddedLabel.style.gap = "4px";
|
|
883
|
+
hideAddedLabel.style.fontSize = "11px";
|
|
884
|
+
hideAddedLabel.style.marginLeft = "8px";
|
|
885
|
+
const hideAddedCheckbox = document.createElement("input");
|
|
886
|
+
hideAddedCheckbox.type = "checkbox";
|
|
887
|
+
hideAddedCheckbox.checked = hideAdded;
|
|
888
|
+
hideAddedCheckbox.style.margin = "0";
|
|
889
|
+
hideAddedCheckbox.style.width = "auto";
|
|
890
|
+
const hideAddedText = document.createElement("span");
|
|
891
|
+
hideAddedText.textContent = "Hide Added";
|
|
892
|
+
hideAddedLabel.appendChild(hideAddedCheckbox);
|
|
893
|
+
hideAddedLabel.appendChild(hideAddedText);
|
|
894
|
+
hideAddedCheckbox.onchange = () => {
|
|
895
|
+
hideAdded = hideAddedCheckbox.checked;
|
|
896
|
+
setDiscoveryHideAddedPreference(hideAdded);
|
|
897
|
+
void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
438
898
|
};
|
|
439
899
|
const searchInput = document.createElement("input");
|
|
440
900
|
searchInput.type = "text";
|
|
441
901
|
searchInput.placeholder = "Search by name, ID, or type...";
|
|
442
|
-
searchInput.style.fontSize = "
|
|
443
|
-
searchInput.style.padding = "
|
|
444
|
-
searchInput.style.borderRadius = "
|
|
902
|
+
searchInput.style.fontSize = "13px";
|
|
903
|
+
searchInput.style.padding = "8px 16px";
|
|
904
|
+
searchInput.style.borderRadius = "6px";
|
|
445
905
|
searchInput.style.border = "1px solid #ccc";
|
|
446
|
-
searchInput.style.flex = "1";
|
|
447
|
-
searchInput.style.minWidth = "
|
|
448
|
-
searchInput.style.maxWidth = "
|
|
906
|
+
searchInput.style.flex = "1 1 0%";
|
|
907
|
+
searchInput.style.minWidth = "120px";
|
|
908
|
+
searchInput.style.maxWidth = "100%";
|
|
909
|
+
searchInput.style.width = "100%";
|
|
449
910
|
searchInput.value = preferences.searchQuery;
|
|
450
911
|
let searchTimeout;
|
|
451
912
|
searchInput.oninput = () => {
|
|
@@ -453,14 +914,139 @@ async function discoverDevices2() {
|
|
|
453
914
|
searchTimeout = setTimeout(() => {
|
|
454
915
|
preferences.searchQuery = searchInput.value;
|
|
455
916
|
setDiscoveryPreferences(preferences);
|
|
456
|
-
updateDiscoveryView(
|
|
917
|
+
void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
457
918
|
}, 300);
|
|
458
919
|
};
|
|
920
|
+
const actionBtnStyle = {
|
|
921
|
+
fontSize: "16px",
|
|
922
|
+
padding: "10px 0",
|
|
923
|
+
borderRadius: "10px",
|
|
924
|
+
margin: "0 12px 0 0",
|
|
925
|
+
width: "100%",
|
|
926
|
+
maxWidth: "220px",
|
|
927
|
+
fontWeight: "bold",
|
|
928
|
+
background: "#ef4444",
|
|
929
|
+
color: "#fff",
|
|
930
|
+
border: "none",
|
|
931
|
+
cursor: "pointer",
|
|
932
|
+
boxShadow: "0 2px 8px #0001",
|
|
933
|
+
transition: "background 0.2s",
|
|
934
|
+
outline: "none",
|
|
935
|
+
display: "block"
|
|
936
|
+
};
|
|
937
|
+
const addSelectedBtn = document.createElement("button");
|
|
938
|
+
addSelectedBtn.textContent = "Add Selected to Config";
|
|
939
|
+
Object.assign(addSelectedBtn.style, actionBtnStyle);
|
|
940
|
+
addSelectedBtn.disabled = true;
|
|
941
|
+
addSelectedBtn.onclick = async () => {
|
|
942
|
+
if (!selectedIds2.size) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
addSelectedBtn.disabled = true;
|
|
946
|
+
addSelectedBtn.textContent = "Adding...";
|
|
947
|
+
try {
|
|
948
|
+
showBusyUi();
|
|
949
|
+
const selectedDevices = discoveredDevices.filter((d) => selectedIds2.has(normalizeId(d.id)));
|
|
950
|
+
const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
|
|
951
|
+
deviceId: d.id,
|
|
952
|
+
name: d.name,
|
|
953
|
+
type: d.type,
|
|
954
|
+
rssi: d.rssi,
|
|
955
|
+
address: d.address,
|
|
956
|
+
model: d.model
|
|
957
|
+
})));
|
|
958
|
+
uiLog.info("Batch add response:", bulkResult);
|
|
959
|
+
if (!bulkResult || bulkResult.success === false) {
|
|
960
|
+
throw new Error(bulkResult?.data?.message || "Batch add failed");
|
|
961
|
+
}
|
|
962
|
+
const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
|
|
963
|
+
const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
|
|
964
|
+
toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
|
|
965
|
+
await loadConfiguredDevices();
|
|
966
|
+
selectedIds2.clear();
|
|
967
|
+
addSelectedBtn.disabled = true;
|
|
968
|
+
addSelectedBtn.textContent = "Add Selected";
|
|
969
|
+
await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
970
|
+
} catch (e) {
|
|
971
|
+
uiLog.error("Batch add error:", e);
|
|
972
|
+
toastError(e instanceof Error ? e.message : "Failed to add devices");
|
|
973
|
+
addSelectedBtn.disabled = false;
|
|
974
|
+
addSelectedBtn.textContent = "Add Selected";
|
|
975
|
+
} finally {
|
|
976
|
+
hideBusyUi();
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
const enableSelectedBtn = document.createElement("button");
|
|
980
|
+
enableSelectedBtn.textContent = "Enable Selected";
|
|
981
|
+
Object.assign(enableSelectedBtn.style, actionBtnStyle);
|
|
982
|
+
enableSelectedBtn.disabled = true;
|
|
983
|
+
enableSelectedBtn.onclick = async () => {
|
|
984
|
+
if (!selectedIds2.size) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
enableSelectedBtn.disabled = true;
|
|
988
|
+
enableSelectedBtn.textContent = "Enabling...";
|
|
989
|
+
try {
|
|
990
|
+
showBusyUi();
|
|
991
|
+
await batchSetDeviceEnabled2(selectedIds2, true);
|
|
992
|
+
toastSuccess("Selected devices enabled");
|
|
993
|
+
await loadConfiguredDevices();
|
|
994
|
+
enableSelectedBtn.disabled = true;
|
|
995
|
+
enableSelectedBtn.textContent = "Enable Selected";
|
|
996
|
+
await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
997
|
+
} catch (e) {
|
|
998
|
+
uiLog.error("Batch enable error:", e);
|
|
999
|
+
toastError(e instanceof Error ? e.message : "Failed to enable devices");
|
|
1000
|
+
enableSelectedBtn.disabled = false;
|
|
1001
|
+
enableSelectedBtn.textContent = "Enable Selected";
|
|
1002
|
+
} finally {
|
|
1003
|
+
hideBusyUi();
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const disableSelectedBtn = document.createElement("button");
|
|
1007
|
+
disableSelectedBtn.textContent = "Disable Selected";
|
|
1008
|
+
Object.assign(disableSelectedBtn.style, actionBtnStyle);
|
|
1009
|
+
disableSelectedBtn.disabled = true;
|
|
1010
|
+
disableSelectedBtn.onclick = async () => {
|
|
1011
|
+
if (!selectedIds2.size) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
disableSelectedBtn.disabled = true;
|
|
1015
|
+
disableSelectedBtn.textContent = "Disabling...";
|
|
1016
|
+
try {
|
|
1017
|
+
showBusyUi();
|
|
1018
|
+
await batchSetDeviceEnabled2(selectedIds2, false);
|
|
1019
|
+
toastSuccess("Selected devices disabled");
|
|
1020
|
+
await loadConfiguredDevices();
|
|
1021
|
+
disableSelectedBtn.disabled = true;
|
|
1022
|
+
disableSelectedBtn.textContent = "Disable Selected";
|
|
1023
|
+
await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
uiLog.error("Batch disable error:", e);
|
|
1026
|
+
toastError(e instanceof Error ? e.message : "Failed to disable devices");
|
|
1027
|
+
disableSelectedBtn.disabled = false;
|
|
1028
|
+
disableSelectedBtn.textContent = "Disable Selected";
|
|
1029
|
+
} finally {
|
|
1030
|
+
hideBusyUi();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
459
1033
|
controlsDiv.appendChild(filterLabel);
|
|
460
1034
|
controlsDiv.appendChild(filterGroup);
|
|
461
1035
|
controlsDiv.appendChild(sortLabel);
|
|
462
1036
|
controlsDiv.appendChild(sortSelect);
|
|
1037
|
+
controlsDiv.appendChild(groupLabel);
|
|
1038
|
+
controlsDiv.appendChild(groupSelect);
|
|
1039
|
+
controlsDiv.appendChild(hideAddedLabel);
|
|
463
1040
|
controlsDiv.appendChild(searchInput);
|
|
1041
|
+
const topActionRow = document.createElement("div");
|
|
1042
|
+
topActionRow.style.display = "flex";
|
|
1043
|
+
topActionRow.style.gap = "20px";
|
|
1044
|
+
topActionRow.style.margin = "18px 0 10px 0";
|
|
1045
|
+
topActionRow.style.justifyContent = "flex-start";
|
|
1046
|
+
topActionRow.appendChild(addSelectedBtn);
|
|
1047
|
+
topActionRow.appendChild(enableSelectedBtn);
|
|
1048
|
+
topActionRow.appendChild(disableSelectedBtn);
|
|
1049
|
+
list.insertBefore(topActionRow, controlsDiv);
|
|
464
1050
|
list.innerHTML = "";
|
|
465
1051
|
list.appendChild(controlsDiv);
|
|
466
1052
|
let deviceListContainer = document.getElementById("discoveredDevices");
|
|
@@ -475,41 +1061,497 @@ async function discoverDevices2() {
|
|
|
475
1061
|
list.appendChild(deviceListContainer);
|
|
476
1062
|
}
|
|
477
1063
|
list.style.display = "block";
|
|
478
|
-
|
|
1064
|
+
controlsInitialized = true;
|
|
1065
|
+
const updateActionButtons = () => {
|
|
1066
|
+
const hasSelection = selectedIds2.size > 0;
|
|
1067
|
+
addSelectedBtn.disabled = !hasSelection;
|
|
1068
|
+
enableSelectedBtn.disabled = !hasSelection;
|
|
1069
|
+
disableSelectedBtn.disabled = !hasSelection;
|
|
1070
|
+
};
|
|
1071
|
+
setInterval(updateActionButtons, 300);
|
|
1072
|
+
};
|
|
1073
|
+
try {
|
|
1074
|
+
const bleSettings = {
|
|
1075
|
+
bleEnabled: !(disableBleCheckbox?.checked ?? false),
|
|
1076
|
+
bleScanDurationSeconds: Math.max(3, Math.min(15, Number(scanSelect?.value || 5))),
|
|
1077
|
+
bleTimeoutSeconds: Math.max(3, Math.min(30, Number(timeoutInput?.value || 8)))
|
|
1078
|
+
};
|
|
1079
|
+
setDiscoveryBleSettings(bleSettings);
|
|
1080
|
+
showBusyUi();
|
|
1081
|
+
btn.disabled = true;
|
|
1082
|
+
btn.textContent = "\u{1F50D} Discovering...";
|
|
1083
|
+
setPhase(bleSettings.bleEnabled ? "Scanning BLE..." : "Skipping BLE scan...");
|
|
1084
|
+
renderProgress();
|
|
1085
|
+
status.classList.remove("error");
|
|
1086
|
+
const devicesFoundDisplay = document.getElementById("discoverDevicesFound");
|
|
1087
|
+
if (devicesFoundDisplay) {
|
|
1088
|
+
devicesFoundDisplay.style.display = "none";
|
|
1089
|
+
devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
|
|
1090
|
+
}
|
|
1091
|
+
if (cancelBtn) {
|
|
1092
|
+
cancelBtn.style.display = "inline-block";
|
|
1093
|
+
cancelBtn.disabled = false;
|
|
1094
|
+
cancelBtn.onclick = () => {
|
|
1095
|
+
cancelled = true;
|
|
1096
|
+
const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
|
|
1097
|
+
status.textContent = `Discovery cancelled (${totalSeconds}s total)`;
|
|
1098
|
+
if (phaseProgress) {
|
|
1099
|
+
phaseProgress.style.display = "none";
|
|
1100
|
+
}
|
|
1101
|
+
if (devicesFoundDisplay) {
|
|
1102
|
+
devicesFoundDisplay.style.display = "none";
|
|
1103
|
+
devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
|
|
1104
|
+
}
|
|
1105
|
+
cancelBtn.style.display = "none";
|
|
1106
|
+
btn.disabled = false;
|
|
1107
|
+
btn.textContent = "\u{1F50D} Discover Devices";
|
|
1108
|
+
hideBusyUi();
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (bleSettings.bleEnabled) {
|
|
1112
|
+
const bleDevicesRaw = await discoverDevices("ble", bleSettings);
|
|
1113
|
+
if (cancelled) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
discoveredDevices = dedupeById(bleDevicesRaw);
|
|
1117
|
+
uiLog.info("BLE discover response:", bleDevicesRaw);
|
|
1118
|
+
if (devicesFoundDisplay && bleDevicesRaw.length > 0) {
|
|
1119
|
+
devicesFoundDisplay.style.display = "inline";
|
|
1120
|
+
devicesFoundDisplay.classList.add("discovery-scanning-pulse");
|
|
1121
|
+
devicesFoundDisplay.textContent = `\u{1F4CA} ${bleDevicesRaw.length} device(s) found (scanning...)`;
|
|
1122
|
+
}
|
|
1123
|
+
} else {
|
|
1124
|
+
discoveredDevices = [];
|
|
1125
|
+
uiLog.info("BLE discovery skipped by user setting");
|
|
1126
|
+
}
|
|
1127
|
+
if (!autoAddAll && discoveredDevices.length > 0) {
|
|
1128
|
+
await ensureDiscoveryControls();
|
|
1129
|
+
await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
|
|
1130
|
+
status.textContent = `Showing ${discoveredDevices.length} device(s) from BLE, fetching OpenAPI...`;
|
|
1131
|
+
}
|
|
1132
|
+
setPhase("Fetching OpenAPI...");
|
|
1133
|
+
renderProgress();
|
|
1134
|
+
try {
|
|
1135
|
+
const openApiDevicesRaw = await discoverDevices("openapi");
|
|
1136
|
+
if (cancelled) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
uiLog.info("OpenAPI discover response:", openApiDevicesRaw);
|
|
1140
|
+
discoveredDevices = mergeDiscoveredDevices(discoveredDevices, openApiDevicesRaw);
|
|
1141
|
+
if (devicesFoundDisplay && discoveredDevices.length > 0) {
|
|
1142
|
+
devicesFoundDisplay.textContent = `\u{1F4CA} ${discoveredDevices.length} device(s) found (complete)`;
|
|
1143
|
+
}
|
|
1144
|
+
} catch (openApiError) {
|
|
1145
|
+
uiLog.warn("OpenAPI phase failed during discovery:", openApiError);
|
|
1146
|
+
if (!discoveredDevices.length) {
|
|
1147
|
+
throw openApiError;
|
|
1148
|
+
}
|
|
1149
|
+
if (devicesFoundDisplay) {
|
|
1150
|
+
devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
setPhase("Complete");
|
|
1154
|
+
renderProgress();
|
|
1155
|
+
uiLog.info("Final merged discover response:", discoveredDevices);
|
|
1156
|
+
if (!discoveredDevices.length) {
|
|
1157
|
+
status.textContent = "No devices found in your SwitchBot account";
|
|
1158
|
+
toastInfo("No devices found in your SwitchBot account");
|
|
1159
|
+
list.style.display = "none";
|
|
1160
|
+
if (devicesFoundDisplay) {
|
|
1161
|
+
devicesFoundDisplay.style.display = "none";
|
|
1162
|
+
devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
|
|
1163
|
+
}
|
|
1164
|
+
clearDiscoveryCache();
|
|
1165
|
+
updateLastScannedStatus();
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (autoAddAll) {
|
|
1169
|
+
status.textContent = `Auto-adding ${discoveredDevices.length} device(s)...`;
|
|
1170
|
+
try {
|
|
1171
|
+
const bulkResult = await addDevicesInBulk(
|
|
1172
|
+
discoveredDevices.map((d) => ({
|
|
1173
|
+
deviceId: d.id,
|
|
1174
|
+
name: d.name,
|
|
1175
|
+
type: d.type,
|
|
1176
|
+
rssi: d.rssi,
|
|
1177
|
+
address: d.address,
|
|
1178
|
+
model: d.model
|
|
1179
|
+
}))
|
|
1180
|
+
);
|
|
1181
|
+
uiLog.info("Bulk add response:", bulkResult);
|
|
1182
|
+
if (!bulkResult || bulkResult.success === false) {
|
|
1183
|
+
throw new Error(bulkResult?.data?.message || "Bulk add failed");
|
|
1184
|
+
}
|
|
1185
|
+
const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
|
|
1186
|
+
const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
|
|
1187
|
+
status.textContent = `\u2713 Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`;
|
|
1188
|
+
if (addedCount > 0) {
|
|
1189
|
+
toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
|
|
1190
|
+
} else if (skippedCount > 0) {
|
|
1191
|
+
toastWarning(`No new devices were added (${skippedCount} skipped)`);
|
|
1192
|
+
}
|
|
1193
|
+
status.classList.remove("error");
|
|
1194
|
+
list.style.display = "none";
|
|
1195
|
+
if (discoveredDevices.length > 0) {
|
|
1196
|
+
const synced = await syncParentPluginConfigFromDisk(true);
|
|
1197
|
+
status.textContent += synced ? " - Config saved automatically." : " - Warning: config may not persist until you close/reopen settings.";
|
|
1198
|
+
if (synced) {
|
|
1199
|
+
toastSuccess("Configuration synced and saved automatically");
|
|
1200
|
+
} else {
|
|
1201
|
+
toastWarning("Configuration sync failed; close and reopen settings before Save");
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
} catch (e) {
|
|
1205
|
+
uiLog.error("Bulk add error:", e);
|
|
1206
|
+
status.textContent = `\u2717 Error: ${e instanceof Error ? e.message : "Failed to add devices"}`;
|
|
1207
|
+
status.classList.add("error");
|
|
1208
|
+
toastError(e instanceof Error ? e.message : "Failed to add devices");
|
|
1209
|
+
}
|
|
1210
|
+
await loadConfiguredDevices();
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
await ensureDiscoveryControls();
|
|
1214
|
+
await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
|
|
1215
|
+
setDiscoveryCache(discoveredDevices);
|
|
1216
|
+
updateLastScannedStatus();
|
|
479
1217
|
} catch (e) {
|
|
1218
|
+
if (cancelled) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
480
1221
|
uiLog.error("Discovery error:", e);
|
|
481
1222
|
status.textContent = `Error: ${e instanceof Error ? e.message : "Discovery failed"}`;
|
|
482
1223
|
status.classList.add("error");
|
|
483
1224
|
toastError(e instanceof Error ? e.message : "Discovery failed");
|
|
484
1225
|
list.style.display = "none";
|
|
485
1226
|
} finally {
|
|
1227
|
+
clearInterval(progressTimer);
|
|
486
1228
|
hideBusyUi();
|
|
1229
|
+
if (phaseProgress) {
|
|
1230
|
+
phaseProgress.style.display = "none";
|
|
1231
|
+
}
|
|
1232
|
+
if (phaseFill) {
|
|
1233
|
+
phaseFill.style.width = "0%";
|
|
1234
|
+
}
|
|
1235
|
+
if (phaseLabel) {
|
|
1236
|
+
phaseLabel.textContent = "";
|
|
1237
|
+
}
|
|
1238
|
+
const devicesFoundDisplay = document.getElementById("discoverDevicesFound");
|
|
1239
|
+
if (devicesFoundDisplay) {
|
|
1240
|
+
devicesFoundDisplay.style.display = "none";
|
|
1241
|
+
devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
|
|
1242
|
+
}
|
|
487
1243
|
btn.disabled = false;
|
|
488
1244
|
btn.textContent = "\u{1F50D} Discover Devices";
|
|
1245
|
+
if (cancelBtn) {
|
|
1246
|
+
cancelBtn.disabled = false;
|
|
1247
|
+
cancelBtn.style.display = "none";
|
|
1248
|
+
cancelBtn.onclick = null;
|
|
1249
|
+
}
|
|
489
1250
|
}
|
|
490
1251
|
}
|
|
491
|
-
async function updateDiscoveryView(allDevices, preferences) {
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
configuredIds
|
|
1252
|
+
async function updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds) {
|
|
1253
|
+
const visibleDevices = allDevices.filter((d) => {
|
|
1254
|
+
if (hideAdded && d.added) {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
return true;
|
|
498
1258
|
});
|
|
1259
|
+
const configuredIds = new Set(
|
|
1260
|
+
allDevices.filter((d) => d.added).map((d) => normalizeId(d.id))
|
|
1261
|
+
);
|
|
1262
|
+
const getConnectionGroup = (device) => {
|
|
1263
|
+
if (device?.isIR) {
|
|
1264
|
+
return "IR";
|
|
1265
|
+
}
|
|
1266
|
+
const connectionType = String(device?.connectionType || "").toLowerCase();
|
|
1267
|
+
if (connectionType.includes("both")) {
|
|
1268
|
+
return "Both";
|
|
1269
|
+
}
|
|
1270
|
+
if (connectionType.includes("ble")) {
|
|
1271
|
+
return "BLE";
|
|
1272
|
+
}
|
|
1273
|
+
if (connectionType.includes("api")) {
|
|
1274
|
+
return "OpenAPI";
|
|
1275
|
+
}
|
|
1276
|
+
return "Unknown";
|
|
1277
|
+
};
|
|
1278
|
+
const getHubGroup = (device) => {
|
|
1279
|
+
const hub = String(device?.hubDeviceId || "").trim();
|
|
1280
|
+
return hub ? `Hub ${hub}` : "No Hub";
|
|
1281
|
+
};
|
|
1282
|
+
const getTypeGroup = (device) => {
|
|
1283
|
+
const type = String(device?.type || "").trim();
|
|
1284
|
+
return type || "Unknown Type";
|
|
1285
|
+
};
|
|
1286
|
+
const groupedDevices = /* @__PURE__ */ new Map();
|
|
1287
|
+
for (const d of visibleDevices) {
|
|
1288
|
+
let group = getConnectionGroup(d);
|
|
1289
|
+
if (groupBy === "hub") {
|
|
1290
|
+
group = getHubGroup(d);
|
|
1291
|
+
} else if (groupBy === "type") {
|
|
1292
|
+
group = getTypeGroup(d);
|
|
1293
|
+
}
|
|
1294
|
+
const groupDevices = groupedDevices.get(group) || [];
|
|
1295
|
+
groupDevices.push(d);
|
|
1296
|
+
groupedDevices.set(group, groupDevices);
|
|
1297
|
+
}
|
|
1298
|
+
let orderedGroups = [];
|
|
1299
|
+
if (groupBy === "hub") {
|
|
1300
|
+
const hubGroups = [...groupedDevices.keys()].filter((group) => group !== "No Hub").sort((a, b) => a.localeCompare(b));
|
|
1301
|
+
orderedGroups = groupedDevices.has("No Hub") ? [...hubGroups, "No Hub"] : hubGroups;
|
|
1302
|
+
} else if (groupBy === "type") {
|
|
1303
|
+
const typeGroups = [...groupedDevices.keys()].filter((group) => group !== "Unknown Type").sort((a, b) => a.localeCompare(b));
|
|
1304
|
+
orderedGroups = groupedDevices.has("Unknown Type") ? [...typeGroups, "Unknown Type"] : typeGroups;
|
|
1305
|
+
} else {
|
|
1306
|
+
const groupOrder = ["Both", "BLE", "OpenAPI", "IR", "Unknown"];
|
|
1307
|
+
orderedGroups = groupOrder.filter((group) => groupedDevices.has(group));
|
|
1308
|
+
}
|
|
1309
|
+
const container = document.createElement("div");
|
|
1310
|
+
container.id = "discoveredDevices";
|
|
1311
|
+
container.className = "discovery-groups";
|
|
1312
|
+
if (!visibleDevices.length) {
|
|
1313
|
+
const empty = document.createElement("div");
|
|
1314
|
+
empty.className = "discovery-group-empty";
|
|
1315
|
+
empty.textContent = hideAdded ? "No devices match current filters (or all are already added)." : "No devices match current filters.";
|
|
1316
|
+
container.appendChild(empty);
|
|
1317
|
+
} else {
|
|
1318
|
+
for (const groupName of orderedGroups) {
|
|
1319
|
+
const groupItems = groupedDevices.get(groupName);
|
|
1320
|
+
if (!groupItems?.length) {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
const groupSection = document.createElement("section");
|
|
1324
|
+
groupSection.className = "discovery-group";
|
|
1325
|
+
const groupStorageKey = `${groupBy}:${groupName}`;
|
|
1326
|
+
let expanded = isDiscoveryGroupExpanded(groupStorageKey);
|
|
1327
|
+
const groupHeader = document.createElement("button");
|
|
1328
|
+
groupHeader.className = "discovery-group-header-btn";
|
|
1329
|
+
groupHeader.type = "button";
|
|
1330
|
+
const setGroupHeaderText = () => {
|
|
1331
|
+
const marker = expanded ? "\u25BE" : "\u25B8";
|
|
1332
|
+
groupHeader.textContent = `${marker} ${groupName} (${groupItems.length})`;
|
|
1333
|
+
};
|
|
1334
|
+
setGroupHeaderText();
|
|
1335
|
+
groupSection.appendChild(groupHeader);
|
|
1336
|
+
const groupList = await renderDiscoveredDevices(groupItems, {
|
|
1337
|
+
configuredIds,
|
|
1338
|
+
selectedIds,
|
|
1339
|
+
onToggleSelect: (device, selected) => {
|
|
1340
|
+
const id = normalizeId(device.id);
|
|
1341
|
+
if (selected) {
|
|
1342
|
+
selectedIds.add(id);
|
|
1343
|
+
} else {
|
|
1344
|
+
selectedIds.delete(id);
|
|
1345
|
+
}
|
|
1346
|
+
const btn = document.querySelector("button")?.parentElement?.querySelector("button");
|
|
1347
|
+
if (btn && btn.textContent?.includes("Add Selected")) {
|
|
1348
|
+
btn.disabled = selectedIds.size === 0;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
if (!expanded) {
|
|
1353
|
+
groupList.style.display = "none";
|
|
1354
|
+
}
|
|
1355
|
+
groupHeader.onclick = () => {
|
|
1356
|
+
expanded = !expanded;
|
|
1357
|
+
setDiscoveryGroupExpanded(groupStorageKey, expanded);
|
|
1358
|
+
setGroupHeaderText();
|
|
1359
|
+
groupList.style.display = expanded ? "grid" : "none";
|
|
1360
|
+
};
|
|
1361
|
+
groupSection.appendChild(groupList);
|
|
1362
|
+
container.appendChild(groupSection);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
499
1365
|
const existingList = document.getElementById("discoveredDevices");
|
|
500
1366
|
if (existingList && existingList.parentNode) {
|
|
501
|
-
existingList.replaceWith(
|
|
1367
|
+
existingList.replaceWith(container);
|
|
502
1368
|
} else {
|
|
503
1369
|
const listContainer = document.getElementById("discoveredList");
|
|
504
1370
|
if (listContainer) {
|
|
505
|
-
listContainer.appendChild(
|
|
1371
|
+
listContainer.appendChild(container);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const legacyAddSelectedBtns = [];
|
|
1375
|
+
const allBtns = document.querySelectorAll("button");
|
|
1376
|
+
for (let i = 0; i < allBtns.length; i++) {
|
|
1377
|
+
const btn = allBtns[i];
|
|
1378
|
+
if (btn.textContent && btn.textContent.trim() === "Add Selected") {
|
|
1379
|
+
legacyAddSelectedBtns.push(btn);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
for (let i = 0; i < legacyAddSelectedBtns.length; i++) {
|
|
1383
|
+
legacyAddSelectedBtns[i].remove();
|
|
1384
|
+
}
|
|
1385
|
+
function updateBatchButtonStates() {
|
|
1386
|
+
const addSelectedBtn = document.getElementById("addSelectedBtn");
|
|
1387
|
+
const enableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Enable Selected"]');
|
|
1388
|
+
const disableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Disable Selected"]');
|
|
1389
|
+
const hasSelection = selectedIds.size > 0;
|
|
1390
|
+
if (addSelectedBtn) addSelectedBtn.disabled = !hasSelection;
|
|
1391
|
+
if (enableSelectedBtn) enableSelectedBtn.disabled = !hasSelection;
|
|
1392
|
+
if (disableSelectedBtn) disableSelectedBtn.disabled = !hasSelection;
|
|
1393
|
+
}
|
|
1394
|
+
window.removeEventListener("discovery-selection-changed", updateBatchButtonStates);
|
|
1395
|
+
window.addEventListener("discovery-selection-changed", updateBatchButtonStates);
|
|
1396
|
+
let batchControls = document.getElementById("batchImportControls");
|
|
1397
|
+
if (!batchControls) {
|
|
1398
|
+
batchControls = document.createElement("div");
|
|
1399
|
+
batchControls.id = "batchImportControls";
|
|
1400
|
+
batchControls.style.display = "flex";
|
|
1401
|
+
batchControls.style.flexWrap = "wrap";
|
|
1402
|
+
batchControls.style.alignItems = "center";
|
|
1403
|
+
batchControls.style.gap = "18px";
|
|
1404
|
+
batchControls.style.margin = "8px 0 18px 0";
|
|
1405
|
+
batchControls.style.width = "100%";
|
|
1406
|
+
const buttonRow = document.createElement("div");
|
|
1407
|
+
buttonRow.style.display = "flex";
|
|
1408
|
+
buttonRow.style.flexWrap = "nowrap";
|
|
1409
|
+
buttonRow.style.alignItems = "center";
|
|
1410
|
+
buttonRow.style.justifyContent = "center";
|
|
1411
|
+
buttonRow.style.gap = "16px";
|
|
1412
|
+
buttonRow.style.width = "100%";
|
|
1413
|
+
const addSelectedBtn = document.createElement("button");
|
|
1414
|
+
addSelectedBtn.id = "addSelectedBtn";
|
|
1415
|
+
addSelectedBtn.textContent = "Add Selected to Config";
|
|
1416
|
+
addSelectedBtn.disabled = selectedIds.size === 0;
|
|
1417
|
+
addSelectedBtn.style.fontWeight = "600";
|
|
1418
|
+
const redButtonStyle = {
|
|
1419
|
+
width: "220px",
|
|
1420
|
+
padding: "12px 0",
|
|
1421
|
+
fontSize: "17px",
|
|
1422
|
+
marginBottom: "0",
|
|
1423
|
+
boxShadow: "0 2px 8px 0 rgba(220,38,38,0.10)",
|
|
1424
|
+
borderRadius: "8px"
|
|
1425
|
+
};
|
|
1426
|
+
Object.assign(addSelectedBtn.style, redButtonStyle);
|
|
1427
|
+
addSelectedBtn.style.background = "#ef4444";
|
|
1428
|
+
addSelectedBtn.style.color = "white";
|
|
1429
|
+
addSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
|
|
1430
|
+
addSelectedBtn.onmouseenter = function() {
|
|
1431
|
+
addSelectedBtn.style.background = "#dc2626";
|
|
1432
|
+
};
|
|
1433
|
+
addSelectedBtn.onmouseleave = function() {
|
|
1434
|
+
addSelectedBtn.style.background = "#ef4444";
|
|
1435
|
+
};
|
|
1436
|
+
addSelectedBtn.onclick = async () => {
|
|
1437
|
+
if (!selectedIds.size) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
addSelectedBtn.disabled = true;
|
|
1441
|
+
addSelectedBtn.textContent = "Adding...";
|
|
1442
|
+
try {
|
|
1443
|
+
const selectedDevices = visibleDevices.filter((d) => selectedIds.has(normalizeId(d.id)));
|
|
1444
|
+
const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
|
|
1445
|
+
deviceId: d.id,
|
|
1446
|
+
name: d.name,
|
|
1447
|
+
type: d.type,
|
|
1448
|
+
rssi: d.rssi,
|
|
1449
|
+
address: d.address,
|
|
1450
|
+
model: d.model
|
|
1451
|
+
})));
|
|
1452
|
+
uiLog.info("Batch add response:", bulkResult);
|
|
1453
|
+
if (!bulkResult || bulkResult.success === false) {
|
|
1454
|
+
throw new Error(bulkResult?.data?.message || "Batch add failed");
|
|
1455
|
+
}
|
|
1456
|
+
const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
|
|
1457
|
+
const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
|
|
1458
|
+
toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
|
|
1459
|
+
await loadConfiguredDevices();
|
|
1460
|
+
selectedIds.clear();
|
|
1461
|
+
addSelectedBtn.disabled = true;
|
|
1462
|
+
addSelectedBtn.textContent = "Add Selected to Config";
|
|
1463
|
+
await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
|
|
1464
|
+
} catch (e) {
|
|
1465
|
+
uiLog.error("Batch add error:", e);
|
|
1466
|
+
toastError(e instanceof Error ? e.message : "Failed to add devices");
|
|
1467
|
+
addSelectedBtn.disabled = false;
|
|
1468
|
+
addSelectedBtn.textContent = "Add Selected to Config";
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
const enableSelectedBtn = document.createElement("button");
|
|
1472
|
+
enableSelectedBtn.textContent = "Enable Selected";
|
|
1473
|
+
enableSelectedBtn.style.fontWeight = "600";
|
|
1474
|
+
Object.assign(enableSelectedBtn.style, redButtonStyle);
|
|
1475
|
+
enableSelectedBtn.style.background = "#ef4444";
|
|
1476
|
+
enableSelectedBtn.style.color = "white";
|
|
1477
|
+
enableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
|
|
1478
|
+
enableSelectedBtn.onmouseenter = function() {
|
|
1479
|
+
enableSelectedBtn.style.background = "#dc2626";
|
|
1480
|
+
};
|
|
1481
|
+
enableSelectedBtn.onmouseleave = function() {
|
|
1482
|
+
enableSelectedBtn.style.background = "#ef4444";
|
|
1483
|
+
};
|
|
1484
|
+
enableSelectedBtn.disabled = selectedIds.size === 0;
|
|
1485
|
+
enableSelectedBtn.onclick = async () => {
|
|
1486
|
+
if (!selectedIds.size) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
enableSelectedBtn.disabled = true;
|
|
1490
|
+
try {
|
|
1491
|
+
showBusyUi();
|
|
1492
|
+
await batchSetDeviceEnabled(selectedIds, true);
|
|
1493
|
+
toastSuccess("Selected devices enabled");
|
|
1494
|
+
await loadConfiguredDevices();
|
|
1495
|
+
await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
uiLog.error("Batch enable error:", e);
|
|
1498
|
+
toastError(e instanceof Error ? e.message : "Failed to enable devices");
|
|
1499
|
+
} finally {
|
|
1500
|
+
hideBusyUi();
|
|
1501
|
+
enableSelectedBtn.disabled = false;
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
const disableSelectedBtn = document.createElement("button");
|
|
1505
|
+
disableSelectedBtn.textContent = "Disable Selected";
|
|
1506
|
+
disableSelectedBtn.style.fontWeight = "600";
|
|
1507
|
+
Object.assign(disableSelectedBtn.style, redButtonStyle);
|
|
1508
|
+
disableSelectedBtn.style.background = "#ef4444";
|
|
1509
|
+
disableSelectedBtn.style.color = "white";
|
|
1510
|
+
disableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
|
|
1511
|
+
disableSelectedBtn.onmouseenter = function() {
|
|
1512
|
+
disableSelectedBtn.style.background = "#dc2626";
|
|
1513
|
+
};
|
|
1514
|
+
disableSelectedBtn.onmouseleave = function() {
|
|
1515
|
+
disableSelectedBtn.style.background = "#ef4444";
|
|
1516
|
+
};
|
|
1517
|
+
disableSelectedBtn.disabled = selectedIds.size === 0;
|
|
1518
|
+
disableSelectedBtn.onclick = async () => {
|
|
1519
|
+
if (!selectedIds.size) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
disableSelectedBtn.disabled = true;
|
|
1523
|
+
try {
|
|
1524
|
+
showBusyUi();
|
|
1525
|
+
await batchSetDeviceEnabled(selectedIds, false);
|
|
1526
|
+
toastSuccess("Selected devices disabled");
|
|
1527
|
+
await loadConfiguredDevices();
|
|
1528
|
+
await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
|
|
1529
|
+
} catch (e) {
|
|
1530
|
+
uiLog.error("Batch disable error:", e);
|
|
1531
|
+
toastError(e instanceof Error ? e.message : "Failed to disable devices");
|
|
1532
|
+
} finally {
|
|
1533
|
+
hideBusyUi();
|
|
1534
|
+
disableSelectedBtn.disabled = false;
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
buttonRow.appendChild(addSelectedBtn);
|
|
1538
|
+
buttonRow.appendChild(enableSelectedBtn);
|
|
1539
|
+
buttonRow.appendChild(disableSelectedBtn);
|
|
1540
|
+
batchControls.appendChild(buttonRow);
|
|
1541
|
+
const listContainer = document.getElementById("discoveredList");
|
|
1542
|
+
if (listContainer) {
|
|
1543
|
+
listContainer.insertBefore(batchControls, listContainer.firstChild);
|
|
1544
|
+
}
|
|
1545
|
+
} else {
|
|
1546
|
+
const addSelectedBtn = document.getElementById("addSelectedBtn");
|
|
1547
|
+
if (addSelectedBtn) {
|
|
1548
|
+
addSelectedBtn.disabled = selectedIds.size === 0;
|
|
506
1549
|
}
|
|
507
1550
|
}
|
|
508
|
-
ul.id = "discoveredDevices";
|
|
509
1551
|
const status = document.getElementById("discoverStatus");
|
|
510
1552
|
if (status) {
|
|
511
1553
|
const totalCount = allDevices.length;
|
|
512
|
-
const filteredCount =
|
|
1554
|
+
const filteredCount = visibleDevices.length;
|
|
513
1555
|
status.textContent = filteredCount === totalCount ? `Found ${totalCount} device(s)` : `Showing ${filteredCount} of ${totalCount} device(s)`;
|
|
514
1556
|
}
|
|
515
1557
|
}
|
|
@@ -517,6 +1559,7 @@ async function addDeviceToConfig(device) {
|
|
|
517
1559
|
const { addDeviceToConfig: addDevice2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
|
|
518
1560
|
await addDevice2(device);
|
|
519
1561
|
}
|
|
1562
|
+
var DISCOVERY_GROUP_BY_KEY, DISCOVERY_GROUP_EXPANDED_KEY, DISCOVERY_BLE_SETTINGS_KEY, DISCOVERY_HIDE_ADDED_KEY, DISCOVERY_CACHE_KEY, DISCOVERY_AUTO_REFRESH_KEY, DISCOVERY_CACHE_TTL_MS, discoveryAutoRefreshTimer, discoveryLastScannedTimer;
|
|
520
1563
|
var init_discovery = __esm({
|
|
521
1564
|
"src/homebridge-ui/public/js/discovery.ts"() {
|
|
522
1565
|
"use strict";
|
|
@@ -526,6 +1569,15 @@ var init_discovery = __esm({
|
|
|
526
1569
|
init_modal();
|
|
527
1570
|
init_render();
|
|
528
1571
|
init_toast();
|
|
1572
|
+
DISCOVERY_GROUP_BY_KEY = "discoveryGroupBy";
|
|
1573
|
+
DISCOVERY_GROUP_EXPANDED_KEY = "discoveryGroupExpanded";
|
|
1574
|
+
DISCOVERY_BLE_SETTINGS_KEY = "discoveryBleSettings";
|
|
1575
|
+
DISCOVERY_HIDE_ADDED_KEY = "discoveryHideAdded";
|
|
1576
|
+
DISCOVERY_CACHE_KEY = "discoveryCache";
|
|
1577
|
+
DISCOVERY_AUTO_REFRESH_KEY = "discoveryAutoRefreshSeconds";
|
|
1578
|
+
DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1579
|
+
discoveryAutoRefreshTimer = null;
|
|
1580
|
+
discoveryLastScannedTimer = null;
|
|
529
1581
|
}
|
|
530
1582
|
});
|
|
531
1583
|
|
|
@@ -743,6 +1795,40 @@ async function importDiscoveredDevice(device) {
|
|
|
743
1795
|
macInput.style.borderRadius = "6px";
|
|
744
1796
|
macInput.style.fontSize = "14px";
|
|
745
1797
|
macInput.style.boxSizing = "border-box";
|
|
1798
|
+
const encryptionKeyLabel = document.createElement("label");
|
|
1799
|
+
encryptionKeyLabel.textContent = "BLE Encryption Key (optional)";
|
|
1800
|
+
encryptionKeyLabel.style.display = "block";
|
|
1801
|
+
encryptionKeyLabel.style.marginBottom = "6px";
|
|
1802
|
+
encryptionKeyLabel.style.fontWeight = "500";
|
|
1803
|
+
encryptionKeyLabel.style.fontSize = "12px";
|
|
1804
|
+
encryptionKeyLabel.style.color = "#6b7280";
|
|
1805
|
+
const encryptionKeyInput = document.createElement("input");
|
|
1806
|
+
encryptionKeyInput.type = "password";
|
|
1807
|
+
encryptionKeyInput.value = device.encryptionKey || "";
|
|
1808
|
+
encryptionKeyInput.placeholder = "Paste device BLE encryption key";
|
|
1809
|
+
encryptionKeyInput.style.width = "100%";
|
|
1810
|
+
encryptionKeyInput.style.marginBottom = "12px";
|
|
1811
|
+
encryptionKeyInput.style.padding = "8px 10px";
|
|
1812
|
+
encryptionKeyInput.style.borderRadius = "6px";
|
|
1813
|
+
encryptionKeyInput.style.fontSize = "14px";
|
|
1814
|
+
encryptionKeyInput.style.boxSizing = "border-box";
|
|
1815
|
+
const keyIdLabel = document.createElement("label");
|
|
1816
|
+
keyIdLabel.textContent = "BLE Key ID (optional)";
|
|
1817
|
+
keyIdLabel.style.display = "block";
|
|
1818
|
+
keyIdLabel.style.marginBottom = "6px";
|
|
1819
|
+
keyIdLabel.style.fontWeight = "500";
|
|
1820
|
+
keyIdLabel.style.fontSize = "12px";
|
|
1821
|
+
keyIdLabel.style.color = "#6b7280";
|
|
1822
|
+
const keyIdInput = document.createElement("input");
|
|
1823
|
+
keyIdInput.type = "text";
|
|
1824
|
+
keyIdInput.value = device.keyId || "";
|
|
1825
|
+
keyIdInput.placeholder = "e.g. ff";
|
|
1826
|
+
keyIdInput.style.width = "100%";
|
|
1827
|
+
keyIdInput.style.marginBottom = "12px";
|
|
1828
|
+
keyIdInput.style.padding = "8px 10px";
|
|
1829
|
+
keyIdInput.style.borderRadius = "6px";
|
|
1830
|
+
keyIdInput.style.fontSize = "14px";
|
|
1831
|
+
keyIdInput.style.boxSizing = "border-box";
|
|
746
1832
|
const buttons = document.createElement("div");
|
|
747
1833
|
buttons.style.display = "flex";
|
|
748
1834
|
buttons.style.gap = "10px";
|
|
@@ -769,7 +1855,9 @@ async function importDiscoveredDevice(device) {
|
|
|
769
1855
|
importBtn.onclick = () => cleanup({
|
|
770
1856
|
configDeviceName: nameInput.value || device.name || device.id,
|
|
771
1857
|
configDeviceType: typeSelect.value || device.type,
|
|
772
|
-
address: macInput.value || void 0
|
|
1858
|
+
address: macInput.value || void 0,
|
|
1859
|
+
encryptionKey: encryptionKeyInput.value || void 0,
|
|
1860
|
+
keyId: keyIdInput.value || void 0
|
|
773
1861
|
});
|
|
774
1862
|
div.addEventListener("click", (event) => {
|
|
775
1863
|
if (event.target === div) {
|
|
@@ -784,6 +1872,10 @@ async function importDiscoveredDevice(device) {
|
|
|
784
1872
|
contentDiv.appendChild(typeSelect);
|
|
785
1873
|
contentDiv.appendChild(macLabel);
|
|
786
1874
|
contentDiv.appendChild(macInput);
|
|
1875
|
+
contentDiv.appendChild(encryptionKeyLabel);
|
|
1876
|
+
contentDiv.appendChild(encryptionKeyInput);
|
|
1877
|
+
contentDiv.appendChild(keyIdLabel);
|
|
1878
|
+
contentDiv.appendChild(keyIdInput);
|
|
787
1879
|
contentDiv.appendChild(buttons);
|
|
788
1880
|
modal.appendChild(title);
|
|
789
1881
|
modal.appendChild(contentDiv);
|
|
@@ -1186,6 +2278,7 @@ __export(render_exports, {
|
|
|
1186
2278
|
getRssiSignalQuality: () => getRssiSignalQuality,
|
|
1187
2279
|
renderBadge: () => renderBadge,
|
|
1188
2280
|
renderConnectionBadge: () => renderConnectionBadge,
|
|
2281
|
+
renderDeviceDetailsPanel: () => renderDeviceDetailsPanel,
|
|
1189
2282
|
renderDeviceList: () => renderDeviceList,
|
|
1190
2283
|
renderDiscoveredDevices: () => renderDiscoveredDevices,
|
|
1191
2284
|
renderIRBadge: () => renderIRBadge,
|
|
@@ -1324,6 +2417,213 @@ function scrollToConfiguredDevice(deviceId) {
|
|
|
1324
2417
|
target.style.background = originalBackground;
|
|
1325
2418
|
}, 1800);
|
|
1326
2419
|
}
|
|
2420
|
+
function createConnectionTestControls(device) {
|
|
2421
|
+
const controls = document.createElement("div");
|
|
2422
|
+
controls.style.display = "inline-flex";
|
|
2423
|
+
controls.style.alignItems = "center";
|
|
2424
|
+
controls.style.gap = "6px";
|
|
2425
|
+
const button = document.createElement("button");
|
|
2426
|
+
button.textContent = "Test Connection";
|
|
2427
|
+
button.className = "secondary";
|
|
2428
|
+
button.style.padding = "4px 9px";
|
|
2429
|
+
button.style.fontSize = "11px";
|
|
2430
|
+
const status = document.createElement("span");
|
|
2431
|
+
status.style.fontSize = "10px";
|
|
2432
|
+
status.style.opacity = "0.85";
|
|
2433
|
+
status.style.whiteSpace = "normal";
|
|
2434
|
+
status.style.overflowWrap = "anywhere";
|
|
2435
|
+
button.onclick = async () => {
|
|
2436
|
+
const startedAt = Date.now();
|
|
2437
|
+
button.disabled = true;
|
|
2438
|
+
button.textContent = "Testing...";
|
|
2439
|
+
status.textContent = "Checking...";
|
|
2440
|
+
status.style.color = "#6b7280";
|
|
2441
|
+
try {
|
|
2442
|
+
const { testDeviceConnection: testDeviceConnection2 } = await Promise.resolve().then(() => (init_api(), api_exports));
|
|
2443
|
+
const result = await testDeviceConnection2({
|
|
2444
|
+
deviceId: String(device?.id || device?.deviceId || ""),
|
|
2445
|
+
connectionType: device?.connectionType,
|
|
2446
|
+
address: device?.address
|
|
2447
|
+
});
|
|
2448
|
+
const measuredLatency = Number(result?.latencyMs) > 0 ? Number(result.latencyMs) : Date.now() - startedAt;
|
|
2449
|
+
if (result?.success) {
|
|
2450
|
+
const method = result?.method || "Auto";
|
|
2451
|
+
status.textContent = `\u2713 ${method} \xB7 ${measuredLatency}ms`;
|
|
2452
|
+
status.style.color = "#16a34a";
|
|
2453
|
+
} else {
|
|
2454
|
+
const detail = result?.message ? ` \xB7 ${result.message}` : "";
|
|
2455
|
+
status.textContent = `\u2717 Failed \xB7 ${measuredLatency}ms${detail}`;
|
|
2456
|
+
status.style.color = "#dc2626";
|
|
2457
|
+
}
|
|
2458
|
+
} catch (e) {
|
|
2459
|
+
status.textContent = `\u2717 Failed \xB7 ${Date.now() - startedAt}ms`;
|
|
2460
|
+
status.style.color = "#dc2626";
|
|
2461
|
+
} finally {
|
|
2462
|
+
button.disabled = false;
|
|
2463
|
+
button.textContent = "Test Connection";
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
controls.appendChild(button);
|
|
2467
|
+
controls.appendChild(status);
|
|
2468
|
+
return controls;
|
|
2469
|
+
}
|
|
2470
|
+
function formatLastSeen(value) {
|
|
2471
|
+
if (!value) {
|
|
2472
|
+
return "N/A";
|
|
2473
|
+
}
|
|
2474
|
+
try {
|
|
2475
|
+
const date = new Date(value);
|
|
2476
|
+
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
2477
|
+
} catch (_e) {
|
|
2478
|
+
return String(value);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function renderDeviceDetailsPanel(device) {
|
|
2482
|
+
const details = document.createElement("div");
|
|
2483
|
+
details.style.cssText = `
|
|
2484
|
+
border-top: 1px solid #ddd;
|
|
2485
|
+
padding: 8px;
|
|
2486
|
+
background: #f9fafb;
|
|
2487
|
+
border-radius: 4px;
|
|
2488
|
+
font-size: 12px;
|
|
2489
|
+
margin-top: 4px;
|
|
2490
|
+
`;
|
|
2491
|
+
const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`;
|
|
2492
|
+
let batteryHistory = [];
|
|
2493
|
+
try {
|
|
2494
|
+
const raw = localStorage.getItem(batteryHistoryKey);
|
|
2495
|
+
if (raw) batteryHistory = JSON.parse(raw);
|
|
2496
|
+
} catch {
|
|
2497
|
+
}
|
|
2498
|
+
const now = Date.now();
|
|
2499
|
+
if (typeof device?.battery === "number") {
|
|
2500
|
+
const last = batteryHistory[batteryHistory.length - 1];
|
|
2501
|
+
if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1e3) {
|
|
2502
|
+
batteryHistory.push({ value: device.battery, ts: now });
|
|
2503
|
+
if (batteryHistory.length > 30) batteryHistory = batteryHistory.slice(-30);
|
|
2504
|
+
try {
|
|
2505
|
+
localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory));
|
|
2506
|
+
} catch {
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
const rows = [
|
|
2511
|
+
{ label: "Device ID", value: String(device?.id || device?.deviceId || "N/A"), copyable: !!(device?.id || device?.deviceId) },
|
|
2512
|
+
{ label: "MAC Address", value: String(device?.address || "N/A"), copyable: !!device?.address },
|
|
2513
|
+
{ label: "Device Type", value: String(device?.type || device?.configDeviceType || "N/A") },
|
|
2514
|
+
{ label: "Model", value: String(device?.model || "N/A") },
|
|
2515
|
+
{ label: "Hub ID", value: String(device?.hubDeviceId || "N/A") },
|
|
2516
|
+
{ label: "Battery", value: device?.battery !== void 0 && device?.battery !== null ? `${device.battery}%` : "N/A" },
|
|
2517
|
+
{ label: "Firmware", value: String(device?.version || device?.firmware || "N/A") },
|
|
2518
|
+
{ label: "Cloud Service", value: device?.enabled === false ? "Disabled" : "Enabled" },
|
|
2519
|
+
{ label: "Last Seen", value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) }
|
|
2520
|
+
];
|
|
2521
|
+
for (const row of rows) {
|
|
2522
|
+
const line = document.createElement("div");
|
|
2523
|
+
line.style.display = "flex";
|
|
2524
|
+
line.style.alignItems = "center";
|
|
2525
|
+
line.style.justifyContent = "space-between";
|
|
2526
|
+
line.style.gap = "8px";
|
|
2527
|
+
line.style.padding = "2px 0";
|
|
2528
|
+
const label = document.createElement("span");
|
|
2529
|
+
label.style.fontWeight = "600";
|
|
2530
|
+
label.style.minWidth = "110px";
|
|
2531
|
+
label.textContent = `${row.label}:`;
|
|
2532
|
+
const valueWrap = document.createElement("span");
|
|
2533
|
+
valueWrap.style.display = "inline-flex";
|
|
2534
|
+
valueWrap.style.alignItems = "center";
|
|
2535
|
+
valueWrap.style.gap = "6px";
|
|
2536
|
+
valueWrap.style.flex = "1";
|
|
2537
|
+
valueWrap.style.justifyContent = "flex-end";
|
|
2538
|
+
valueWrap.style.minWidth = "0";
|
|
2539
|
+
const value = document.createElement("span");
|
|
2540
|
+
value.style.fontFamily = "monospace";
|
|
2541
|
+
value.style.fontSize = "11px";
|
|
2542
|
+
value.style.opacity = "0.9";
|
|
2543
|
+
value.style.whiteSpace = "normal";
|
|
2544
|
+
value.style.overflowWrap = "anywhere";
|
|
2545
|
+
value.style.wordBreak = "break-word";
|
|
2546
|
+
value.style.textAlign = "right";
|
|
2547
|
+
value.textContent = row.value;
|
|
2548
|
+
valueWrap.appendChild(value);
|
|
2549
|
+
if (row.copyable && row.value && row.value !== "N/A") {
|
|
2550
|
+
const copyBtn = document.createElement("button");
|
|
2551
|
+
copyBtn.textContent = "\u{1F4CB}";
|
|
2552
|
+
copyBtn.title = `Copy ${row.label}`;
|
|
2553
|
+
copyBtn.style.padding = "2px 6px";
|
|
2554
|
+
copyBtn.style.fontSize = "10px";
|
|
2555
|
+
copyBtn.style.lineHeight = "1";
|
|
2556
|
+
copyBtn.style.background = "#e5e7eb";
|
|
2557
|
+
copyBtn.style.color = "#111827";
|
|
2558
|
+
copyBtn.onclick = async () => {
|
|
2559
|
+
try {
|
|
2560
|
+
await navigator.clipboard.writeText(row.value);
|
|
2561
|
+
copyBtn.textContent = "\u2713";
|
|
2562
|
+
setTimeout(() => {
|
|
2563
|
+
copyBtn.textContent = "\u{1F4CB}";
|
|
2564
|
+
}, 1200);
|
|
2565
|
+
} catch (_e) {
|
|
2566
|
+
copyBtn.textContent = "!";
|
|
2567
|
+
setTimeout(() => {
|
|
2568
|
+
copyBtn.textContent = "\u{1F4CB}";
|
|
2569
|
+
}, 1200);
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
valueWrap.appendChild(copyBtn);
|
|
2573
|
+
}
|
|
2574
|
+
line.appendChild(label);
|
|
2575
|
+
line.appendChild(valueWrap);
|
|
2576
|
+
details.appendChild(line);
|
|
2577
|
+
}
|
|
2578
|
+
const featureKeys = [
|
|
2579
|
+
"airQuality",
|
|
2580
|
+
"pm25",
|
|
2581
|
+
"pm10",
|
|
2582
|
+
"voc",
|
|
2583
|
+
"co2",
|
|
2584
|
+
"humidity",
|
|
2585
|
+
"temperature",
|
|
2586
|
+
"preset",
|
|
2587
|
+
"mode",
|
|
2588
|
+
"presetMode",
|
|
2589
|
+
"direction",
|
|
2590
|
+
"calibration",
|
|
2591
|
+
"multiCommand",
|
|
2592
|
+
"extendedInfo",
|
|
2593
|
+
"segmentedControl",
|
|
2594
|
+
"features",
|
|
2595
|
+
"capabilities",
|
|
2596
|
+
"state"
|
|
2597
|
+
];
|
|
2598
|
+
const shown = new Set(rows.map((r) => r.label.toLowerCase().replace(/\s/g, "")));
|
|
2599
|
+
for (const key of featureKeys) {
|
|
2600
|
+
if (device && device[key] !== void 0 && !shown.has(key.toLowerCase())) {
|
|
2601
|
+
const line = document.createElement("div");
|
|
2602
|
+
line.style.display = "flex";
|
|
2603
|
+
line.style.alignItems = "center";
|
|
2604
|
+
line.style.justifyContent = "space-between";
|
|
2605
|
+
line.style.gap = "8px";
|
|
2606
|
+
line.style.padding = "2px 0";
|
|
2607
|
+
const label = document.createElement("span");
|
|
2608
|
+
label.style.fontWeight = "600";
|
|
2609
|
+
label.style.minWidth = "110px";
|
|
2610
|
+
label.textContent = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()) + ":";
|
|
2611
|
+
const value = document.createElement("span");
|
|
2612
|
+
value.style.fontFamily = "monospace";
|
|
2613
|
+
value.style.fontSize = "11px";
|
|
2614
|
+
value.style.opacity = "0.9";
|
|
2615
|
+
value.style.whiteSpace = "normal";
|
|
2616
|
+
value.style.overflowWrap = "anywhere";
|
|
2617
|
+
value.style.wordBreak = "break-word";
|
|
2618
|
+
value.style.textAlign = "right";
|
|
2619
|
+
value.textContent = typeof device[key] === "object" ? JSON.stringify(device[key]) : String(device[key]);
|
|
2620
|
+
line.appendChild(label);
|
|
2621
|
+
line.appendChild(value);
|
|
2622
|
+
details.appendChild(line);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return details;
|
|
2626
|
+
}
|
|
1327
2627
|
async function renderDiscoveredDevices(devices, options = {}) {
|
|
1328
2628
|
const ul = document.createElement("ul");
|
|
1329
2629
|
ul.className = "device-grid";
|
|
@@ -1335,6 +2635,8 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1335
2635
|
const { addDeviceToConfig: addDeviceToConfig3 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
|
|
1336
2636
|
const { loadConfiguredDevices: loadConfiguredDevices2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
|
|
1337
2637
|
const configuredIds = options.configuredIds ?? /* @__PURE__ */ new Set();
|
|
2638
|
+
const selectedIds = options.selectedIds ?? /* @__PURE__ */ new Set();
|
|
2639
|
+
const onToggleSelect = options.onToggleSelect;
|
|
1338
2640
|
for (const d of devices) {
|
|
1339
2641
|
const deviceId = normalizeId2(d.id);
|
|
1340
2642
|
const alreadyAdded = configuredIds.has(deviceId);
|
|
@@ -1362,7 +2664,66 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1362
2664
|
name.style.fontWeight = "500";
|
|
1363
2665
|
name.style.fontSize = "13px";
|
|
1364
2666
|
name.textContent = d.name || d.id;
|
|
2667
|
+
const selectCheckbox = document.createElement("input");
|
|
2668
|
+
selectCheckbox.type = "checkbox";
|
|
2669
|
+
selectCheckbox.style.width = "auto";
|
|
2670
|
+
selectCheckbox.style.margin = "0 2px 0 0";
|
|
2671
|
+
selectCheckbox.checked = selectedIds.has(deviceId);
|
|
2672
|
+
if (alreadyAdded) {
|
|
2673
|
+
selectCheckbox.disabled = true;
|
|
2674
|
+
selectCheckbox.title = "Already configured";
|
|
2675
|
+
}
|
|
2676
|
+
selectCheckbox.onchange = () => {
|
|
2677
|
+
onToggleSelect?.(d, selectCheckbox.checked);
|
|
2678
|
+
window.dispatchEvent(new CustomEvent("discovery-selection-changed"));
|
|
2679
|
+
};
|
|
2680
|
+
nameContainer.appendChild(selectCheckbox);
|
|
1365
2681
|
nameContainer.appendChild(name);
|
|
2682
|
+
if (d.firmwareUpdateAvailable) {
|
|
2683
|
+
const fwBadge = document.createElement("span");
|
|
2684
|
+
fwBadge.textContent = "Update Available";
|
|
2685
|
+
fwBadge.style.cssText = "background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
|
|
2686
|
+
fwBadge.title = "A firmware update is available for this device.";
|
|
2687
|
+
nameContainer.appendChild(fwBadge);
|
|
2688
|
+
}
|
|
2689
|
+
let offline = false;
|
|
2690
|
+
let lastSeen = d.lastSeen || d.lastseen || d.updatedAt;
|
|
2691
|
+
if (typeof d.offline === "boolean") {
|
|
2692
|
+
offline = d.offline;
|
|
2693
|
+
} else if (lastSeen) {
|
|
2694
|
+
try {
|
|
2695
|
+
const last = new Date(lastSeen).getTime();
|
|
2696
|
+
if (!isNaN(last) && Date.now() - last > 1e3 * 60 * 60) {
|
|
2697
|
+
offline = true;
|
|
2698
|
+
}
|
|
2699
|
+
} catch {
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (offline) {
|
|
2703
|
+
const offlineBadge = document.createElement("span");
|
|
2704
|
+
offlineBadge.textContent = "Offline";
|
|
2705
|
+
offlineBadge.style.cssText = "background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
|
|
2706
|
+
offlineBadge.title = "Device is offline or unreachable.";
|
|
2707
|
+
nameContainer.appendChild(offlineBadge);
|
|
2708
|
+
}
|
|
2709
|
+
const expandedDetails = document.createElement("div");
|
|
2710
|
+
expandedDetails.style.display = "none";
|
|
2711
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d));
|
|
2712
|
+
const expandBtn = document.createElement("button");
|
|
2713
|
+
expandBtn.textContent = "\u25BE";
|
|
2714
|
+
expandBtn.title = "Show details";
|
|
2715
|
+
expandBtn.style.padding = "2px 6px";
|
|
2716
|
+
expandBtn.style.fontSize = "11px";
|
|
2717
|
+
expandBtn.style.marginLeft = "4px";
|
|
2718
|
+
expandBtn.style.background = "#e5e7eb";
|
|
2719
|
+
expandBtn.style.color = "#111827";
|
|
2720
|
+
expandBtn.style.transition = "transform 0.2s ease";
|
|
2721
|
+
expandBtn.onclick = () => {
|
|
2722
|
+
const isHidden = expandedDetails.style.display === "none";
|
|
2723
|
+
expandedDetails.style.display = isHidden ? "block" : "none";
|
|
2724
|
+
expandBtn.style.transform = isHidden ? "rotate(180deg)" : "rotate(0deg)";
|
|
2725
|
+
};
|
|
2726
|
+
nameContainer.appendChild(expandBtn);
|
|
1366
2727
|
const duplicateBadge = document.createElement("span");
|
|
1367
2728
|
duplicateBadge.textContent = alreadyAdded ? "\u2713 Already Added" : "\u2795 New Device";
|
|
1368
2729
|
duplicateBadge.style.cssText = alreadyAdded ? "background: #16a34a; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;" : "background: #2563eb; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;";
|
|
@@ -1380,6 +2741,13 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1380
2741
|
nameContainer.appendChild(renderSignalBars(d.rssi));
|
|
1381
2742
|
nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
|
|
1382
2743
|
}
|
|
2744
|
+
if (typeof d.battery === "number" && d.battery < 20) {
|
|
2745
|
+
const batteryWarn = document.createElement("span");
|
|
2746
|
+
batteryWarn.textContent = `\u26A0\uFE0F ${d.battery}%`;
|
|
2747
|
+
batteryWarn.style.cssText = d.battery < 10 ? "background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;" : "background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
|
|
2748
|
+
batteryWarn.title = d.battery < 10 ? "Battery critically low" : "Battery low";
|
|
2749
|
+
nameContainer.appendChild(batteryWarn);
|
|
2750
|
+
}
|
|
1383
2751
|
const details = document.createElement("div");
|
|
1384
2752
|
details.style.fontSize = "10px";
|
|
1385
2753
|
details.style.opacity = "0.7";
|
|
@@ -1398,6 +2766,7 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1398
2766
|
details.textContent = detailsText;
|
|
1399
2767
|
info.appendChild(nameContainer);
|
|
1400
2768
|
info.appendChild(details);
|
|
2769
|
+
info.appendChild(expandedDetails);
|
|
1401
2770
|
const addBtn = document.createElement("button");
|
|
1402
2771
|
addBtn.textContent = alreadyAdded ? "Already Added" : "Add to Config";
|
|
1403
2772
|
addBtn.style.marginLeft = "0";
|
|
@@ -1442,6 +2811,7 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1442
2811
|
actions2.style.gap = "5px";
|
|
1443
2812
|
actions2.appendChild(viewBtn);
|
|
1444
2813
|
actions2.appendChild(addBtn);
|
|
2814
|
+
actions2.appendChild(createConnectionTestControls(d));
|
|
1445
2815
|
li.appendChild(actions2);
|
|
1446
2816
|
ul.appendChild(li);
|
|
1447
2817
|
continue;
|
|
@@ -1456,6 +2826,7 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1456
2826
|
actions.style.marginTop = "2px";
|
|
1457
2827
|
actions.style.gap = "5px";
|
|
1458
2828
|
actions.appendChild(addBtn);
|
|
2829
|
+
actions.appendChild(createConnectionTestControls(d));
|
|
1459
2830
|
li.appendChild(info);
|
|
1460
2831
|
li.appendChild(actions);
|
|
1461
2832
|
ul.appendChild(li);
|
|
@@ -1596,6 +2967,23 @@ function renderDeviceList(list) {
|
|
|
1596
2967
|
name.style.fontWeight = "500";
|
|
1597
2968
|
name.style.fontSize = "13px";
|
|
1598
2969
|
name.textContent = d.name || d.id;
|
|
2970
|
+
const expandedDetails = document.createElement("div");
|
|
2971
|
+
expandedDetails.style.display = "none";
|
|
2972
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d));
|
|
2973
|
+
const expandBtn = document.createElement("button");
|
|
2974
|
+
expandBtn.textContent = "\u25BE";
|
|
2975
|
+
expandBtn.title = "Show details";
|
|
2976
|
+
expandBtn.style.padding = "2px 6px";
|
|
2977
|
+
expandBtn.style.fontSize = "11px";
|
|
2978
|
+
expandBtn.style.marginLeft = "4px";
|
|
2979
|
+
expandBtn.style.background = "#e5e7eb";
|
|
2980
|
+
expandBtn.style.color = "#111827";
|
|
2981
|
+
expandBtn.style.transition = "transform 0.2s ease";
|
|
2982
|
+
expandBtn.onclick = () => {
|
|
2983
|
+
const isHidden = expandedDetails.style.display === "none";
|
|
2984
|
+
expandedDetails.style.display = isHidden ? "block" : "none";
|
|
2985
|
+
expandBtn.style.transform = isHidden ? "rotate(180deg)" : "rotate(0deg)";
|
|
2986
|
+
};
|
|
1599
2987
|
const code = document.createElement("code");
|
|
1600
2988
|
code.textContent = d.id;
|
|
1601
2989
|
code.style.fontSize = "10px";
|
|
@@ -1605,7 +2993,13 @@ function renderDeviceList(list) {
|
|
|
1605
2993
|
code.style.overflowWrap = "anywhere";
|
|
1606
2994
|
code.style.wordBreak = "break-word";
|
|
1607
2995
|
code.style.maxWidth = "100%";
|
|
1608
|
-
|
|
2996
|
+
const headerRow = document.createElement("div");
|
|
2997
|
+
headerRow.style.display = "inline-flex";
|
|
2998
|
+
headerRow.style.alignItems = "center";
|
|
2999
|
+
headerRow.style.gap = "4px";
|
|
3000
|
+
headerRow.appendChild(name);
|
|
3001
|
+
headerRow.appendChild(expandBtn);
|
|
3002
|
+
nameContainer.appendChild(headerRow);
|
|
1609
3003
|
nameContainer.appendChild(code);
|
|
1610
3004
|
if (d.rssi !== void 0 && d.rssi !== null && d.rssi !== 0) {
|
|
1611
3005
|
nameContainer.appendChild(renderSignalBars(d.rssi));
|
|
@@ -1621,6 +3015,7 @@ function renderDeviceList(list) {
|
|
|
1621
3015
|
meta.textContent = [typeText, connText, roomText].filter(Boolean).join(" | ");
|
|
1622
3016
|
info.appendChild(nameContainer);
|
|
1623
3017
|
info.appendChild(meta);
|
|
3018
|
+
info.appendChild(expandedDetails);
|
|
1624
3019
|
const buttons = document.createElement("div");
|
|
1625
3020
|
buttons.className = "device-actions";
|
|
1626
3021
|
buttons.style.display = "flex";
|
|
@@ -1666,6 +3061,7 @@ function renderDeviceList(list) {
|
|
|
1666
3061
|
};
|
|
1667
3062
|
buttons.appendChild(editBtn);
|
|
1668
3063
|
buttons.appendChild(copyBtn);
|
|
3064
|
+
buttons.appendChild(createConnectionTestControls(d));
|
|
1669
3065
|
buttons.appendChild(deleteBtn);
|
|
1670
3066
|
li.appendChild(info);
|
|
1671
3067
|
li.appendChild(buttons);
|
|
@@ -1698,7 +3094,9 @@ async function addDeviceToConfig2(device, options = {}) {
|
|
|
1698
3094
|
const resp = await addDevice(device.id, importValues.configDeviceName, importValues.configDeviceType, {
|
|
1699
3095
|
address: importValues.address,
|
|
1700
3096
|
model: device.model,
|
|
1701
|
-
rssi: device.rssi
|
|
3097
|
+
rssi: device.rssi,
|
|
3098
|
+
encryptionKey: importValues.encryptionKey,
|
|
3099
|
+
keyId: importValues.keyId
|
|
1702
3100
|
});
|
|
1703
3101
|
uiLog.info("Add device response:", resp);
|
|
1704
3102
|
if (!resp || resp.success === false) {
|
|
@@ -1866,6 +3264,7 @@ window.saveCredentials = saveCredentials2;
|
|
|
1866
3264
|
window.discoverDevices = discoverDevices2;
|
|
1867
3265
|
async function init() {
|
|
1868
3266
|
await loadCredentialStatus();
|
|
3267
|
+
await initializeDiscoverySettings();
|
|
1869
3268
|
const list = await loadConfiguredDevices();
|
|
1870
3269
|
await initRemoveAllButton();
|
|
1871
3270
|
}
|