@switchbot/homebridge-switchbot 5.0.0-beta.111 → 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 +209 -16
- package/dist/homebridge-ui/public/index.html +87 -56
- 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 +1604 -143
- 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 +1244 -124
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1344 -142
- 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 +389 -36
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +421 -38
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +3 -4
- 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 +9 -8
- 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 +209 -16
- package/src/homebridge-ui/public/index.html +87 -56
- 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 +1344 -142
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +421 -38
- package/src/platform.ts +3 -4
- package/src/switchbotClient.ts +44 -9
- package/test/e2e/run-e2e.spec.ts +12 -10
- package/test/helpers/matter-harness.ts +2 -2
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +313 -0
- package/todo/TODO.md +80 -66
- package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
|
@@ -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,44 +914,644 @@ 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);
|
|
1052
|
+
let deviceListContainer = document.getElementById("discoveredDevices");
|
|
1053
|
+
if (!deviceListContainer) {
|
|
1054
|
+
deviceListContainer = document.createElement("ul");
|
|
1055
|
+
deviceListContainer.id = "discoveredDevices";
|
|
1056
|
+
deviceListContainer.style.maxHeight = "400px";
|
|
1057
|
+
deviceListContainer.style.overflowY = "auto";
|
|
1058
|
+
deviceListContainer.style.marginTop = "12px";
|
|
1059
|
+
deviceListContainer.style.padding = "0";
|
|
1060
|
+
deviceListContainer.style.listStyle = "none";
|
|
1061
|
+
list.appendChild(deviceListContainer);
|
|
1062
|
+
}
|
|
466
1063
|
list.style.display = "block";
|
|
467
|
-
|
|
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();
|
|
468
1217
|
} catch (e) {
|
|
1218
|
+
if (cancelled) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
469
1221
|
uiLog.error("Discovery error:", e);
|
|
470
1222
|
status.textContent = `Error: ${e instanceof Error ? e.message : "Discovery failed"}`;
|
|
471
1223
|
status.classList.add("error");
|
|
472
1224
|
toastError(e instanceof Error ? e.message : "Discovery failed");
|
|
473
1225
|
list.style.display = "none";
|
|
474
1226
|
} finally {
|
|
1227
|
+
clearInterval(progressTimer);
|
|
475
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
|
+
}
|
|
476
1243
|
btn.disabled = false;
|
|
477
1244
|
btn.textContent = "\u{1F50D} Discover Devices";
|
|
1245
|
+
if (cancelBtn) {
|
|
1246
|
+
cancelBtn.disabled = false;
|
|
1247
|
+
cancelBtn.style.display = "none";
|
|
1248
|
+
cancelBtn.onclick = null;
|
|
1249
|
+
}
|
|
478
1250
|
}
|
|
479
1251
|
}
|
|
480
|
-
async function updateDiscoveryView(allDevices, preferences) {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
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;
|
|
487
1258
|
});
|
|
488
|
-
|
|
489
|
-
|
|
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
|
+
}
|
|
1365
|
+
const existingList = document.getElementById("discoveredDevices");
|
|
1366
|
+
if (existingList && existingList.parentNode) {
|
|
1367
|
+
existingList.replaceWith(container);
|
|
1368
|
+
} else {
|
|
1369
|
+
const listContainer = document.getElementById("discoveredList");
|
|
1370
|
+
if (listContainer) {
|
|
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;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
490
1551
|
const status = document.getElementById("discoverStatus");
|
|
491
1552
|
if (status) {
|
|
492
1553
|
const totalCount = allDevices.length;
|
|
493
|
-
const filteredCount =
|
|
1554
|
+
const filteredCount = visibleDevices.length;
|
|
494
1555
|
status.textContent = filteredCount === totalCount ? `Found ${totalCount} device(s)` : `Showing ${filteredCount} of ${totalCount} device(s)`;
|
|
495
1556
|
}
|
|
496
1557
|
}
|
|
@@ -498,6 +1559,7 @@ async function addDeviceToConfig(device) {
|
|
|
498
1559
|
const { addDeviceToConfig: addDevice2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
|
|
499
1560
|
await addDevice2(device);
|
|
500
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;
|
|
501
1563
|
var init_discovery = __esm({
|
|
502
1564
|
"src/homebridge-ui/public/js/discovery.ts"() {
|
|
503
1565
|
"use strict";
|
|
@@ -507,6 +1569,15 @@ var init_discovery = __esm({
|
|
|
507
1569
|
init_modal();
|
|
508
1570
|
init_render();
|
|
509
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;
|
|
510
1581
|
}
|
|
511
1582
|
});
|
|
512
1583
|
|
|
@@ -724,6 +1795,40 @@ async function importDiscoveredDevice(device) {
|
|
|
724
1795
|
macInput.style.borderRadius = "6px";
|
|
725
1796
|
macInput.style.fontSize = "14px";
|
|
726
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";
|
|
727
1832
|
const buttons = document.createElement("div");
|
|
728
1833
|
buttons.style.display = "flex";
|
|
729
1834
|
buttons.style.gap = "10px";
|
|
@@ -750,7 +1855,9 @@ async function importDiscoveredDevice(device) {
|
|
|
750
1855
|
importBtn.onclick = () => cleanup({
|
|
751
1856
|
configDeviceName: nameInput.value || device.name || device.id,
|
|
752
1857
|
configDeviceType: typeSelect.value || device.type,
|
|
753
|
-
address: macInput.value || void 0
|
|
1858
|
+
address: macInput.value || void 0,
|
|
1859
|
+
encryptionKey: encryptionKeyInput.value || void 0,
|
|
1860
|
+
keyId: keyIdInput.value || void 0
|
|
754
1861
|
});
|
|
755
1862
|
div.addEventListener("click", (event) => {
|
|
756
1863
|
if (event.target === div) {
|
|
@@ -765,6 +1872,10 @@ async function importDiscoveredDevice(device) {
|
|
|
765
1872
|
contentDiv.appendChild(typeSelect);
|
|
766
1873
|
contentDiv.appendChild(macLabel);
|
|
767
1874
|
contentDiv.appendChild(macInput);
|
|
1875
|
+
contentDiv.appendChild(encryptionKeyLabel);
|
|
1876
|
+
contentDiv.appendChild(encryptionKeyInput);
|
|
1877
|
+
contentDiv.appendChild(keyIdLabel);
|
|
1878
|
+
contentDiv.appendChild(keyIdInput);
|
|
768
1879
|
contentDiv.appendChild(buttons);
|
|
769
1880
|
modal.appendChild(title);
|
|
770
1881
|
modal.appendChild(contentDiv);
|
|
@@ -1167,6 +2278,7 @@ __export(render_exports, {
|
|
|
1167
2278
|
getRssiSignalQuality: () => getRssiSignalQuality,
|
|
1168
2279
|
renderBadge: () => renderBadge,
|
|
1169
2280
|
renderConnectionBadge: () => renderConnectionBadge,
|
|
2281
|
+
renderDeviceDetailsPanel: () => renderDeviceDetailsPanel,
|
|
1170
2282
|
renderDeviceList: () => renderDeviceList,
|
|
1171
2283
|
renderDiscoveredDevices: () => renderDiscoveredDevices,
|
|
1172
2284
|
renderIRBadge: () => renderIRBadge,
|
|
@@ -1305,8 +2417,216 @@ function scrollToConfiguredDevice(deviceId) {
|
|
|
1305
2417
|
target.style.background = originalBackground;
|
|
1306
2418
|
}, 1800);
|
|
1307
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
|
+
}
|
|
1308
2627
|
async function renderDiscoveredDevices(devices, options = {}) {
|
|
1309
2628
|
const ul = document.createElement("ul");
|
|
2629
|
+
ul.className = "device-grid";
|
|
1310
2630
|
ul.style.maxHeight = "400px";
|
|
1311
2631
|
ul.style.overflowY = "auto";
|
|
1312
2632
|
ul.style.marginTop = "12px";
|
|
@@ -1315,34 +2635,98 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1315
2635
|
const { addDeviceToConfig: addDeviceToConfig3 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
|
|
1316
2636
|
const { loadConfiguredDevices: loadConfiguredDevices2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
|
|
1317
2637
|
const configuredIds = options.configuredIds ?? /* @__PURE__ */ new Set();
|
|
2638
|
+
const selectedIds = options.selectedIds ?? /* @__PURE__ */ new Set();
|
|
2639
|
+
const onToggleSelect = options.onToggleSelect;
|
|
1318
2640
|
for (const d of devices) {
|
|
1319
2641
|
const deviceId = normalizeId2(d.id);
|
|
1320
2642
|
const alreadyAdded = configuredIds.has(deviceId);
|
|
1321
2643
|
const li = document.createElement("li");
|
|
1322
2644
|
li.className = "device-item";
|
|
1323
2645
|
li.style.display = "flex";
|
|
1324
|
-
li.style.
|
|
1325
|
-
li.style.
|
|
1326
|
-
li.style.
|
|
1327
|
-
li.style.
|
|
1328
|
-
li.style.
|
|
2646
|
+
li.style.flexDirection = "column";
|
|
2647
|
+
li.style.alignItems = "stretch";
|
|
2648
|
+
li.style.justifyContent = "flex-start";
|
|
2649
|
+
li.style.padding = "5px 8px";
|
|
2650
|
+
li.style.marginBottom = "0";
|
|
2651
|
+
li.style.borderRadius = "5px";
|
|
1329
2652
|
li.style.transition = "all 0.2s ease";
|
|
1330
2653
|
const info = document.createElement("div");
|
|
1331
|
-
info.style.flex = "1";
|
|
2654
|
+
info.style.flex = "1 1 auto";
|
|
2655
|
+
info.style.width = "100%";
|
|
2656
|
+
info.style.minWidth = "0";
|
|
1332
2657
|
const nameContainer = document.createElement("div");
|
|
1333
2658
|
nameContainer.style.display = "flex";
|
|
1334
2659
|
nameContainer.style.alignItems = "center";
|
|
1335
|
-
nameContainer.style.marginBottom = "
|
|
2660
|
+
nameContainer.style.marginBottom = "0";
|
|
1336
2661
|
nameContainer.style.flexWrap = "wrap";
|
|
1337
|
-
nameContainer.style.gap = "
|
|
2662
|
+
nameContainer.style.gap = "4px";
|
|
1338
2663
|
const name = document.createElement("div");
|
|
1339
2664
|
name.style.fontWeight = "500";
|
|
1340
|
-
name.style.fontSize = "
|
|
2665
|
+
name.style.fontSize = "13px";
|
|
1341
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);
|
|
1342
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);
|
|
1343
2727
|
const duplicateBadge = document.createElement("span");
|
|
1344
2728
|
duplicateBadge.textContent = alreadyAdded ? "\u2713 Already Added" : "\u2795 New Device";
|
|
1345
|
-
duplicateBadge.style.cssText = alreadyAdded ? "background: #16a34a; color: white; padding:
|
|
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;";
|
|
1346
2730
|
nameContainer.appendChild(duplicateBadge);
|
|
1347
2731
|
if (d.connectionType) {
|
|
1348
2732
|
const badge = renderConnectionBadge(d.connectionType);
|
|
@@ -1357,11 +2741,21 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1357
2741
|
nameContainer.appendChild(renderSignalBars(d.rssi));
|
|
1358
2742
|
nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
|
|
1359
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
|
+
}
|
|
1360
2751
|
const details = document.createElement("div");
|
|
1361
|
-
details.style.fontSize = "
|
|
2752
|
+
details.style.fontSize = "10px";
|
|
1362
2753
|
details.style.opacity = "0.7";
|
|
1363
|
-
details.style.marginTop = "
|
|
2754
|
+
details.style.marginTop = "0";
|
|
1364
2755
|
details.style.fontFamily = "monospace";
|
|
2756
|
+
details.style.whiteSpace = "normal";
|
|
2757
|
+
details.style.overflowWrap = "anywhere";
|
|
2758
|
+
details.style.wordBreak = "break-word";
|
|
1365
2759
|
let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || "N/A"}`;
|
|
1366
2760
|
if (d.hubDeviceId) {
|
|
1367
2761
|
detailsText += ` | Hub: ${d.hubDeviceId}`;
|
|
@@ -1372,11 +2766,13 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1372
2766
|
details.textContent = detailsText;
|
|
1373
2767
|
info.appendChild(nameContainer);
|
|
1374
2768
|
info.appendChild(details);
|
|
2769
|
+
info.appendChild(expandedDetails);
|
|
1375
2770
|
const addBtn = document.createElement("button");
|
|
1376
2771
|
addBtn.textContent = alreadyAdded ? "Already Added" : "Add to Config";
|
|
1377
|
-
addBtn.style.marginLeft = "
|
|
1378
|
-
addBtn.style.
|
|
1379
|
-
addBtn.style.
|
|
2772
|
+
addBtn.style.marginLeft = "0";
|
|
2773
|
+
addBtn.style.marginTop = "2px";
|
|
2774
|
+
addBtn.style.padding = "4px 9px";
|
|
2775
|
+
addBtn.style.fontSize = "11px";
|
|
1380
2776
|
addBtn.style.whiteSpace = "nowrap";
|
|
1381
2777
|
addBtn.style.flexShrink = "0";
|
|
1382
2778
|
addBtn.disabled = alreadyAdded;
|
|
@@ -1395,25 +2791,44 @@ async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
1395
2791
|
const viewBtn = document.createElement("button");
|
|
1396
2792
|
viewBtn.textContent = "View in Config";
|
|
1397
2793
|
viewBtn.className = "secondary";
|
|
1398
|
-
viewBtn.style.marginLeft = "
|
|
1399
|
-
viewBtn.style.padding = "
|
|
1400
|
-
viewBtn.style.fontSize = "
|
|
2794
|
+
viewBtn.style.marginLeft = "0";
|
|
2795
|
+
viewBtn.style.padding = "4px 9px";
|
|
2796
|
+
viewBtn.style.fontSize = "11px";
|
|
1401
2797
|
viewBtn.onclick = async () => {
|
|
1402
2798
|
await loadConfiguredDevices2();
|
|
1403
2799
|
scrollToConfiguredDevice(d.id);
|
|
1404
2800
|
};
|
|
1405
2801
|
li.appendChild(info);
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
2802
|
+
const actions2 = document.createElement("div");
|
|
2803
|
+
actions2.className = "device-actions";
|
|
2804
|
+
actions2.style.display = "flex";
|
|
2805
|
+
actions2.style.alignItems = "center";
|
|
2806
|
+
actions2.style.flexWrap = "wrap";
|
|
2807
|
+
actions2.style.justifyContent = "flex-start";
|
|
2808
|
+
actions2.style.marginLeft = "0";
|
|
2809
|
+
actions2.style.width = "100%";
|
|
2810
|
+
actions2.style.marginTop = "2px";
|
|
2811
|
+
actions2.style.gap = "5px";
|
|
2812
|
+
actions2.appendChild(viewBtn);
|
|
2813
|
+
actions2.appendChild(addBtn);
|
|
2814
|
+
actions2.appendChild(createConnectionTestControls(d));
|
|
2815
|
+
li.appendChild(actions2);
|
|
1412
2816
|
ul.appendChild(li);
|
|
1413
2817
|
continue;
|
|
1414
2818
|
}
|
|
2819
|
+
const actions = document.createElement("div");
|
|
2820
|
+
actions.className = "device-actions";
|
|
2821
|
+
actions.style.display = "flex";
|
|
2822
|
+
actions.style.flexWrap = "wrap";
|
|
2823
|
+
actions.style.justifyContent = "flex-start";
|
|
2824
|
+
actions.style.marginLeft = "0";
|
|
2825
|
+
actions.style.width = "100%";
|
|
2826
|
+
actions.style.marginTop = "2px";
|
|
2827
|
+
actions.style.gap = "5px";
|
|
2828
|
+
actions.appendChild(addBtn);
|
|
2829
|
+
actions.appendChild(createConnectionTestControls(d));
|
|
1415
2830
|
li.appendChild(info);
|
|
1416
|
-
li.appendChild(
|
|
2831
|
+
li.appendChild(actions);
|
|
1417
2832
|
ul.appendChild(li);
|
|
1418
2833
|
}
|
|
1419
2834
|
return ul;
|
|
@@ -1526,6 +2941,7 @@ function renderDeviceList(list) {
|
|
|
1526
2941
|
return;
|
|
1527
2942
|
}
|
|
1528
2943
|
status.textContent = `Found ${list.length} device(s)`;
|
|
2944
|
+
ul.classList.add("device-grid");
|
|
1529
2945
|
ul.innerHTML = "";
|
|
1530
2946
|
if (removeAllContainer) {
|
|
1531
2947
|
removeAllContainer.style.display = "block";
|
|
@@ -1534,22 +2950,56 @@ function renderDeviceList(list) {
|
|
|
1534
2950
|
const li = document.createElement("li");
|
|
1535
2951
|
li.className = "device-item";
|
|
1536
2952
|
li.setAttribute("data-device-id", normalizeId2(d.id));
|
|
2953
|
+
li.style.display = "flex";
|
|
2954
|
+
li.style.flexDirection = "column";
|
|
2955
|
+
li.style.alignItems = "stretch";
|
|
2956
|
+
li.style.marginBottom = "0";
|
|
1537
2957
|
const info = document.createElement("div");
|
|
1538
|
-
info.style.flex = "1";
|
|
2958
|
+
info.style.flex = "1 1 auto";
|
|
2959
|
+
info.style.width = "100%";
|
|
2960
|
+
info.style.minWidth = "0";
|
|
1539
2961
|
const nameContainer = document.createElement("div");
|
|
1540
2962
|
nameContainer.style.display = "flex";
|
|
1541
|
-
nameContainer.style.
|
|
1542
|
-
nameContainer.style.
|
|
1543
|
-
nameContainer.style.gap = "
|
|
2963
|
+
nameContainer.style.flexDirection = "column";
|
|
2964
|
+
nameContainer.style.alignItems = "flex-start";
|
|
2965
|
+
nameContainer.style.gap = "0";
|
|
1544
2966
|
const name = document.createElement("div");
|
|
1545
2967
|
name.style.fontWeight = "500";
|
|
2968
|
+
name.style.fontSize = "13px";
|
|
1546
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
|
+
};
|
|
1547
2987
|
const code = document.createElement("code");
|
|
1548
2988
|
code.textContent = d.id;
|
|
1549
|
-
code.style.fontSize = "
|
|
2989
|
+
code.style.fontSize = "10px";
|
|
1550
2990
|
code.style.opacity = "0.75";
|
|
1551
|
-
code.style.marginLeft = "
|
|
1552
|
-
|
|
2991
|
+
code.style.marginLeft = "0";
|
|
2992
|
+
code.style.whiteSpace = "normal";
|
|
2993
|
+
code.style.overflowWrap = "anywhere";
|
|
2994
|
+
code.style.wordBreak = "break-word";
|
|
2995
|
+
code.style.maxWidth = "100%";
|
|
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);
|
|
1553
3003
|
nameContainer.appendChild(code);
|
|
1554
3004
|
if (d.rssi !== void 0 && d.rssi !== null && d.rssi !== 0) {
|
|
1555
3005
|
nameContainer.appendChild(renderSignalBars(d.rssi));
|
|
@@ -1557,29 +3007,36 @@ function renderDeviceList(list) {
|
|
|
1557
3007
|
}
|
|
1558
3008
|
const meta = document.createElement("div");
|
|
1559
3009
|
meta.style.opacity = "0.75";
|
|
1560
|
-
meta.style.marginTop = "
|
|
1561
|
-
meta.style.fontSize = "
|
|
3010
|
+
meta.style.marginTop = "0";
|
|
3011
|
+
meta.style.fontSize = "11px";
|
|
1562
3012
|
const typeText = d.type ? `type: ${d.type}` : "";
|
|
1563
3013
|
const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : "";
|
|
1564
3014
|
const roomText = d.room ? `room: ${d.room}` : "";
|
|
1565
3015
|
meta.textContent = [typeText, connText, roomText].filter(Boolean).join(" | ");
|
|
1566
3016
|
info.appendChild(nameContainer);
|
|
1567
3017
|
info.appendChild(meta);
|
|
3018
|
+
info.appendChild(expandedDetails);
|
|
1568
3019
|
const buttons = document.createElement("div");
|
|
3020
|
+
buttons.className = "device-actions";
|
|
1569
3021
|
buttons.style.display = "flex";
|
|
1570
|
-
buttons.style.
|
|
3022
|
+
buttons.style.flexWrap = "wrap";
|
|
3023
|
+
buttons.style.justifyContent = "flex-start";
|
|
3024
|
+
buttons.style.marginLeft = "0";
|
|
3025
|
+
buttons.style.width = "100%";
|
|
3026
|
+
buttons.style.marginTop = "2px";
|
|
3027
|
+
buttons.style.gap = "5px";
|
|
1571
3028
|
const editBtn = document.createElement("button");
|
|
1572
3029
|
editBtn.textContent = "\u270F\uFE0F Edit";
|
|
1573
|
-
editBtn.style.padding = "
|
|
1574
|
-
editBtn.style.fontSize = "
|
|
3030
|
+
editBtn.style.padding = "4px 9px";
|
|
3031
|
+
editBtn.style.fontSize = "11px";
|
|
1575
3032
|
editBtn.onclick = async () => {
|
|
1576
3033
|
const { editDevice: editDevice2 } = await Promise.resolve().then(() => (init_modals(), modals_exports));
|
|
1577
3034
|
await editDevice2(d);
|
|
1578
3035
|
};
|
|
1579
3036
|
const copyBtn = document.createElement("button");
|
|
1580
3037
|
copyBtn.textContent = "Copy ID";
|
|
1581
|
-
copyBtn.style.padding = "
|
|
1582
|
-
copyBtn.style.fontSize = "
|
|
3038
|
+
copyBtn.style.padding = "4px 9px";
|
|
3039
|
+
copyBtn.style.fontSize = "11px";
|
|
1583
3040
|
copyBtn.addEventListener("click", async () => {
|
|
1584
3041
|
try {
|
|
1585
3042
|
await navigator.clipboard.writeText(d.id);
|
|
@@ -1595,8 +3052,8 @@ function renderDeviceList(list) {
|
|
|
1595
3052
|
});
|
|
1596
3053
|
const deleteBtn = document.createElement("button");
|
|
1597
3054
|
deleteBtn.textContent = "\u{1F5D1}\uFE0F Delete";
|
|
1598
|
-
deleteBtn.style.padding = "
|
|
1599
|
-
deleteBtn.style.fontSize = "
|
|
3055
|
+
deleteBtn.style.padding = "4px 9px";
|
|
3056
|
+
deleteBtn.style.fontSize = "11px";
|
|
1600
3057
|
deleteBtn.style.background = "#ef4444";
|
|
1601
3058
|
deleteBtn.onclick = async () => {
|
|
1602
3059
|
const { deleteDeviceFromConfig: deleteDeviceFromConfig2 } = await Promise.resolve().then(() => (init_devices_delete(), devices_delete_exports));
|
|
@@ -1604,6 +3061,7 @@ function renderDeviceList(list) {
|
|
|
1604
3061
|
};
|
|
1605
3062
|
buttons.appendChild(editBtn);
|
|
1606
3063
|
buttons.appendChild(copyBtn);
|
|
3064
|
+
buttons.appendChild(createConnectionTestControls(d));
|
|
1607
3065
|
buttons.appendChild(deleteBtn);
|
|
1608
3066
|
li.appendChild(info);
|
|
1609
3067
|
li.appendChild(buttons);
|
|
@@ -1636,7 +3094,9 @@ async function addDeviceToConfig2(device, options = {}) {
|
|
|
1636
3094
|
const resp = await addDevice(device.id, importValues.configDeviceName, importValues.configDeviceType, {
|
|
1637
3095
|
address: importValues.address,
|
|
1638
3096
|
model: device.model,
|
|
1639
|
-
rssi: device.rssi
|
|
3097
|
+
rssi: device.rssi,
|
|
3098
|
+
encryptionKey: importValues.encryptionKey,
|
|
3099
|
+
keyId: importValues.keyId
|
|
1640
3100
|
});
|
|
1641
3101
|
uiLog.info("Add device response:", resp);
|
|
1642
3102
|
if (!resp || resp.success === false) {
|
|
@@ -1804,6 +3264,7 @@ window.saveCredentials = saveCredentials2;
|
|
|
1804
3264
|
window.discoverDevices = discoverDevices2;
|
|
1805
3265
|
async function init() {
|
|
1806
3266
|
await loadCredentialStatus();
|
|
3267
|
+
await initializeDiscoverySettings();
|
|
1807
3268
|
const list = await loadConfiguredDevices();
|
|
1808
3269
|
await initRemoveAllButton();
|
|
1809
3270
|
}
|