diff --git a/server/src/wled_controller/api/routes.py b/server/src/wled_controller/api/routes.py index 05347c8..9ecc691 100644 --- a/server/src/wled_controller/api/routes.py +++ b/server/src/wled_controller/api/routes.py @@ -667,6 +667,60 @@ async def update_settings( raise HTTPException(status_code=500, detail=str(e)) +# ===== WLED BRIGHTNESS ENDPOINT ===== + +@router.get("/api/v1/devices/{device_id}/brightness", tags=["Settings"]) +async def get_device_brightness( + device_id: str, + _auth: AuthRequired, + store: DeviceStore = Depends(get_device_store), +): + """Get current brightness from the WLED device.""" + device = store.get_device(device_id) + if not device: + raise HTTPException(status_code=404, detail=f"Device {device_id} not found") + + try: + async with httpx.AsyncClient(timeout=5.0) as http_client: + resp = await http_client.get(f"{device.url}/json/state") + resp.raise_for_status() + state = resp.json() + bri = state.get("bri", 255) + return {"brightness": bri} + except Exception as e: + logger.error(f"Failed to get WLED brightness for {device_id}: {e}") + raise HTTPException(status_code=502, detail=f"Failed to reach WLED device: {e}") + + +@router.put("/api/v1/devices/{device_id}/brightness", tags=["Settings"]) +async def set_device_brightness( + device_id: str, + body: dict, + _auth: AuthRequired, + store: DeviceStore = Depends(get_device_store), +): + """Set brightness on the WLED device directly.""" + device = store.get_device(device_id) + if not device: + raise HTTPException(status_code=404, detail=f"Device {device_id} not found") + + bri = body.get("brightness") + if bri is None or not isinstance(bri, int) or not 0 <= bri <= 255: + raise HTTPException(status_code=400, detail="brightness must be an integer 0-255") + + try: + async with httpx.AsyncClient(timeout=5.0) as http_client: + resp = await http_client.post( + f"{device.url}/json/state", + json={"bri": bri}, + ) + resp.raise_for_status() + return {"brightness": bri} + except Exception as e: + logger.error(f"Failed to set WLED brightness for {device_id}: {e}") + raise HTTPException(status_code=502, detail=f"Failed to reach WLED device: {e}") + + # ===== CALIBRATION ENDPOINTS ===== @router.get("/api/v1/devices/{device_id}/calibration", response_model=CalibrationSchema, tags=["Calibration"]) diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index 8f71376..8d8f05b 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -219,9 +219,40 @@ function formatDisplayLabel(displayIndex, display) { return `Display ${displayIndex}`; } +let _streamNameManuallyEdited = false; + function onStreamDisplaySelected(displayIndex, display) { document.getElementById('stream-display-index').value = displayIndex; document.getElementById('stream-display-picker-label').textContent = formatDisplayLabel(displayIndex, display); + _autoGenerateStreamName(); +} + +let _streamModalPPTemplates = []; + +function _autoGenerateStreamName() { + if (_streamNameManuallyEdited) return; + if (document.getElementById('stream-id').value) return; // editing, not creating + const streamType = document.getElementById('stream-type').value; + const nameInput = document.getElementById('stream-name'); + + if (streamType === 'raw') { + const displayIndex = document.getElementById('stream-display-index').value; + const templateSelect = document.getElementById('stream-capture-template'); + const templateName = templateSelect.selectedOptions[0]?.dataset?.name || ''; + if (displayIndex === '' || !templateName) return; + nameInput.value = `D${displayIndex}_${templateName}`; + } else if (streamType === 'processed') { + const sourceSelect = document.getElementById('stream-source'); + const sourceName = sourceSelect.selectedOptions[0]?.dataset?.name || ''; + const ppTemplateId = document.getElementById('stream-pp-template').value; + const ppTemplate = _streamModalPPTemplates.find(t => t.id === ppTemplateId); + if (!sourceName) return; + if (ppTemplate && ppTemplate.name) { + nameInput.value = `${sourceName} (${ppTemplate.name})`; + } else { + nameInput.value = sourceName; + } + } } function onTestDisplaySelected(displayIndex, display) { @@ -352,6 +383,7 @@ function updateAllText() { if (apiKey) { loadDisplays(); loadDevices(); + loadPictureStreams(); } } @@ -644,9 +676,10 @@ async function loadDevices() { webuiLink.rel = 'noopener'; } - // Attach event listeners + // Attach event listeners and fetch real WLED brightness devicesWithState.forEach(device => { attachDeviceListeners(device.id); + fetchDeviceBrightness(device.id); }); } catch (error) { console.error('Failed to load devices:', error); @@ -661,7 +694,6 @@ function createDeviceCard(device) { const settings = device.settings || {}; const isProcessing = state.processing || false; - const brightnessPercent = Math.round((settings.brightness !== undefined ? settings.brightness : 1.0) * 100); const statusKey = isProcessing ? 'device.status.processing' : 'device.status.idle'; const status = isProcessing ? 'processing' : 'idle'; @@ -733,11 +765,11 @@ function createDeviceCard(device) { ` : ''}
- + title="${_deviceBrightnessCache[device.id] != null ? Math.round(_deviceBrightnessCache[device.id] / 255 * 100) + '%' : '...'}">
${isProcessing ? ` @@ -1163,19 +1195,23 @@ async function saveCaptureSettings() { } } -// Card brightness controls +// Brightness cache: stores last known WLED brightness per device (0-255) +const _deviceBrightnessCache = {}; + +// Card brightness controls — talks directly to WLED device function updateBrightnessLabel(deviceId, value) { - const slider = document.querySelector(`[data-device-id="${deviceId}"] .brightness-slider`); - if (slider) slider.title = value + '%'; + const slider = document.querySelector(`[data-device-brightness="${deviceId}"]`); + if (slider) slider.title = Math.round(parseInt(value) / 255 * 100) + '%'; } async function saveCardBrightness(deviceId, value) { - const brightness = parseInt(value) / 100.0; + const bri = parseInt(value); + _deviceBrightnessCache[deviceId] = bri; try { - await fetch(`${API_BASE}/devices/${deviceId}/settings`, { + await fetch(`${API_BASE}/devices/${deviceId}/brightness`, { method: 'PUT', headers: getHeaders(), - body: JSON.stringify({ brightness }) + body: JSON.stringify({ brightness: bri }) }); } catch (err) { console.error('Failed to update brightness:', err); @@ -1183,6 +1219,24 @@ async function saveCardBrightness(deviceId, value) { } } +async function fetchDeviceBrightness(deviceId) { + try { + const resp = await fetch(`${API_BASE}/devices/${deviceId}/brightness`, { + headers: getHeaders() + }); + if (!resp.ok) return; + const data = await resp.json(); + _deviceBrightnessCache[deviceId] = data.brightness; + const slider = document.querySelector(`[data-device-brightness="${deviceId}"]`); + if (slider) { + slider.value = data.brightness; + slider.title = Math.round(data.brightness / 255 * 100) + '%'; + } + } catch (err) { + // Silently fail — device may be offline + } +} + // Add device modal function showAddDevice() { const modal = document.getElementById('add-device-modal'); @@ -2225,7 +2279,7 @@ const calibrationTutorialSteps = [ { selector: '.offset-control', textKey: 'calibration.tip.offset', position: 'bottom' }, { selector: '.edge-span-bar[data-edge="top"]', textKey: 'calibration.tip.span', position: 'bottom' }, { selector: '.toggle-top', textKey: 'calibration.tip.test', position: 'top' }, - { selector: '.preview-screen-total', textKey: 'calibration.tip.toggle_inputs', position: 'bottom' } + { selector: '.preview-screen-total', textKey: 'calibration.tip.toggle_inputs', position: 'top' } ]; const deviceTutorialSteps = [ @@ -2467,6 +2521,8 @@ function getEngineIcon(engineType) { } // Show add template modal +let _templateNameManuallyEdited = false; + async function showAddTemplateModal() { currentEditingTemplateId = null; document.getElementById('template-modal-title').textContent = t('templates.add'); @@ -2475,6 +2531,10 @@ async function showAddTemplateModal() { document.getElementById('engine-config-section').style.display = 'none'; document.getElementById('template-error').style.display = 'none'; + // Auto-name: reset flag and wire listener + _templateNameManuallyEdited = false; + document.getElementById('template-name').oninput = () => { _templateNameManuallyEdited = true; }; + // Load available engines await loadAvailableEngines(); @@ -2738,6 +2798,11 @@ async function onEngineChange() { return; } + // Auto-name: set template name to engine name if user hasn't edited + if (!_templateNameManuallyEdited && !document.getElementById('template-id').value) { + document.getElementById('template-name').value = engine.name || engineType; + } + // Show availability hint const hint = document.getElementById('engine-availability-hint'); if (!engine.available) { @@ -3069,7 +3134,7 @@ function renderPictureStreamsList(streams) { detailsHtml = `
🖥️ ${stream.display_index ?? 0} ⚡ ${stream.target_fps ?? 30} - ${capTmplName ? `${capTmplName}` : ''} + ${capTmplName ? `📷 ${capTmplName}` : ''}
`; } else if (stream.stream_type === 'processed') { const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id); @@ -3284,6 +3349,13 @@ async function showAddStreamModal(presetType) { imgSrcInput.onpaste = () => setTimeout(() => validateStaticImage(), 0); onStreamTypeChange(); + // Auto-name: reset flag and wire listeners + _streamNameManuallyEdited = false; + document.getElementById('stream-name').oninput = () => { _streamNameManuallyEdited = true; }; + document.getElementById('stream-capture-template').onchange = () => _autoGenerateStreamName(); + document.getElementById('stream-source').onchange = () => _autoGenerateStreamName(); + document.getElementById('stream-pp-template').onchange = () => _autoGenerateStreamName(); + // Populate dropdowns await populateStreamModalDropdowns(); @@ -3379,6 +3451,7 @@ async function populateStreamModalDropdowns() { (data.templates || []).forEach(tmpl => { const opt = document.createElement('option'); opt.value = tmpl.id; + opt.dataset.name = tmpl.name; opt.textContent = `${getEngineIcon(tmpl.engine_type)} ${tmpl.name}`; templateSelect.appendChild(opt); }); @@ -3395,6 +3468,7 @@ async function populateStreamModalDropdowns() { if (s.id === editingId) return; const opt = document.createElement('option'); opt.value = s.id; + opt.dataset.name = s.name; const typeLabels = { raw: '🖥️', processed: '🎨', static_image: '🖼️' }; const typeLabel = typeLabels[s.stream_type] || '📺'; opt.textContent = `${typeLabel} ${s.name}`; @@ -3403,17 +3477,22 @@ async function populateStreamModalDropdowns() { } // PP templates + _streamModalPPTemplates = []; const ppSelect = document.getElementById('stream-pp-template'); ppSelect.innerHTML = ''; if (ppTemplatesRes.ok) { const data = await ppTemplatesRes.json(); - (data.templates || []).forEach(tmpl => { + _streamModalPPTemplates = data.templates || []; + _streamModalPPTemplates.forEach(tmpl => { const opt = document.createElement('option'); opt.value = tmpl.id; opt.textContent = tmpl.name; ppSelect.appendChild(opt); }); } + + // Trigger auto-name after dropdowns are populated + _autoGenerateStreamName(); } async function saveStream() { @@ -3854,6 +3933,7 @@ function addFilterFromSelect() { _modalFilters.push({ filter_id: filterId, options, _expanded: true }); select.value = ''; renderModalFilterList(); + _autoGeneratePPTemplateName(); } function toggleFilterExpand(index) { @@ -3866,6 +3946,7 @@ function toggleFilterExpand(index) { function removeFilter(index) { _modalFilters.splice(index, 1); renderModalFilterList(); + _autoGeneratePPTemplateName(); } function moveFilter(index, direction) { @@ -3875,6 +3956,7 @@ function moveFilter(index, direction) { _modalFilters[index] = _modalFilters[newIndex]; _modalFilters[newIndex] = tmp; renderModalFilterList(); + _autoGeneratePPTemplateName(); } function updateFilterOption(filterIndex, optionKey, value) { @@ -3904,6 +3986,20 @@ function collectFilters() { })); } +let _ppTemplateNameManuallyEdited = false; + +function _autoGeneratePPTemplateName() { + if (_ppTemplateNameManuallyEdited) return; + if (document.getElementById('pp-template-id').value) return; // editing, not creating + const nameInput = document.getElementById('pp-template-name'); + if (_modalFilters.length > 0) { + const filterNames = _modalFilters.map(f => _getFilterName(f.filter_id)).join(' + '); + nameInput.value = filterNames; + } else { + nameInput.value = ''; + } +} + async function showAddPPTemplateModal() { if (_availableFilters.length === 0) await loadAvailableFilters(); @@ -3914,6 +4010,10 @@ async function showAddPPTemplateModal() { _modalFilters = []; + // Auto-name: reset flag and wire listener + _ppTemplateNameManuallyEdited = false; + document.getElementById('pp-template-name').oninput = () => { _ppTemplateNameManuallyEdited = true; }; + _populateFilterSelect(); renderModalFilterList(); @@ -4141,7 +4241,7 @@ async function updateStreamSelectorInfo(streamId) { propsHtml = ` 🖥️ ${stream.display_index ?? 0} ⚡ ${stream.target_fps ?? 30} - ${capTmplName ? `${capTmplName}` : ''} + ${capTmplName ? `📷 ${capTmplName}` : ''} `; } else if (stream.stream_type === 'processed') { if ((!_cachedStreams || _cachedStreams.length === 0) && stream.source_stream_id) {