Add WLED auto-discovery via mDNS with zeroconf

Scan the local network for WLED devices advertising _wled._tcp.local.
and present them in the Add Device modal for one-click selection.

- New discovery.py: async mDNS browse + parallel /json/info enrichment
- GET /api/v1/devices/discover endpoint with already_added dedup
- Header scan button (magnifying glass icon) in add-device modal
- Discovered devices show name, IP, LED count, version; click to fill form
- en/ru locale strings for discovery UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 13:06:29 +03:00
parent b5a6885126
commit 638dc526f9
9 changed files with 378 additions and 1 deletions

View File

@@ -879,6 +879,16 @@ function showAddDevice() {
const error = document.getElementById('add-device-error');
form.reset();
error.style.display = 'none';
// Reset discovery section
const section = document.getElementById('discovery-section');
if (section) {
section.style.display = 'none';
document.getElementById('discovery-list').innerHTML = '';
document.getElementById('discovery-empty').style.display = 'none';
document.getElementById('discovery-loading').style.display = 'none';
}
const scanBtn = document.getElementById('scan-network-btn');
if (scanBtn) scanBtn.disabled = false;
modal.style.display = 'flex';
lockBody();
setTimeout(() => document.getElementById('device-name').focus(), 100);
@@ -890,6 +900,79 @@ function closeAddDeviceModal() {
unlockBody();
}
async function scanForDevices() {
const loading = document.getElementById('discovery-loading');
const list = document.getElementById('discovery-list');
const empty = document.getElementById('discovery-empty');
const section = document.getElementById('discovery-section');
const scanBtn = document.getElementById('scan-network-btn');
section.style.display = 'block';
loading.style.display = 'flex';
list.innerHTML = '';
empty.style.display = 'none';
if (scanBtn) scanBtn.disabled = true;
try {
const response = await fetch(`${API_BASE}/devices/discover?timeout=3`, {
headers: getHeaders()
});
if (response.status === 401) { handle401Error(); return; }
loading.style.display = 'none';
if (scanBtn) scanBtn.disabled = false;
if (!response.ok) {
empty.style.display = 'block';
empty.querySelector('small').textContent = t('device.scan.error');
return;
}
const data = await response.json();
if (data.devices.length === 0) {
empty.style.display = 'block';
return;
}
data.devices.forEach(device => {
const card = document.createElement('div');
card.className = 'discovery-item' + (device.already_added ? ' discovery-item--added' : '');
const meta = [device.ip];
if (device.led_count) meta.push(device.led_count + ' LEDs');
if (device.version) meta.push('v' + device.version);
card.innerHTML = `
<div class="discovery-item-info">
<strong>${escapeHtml(device.name)}</strong>
<small>${escapeHtml(meta.join(' \u00b7 '))}</small>
</div>
${device.already_added
? '<span class="discovery-badge">' + t('device.scan.already_added') + '</span>'
: ''}
`;
if (!device.already_added) {
card.onclick = () => selectDiscoveredDevice(device);
}
list.appendChild(card);
});
} catch (err) {
loading.style.display = 'none';
if (scanBtn) scanBtn.disabled = false;
empty.style.display = 'block';
empty.querySelector('small').textContent = t('device.scan.error');
console.error('Device scan failed:', err);
}
}
function selectDiscoveredDevice(device) {
document.getElementById('device-name').value = device.name;
document.getElementById('device-url').value = device.url;
const typeSelect = document.getElementById('device-type');
if (typeSelect) typeSelect.value = device.device_type;
showToast(t('device.scan.selected'), 'info');
}
async function handleAddDevice(event) {
event.preventDefault();