// LED Control Page - Unified interface for WLED and DW LEDs let ledConfig = null; // Utility function to show status messages function showStatus(message, type = 'info') { const statusDiv = document.getElementById('dw-leds-status'); if (!statusDiv) return; const iconMap = { 'success': 'check_circle', 'error': 'error', 'warning': 'warning', 'info': 'info' }; const colorMap = { 'success': 'text-green-700 bg-green-50 border-green-200', 'error': 'text-red-700 bg-red-50 border-red-200', 'warning': 'text-amber-700 bg-amber-50 border-amber-200', 'info': 'text-gray-700 bg-gray-100 border-slate-200' }; const icon = iconMap[type] || 'info'; const colorClass = colorMap[type] || colorMap.info; statusDiv.className = `p-4 rounded-lg border ${colorClass}`; statusDiv.innerHTML = `
${icon} ${message}
`; } // Initialize the page based on LED configuration async function initializeLedPage() { try { const response = await fetch('/get_led_config'); if (!response.ok) throw new Error('Failed to fetch LED config'); ledConfig = await response.json(); const notConfigured = document.getElementById('led-not-configured'); const wledContainer = document.getElementById('wled-container'); const dwLedsContainer = document.getElementById('dw-leds-container'); // Hide all containers first notConfigured.classList.add('hidden'); wledContainer.classList.add('hidden'); dwLedsContainer.classList.add('hidden'); if (ledConfig.provider === 'wled' && ledConfig.wled_ip) { // Show WLED iframe wledContainer.classList.remove('hidden'); const wledFrame = document.getElementById('wled-frame'); if (wledFrame) { wledFrame.src = `http://${ledConfig.wled_ip}`; } } else if (ledConfig.provider === 'dw_leds') { // Show DW LEDs controls dwLedsContainer.classList.remove('hidden'); await initializeDWLedsControls(); } else { // Show not configured message notConfigured.classList.remove('hidden'); } } catch (error) { console.error('Error initializing LED page:', error); document.getElementById('led-not-configured').classList.remove('hidden'); } } // Initialize DW LEDs controls async function initializeDWLedsControls() { // Check status and load available effects/palettes await checkDWLedsStatus(); await loadEffectsAndPalettes(); // Power toggle button document.getElementById('dw-leds-power-toggle')?.addEventListener('click', async () => { try { const response = await fetch('/api/dw_leds/power', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ state: 2 }) // Toggle }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { showStatus(`Power ${data.power_on ? 'ON' : 'OFF'}`, 'success'); await checkDWLedsStatus(); } else { showStatus(data.error || 'Failed to toggle power', 'error'); } } catch (error) { showStatus(`Failed to toggle power: ${error.message}`, 'error'); } }); // Brightness slider const brightnessSlider = document.getElementById('dw-leds-brightness'); const brightnessValue = document.getElementById('dw-leds-brightness-value'); brightnessSlider?.addEventListener('input', (e) => { brightnessValue.textContent = `${e.target.value}%`; }); brightnessSlider?.addEventListener('change', async (e) => { try { const response = await fetch('/api/dw_leds/brightness', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: parseInt(e.target.value) }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { showStatus(`Brightness set to ${e.target.value}%`, 'success'); } else { showStatus(data.error || 'Failed to set brightness', 'error'); } } catch (error) { showStatus(`Failed to set brightness: ${error.message}`, 'error'); } }); // Effect color pickers - apply immediately on change document.querySelectorAll('.effect-color-picker').forEach(picker => { picker.addEventListener('change', async () => { const color1 = document.getElementById('dw-leds-color1')?.value; const color2 = document.getElementById('dw-leds-color2')?.value; const color3 = document.getElementById('dw-leds-color3')?.value; if (color1 && color2 && color3) { await applyAllColors(color1, color2, color3); } }); }); // Effect selector document.getElementById('dw-leds-effect-select')?.addEventListener('change', async (e) => { const effectId = parseInt(e.target.value); if (isNaN(effectId)) return; try { const response = await fetch('/api/dw_leds/effect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ effect_id: effectId }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { showStatus(`Effect changed`, 'success'); // Update power button state if backend auto-powered on if (data.power_on !== undefined) { updatePowerButtonUI(data.power_on); } } else { showStatus(data.error || 'Failed to set effect', 'error'); } } catch (error) { showStatus(`Failed to set effect: ${error.message}`, 'error'); } }); // Palette selector document.getElementById('dw-leds-palette-select')?.addEventListener('change', async (e) => { const paletteId = parseInt(e.target.value); if (isNaN(paletteId)) return; try { const response = await fetch('/api/dw_leds/palette', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ palette_id: paletteId }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { showStatus(`Palette changed`, 'success'); // Update power button state if backend auto-powered on if (data.power_on !== undefined) { updatePowerButtonUI(data.power_on); } } else { showStatus(data.error || 'Failed to set palette', 'error'); } } catch (error) { showStatus(`Failed to set palette: ${error.message}`, 'error'); } }); // Speed slider const speedSlider = document.getElementById('dw-leds-speed'); const speedValue = document.getElementById('dw-leds-speed-value'); speedSlider?.addEventListener('input', (e) => { speedValue.textContent = e.target.value; }); speedSlider?.addEventListener('change', async (e) => { try { const response = await fetch('/api/dw_leds/speed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ speed: parseInt(e.target.value) }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { showStatus(`Speed updated`, 'success'); } else { showStatus(data.error || 'Failed to set speed', 'error'); } } catch (error) { showStatus(`Failed to set speed: ${error.message}`, 'error'); } }); // Intensity slider const intensitySlider = document.getElementById('dw-leds-intensity'); const intensityValue = document.getElementById('dw-leds-intensity-value'); intensitySlider?.addEventListener('input', (e) => { intensityValue.textContent = e.target.value; }); intensitySlider?.addEventListener('change', async (e) => { try { const response = await fetch('/api/dw_leds/intensity', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ intensity: parseInt(e.target.value) }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { showStatus(`Intensity updated`, 'success'); } else { showStatus(data.error || 'Failed to set intensity', 'error'); } } catch (error) { showStatus(`Failed to set intensity: ${error.message}`, 'error'); } }); // Save Current Idle Effect document.getElementById('dw-leds-save-current-idle')?.addEventListener('click', async () => { await saveCurrentEffectSettings('idle'); }); // Clear Idle Effect document.getElementById('dw-leds-clear-idle')?.addEventListener('click', async () => { await clearEffectSettings('idle'); }); // Save Current Playing Effect document.getElementById('dw-leds-save-current-playing')?.addEventListener('click', async () => { await saveCurrentEffectSettings('playing'); }); // Clear Playing Effect document.getElementById('dw-leds-clear-playing')?.addEventListener('click', async () => { await clearEffectSettings('playing'); }); // Load and display saved effect settings await loadEffectSettings(); // Idle timeout controls await loadIdleTimeout(); const idleTimeoutEnabled = document.getElementById('dw-leds-idle-timeout-enabled'); const idleTimeoutSettings = document.getElementById('idle-timeout-settings'); const idleTimeoutDisabledHelp = document.getElementById('idle-timeout-disabled-help'); // Toggle idle timeout settings visibility and help text idleTimeoutEnabled?.addEventListener('change', (e) => { const isEnabled = e.target.checked; if (isEnabled) { idleTimeoutSettings?.classList.remove('opacity-50', 'pointer-events-none'); idleTimeoutDisabledHelp?.classList.add('hidden'); } else { idleTimeoutSettings?.classList.add('opacity-50', 'pointer-events-none'); idleTimeoutDisabledHelp?.classList.remove('hidden'); } // Auto-save when toggle changes for better UX saveIdleTimeout(); }); // Save idle timeout settings document.getElementById('dw-leds-save-idle-timeout')?.addEventListener('click', async () => { await saveIdleTimeout(); }); // Update remaining time periodically let idleTimeoutInterval = setInterval(updateIdleTimeoutRemaining, 60000); // Update every minute // Clean up interval when page unloads window.addEventListener('beforeunload', () => { if (idleTimeoutInterval) { clearInterval(idleTimeoutInterval); idleTimeoutInterval = null; } }); // Initialize Coloris color picker for effect colors initializeColoris(); } // Save current LED settings as idle or playing effect async function saveCurrentEffectSettings(type) { try { const effectId = parseInt(document.getElementById('dw-leds-effect-select')?.value) || 0; const paletteId = parseInt(document.getElementById('dw-leds-palette-select')?.value) || 0; const speed = parseInt(document.getElementById('dw-leds-speed')?.value) || 128; const intensity = parseInt(document.getElementById('dw-leds-intensity')?.value) || 128; // Get effect colors const color1 = document.getElementById('dw-leds-color1')?.value || '#ff0000'; const color2 = document.getElementById('dw-leds-color2')?.value || '#000000'; const color3 = document.getElementById('dw-leds-color3')?.value || '#0000ff'; const settings = { type: type, // 'idle' or 'playing' effect_id: effectId, palette_id: paletteId, speed: speed, intensity: intensity, color1: color1, color2: color2, color3: color3 }; const response = await fetch('/api/dw_leds/save_effect_settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); await response.json(); showStatus(`${type.charAt(0).toUpperCase() + type.slice(1)} effect settings saved successfully`, 'success'); // Refresh display await loadEffectSettings(); } catch (error) { showStatus(`Failed to save ${type} effect settings: ${error.message}`, 'error'); } } // Clear effect settings async function clearEffectSettings(type) { try { const response = await fetch('/api/dw_leds/clear_effect_settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: type }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); showStatus(`${type.charAt(0).toUpperCase() + type.slice(1)} effect cleared`, 'success'); // Refresh display await loadEffectSettings(); } catch (error) { showStatus(`Failed to clear ${type} effect: ${error.message}`, 'error'); } } // Load and display saved effect settings async function loadEffectSettings() { try { const response = await fetch('/api/dw_leds/get_effect_settings'); if (!response.ok) return; const data = await response.json(); // Display idle settings const idleDisplay = document.getElementById('idle-settings-display'); if (idleDisplay) { idleDisplay.textContent = formatEffectSettings(data.idle_effect); } // Display playing settings const playingDisplay = document.getElementById('playing-settings-display'); if (playingDisplay) { playingDisplay.textContent = formatEffectSettings(data.playing_effect); } } catch (error) { console.error('Failed to load effect settings:', error); } } // Format effect settings for display function formatEffectSettings(settings) { if (!settings) { return 'Not configured (do nothing)'; } const parts = []; // Get effect name from select (if available) const effectSelect = document.getElementById('dw-leds-effect-select'); if (effectSelect && settings.effect_id !== undefined) { const effectOption = effectSelect.querySelector(`option[value="${settings.effect_id}"]`); parts.push(`Effect: ${effectOption ? effectOption.textContent : settings.effect_id}`); } // Get palette name from select (if available) const paletteSelect = document.getElementById('dw-leds-palette-select'); if (paletteSelect && settings.palette_id !== undefined) { const paletteOption = paletteSelect.querySelector(`option[value="${settings.palette_id}"]`); parts.push(`Palette: ${paletteOption ? paletteOption.textContent : settings.palette_id}`); } if (settings.speed !== undefined) { parts.push(`Speed: ${settings.speed}`); } if (settings.intensity !== undefined) { parts.push(`Intensity: ${settings.intensity}`); } if (settings.color1) { parts.push(`Colors: ${settings.color1}, ${settings.color2 || '#000000'}, ${settings.color3 || '#0000ff'}`); } return parts.join(' | '); } // Load idle timeout settings async function loadIdleTimeout() { try { const response = await fetch('/api/dw_leds/idle_timeout'); if (!response.ok) return; const data = await response.json(); const enabledCheckbox = document.getElementById('dw-leds-idle-timeout-enabled'); const minutesInput = document.getElementById('dw-leds-idle-timeout-minutes'); const idleTimeoutSettings = document.getElementById('idle-timeout-settings'); const idleTimeoutDisabledHelp = document.getElementById('idle-timeout-disabled-help'); if (enabledCheckbox) { enabledCheckbox.checked = data.enabled; } if (minutesInput) { minutesInput.value = data.minutes; } // Set initial state of settings panel and help text if (data.enabled) { idleTimeoutSettings?.classList.remove('opacity-50', 'pointer-events-none'); idleTimeoutDisabledHelp?.classList.add('hidden'); } else { idleTimeoutSettings?.classList.add('opacity-50', 'pointer-events-none'); idleTimeoutDisabledHelp?.classList.remove('hidden'); } // Update remaining time display updateIdleTimeoutRemainingDisplay(data.remaining_minutes); } catch (error) { console.error('Failed to load idle timeout settings:', error); } } // Save idle timeout settings async function saveIdleTimeout() { try { const enabled = document.getElementById('dw-leds-idle-timeout-enabled')?.checked || false; const minutes = parseInt(document.getElementById('dw-leds-idle-timeout-minutes')?.value) || 30; const response = await fetch('/api/dw_leds/idle_timeout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, minutes }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.success) { showStatus(`Idle timeout ${enabled ? 'enabled' : 'disabled'} (${minutes} minutes)`, 'success'); await loadIdleTimeout(); // Reload to get updated remaining time } else { showStatus('Failed to save idle timeout settings', 'error'); } } catch (error) { showStatus(`Failed to save idle timeout: ${error.message}`, 'error'); } } // Update idle timeout remaining time async function updateIdleTimeoutRemaining() { try { const response = await fetch('/api/dw_leds/idle_timeout'); if (!response.ok) return; const data = await response.json(); updateIdleTimeoutRemainingDisplay(data.remaining_minutes); } catch (error) { console.error('Failed to update idle timeout remaining:', error); } } // Update idle timeout remaining time display function updateIdleTimeoutRemainingDisplay(remainingMinutes) { const remainingDiv = document.getElementById('idle-timeout-remaining'); const remainingDisplay = document.getElementById('idle-timeout-remaining-display'); if (!remainingDiv || !remainingDisplay) return; if (remainingMinutes !== null && remainingMinutes !== undefined) { remainingDiv.classList.remove('hidden'); if (remainingMinutes <= 0) { remainingDisplay.textContent = 'Timeout expired - LEDs will turn off'; } else if (remainingMinutes < 1) { remainingDisplay.textContent = 'Less than 1 minute'; } else { const hours = Math.floor(remainingMinutes / 60); const mins = Math.round(remainingMinutes % 60); if (hours > 0) { remainingDisplay.textContent = `${hours}h ${mins}m`; } else { remainingDisplay.textContent = `${mins} minutes`; } } } else { remainingDiv.classList.add('hidden'); } } // Helper function to apply all effect colors async function applyAllColors(hexColor1, hexColor2, hexColor3) { try { const payload = {}; if (hexColor1) { const r = parseInt(hexColor1.slice(1, 3), 16); const g = parseInt(hexColor1.slice(3, 5), 16); const b = parseInt(hexColor1.slice(5, 7), 16); payload.color1 = [r, g, b]; } if (hexColor2) { const r = parseInt(hexColor2.slice(1, 3), 16); const g = parseInt(hexColor2.slice(3, 5), 16); const b = parseInt(hexColor2.slice(5, 7), 16); payload.color2 = [r, g, b]; } if (hexColor3) { const r = parseInt(hexColor3.slice(1, 3), 16); const g = parseInt(hexColor3.slice(3, 5), 16); const b = parseInt(hexColor3.slice(5, 7), 16); payload.color3 = [r, g, b]; } const response = await fetch('/api/dw_leds/colors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { showStatus(`Effect colors updated`, 'success'); } else { showStatus(data.error || 'Failed to set colors', 'error'); } } catch (error) { showStatus(`Failed to set colors: ${error.message}`, 'error'); } } // Load available effects and palettes async function loadEffectsAndPalettes() { try { // Load effects const effectsResponse = await fetch('/api/dw_leds/effects'); if (effectsResponse.ok) { const effectsData = await effectsResponse.json(); const effectSelect = document.getElementById('dw-leds-effect-select'); const idleEffectSelect = document.getElementById('dw-leds-idle-effect'); const playingEffectSelect = document.getElementById('dw-leds-playing-effect'); if (effectSelect && effectsData.effects) { effectSelect.innerHTML = ''; // Sort effects alphabetically by name const sortedEffects = [...effectsData.effects].sort((a, b) => a[1].localeCompare(b[1]) ); sortedEffects.forEach(([id, name]) => { const option = document.createElement('option'); option.value = id; option.textContent = name; effectSelect.appendChild(option); }); } // Add effects to automation selectors if (idleEffectSelect && effectsData.effects) { idleEffectSelect.innerHTML = ''; // Sort effects alphabetically by name const sortedEffects = [...effectsData.effects].sort((a, b) => a[1].localeCompare(b[1]) ); sortedEffects.forEach(([, name]) => { const option = document.createElement('option'); option.value = name.toLowerCase(); option.textContent = name; idleEffectSelect.appendChild(option); }); } if (playingEffectSelect && effectsData.effects) { playingEffectSelect.innerHTML = ''; // Sort effects alphabetically by name const sortedEffects = [...effectsData.effects].sort((a, b) => a[1].localeCompare(b[1]) ); sortedEffects.forEach(([, name]) => { const option = document.createElement('option'); option.value = name.toLowerCase(); option.textContent = name; playingEffectSelect.appendChild(option); }); } // Load saved automation settings const configResponse = await fetch('/get_led_config'); if (configResponse.ok) { const config = await configResponse.json(); if (idleEffectSelect && config.dw_led_idle_effect) { idleEffectSelect.value = config.dw_led_idle_effect; } if (playingEffectSelect && config.dw_led_playing_effect) { playingEffectSelect.value = config.dw_led_playing_effect; } } } // Load palettes const palettesResponse = await fetch('/api/dw_leds/palettes'); if (palettesResponse.ok) { const palettesData = await palettesResponse.json(); const paletteSelect = document.getElementById('dw-leds-palette-select'); if (paletteSelect && palettesData.palettes) { paletteSelect.innerHTML = ''; // Sort palettes alphabetically by name const sortedPalettes = [...palettesData.palettes].sort((a, b) => a[1].localeCompare(b[1]) ); sortedPalettes.forEach(([id, name]) => { const option = document.createElement('option'); option.value = id; option.textContent = name; paletteSelect.appendChild(option); }); } } } catch (error) { console.error('Failed to load effects and palettes:', error); showStatus('Failed to load effects and palettes', 'error'); } } // Helper function to update power button UI based on power state function updatePowerButtonUI(powerOn) { const powerButton = document.getElementById('dw-leds-power-toggle'); const powerButtonText = document.getElementById('dw-leds-power-text'); if (powerButton && powerButtonText) { if (powerOn) { powerButton.className = 'flex items-center justify-center gap-2 rounded-lg bg-red-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-red-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2'; powerButtonText.textContent = 'Turn OFF'; } else { powerButton.className = 'flex items-center justify-center gap-2 rounded-lg bg-green-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-green-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2'; powerButtonText.textContent = 'Turn ON'; } } } // Check DW LEDs connection status async function checkDWLedsStatus() { try { const response = await fetch('/api/dw_leds/status'); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); if (data.connected) { const powerState = data.power_on ? 'ON' : 'OFF'; showStatus(`Connected: ${data.num_leds} LEDs on GPIO ${data.gpio_pin} - Power: ${powerState}`, 'success'); // Update power button appearance updatePowerButtonUI(data.power_on); // Update slider values const brightnessSlider = document.getElementById('dw-leds-brightness'); const brightnessValue = document.getElementById('dw-leds-brightness-value'); if (brightnessSlider && data.brightness !== undefined) { brightnessSlider.value = data.brightness; if (brightnessValue) brightnessValue.textContent = `${data.brightness}%`; } const speedSlider = document.getElementById('dw-leds-speed'); const speedValue = document.getElementById('dw-leds-speed-value'); if (speedSlider && data.speed !== undefined) { speedSlider.value = data.speed; if (speedValue) speedValue.textContent = data.speed; } const intensitySlider = document.getElementById('dw-leds-intensity'); const intensityValue = document.getElementById('dw-leds-intensity-value'); if (intensitySlider && data.intensity !== undefined) { intensitySlider.value = data.intensity; if (intensityValue) intensityValue.textContent = data.intensity; } // Update effect and palette selectors const effectSelect = document.getElementById('dw-leds-effect-select'); if (effectSelect && data.current_effect !== undefined) { effectSelect.value = data.current_effect; } const paletteSelect = document.getElementById('dw-leds-palette-select'); if (paletteSelect && data.current_palette !== undefined) { paletteSelect.value = data.current_palette; } // Update color pickers if colors are provided if (data.colors && Array.isArray(data.colors)) { const color1 = document.getElementById('dw-leds-color1'); const color2 = document.getElementById('dw-leds-color2'); const color3 = document.getElementById('dw-leds-color3'); if (color1 && data.colors[0]) { color1.value = data.colors[0]; updateColorPickerStyle(color1, data.colors[0]); } if (color2 && data.colors[1]) { color2.value = data.colors[1]; updateColorPickerStyle(color2, data.colors[1]); } if (color3 && data.colors[2]) { color3.value = data.colors[2]; updateColorPickerStyle(color3, data.colors[2]); } } } else { // Show error message from controller const errorMsg = data.error || 'Connection failed'; showStatus(errorMsg, 'error'); } } catch (error) { showStatus(`Cannot connect to DW LEDs: ${error.message}`, 'error'); } } // Helper function to update color picker background function updateColorPickerStyle(input, color) { if (!input || !color) return; input.style.backgroundColor = color; } // Initialize Coloris color picker function initializeColoris() { // Initialize Coloris with custom configuration Coloris({ theme: 'polaroid', themeMode: 'auto', formatToggle: true, alpha: false, // No transparency for LED colors swatches: [ '#ff0000', // Red '#00ff00', // Green '#0000ff', // Blue '#ffff00', // Yellow '#ff00ff', // Magenta '#00ffff', // Cyan '#ff8000', // Orange '#ffffff', // White '#2a9d8f', // Teal '#e9c46a', // Sand 'coral', // Coral 'Crimson' // Crimson ], onChange: (color, input) => { // Update the input background to show the selected color updateColorPickerStyle(input, color); } }); // Apply Coloris to all effect color pickers and set initial background colors const colorPickers = document.querySelectorAll('.effect-color-picker'); colorPickers.forEach(picker => { picker.setAttribute('data-coloris', ''); // Set initial background color and text color updateColorPickerStyle(picker, picker.value); }); } // Initialize on page load document.addEventListener('DOMContentLoaded', initializeLedPage);