led-control.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  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. // Default (Off) button - clears priority
  160. document.getElementById('hyperion-clear-priority')?.addEventListener('click', async () => {
  161. try {
  162. await hyperionController.sendCommand('clear', {});
  163. showStatus('Returned to default state (off)', 'success');
  164. } catch (error) {
  165. showStatus(`Failed to return to default: ${error.message}`, 'error');
  166. }
  167. });
  168. // Save effect settings button
  169. document.getElementById('save-hyperion-effects')?.addEventListener('click', async () => {
  170. try {
  171. const idleEffect = document.getElementById('hyperion-idle-effect')?.value || '';
  172. const playingEffect = document.getElementById('hyperion-playing-effect')?.value || '';
  173. const response = await fetch('/api/hyperion/set_effects', {
  174. method: 'POST',
  175. headers: { 'Content-Type': 'application/json' },
  176. body: JSON.stringify({
  177. idle_effect: idleEffect,
  178. playing_effect: playingEffect
  179. })
  180. });
  181. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  182. await response.json();
  183. showStatus('Effect settings saved successfully', 'success');
  184. } catch (error) {
  185. showStatus(`Failed to save effect settings: ${error.message}`, 'error');
  186. }
  187. });
  188. }
  189. // Load available Hyperion effects
  190. async function loadEffectsList() {
  191. try {
  192. const response = await fetch('/api/hyperion/effects');
  193. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  194. const data = await response.json();
  195. const effectSelect = document.getElementById('hyperion-effect-select');
  196. const idleEffectSelect = document.getElementById('hyperion-idle-effect');
  197. const playingEffectSelect = document.getElementById('hyperion-playing-effect');
  198. if (!effectSelect) return;
  199. // Clear loading option
  200. effectSelect.innerHTML = '<option value="">-- Select an effect --</option>';
  201. // Add "Default (Off)" option to idle and playing effect selectors
  202. if (idleEffectSelect) {
  203. idleEffectSelect.innerHTML = '<option value="off">Default (Off)</option>';
  204. }
  205. if (playingEffectSelect) {
  206. playingEffectSelect.innerHTML = '<option value="off">Default (Off)</option>';
  207. }
  208. // Add effects to all dropdowns
  209. if (data.effects && data.effects.length > 0) {
  210. data.effects.forEach(effect => {
  211. // Main effect selector
  212. const option = document.createElement('option');
  213. option.value = effect.name;
  214. option.textContent = effect.name;
  215. effectSelect.appendChild(option);
  216. // Idle effect selector
  217. if (idleEffectSelect) {
  218. const idleOption = document.createElement('option');
  219. idleOption.value = effect.name;
  220. idleOption.textContent = effect.name;
  221. idleEffectSelect.appendChild(idleOption);
  222. }
  223. // Playing effect selector
  224. if (playingEffectSelect) {
  225. const playingOption = document.createElement('option');
  226. playingOption.value = effect.name;
  227. playingOption.textContent = effect.name;
  228. playingEffectSelect.appendChild(playingOption);
  229. }
  230. });
  231. // Load saved settings from config
  232. const configResponse = await fetch('/get_led_config');
  233. if (configResponse.ok) {
  234. const config = await configResponse.json();
  235. if (idleEffectSelect && config.hyperion_idle_effect) {
  236. idleEffectSelect.value = config.hyperion_idle_effect;
  237. }
  238. if (playingEffectSelect && config.hyperion_playing_effect) {
  239. playingEffectSelect.value = config.hyperion_playing_effect;
  240. }
  241. }
  242. } else {
  243. effectSelect.innerHTML = '<option value="">No effects available</option>';
  244. }
  245. } catch (error) {
  246. console.error('Failed to load effects:', error);
  247. const effectSelect = document.getElementById('hyperion-effect-select');
  248. if (effectSelect) {
  249. effectSelect.innerHTML = '<option value="">Failed to load effects</option>';
  250. }
  251. }
  252. }
  253. // Check Hyperion connection status
  254. async function checkHyperionStatus() {
  255. try {
  256. const response = await fetch('/api/hyperion/status');
  257. if (!response.ok) throw new Error(`HTTP ${response.status}`);
  258. const data = await response.json();
  259. if (data.connected) {
  260. const version = data.version || 'unknown';
  261. const hostname = data.hostname || 'unknown';
  262. const isOn = data.is_on;
  263. const state = isOn ? 'ON' : 'OFF';
  264. // Update power button appearance - shows current state with appropriate action
  265. const powerButton = document.getElementById('hyperion-power-toggle');
  266. const powerButtonText = document.getElementById('power-button-text');
  267. if (powerButton && powerButtonText) {
  268. if (isOn) {
  269. 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';
  270. powerButtonText.textContent = 'Turn OFF';
  271. } else {
  272. 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';
  273. powerButtonText.textContent = 'Turn ON';
  274. }
  275. }
  276. showStatus(`Connected to ${hostname} (${version}) - Power: ${state}`, 'success');
  277. } else {
  278. showStatus(`Connection failed: ${data.message}`, 'error');
  279. }
  280. } catch (error) {
  281. showStatus(`Cannot connect to Hyperion: ${error.message}`, 'error');
  282. }
  283. }
  284. // Initialize on page load
  285. document.addEventListener('DOMContentLoaded', initializeLedPage);