led-control.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // LED Control Page - Unified interface for WLED and Hyperion
  2. let ledConfig = null;
  3. let hyperionController = null;
  4. // Utility function to show status messages
  5. function showStatus(message, type = 'info') {
  6. const statusDiv = document.getElementById('hyperion-status');
  7. if (!statusDiv) return;
  8. const iconMap = {
  9. 'success': 'check_circle',
  10. 'error': 'error',
  11. 'warning': 'warning',
  12. 'info': 'info'
  13. };
  14. const colorMap = {
  15. 'success': 'text-green-700 bg-green-50 border-green-200',
  16. 'error': 'text-red-700 bg-red-50 border-red-200',
  17. 'warning': 'text-amber-700 bg-amber-50 border-amber-200',
  18. 'info': 'text-gray-700 bg-gray-100 border-slate-200'
  19. };
  20. const icon = iconMap[type] || 'info';
  21. const colorClass = colorMap[type] || colorMap.info;
  22. statusDiv.className = `p-4 rounded-lg border ${colorClass}`;
  23. statusDiv.innerHTML = `
  24. <div class="flex items-center gap-2">
  25. <span class="material-icons">${icon}</span>
  26. <span class="text-sm">${message}</span>
  27. </div>
  28. `;
  29. }
  30. // Initialize the page based on LED configuration
  31. async function initializeLedPage() {
  32. try {
  33. const response = await fetch('/get_led_config');
  34. if (!response.ok) throw new Error('Failed to fetch LED config');
  35. ledConfig = await response.json();
  36. const notConfigured = document.getElementById('led-not-configured');
  37. const wledContainer = document.getElementById('wled-container');
  38. const hyperionContainer = document.getElementById('hyperion-container');
  39. // Hide all containers first
  40. notConfigured.classList.add('hidden');
  41. wledContainer.classList.add('hidden');
  42. hyperionContainer.classList.add('hidden');
  43. if (ledConfig.provider === 'wled' && ledConfig.wled_ip) {
  44. // Show WLED iframe
  45. wledContainer.classList.remove('hidden');
  46. const wledFrame = document.getElementById('wled-frame');
  47. if (wledFrame) {
  48. wledFrame.src = `http://${ledConfig.wled_ip}`;
  49. }
  50. } else if (ledConfig.provider === 'hyperion' && ledConfig.hyperion_ip) {
  51. // Show Hyperion controls
  52. hyperionContainer.classList.remove('hidden');
  53. await initializeHyperionControls();
  54. } else {
  55. // Show not configured message
  56. notConfigured.classList.remove('hidden');
  57. }
  58. } catch (error) {
  59. console.error('Error initializing LED page:', error);
  60. document.getElementById('led-not-configured').classList.remove('hidden');
  61. }
  62. }
  63. // Initialize Hyperion controls
  64. async function initializeHyperionControls() {
  65. // Create API helper
  66. hyperionController = {
  67. async sendCommand(endpoint, data) {
  68. try {
  69. const response = await fetch(`/api/hyperion/${endpoint}`, {
  70. method: 'POST',
  71. headers: { 'Content-Type': 'application/json' },
  72. body: JSON.stringify(data)
  73. });
  74. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  75. return await response.json();
  76. } catch (error) {
  77. throw error;
  78. }
  79. }
  80. };
  81. // Check connection status and load effects
  82. await checkHyperionStatus();
  83. await loadEffectsList();
  84. // Power toggle button
  85. document.getElementById('hyperion-power-toggle')?.addEventListener('click', async () => {
  86. try {
  87. // Toggle using state 2
  88. await hyperionController.sendCommand('power', { state: 2 });
  89. showStatus('Power toggled', 'success');
  90. await checkHyperionStatus();
  91. } catch (error) {
  92. showStatus(`Failed to toggle power: ${error.message}`, 'error');
  93. }
  94. });
  95. // Brightness slider
  96. const brightnessSlider = document.getElementById('hyperion-brightness');
  97. const brightnessValue = document.getElementById('brightness-value');
  98. brightnessSlider?.addEventListener('input', (e) => {
  99. brightnessValue.textContent = `${e.target.value}%`;
  100. });
  101. brightnessSlider?.addEventListener('change', async (e) => {
  102. try {
  103. await hyperionController.sendCommand('brightness', { value: parseInt(e.target.value) });
  104. showStatus(`Brightness set to ${e.target.value}%`, 'success');
  105. } catch (error) {
  106. showStatus(`Failed to set brightness: ${error.message}`, 'error');
  107. }
  108. });
  109. // Color picker - update display when color changes
  110. const colorPicker = document.getElementById('hyperion-color');
  111. const colorHexDisplay = document.getElementById('color-hex-display');
  112. colorPicker?.addEventListener('input', (e) => {
  113. if (colorHexDisplay) {
  114. colorHexDisplay.textContent = e.target.value.toUpperCase();
  115. }
  116. });
  117. // Color picker - apply button
  118. document.getElementById('hyperion-set-color')?.addEventListener('click', async () => {
  119. const hexColor = colorPicker.value;
  120. try {
  121. await hyperionController.sendCommand('color', { hex: hexColor });
  122. showStatus(`Color set to ${hexColor.toUpperCase()}`, 'success');
  123. } catch (error) {
  124. showStatus(`Failed to set color: ${error.message}`, 'error');
  125. }
  126. });
  127. // Quick color buttons
  128. document.querySelectorAll('.quick-color').forEach(button => {
  129. button.addEventListener('click', async () => {
  130. const hexColor = button.getAttribute('data-color');
  131. try {
  132. await hyperionController.sendCommand('color', { hex: hexColor });
  133. showStatus(`Color set to ${hexColor.toUpperCase()}`, 'success');
  134. // Update color picker and hex display to match
  135. const colorPicker = document.getElementById('hyperion-color');
  136. const colorHexDisplay = document.getElementById('color-hex-display');
  137. if (colorPicker) colorPicker.value = hexColor;
  138. if (colorHexDisplay) colorHexDisplay.textContent = hexColor.toUpperCase();
  139. } catch (error) {
  140. showStatus(`Failed to set color: ${error.message}`, 'error');
  141. }
  142. });
  143. });
  144. // Effects selection
  145. document.getElementById('hyperion-set-effect')?.addEventListener('click', async () => {
  146. const effectSelect = document.getElementById('hyperion-effect-select');
  147. const effectName = effectSelect.value;
  148. if (!effectName) {
  149. showStatus('Please select an effect', 'warning');
  150. return;
  151. }
  152. try {
  153. await hyperionController.sendCommand('effect', { effect_name: effectName });
  154. showStatus(`Effect '${effectName}' activated`, 'success');
  155. } catch (error) {
  156. showStatus(`Failed to set effect: ${error.message}`, 'error');
  157. }
  158. });
  159. // Save effect settings button
  160. document.getElementById('save-hyperion-effects')?.addEventListener('click', async () => {
  161. try {
  162. const idleEffect = document.getElementById('hyperion-idle-effect')?.value || '';
  163. const playingEffect = document.getElementById('hyperion-playing-effect')?.value || '';
  164. const response = await fetch('/api/hyperion/set_effects', {
  165. method: 'POST',
  166. headers: { 'Content-Type': 'application/json' },
  167. body: JSON.stringify({
  168. idle_effect: idleEffect,
  169. playing_effect: playingEffect
  170. })
  171. });
  172. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  173. await response.json();
  174. showStatus('Effect settings saved successfully', 'success');
  175. } catch (error) {
  176. showStatus(`Failed to save effect settings: ${error.message}`, 'error');
  177. }
  178. });
  179. }
  180. // Load available Hyperion effects
  181. async function loadEffectsList() {
  182. try {
  183. const response = await fetch('/api/hyperion/effects');
  184. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  185. const data = await response.json();
  186. const effectSelect = document.getElementById('hyperion-effect-select');
  187. const idleEffectSelect = document.getElementById('hyperion-idle-effect');
  188. const playingEffectSelect = document.getElementById('hyperion-playing-effect');
  189. if (!effectSelect) return;
  190. // Clear loading option
  191. effectSelect.innerHTML = '<option value="">-- Select an effect --</option>';
  192. // Add "Default (Off)" option to idle and playing effect selectors
  193. if (idleEffectSelect) {
  194. idleEffectSelect.innerHTML = '<option value="off">Default (Off)</option>';
  195. }
  196. if (playingEffectSelect) {
  197. playingEffectSelect.innerHTML = '<option value="off">Default (Off)</option>';
  198. }
  199. // Add effects to all dropdowns
  200. if (data.effects && data.effects.length > 0) {
  201. data.effects.forEach(effect => {
  202. // Main effect selector
  203. const option = document.createElement('option');
  204. option.value = effect.name;
  205. option.textContent = effect.name;
  206. effectSelect.appendChild(option);
  207. // Idle effect selector
  208. if (idleEffectSelect) {
  209. const idleOption = document.createElement('option');
  210. idleOption.value = effect.name;
  211. idleOption.textContent = effect.name;
  212. idleEffectSelect.appendChild(idleOption);
  213. }
  214. // Playing effect selector
  215. if (playingEffectSelect) {
  216. const playingOption = document.createElement('option');
  217. playingOption.value = effect.name;
  218. playingOption.textContent = effect.name;
  219. playingEffectSelect.appendChild(playingOption);
  220. }
  221. });
  222. // Load saved settings from config
  223. const configResponse = await fetch('/get_led_config');
  224. if (configResponse.ok) {
  225. const config = await configResponse.json();
  226. if (idleEffectSelect && config.hyperion_idle_effect) {
  227. idleEffectSelect.value = config.hyperion_idle_effect;
  228. }
  229. if (playingEffectSelect && config.hyperion_playing_effect) {
  230. playingEffectSelect.value = config.hyperion_playing_effect;
  231. }
  232. }
  233. } else {
  234. effectSelect.innerHTML = '<option value="">No effects available</option>';
  235. }
  236. } catch (error) {
  237. console.error('Failed to load effects:', error);
  238. const effectSelect = document.getElementById('hyperion-effect-select');
  239. if (effectSelect) {
  240. effectSelect.innerHTML = '<option value="">Failed to load effects</option>';
  241. }
  242. }
  243. }
  244. // Check Hyperion connection status
  245. async function checkHyperionStatus() {
  246. try {
  247. const response = await fetch('/api/hyperion/status');
  248. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  249. const data = await response.json();
  250. if (data.connected) {
  251. const version = data.version || 'unknown';
  252. const hostname = data.hostname || 'unknown';
  253. const isOn = data.is_on;
  254. const state = isOn ? 'ON' : 'OFF';
  255. // Update power button appearance - shows current state with appropriate action
  256. const powerButton = document.getElementById('hyperion-power-toggle');
  257. const powerButtonText = document.getElementById('power-button-text');
  258. if (powerButton && powerButtonText) {
  259. if (isOn) {
  260. 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';
  261. powerButtonText.textContent = 'Turn OFF';
  262. } else {
  263. 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';
  264. powerButtonText.textContent = 'Turn ON';
  265. }
  266. }
  267. showStatus(`Connected to ${hostname} (${version}) - Power: ${state}`, 'success');
  268. } else {
  269. showStatus(`Connection failed: ${data.message}`, 'error');
  270. }
  271. } catch (error) {
  272. showStatus(`Cannot connect to Hyperion: ${error.message}`, 'error');
  273. }
  274. }
  275. // Initialize on page load
  276. document.addEventListener('DOMContentLoaded', initializeLedPage);