Kaynağa Gözat

Add Hyperion support

tuanchris 3 ay önce
ebeveyn
işleme
5ad0518775

+ 279 - 3
main.py

@@ -20,6 +20,7 @@ import sys
 import asyncio
 from contextlib import asynccontextmanager
 from modules.led.led_controller import LEDController, effect_idle
+from modules.led.led_interface import LEDInterface
 import math
 from modules.core.cache_manager import generate_all_image_previews, get_cache_path, generate_image_preview, get_pattern_metadata
 from modules.core.version_manager import version_manager
@@ -84,7 +85,36 @@ async def lifespan(app: FastAPI):
         connection_manager.connect_device()
     except Exception as e:
         logger.warning(f"Failed to auto-connect to serial port: {str(e)}")
-    
+
+    # Initialize LED controller based on saved configuration
+    try:
+        # Auto-detect provider for backward compatibility with existing installations
+        if not state.led_provider or state.led_provider == "none":
+            if state.wled_ip:
+                state.led_provider = "wled"
+                logger.info("Auto-detected WLED provider from existing configuration")
+            elif state.hyperion_ip:
+                state.led_provider = "hyperion"
+                logger.info("Auto-detected Hyperion provider from existing configuration")
+
+        # Initialize the appropriate controller
+        if state.led_provider == "wled" and state.wled_ip:
+            state.led_controller = LEDInterface("wled", state.wled_ip)
+            logger.info(f"LED controller initialized: WLED at {state.wled_ip}")
+        elif state.led_provider == "hyperion" and state.hyperion_ip:
+            state.led_controller = LEDInterface("hyperion", state.hyperion_ip, state.hyperion_port)
+            logger.info(f"LED controller initialized: Hyperion at {state.hyperion_ip}:{state.hyperion_port}")
+        else:
+            state.led_controller = None
+            logger.info("LED controller not configured")
+
+        # Save if provider was auto-detected
+        if state.led_provider and (state.wled_ip or state.hyperion_ip):
+            state.save()
+    except Exception as e:
+        logger.warning(f"Failed to initialize LED controller: {str(e)}")
+        state.led_controller = None
+
     # Check if auto_play mode is enabled and auto-play playlist (right after connection attempt)
     if state.auto_play_enabled and state.auto_play_playlist:
         logger.info(f"auto_play mode enabled, checking for connection before auto-playing playlist: {state.auto_play_playlist}")
@@ -192,6 +222,11 @@ class SpeedRequest(BaseModel):
 class WLEDRequest(BaseModel):
     wled_ip: Optional[str] = None
 
+class LEDConfigRequest(BaseModel):
+    provider: str  # "wled", "hyperion", or "none"
+    ip_address: Optional[str] = None
+    port: Optional[int] = None
+
 class DeletePlaylistRequest(BaseModel):
     playlist_name: str
 
@@ -1077,19 +1112,98 @@ async def update_software():
 
 @app.post("/set_wled_ip")
 async def set_wled_ip(request: WLEDRequest):
+    """Legacy endpoint for backward compatibility - sets WLED as LED provider"""
     state.wled_ip = request.wled_ip
-    state.led_controller = LEDController(request.wled_ip)
-    effect_idle(state.led_controller)
+    state.led_provider = "wled" if request.wled_ip else "none"
+    state.led_controller = LEDInterface("wled", request.wled_ip) if request.wled_ip else None
+    if state.led_controller:
+        state.led_controller.effect_idle()
     state.save()
     logger.info(f"WLED IP updated: {request.wled_ip}")
     return {"success": True, "wled_ip": state.wled_ip}
 
 @app.get("/get_wled_ip")
 async def get_wled_ip():
+    """Legacy endpoint for backward compatibility"""
     if not state.wled_ip:
         raise HTTPException(status_code=404, detail="No WLED IP set")
     return {"success": True, "wled_ip": state.wled_ip}
 
+@app.post("/set_led_config")
+async def set_led_config(request: LEDConfigRequest):
+    """Configure LED provider (WLED, Hyperion, or none)"""
+    if request.provider not in ["wled", "hyperion", "none"]:
+        raise HTTPException(status_code=400, detail="Invalid provider. Must be 'wled', 'hyperion', or 'none'")
+
+    state.led_provider = request.provider
+
+    if request.provider == "wled":
+        if not request.ip_address:
+            raise HTTPException(status_code=400, detail="IP address required for WLED")
+        state.wled_ip = request.ip_address
+        state.hyperion_ip = None  # Clear other provider
+        state.led_controller = LEDInterface("wled", request.ip_address)
+        logger.info(f"LED provider set to WLED at {request.ip_address}")
+
+    elif request.provider == "hyperion":
+        if not request.ip_address:
+            raise HTTPException(status_code=400, detail="IP address required for Hyperion")
+        state.hyperion_ip = request.ip_address
+        state.hyperion_port = request.port or 8090
+        state.wled_ip = None  # Clear other provider
+        state.led_controller = LEDInterface("hyperion", request.ip_address, request.port or 8090)
+        logger.info(f"LED provider set to Hyperion at {request.ip_address}:{request.port or 8090}")
+
+    else:  # none
+        state.wled_ip = None
+        state.hyperion_ip = None
+        state.led_controller = None
+        logger.info("LED provider disabled")
+
+    # Show idle effect if controller is configured
+    if state.led_controller:
+        state.led_controller.effect_idle()
+
+    state.save()
+
+    return {
+        "success": True,
+        "provider": state.led_provider,
+        "wled_ip": state.wled_ip,
+        "hyperion_ip": state.hyperion_ip,
+        "hyperion_port": state.hyperion_port
+    }
+
+@app.get("/get_led_config")
+async def get_led_config():
+    """Get current LED provider configuration"""
+    # Auto-detect provider for backward compatibility with existing installations
+    provider = state.led_provider
+    if not provider or provider == "none":
+        # If no provider set but we have IPs configured, auto-detect
+        if state.wled_ip:
+            provider = "wled"
+            state.led_provider = "wled"
+            state.save()
+            logger.info("Auto-detected WLED provider from existing configuration")
+        elif state.hyperion_ip:
+            provider = "hyperion"
+            state.led_provider = "hyperion"
+            state.save()
+            logger.info("Auto-detected Hyperion provider from existing configuration")
+        else:
+            provider = "none"
+
+    return {
+        "success": True,
+        "provider": provider,
+        "wled_ip": state.wled_ip,
+        "hyperion_ip": state.hyperion_ip,
+        "hyperion_port": state.hyperion_port,
+        "hyperion_idle_effect": state.hyperion_idle_effect,
+        "hyperion_playing_effect": state.hyperion_playing_effect
+    }
+
 @app.post("/skip_pattern")
 async def skip_pattern():
     if not state.current_playlist:
@@ -1288,6 +1402,168 @@ async def image2sand(request: Request):
 async def wled(request: Request):
     return templates.TemplateResponse("wled.html", {"request": request, "app_name": state.app_name})
 
+# Hyperion control endpoints
+@app.get("/api/hyperion/status")
+async def hyperion_status():
+    """Get Hyperion connection status"""
+    if not state.led_controller or state.led_provider != "hyperion":
+        raise HTTPException(status_code=400, detail="Hyperion not configured")
+
+    try:
+        status = state.led_controller.check_status()
+        return status
+    except Exception as e:
+        logger.error(f"Failed to check Hyperion status: {str(e)}")
+        return {"connected": False, "message": str(e)}
+
+@app.post("/api/hyperion/power")
+async def hyperion_power(request: dict):
+    """Control Hyperion power state"""
+    if not state.led_controller or state.led_provider != "hyperion":
+        raise HTTPException(status_code=400, detail="Hyperion not configured")
+
+    state_value = request.get("state", 1)
+    if state_value not in [0, 1, 2]:
+        raise HTTPException(status_code=400, detail="State must be 0 (off), 1 (on), or 2 (toggle)")
+
+    try:
+        result = state.led_controller.set_power(state_value)
+        return result
+    except Exception as e:
+        logger.error(f"Failed to set Hyperion power: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/hyperion/brightness")
+async def hyperion_brightness(request: dict):
+    """Set Hyperion brightness"""
+    if not state.led_controller or state.led_provider != "hyperion":
+        raise HTTPException(status_code=400, detail="Hyperion not configured")
+
+    value = request.get("value", 100)
+    if not 0 <= value <= 100:
+        raise HTTPException(status_code=400, detail="Brightness must be between 0 and 100")
+
+    try:
+        controller = state.led_controller.get_controller()
+        result = controller.set_brightness(value)
+        return result
+    except Exception as e:
+        logger.error(f"Failed to set Hyperion brightness: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/hyperion/color")
+async def hyperion_color(request: dict):
+    """Set Hyperion color"""
+    if not state.led_controller or state.led_provider != "hyperion":
+        raise HTTPException(status_code=400, detail="Hyperion not configured")
+
+    hex_color = request.get("hex")
+    r = request.get("r")
+    g = request.get("g")
+    b = request.get("b")
+
+    if not hex_color and (r is None or g is None or b is None):
+        raise HTTPException(status_code=400, detail="Either hex or RGB values required")
+
+    try:
+        controller = state.led_controller.get_controller()
+
+        # Convert hex to RGB if hex was provided
+        if hex_color:
+            hex_color = hex_color.lstrip('#')
+            if len(hex_color) != 6:
+                raise HTTPException(status_code=400, detail="Hex color must be 6 characters")
+            r = int(hex_color[0:2], 16)
+            g = int(hex_color[2:4], 16)
+            b = int(hex_color[4:6], 16)
+
+        result = controller.set_color(r=r, g=g, b=b)
+        return result
+    except ValueError as e:
+        logger.error(f"Failed to parse hex color: {str(e)}")
+        raise HTTPException(status_code=400, detail="Invalid hex color format")
+    except Exception as e:
+        logger.error(f"Failed to set Hyperion color: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/hyperion/clear")
+async def hyperion_clear(request: dict):
+    """Clear Hyperion priority"""
+    if not state.led_controller or state.led_provider != "hyperion":
+        raise HTTPException(status_code=400, detail="Hyperion not configured")
+
+    try:
+        controller = state.led_controller.get_controller()
+        result = controller.clear_priority()
+        return result
+    except Exception as e:
+        logger.error(f"Failed to clear Hyperion priority: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.get("/api/hyperion/effects")
+async def hyperion_effects():
+    """Get list of available Hyperion effects"""
+    if not state.led_controller or state.led_provider != "hyperion":
+        raise HTTPException(status_code=400, detail="Hyperion not configured")
+
+    try:
+        import requests as req
+        response = req.post(
+            f"http://{state.hyperion_ip}:{state.hyperion_port}/json-rpc",
+            json={"command": "serverinfo"},
+            timeout=2
+        )
+        data = response.json()
+        effects = data.get('info', {}).get('effects', [])
+
+        # Return sorted list of effects
+        effects_list = [{"name": e.get("name"), "args": e.get("args", {})} for e in effects]
+        effects_list.sort(key=lambda x: x["name"])
+
+        return {"success": True, "effects": effects_list}
+    except Exception as e:
+        logger.error(f"Failed to get Hyperion effects: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/hyperion/effect")
+async def hyperion_effect(request: dict):
+    """Set Hyperion effect"""
+    if not state.led_controller or state.led_provider != "hyperion":
+        raise HTTPException(status_code=400, detail="Hyperion not configured")
+
+    effect_name = request.get("effect_name")
+    effect_args = request.get("args", {})
+
+    if not effect_name:
+        raise HTTPException(status_code=400, detail="effect_name required")
+
+    try:
+        controller = state.led_controller.get_controller()
+        result = controller.set_effect(effect_name, effect_args)
+        return result
+    except Exception as e:
+        logger.error(f"Failed to set Hyperion effect: {str(e)}")
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/api/hyperion/set_effects")
+async def hyperion_set_effects(request: dict):
+    """Configure idle and playing effects for Hyperion"""
+    idle_effect = request.get("idle_effect")
+    playing_effect = request.get("playing_effect")
+
+    # Allow None/empty string to clear the setting
+    state.hyperion_idle_effect = idle_effect if idle_effect else None
+    state.hyperion_playing_effect = playing_effect if playing_effect else None
+
+    state.save()
+    logger.info(f"Hyperion effects configured - Idle: {state.hyperion_idle_effect}, Playing: {state.hyperion_playing_effect}")
+
+    return {
+        "success": True,
+        "idle_effect": state.hyperion_idle_effect,
+        "playing_effect": state.hyperion_playing_effect
+    }
+
 @app.get("/table_control")
 async def table_control(request: Request):
     return templates.TemplateResponse("table_control.html", {"request": request, "app_name": state.app_name})

+ 16 - 15
modules/core/pattern_manager.py

@@ -11,6 +11,7 @@ from modules.core.state import state
 from math import pi
 import asyncio
 import json
+# Import for legacy support, but we'll use LED interface through state
 from modules.led.led_controller import effect_playing, effect_idle
 import queue
 from dataclasses import dataclass
@@ -634,7 +635,7 @@ async def run_theta_rho_file(file_path, is_playlist=False):
         
         start_time = time.time()
         if state.led_controller:
-            effect_playing(state.led_controller)
+            state.led_controller.effect_playing(state.hyperion_playing_effect)
             
         with tqdm(
             total=total_coordinates,
@@ -649,14 +650,14 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                 if state.stop_requested:
                     logger.info("Execution stopped by user")
                     if state.led_controller:
-                        effect_idle(state.led_controller)
+                        state.led_controller.effect_idle(state.hyperion_idle_effect)
                     break
                 
                 if state.skip_requested:
                     logger.info("Skipping pattern...")
                     await connection_manager.check_idle_async()
                     if state.led_controller:
-                        effect_idle(state.led_controller)
+                        state.led_controller.effect_idle(state.hyperion_idle_effect)
                     break
 
                 # Wait for resume if paused (manual or scheduled)
@@ -670,17 +671,17 @@ async def run_theta_rho_file(file_path, is_playlist=False):
                         logger.info("Execution paused (manual)...")
                     else:
                         logger.info("Execution paused (scheduled pause period)...")
-                        # Turn off WLED if scheduled pause and control_wled is enabled
+                        # Turn off LED controller if scheduled pause and control_wled is enabled
                         if state.scheduled_pause_control_wled and state.led_controller:
-                            logger.info("Turning off WLED lights during Still Sands period")
+                            logger.info("Turning off LED lights during Still Sands period")
                             state.led_controller.set_power(0)
 
-                    # Only show idle effect if NOT in scheduled pause with WLED control
+                    # Only show idle effect if NOT in scheduled pause with LED control
                     # (manual pause always shows idle effect)
                     if state.led_controller and not (scheduled_pause and state.scheduled_pause_control_wled):
-                        effect_idle(state.led_controller)
+                        state.led_controller.effect_idle(state.hyperion_idle_effect)
 
-                    # Remember if we turned off WLED for scheduled pause
+                    # Remember if we turned off LED controller for scheduled pause
                     wled_was_off_for_scheduled = scheduled_pause and state.scheduled_pause_control_wled and not manual_pause
 
                     # Wait until both manual pause is released AND we're outside scheduled pause period
@@ -692,11 +693,11 @@ async def run_theta_rho_file(file_path, is_playlist=False):
 
                     logger.info("Execution resumed...")
                     if state.led_controller:
-                        # Turn WLED back on if it was turned off for scheduled pause
+                        # Turn LED controller back on if it was turned off for scheduled pause
                         if wled_was_off_for_scheduled:
-                            logger.info("Turning WLED lights back on as Still Sands period ended")
+                            logger.info("Turning LED lights back on as Still Sands period ended")
                             state.led_controller.set_power(1)
-                        effect_playing(state.led_controller)
+                        state.led_controller.effect_playing(state.hyperion_playing_effect)
 
                 # Dynamically determine the speed for each movement
                 # Use clear_pattern_speed if it's set and this is a clear file, otherwise use state.speed
@@ -730,7 +731,7 @@ async def run_theta_rho_file(file_path, is_playlist=False):
         
         # Set LED back to idle when pattern completes normally (not stopped early)
         if state.led_controller and not state.stop_requested:
-            effect_idle(state.led_controller)
+            state.led_controller.effect_idle(state.hyperion_idle_effect)
             logger.debug("LED effect set to idle after pattern completion")
         
         # Only clear state if not part of a playlist
@@ -866,10 +867,10 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
         state.current_playlist = None
         state.current_playlist_index = None
         state.playlist_mode = None
-        
+
         if state.led_controller:
-            effect_idle(state.led_controller)
-        
+            state.led_controller.effect_idle(state.hyperion_idle_effect)
+
         logger.info("All requested patterns completed (or stopped) and state cleared")
 
 async def stop_actions(clear_playlist = True, wait_for_lock = True):

+ 15 - 0
modules/core/state.py

@@ -40,7 +40,12 @@ class AppState:
         self.conn = None
         self.port = None
         self.wled_ip = None
+        self.hyperion_ip = None
+        self.hyperion_port = 8090
+        self.led_provider = "none"  # "wled", "hyperion", or "none"
         self.led_controller = None
+        self.hyperion_idle_effect = None  # Effect to show when idle (None = clear priority)
+        self.hyperion_playing_effect = None  # Effect to show when playing (None = clear priority)
         self.skip_requested = False
         self.table_type = None
         self._playlist_mode = "loop"
@@ -190,6 +195,11 @@ class AppState:
             "custom_clear_from_out": self.custom_clear_from_out,
             "port": self.port,
             "wled_ip": self.wled_ip,
+            "hyperion_ip": self.hyperion_ip,
+            "hyperion_port": self.hyperion_port,
+            "led_provider": self.led_provider,
+            "hyperion_idle_effect": self.hyperion_idle_effect,
+            "hyperion_playing_effect": self.hyperion_playing_effect,
             "app_name": self.app_name,
             "auto_play_enabled": self.auto_play_enabled,
             "auto_play_playlist": self.auto_play_playlist,
@@ -229,6 +239,11 @@ class AppState:
         self.custom_clear_from_out = data.get("custom_clear_from_out", None)
         self.port = data.get("port", None)
         self.wled_ip = data.get('wled_ip', None)
+        self.hyperion_ip = data.get('hyperion_ip', None)
+        self.hyperion_port = data.get('hyperion_port', 8090)
+        self.led_provider = data.get('led_provider', "none")
+        self.hyperion_idle_effect = data.get('hyperion_idle_effect', None)
+        self.hyperion_playing_effect = data.get('hyperion_playing_effect', None)
         self.app_name = data.get("app_name", "Dune Weaver")
         self.auto_play_enabled = data.get("auto_play_enabled", False)
         self.auto_play_playlist = data.get("auto_play_playlist", None)

+ 220 - 0
modules/led/hyperion_controller.py

@@ -0,0 +1,220 @@
+import requests
+import json
+from typing import Dict, Optional
+import time
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class HyperionController:
+    """Controller for Hyperion LED system using JSON-RPC API"""
+
+    def __init__(self, ip_address: Optional[str] = None, port: int = 8090):
+        self.ip_address = ip_address
+        self.port = port
+        # Priority for Dune Weaver effects (lower = higher priority)
+        # Using 100 to allow user to override with lower priorities if needed
+        self.priority = 100
+
+    def _get_base_url(self) -> str:
+        """Get base URL for Hyperion JSON-RPC API"""
+        if not self.ip_address:
+            raise ValueError("No Hyperion IP configured")
+        return f"http://{self.ip_address}:{self.port}/json-rpc"
+
+    def set_ip(self, ip_address: str, port: int = 8090) -> None:
+        """Update the Hyperion IP address and port"""
+        self.ip_address = ip_address
+        self.port = port
+
+    def _send_command(self, command: str, **params) -> Dict:
+        """Send JSON-RPC command to Hyperion and return response"""
+        try:
+            url = self._get_base_url()
+
+            payload = {
+                "command": command,
+                **params
+            }
+
+            response = requests.post(url, json=payload, timeout=2)
+            response.raise_for_status()
+            result = response.json()
+
+            if not result.get("success", False):
+                error_msg = result.get("error", "Unknown error")
+                return {
+                    "connected": False,
+                    "message": f"Hyperion command failed: {error_msg}"
+                }
+
+            return {
+                "connected": True,
+                "message": "Command successful",
+                "response": result
+            }
+
+        except ValueError as e:
+            return {"connected": False, "message": str(e)}
+        except requests.RequestException as e:
+            return {"connected": False, "message": f"Cannot connect to Hyperion: {str(e)}"}
+        except json.JSONDecodeError as e:
+            return {"connected": False, "message": f"Error parsing Hyperion response: {str(e)}"}
+
+    def check_hyperion_status(self) -> Dict:
+        """Check Hyperion connection status and component state"""
+        result = self._send_command("serverinfo")
+
+        if result.get("connected"):
+            response = result.get("response", {})
+            info = response.get("info", {})
+            components = {c["name"]: c["enabled"] for c in info.get("components", [])}
+
+            return {
+                "connected": True,
+                "is_on": components.get("ALL", False),
+                "ledstream_on": components.get("LEDDEVICE", False),
+                "hostname": info.get("hostname", "unknown"),
+                "version": info.get("version", "unknown"),
+                "message": "Hyperion is ON" if components.get("ALL", False) else "Hyperion is OFF"
+            }
+
+        return result
+
+    def set_power(self, state: int) -> Dict:
+        """
+        Set Hyperion power state (component control)
+        Args:
+            state: 0=Off, 1=On, 2=Toggle
+        """
+        if state not in [0, 1, 2]:
+            return {"connected": False, "message": "Power state must be 0 (Off), 1 (On), or 2 (Toggle)"}
+
+        if state == 2:
+            # Get current state and toggle
+            status = self.check_hyperion_status()
+            if not status.get("connected"):
+                return status
+            state = 0 if status.get("is_on", False) else 1
+
+        result = self._send_command(
+            "componentstate",
+            componentstate={
+                "component": "ALL",
+                "state": bool(state)
+            }
+        )
+
+        return result
+
+    def set_color(self, r: int = 0, g: int = 0, b: int = 0, duration: int = 86400000) -> Dict:
+        """
+        Set solid color on Hyperion
+        Args:
+            r, g, b: RGB values (0-255)
+            duration: Duration in milliseconds (default = 86400000ms = 24 hours)
+                     Note: Some Hyperion instances don't support duration=0 for infinite
+        """
+        if not all(0 <= val <= 255 for val in [r, g, b]):
+            return {"connected": False, "message": "RGB values must be between 0 and 255"}
+
+        result = self._send_command(
+            "color",
+            priority=self.priority,
+            color=[r, g, b],
+            duration=duration
+        )
+
+        return result
+
+    def set_effect(self, effect_name: str, args: Optional[Dict] = None, duration: int = 86400000) -> Dict:
+        """
+        Set Hyperion effect
+        Args:
+            effect_name: Name of the effect (e.g., 'Rainbow swirl', 'Warm mood blobs')
+            args: Optional effect arguments
+            duration: Duration in milliseconds (default = 86400000ms = 24 hours)
+        """
+        params = {
+            "priority": self.priority,
+            "effect": {"name": effect_name},
+            "duration": duration
+        }
+
+        if args:
+            params["effect"]["args"] = args
+
+        result = self._send_command("effect", **params)
+        return result
+
+    def clear_priority(self, priority: Optional[int] = None) -> Dict:
+        """
+        Clear a specific priority or Dune Weaver's priority
+        Args:
+            priority: Priority to clear (defaults to self.priority)
+        """
+        if priority is None:
+            priority = self.priority
+
+        result = self._send_command("clear", priority=priority)
+        return result
+
+    def clear_all(self) -> Dict:
+        """Clear all priorities (return to default state)"""
+        result = self._send_command("clear", priority=-1)
+        return result
+
+    def set_brightness(self, value: int) -> Dict:
+        """
+        Set Hyperion brightness
+        Args:
+            value: Brightness (0-100)
+        """
+        if not 0 <= value <= 100:
+            return {"connected": False, "message": "Brightness must be between 0 and 100"}
+
+        result = self._send_command(
+            "adjustment",
+            adjustment={
+                "brightness": value
+            }
+        )
+
+        return result
+
+
+def effect_loading(hyperion_controller: HyperionController) -> bool:
+    """Show loading effect - orange color (24 hour duration)"""
+    res = hyperion_controller.set_color(r=255, g=160, b=0, duration=86400000)
+    return res.get('connected', False)
+
+
+def effect_idle(hyperion_controller: HyperionController, effect_name: str = None) -> bool:
+    """Show idle effect - use configured effect or clear priority to return to default"""
+    if effect_name:
+        res = hyperion_controller.set_effect(effect_name)
+    else:
+        res = hyperion_controller.clear_priority()
+    return res.get('connected', False)
+
+
+def effect_connected(hyperion_controller: HyperionController) -> bool:
+    """Show connected effect - green flash"""
+    # Flash green twice with explicit 1 second durations
+    res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
+    time.sleep(1.2)  # Wait for flash to complete
+    res = hyperion_controller.set_color(r=8, g=255, b=0, duration=1000)
+    time.sleep(1.2)  # Wait for flash to complete
+    effect_idle(hyperion_controller)
+    return res.get('connected', False)
+
+
+def effect_playing(hyperion_controller: HyperionController, effect_name: str = None) -> bool:
+    """Show playing effect - use configured effect or clear to show default"""
+    if effect_name:
+        res = hyperion_controller.set_effect(effect_name)
+    else:
+        # Clear priority to show the user's configured effect/color
+        res = hyperion_controller.clear_priority()
+    return res.get('connected', False)

+ 111 - 0
modules/led/led_interface.py

@@ -0,0 +1,111 @@
+"""
+Unified LED interface for different LED control systems (WLED, Hyperion, etc.)
+Provides a common abstraction layer for pattern manager integration.
+"""
+from typing import Optional, Literal
+from modules.led.led_controller import LEDController, effect_loading as wled_loading, effect_idle as wled_idle, effect_connected as wled_connected, effect_playing as wled_playing
+from modules.led.hyperion_controller import HyperionController, effect_loading as hyperion_loading, effect_idle as hyperion_idle, effect_connected as hyperion_connected, effect_playing as hyperion_playing
+
+
+LEDProviderType = Literal["wled", "hyperion", "none"]
+
+
+class LEDInterface:
+    """
+    Unified interface for LED control that works with multiple backends.
+    Automatically delegates to the appropriate controller based on configuration.
+    """
+
+    def __init__(self, provider: LEDProviderType = "none", ip_address: Optional[str] = None, port: Optional[int] = None):
+        self.provider = provider
+        self._controller = None
+
+        if provider == "wled" and ip_address:
+            self._controller = LEDController(ip_address)
+        elif provider == "hyperion" and ip_address:
+            port = port or 8090  # Default Hyperion port
+            self._controller = HyperionController(ip_address, port)
+
+    @property
+    def is_configured(self) -> bool:
+        """Check if LED controller is configured"""
+        return self._controller is not None
+
+    def update_config(self, provider: LEDProviderType, ip_address: Optional[str] = None, port: Optional[int] = None):
+        """Update LED provider configuration"""
+        self.provider = provider
+
+        if provider == "wled" and ip_address:
+            self._controller = LEDController(ip_address)
+        elif provider == "hyperion" and ip_address:
+            port = port or 8090
+            self._controller = HyperionController(ip_address, port)
+        else:
+            self._controller = None
+
+    def effect_loading(self) -> bool:
+        """Show loading effect"""
+        if not self.is_configured:
+            return False
+
+        if self.provider == "wled":
+            return wled_loading(self._controller)
+        elif self.provider == "hyperion":
+            return hyperion_loading(self._controller)
+        return False
+
+    def effect_idle(self, effect_name: Optional[str] = None) -> bool:
+        """Show idle effect"""
+        if not self.is_configured:
+            return False
+
+        if self.provider == "wled":
+            return wled_idle(self._controller)
+        elif self.provider == "hyperion":
+            return hyperion_idle(self._controller, effect_name)
+        return False
+
+    def effect_connected(self) -> bool:
+        """Show connected effect"""
+        if not self.is_configured:
+            return False
+
+        if self.provider == "wled":
+            return wled_connected(self._controller)
+        elif self.provider == "hyperion":
+            return hyperion_connected(self._controller)
+        return False
+
+    def effect_playing(self, effect_name: Optional[str] = None) -> bool:
+        """Show playing effect"""
+        if not self.is_configured:
+            return False
+
+        if self.provider == "wled":
+            return wled_playing(self._controller)
+        elif self.provider == "hyperion":
+            return hyperion_playing(self._controller, effect_name)
+        return False
+
+    def set_power(self, state: int) -> dict:
+        """Set power state (0=Off, 1=On, 2=Toggle)"""
+        if not self.is_configured:
+            return {"connected": False, "message": "No LED controller configured"}
+
+        return self._controller.set_power(state)
+
+    def check_status(self) -> dict:
+        """Check controller status"""
+        if not self.is_configured:
+            return {"connected": False, "message": "No LED controller configured"}
+
+        if self.provider == "wled":
+            return self._controller.check_wled_status()
+        elif self.provider == "hyperion":
+            return self._controller.check_hyperion_status()
+
+        return {"connected": False, "message": "Unknown provider"}
+
+    def get_controller(self):
+        """Get the underlying controller instance (for advanced usage)"""
+        return self._controller

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/css/tailwind.css


+ 25 - 0
static/js/base.js

@@ -1,5 +1,30 @@
 // Player status bar functionality - Updated to fix logMessage errors
 
+// Update LED nav label based on provider
+async function updateLedNavLabel() {
+    try {
+        const response = await fetch('/get_led_config');
+        if (response.ok) {
+            const data = await response.json();
+            const navLabel = document.getElementById('led-nav-label');
+            if (navLabel) {
+                if (data.provider === 'wled') {
+                    navLabel.textContent = 'WLED';
+                } else if (data.provider === 'hyperion') {
+                    navLabel.textContent = 'Hyperion';
+                } else {
+                    navLabel.textContent = 'LED';
+                }
+            }
+        }
+    } catch (error) {
+        console.error('Error updating LED nav label:', error);
+    }
+}
+
+// Call on page load
+document.addEventListener('DOMContentLoaded', updateLedNavLabel);
+
 // Pattern files cache for improved performance with localStorage persistence
 const PATTERN_CACHE_KEY = 'dune_weaver_pattern_files_cache';
 const PATTERN_CACHE_EXPIRY = 30 * 60 * 1000; // 30 minutes cache (longer since it persists)

+ 326 - 0
static/js/led-control.js

@@ -0,0 +1,326 @@
+// LED Control Page - Unified interface for WLED and Hyperion
+
+let ledConfig = null;
+let hyperionController = null;
+
+// Utility function to show status messages
+function showStatus(message, type = 'info') {
+    const statusDiv = document.getElementById('hyperion-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 = `
+        <div class="flex items-center gap-2">
+            <span class="material-icons">${icon}</span>
+            <span class="text-sm">${message}</span>
+        </div>
+    `;
+}
+
+// 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 hyperionContainer = document.getElementById('hyperion-container');
+
+        // Hide all containers first
+        notConfigured.classList.add('hidden');
+        wledContainer.classList.add('hidden');
+        hyperionContainer.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 === 'hyperion' && ledConfig.hyperion_ip) {
+            // Show Hyperion controls
+            hyperionContainer.classList.remove('hidden');
+            await initializeHyperionControls();
+        } 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 Hyperion controls
+async function initializeHyperionControls() {
+    // Create API helper
+    hyperionController = {
+        async sendCommand(endpoint, data) {
+            try {
+                const response = await fetch(`/api/hyperion/${endpoint}`, {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify(data)
+                });
+
+                if (!response.ok) throw new Error(`HTTP ${response.status}`);
+                return await response.json();
+            } catch (error) {
+                throw error;
+            }
+        }
+    };
+
+    // Check connection status and load effects
+    await checkHyperionStatus();
+    await loadEffectsList();
+
+    // Power toggle button
+    document.getElementById('hyperion-power-toggle')?.addEventListener('click', async () => {
+        try {
+            // Toggle using state 2
+            await hyperionController.sendCommand('power', { state: 2 });
+            showStatus('Power toggled', 'success');
+            await checkHyperionStatus();
+        } catch (error) {
+            showStatus(`Failed to toggle power: ${error.message}`, 'error');
+        }
+    });
+
+    // Brightness slider
+    const brightnessSlider = document.getElementById('hyperion-brightness');
+    const brightnessValue = document.getElementById('brightness-value');
+
+    brightnessSlider?.addEventListener('input', (e) => {
+        brightnessValue.textContent = `${e.target.value}%`;
+    });
+
+    brightnessSlider?.addEventListener('change', async (e) => {
+        try {
+            await hyperionController.sendCommand('brightness', { value: parseInt(e.target.value) });
+            showStatus(`Brightness set to ${e.target.value}%`, 'success');
+        } catch (error) {
+            showStatus(`Failed to set brightness: ${error.message}`, 'error');
+        }
+    });
+
+    // Color picker - update display when color changes
+    const colorPicker = document.getElementById('hyperion-color');
+    const colorHexDisplay = document.getElementById('color-hex-display');
+
+    colorPicker?.addEventListener('input', (e) => {
+        if (colorHexDisplay) {
+            colorHexDisplay.textContent = e.target.value.toUpperCase();
+        }
+    });
+
+    // Color picker - apply button
+    document.getElementById('hyperion-set-color')?.addEventListener('click', async () => {
+        const hexColor = colorPicker.value;
+
+        try {
+            await hyperionController.sendCommand('color', { hex: hexColor });
+            showStatus(`Color set to ${hexColor.toUpperCase()}`, 'success');
+        } catch (error) {
+            showStatus(`Failed to set color: ${error.message}`, 'error');
+        }
+    });
+
+    // Quick color buttons
+    document.querySelectorAll('.quick-color').forEach(button => {
+        button.addEventListener('click', async () => {
+            const hexColor = button.getAttribute('data-color');
+
+            try {
+                await hyperionController.sendCommand('color', { hex: hexColor });
+                showStatus(`Color set to ${hexColor.toUpperCase()}`, 'success');
+
+                // Update color picker and hex display to match
+                const colorPicker = document.getElementById('hyperion-color');
+                const colorHexDisplay = document.getElementById('color-hex-display');
+                if (colorPicker) colorPicker.value = hexColor;
+                if (colorHexDisplay) colorHexDisplay.textContent = hexColor.toUpperCase();
+            } catch (error) {
+                showStatus(`Failed to set color: ${error.message}`, 'error');
+            }
+        });
+    });
+
+    // Effects selection
+    document.getElementById('hyperion-set-effect')?.addEventListener('click', async () => {
+        const effectSelect = document.getElementById('hyperion-effect-select');
+        const effectName = effectSelect.value;
+
+        if (!effectName) {
+            showStatus('Please select an effect', 'warning');
+            return;
+        }
+
+        try {
+            await hyperionController.sendCommand('effect', { effect_name: effectName });
+            showStatus(`Effect '${effectName}' activated`, 'success');
+        } catch (error) {
+            showStatus(`Failed to set effect: ${error.message}`, 'error');
+        }
+    });
+
+    // Clear priority button
+    document.getElementById('hyperion-clear-priority')?.addEventListener('click', async () => {
+        try {
+            await hyperionController.sendCommand('clear', {});
+            showStatus('Priority cleared - returned to default state', 'success');
+        } catch (error) {
+            showStatus(`Failed to clear priority: ${error.message}`, 'error');
+        }
+    });
+
+    // Save effect settings button
+    document.getElementById('save-hyperion-effects')?.addEventListener('click', async () => {
+        try {
+            const idleEffect = document.getElementById('hyperion-idle-effect')?.value || '';
+            const playingEffect = document.getElementById('hyperion-playing-effect')?.value || '';
+
+            const response = await fetch('/api/hyperion/set_effects', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({
+                    idle_effect: idleEffect,
+                    playing_effect: playingEffect
+                })
+            });
+
+            if (!response.ok) throw new Error(`HTTP ${response.status}`);
+
+            const result = await response.json();
+            showStatus('Effect settings saved successfully', 'success');
+        } catch (error) {
+            showStatus(`Failed to save effect settings: ${error.message}`, 'error');
+        }
+    });
+}
+
+// Load available Hyperion effects
+async function loadEffectsList() {
+    try {
+        const response = await fetch('/api/hyperion/effects');
+        if (!response.ok) throw new Error(`HTTP ${response.status}`);
+
+        const data = await response.json();
+        const effectSelect = document.getElementById('hyperion-effect-select');
+        const idleEffectSelect = document.getElementById('hyperion-idle-effect');
+        const playingEffectSelect = document.getElementById('hyperion-playing-effect');
+
+        if (!effectSelect) return;
+
+        // Clear loading option
+        effectSelect.innerHTML = '<option value="">-- Select an effect --</option>';
+
+        // Add effects to all dropdowns
+        if (data.effects && data.effects.length > 0) {
+            data.effects.forEach(effect => {
+                // Main effect selector
+                const option = document.createElement('option');
+                option.value = effect.name;
+                option.textContent = effect.name;
+                effectSelect.appendChild(option);
+
+                // Idle effect selector
+                if (idleEffectSelect) {
+                    const idleOption = document.createElement('option');
+                    idleOption.value = effect.name;
+                    idleOption.textContent = effect.name;
+                    idleEffectSelect.appendChild(idleOption);
+                }
+
+                // Playing effect selector
+                if (playingEffectSelect) {
+                    const playingOption = document.createElement('option');
+                    playingOption.value = effect.name;
+                    playingOption.textContent = effect.name;
+                    playingEffectSelect.appendChild(playingOption);
+                }
+            });
+
+            // Load saved settings from config
+            const configResponse = await fetch('/get_led_config');
+            if (configResponse.ok) {
+                const config = await configResponse.json();
+                if (idleEffectSelect && config.hyperion_idle_effect) {
+                    idleEffectSelect.value = config.hyperion_idle_effect;
+                }
+                if (playingEffectSelect && config.hyperion_playing_effect) {
+                    playingEffectSelect.value = config.hyperion_playing_effect;
+                }
+            }
+        } else {
+            effectSelect.innerHTML = '<option value="">No effects available</option>';
+        }
+    } catch (error) {
+        console.error('Failed to load effects:', error);
+        const effectSelect = document.getElementById('hyperion-effect-select');
+        if (effectSelect) {
+            effectSelect.innerHTML = '<option value="">Failed to load effects</option>';
+        }
+    }
+}
+
+// Check Hyperion connection status
+async function checkHyperionStatus() {
+    try {
+        const response = await fetch('/api/hyperion/status');
+        if (!response.ok) throw new Error(`HTTP ${response.status}`);
+
+        const data = await response.json();
+
+        if (data.connected) {
+            const version = data.version || 'unknown';
+            const hostname = data.hostname || 'unknown';
+            const isOn = data.is_on;
+            const state = isOn ? 'ON' : 'OFF';
+
+            // Update power button appearance - shows current state with appropriate action
+            const powerButton = document.getElementById('hyperion-power-toggle');
+            const powerButtonText = document.getElementById('power-button-text');
+
+            if (powerButton && powerButtonText) {
+                if (isOn) {
+                    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';
+                }
+            }
+
+            showStatus(`Connected to ${hostname} (${version}) - Power: ${state}`, 'success');
+        } else {
+            showStatus(`Connection failed: ${data.message}`, 'error');
+        }
+    } catch (error) {
+        showStatus(`Cannot connect to Hyperion: ${error.message}`, 'error');
+    }
+}
+
+// Initialize on page load
+document.addEventListener('DOMContentLoaded', initializeLedPage);

+ 145 - 51
static/js/settings.js

@@ -151,6 +151,71 @@ function setWledButtonState(isSet) {
     }
 }
 
+// Handle LED provider selection and show/hide appropriate config sections
+function updateLedProviderUI() {
+    const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none';
+    const wledConfig = document.getElementById('wledConfig');
+    const hyperionConfig = document.getElementById('hyperionConfig');
+
+    if (wledConfig && hyperionConfig) {
+        if (provider === 'wled') {
+            wledConfig.classList.remove('hidden');
+            hyperionConfig.classList.add('hidden');
+        } else if (provider === 'hyperion') {
+            wledConfig.classList.add('hidden');
+            hyperionConfig.classList.remove('hidden');
+        } else {
+            wledConfig.classList.add('hidden');
+            hyperionConfig.classList.add('hidden');
+        }
+    }
+}
+
+// Load LED configuration from server
+async function loadLedConfig() {
+    try {
+        const response = await fetch('/get_led_config');
+        if (response.ok) {
+            const data = await response.json();
+
+            // Set provider radio button
+            const providerRadio = document.getElementById(`ledProvider${data.provider.charAt(0).toUpperCase() + data.provider.slice(1)}`);
+            if (providerRadio) {
+                providerRadio.checked = true;
+            } else {
+                document.getElementById('ledProviderNone').checked = true;
+            }
+
+            // Set WLED IP if configured
+            if (data.wled_ip) {
+                const wledIpInput = document.getElementById('wledIpInput');
+                if (wledIpInput) {
+                    wledIpInput.value = data.wled_ip;
+                }
+            }
+
+            // Set Hyperion IP and port if configured
+            if (data.hyperion_ip) {
+                const hyperionIpInput = document.getElementById('hyperionIpInput');
+                if (hyperionIpInput) {
+                    hyperionIpInput.value = data.hyperion_ip;
+                }
+            }
+            if (data.hyperion_port) {
+                const hyperionPortInput = document.getElementById('hyperionPortInput');
+                if (hyperionPortInput) {
+                    hyperionPortInput.value = data.hyperion_port;
+                }
+            }
+
+            // Update UI to show correct config section
+            updateLedProviderUI();
+        }
+    } catch (error) {
+        logMessage(`Error loading LED config: ${error.message}`, LOG_TYPE.ERROR);
+    }
+}
+
 // Initialize settings page
 document.addEventListener('DOMContentLoaded', async () => {
     // Initialize UI with default disconnected state
@@ -160,9 +225,9 @@ document.addEventListener('DOMContentLoaded', async () => {
     Promise.all([
         // Check connection status
         fetch('/serial_status').then(response => response.json()).catch(() => ({ connected: false })),
-        
-        // Load current WLED IP
-        fetch('/get_wled_ip').then(response => response.json()).catch(() => ({ wled_ip: null })),
+
+        // Load LED configuration (replaces old WLED-only loading)
+        fetch('/get_led_config').then(response => response.json()).catch(() => ({ provider: 'none', wled_ip: null, hyperion_ip: null, hyperion_port: 8090 })),
         
         // Load current version and check for updates
         fetch('/api/version').then(response => response.json()).catch(() => ({ current: '1.0.0', latest: '1.0.0', update_available: false })),
@@ -184,18 +249,35 @@ document.addEventListener('DOMContentLoaded', async () => {
 
         // Load Still Sands settings
         fetch('/api/scheduled-pause').then(response => response.json()).catch(() => ({ enabled: false, time_slots: [] }))
-    ]).then(([statusData, wledData, updateData, ports, patterns, clearPatterns, clearSpeedData, appNameData, scheduledPauseData]) => {
+    ]).then(([statusData, ledConfigData, updateData, ports, patterns, clearPatterns, clearSpeedData, appNameData, scheduledPauseData]) => {
         // Update connection status
         setCachedConnectionStatus(statusData);
         updateConnectionUI(statusData);
-        
-        // Update WLED IP
-        if (wledData.wled_ip) {
-            document.getElementById('wledIpInput').value = wledData.wled_ip;
-            setWledButtonState(true);
+
+        // Update LED configuration
+        const providerRadio = document.getElementById(`ledProvider${ledConfigData.provider.charAt(0).toUpperCase() + ledConfigData.provider.slice(1)}`);
+        if (providerRadio) {
+            providerRadio.checked = true;
         } else {
-            setWledButtonState(false);
+            document.getElementById('ledProviderNone').checked = true;
+        }
+
+        if (ledConfigData.wled_ip) {
+            const wledIpInput = document.getElementById('wledIpInput');
+            if (wledIpInput) wledIpInput.value = ledConfigData.wled_ip;
         }
+
+        if (ledConfigData.hyperion_ip) {
+            const hyperionIpInput = document.getElementById('hyperionIpInput');
+            if (hyperionIpInput) hyperionIpInput.value = ledConfigData.hyperion_ip;
+        }
+
+        if (ledConfigData.hyperion_port) {
+            const hyperionPortInput = document.getElementById('hyperionPortInput');
+            if (hyperionPortInput) hyperionPortInput.value = ledConfigData.hyperion_port;
+        }
+
+        updateLedProviderUI()
         
         // Update version display
         const currentVersionText = document.getElementById('currentVersionText');
@@ -359,50 +441,62 @@ function setupEventListeners() {
         });
     }
     
-    // Save/Clear WLED configuration
-    const saveWledConfig = document.getElementById('saveWledConfig');
-    const wledIpInput = document.getElementById('wledIpInput');
-    if (saveWledConfig && wledIpInput) {
-        saveWledConfig.addEventListener('click', async () => {
-            if (saveWledConfig.textContent.includes('Clear')) {
-                // Clear WLED IP
-                wledIpInput.value = '';
-                try {
-                    const response = await fetch('/set_wled_ip', {
-                        method: 'POST',
-                        headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify({ wled_ip: '' })
-                    });
-                    if (response.ok) {
-                        setWledButtonState(false);
-                        localStorage.removeItem('wled_ip');
-                        showStatusMessage('WLED IP cleared successfully', 'success');
-                    } else {
-                        throw new Error('Failed to clear WLED IP');
-                    }
-                } catch (error) {
-                    showStatusMessage(`Failed to clear WLED IP: ${error.message}`, 'error');
+    // LED provider selection change handlers
+    const ledProviderRadios = document.querySelectorAll('input[name="ledProvider"]');
+    ledProviderRadios.forEach(radio => {
+        radio.addEventListener('change', updateLedProviderUI);
+    });
+
+    // Save LED configuration
+    const saveLedConfig = document.getElementById('saveLedConfig');
+    if (saveLedConfig) {
+        saveLedConfig.addEventListener('click', async () => {
+            const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none';
+
+            let requestBody = { provider };
+
+            if (provider === 'wled') {
+                const wledIp = document.getElementById('wledIpInput')?.value;
+                if (!wledIp) {
+                    showStatusMessage('Please enter a WLED IP address', 'error');
+                    return;
                 }
-            } else {
-                // Save WLED IP
-                const wledIp = wledIpInput.value;
-                try {
-                    const response = await fetch('/set_wled_ip', {
-                        method: 'POST',
-                        headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify({ wled_ip: wledIp })
-                    });
-                    if (response.ok && wledIp) {
-                        setWledButtonState(true);
-                        localStorage.setItem('wled_ip', wledIp);
-                        showStatusMessage('WLED IP configured successfully', 'success');
-                    } else {
-                        setWledButtonState(false);
-                        throw new Error('Failed to save WLED configuration');
+                requestBody.ip_address = wledIp;
+            } else if (provider === 'hyperion') {
+                const hyperionIp = document.getElementById('hyperionIpInput')?.value;
+                const hyperionPort = parseInt(document.getElementById('hyperionPortInput')?.value) || 8090;
+                if (!hyperionIp) {
+                    showStatusMessage('Please enter a Hyperion IP address', 'error');
+                    return;
+                }
+                requestBody.ip_address = hyperionIp;
+                requestBody.port = hyperionPort;
+            }
+
+            try {
+                const response = await fetch('/set_led_config', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify(requestBody)
+                });
+
+                if (response.ok) {
+                    const data = await response.json();
+
+                    if (provider === 'wled' && data.wled_ip) {
+                        localStorage.setItem('wled_ip', data.wled_ip);
+                        showStatusMessage('WLED configured successfully', 'success');
+                    } else if (provider === 'hyperion' && data.hyperion_ip) {
+                        showStatusMessage('Hyperion configured successfully', 'success');
+                    } else if (provider === 'none') {
+                        localStorage.removeItem('wled_ip');
+                        showStatusMessage('LED controller disabled', 'success');
                     }
-                } catch (error) {
-                    showStatusMessage(`Failed to save WLED IP: ${error.message}`, 'error');
+                } else {
+                    throw new Error('Failed to save LED configuration');
                 }
+            } catch (error) {
+                showStatusMessage(`Failed to save LED configuration: ${error.message}`, 'error');
             }
         });
     }

+ 1 - 1
templates/base.html

@@ -311,7 +311,7 @@
             class="{% if request.url.path == '/wled' %}active-tab{% else %}inactive-tab{% endif %} flex flex-1 flex-col items-center justify-center gap-1 border-b-[3px] py-3 text-xs font-medium sm:text-sm"
             href="/wled"
           >
-            <span class="material-icons">lightbulb</span> WLED
+            <span class="material-icons">lightbulb</span> <span id="led-nav-label">LED</span>
           </a>
             <a
               class="{% if request.url.path == '/settings' %}active-tab{% else %}inactive-tab{% endif %} flex flex-1 flex-col items-center justify-center gap-1 border-b-[3px] py-3 text-xs font-medium sm:text-sm"

+ 79 - 18
templates/settings.html

@@ -472,14 +472,35 @@ input:checked + .slider:before {
     <h2
       class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
     >
-      WLED Configuration
+      LED Controller Configuration
     </h2>
     <div class="px-6 py-5 space-y-6">
-      <label class="flex flex-col gap-1.5">
-        <span class="text-slate-700 text-sm font-medium leading-normal"
-          >IP Address</span
-        >
-        <div class="flex gap-3 items-center">
+      <!-- LED Provider Selection -->
+      <div class="flex flex-col gap-2">
+        <span class="text-slate-700 text-sm font-medium leading-normal">LED Provider</span>
+        <div class="flex gap-3">
+          <label class="flex items-center gap-2 cursor-pointer">
+            <input type="radio" name="ledProvider" value="none" id="ledProviderNone" class="w-4 h-4 text-sky-600 border-slate-300 focus:ring-sky-500">
+            <span class="text-sm text-slate-700">None</span>
+          </label>
+          <label class="flex items-center gap-2 cursor-pointer">
+            <input type="radio" name="ledProvider" value="wled" id="ledProviderWled" class="w-4 h-4 text-sky-600 border-slate-300 focus:ring-sky-500">
+            <span class="text-sm text-slate-700">WLED</span>
+          </label>
+          <label class="flex items-center gap-2 cursor-pointer">
+            <input type="radio" name="ledProvider" value="hyperion" id="ledProviderHyperion" class="w-4 h-4 text-sky-600 border-slate-300 focus:ring-sky-500">
+            <span class="text-sm text-slate-700">Hyperion</span>
+          </label>
+        </div>
+        <p class="text-xs text-slate-500">
+          Select your LED control system (settings are mutually exclusive)
+        </p>
+      </div>
+
+      <!-- WLED Configuration (shown when WLED is selected) -->
+      <div id="wledConfig" class="flex flex-col gap-4 hidden">
+        <label class="flex flex-col gap-1.5">
+          <span class="text-slate-700 text-sm font-medium leading-normal">WLED IP Address</span>
           <div class="relative flex-1">
             <input
               id="wledIpInput"
@@ -496,18 +517,58 @@ input:checked + .slider:before {
               <span class="material-icons">close</span>
             </button>
           </div>
-          <button
-            id="saveWledConfig"
-            class="flex items-center justify-center gap-2 min-w-[140px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors flex-shrink-0"
-          >
-            <span class="material-icons text-lg">save</span>
-            <span class="truncate">Save Configuration</span>
-          </button>
-        </div>
-        <p class="text-xs text-slate-500 mt-2">
-          Enter the IP address of your WLED controller.
-        </p>
-      </label>
+          <p class="text-xs text-slate-500">
+            Enter the IP address of your WLED controller
+          </p>
+        </label>
+      </div>
+
+      <!-- Hyperion Configuration (shown when Hyperion is selected) -->
+      <div id="hyperionConfig" class="flex flex-col gap-4 hidden">
+        <label class="flex flex-col gap-1.5">
+          <span class="text-slate-700 text-sm font-medium leading-normal">Hyperion IP Address</span>
+          <div class="relative flex-1">
+            <input
+              id="hyperionIpInput"
+              class="form-input flex w-full min-w-0 resize-none overflow-hidden rounded-lg text-slate-900 focus:outline-0 focus:ring-2 focus:ring-sky-500 border border-slate-300 bg-white focus:border-sky-500 h-10 placeholder:text-slate-400 px-4 pr-10 text-base font-normal leading-normal transition-colors"
+              placeholder="e.g., 192.168.1.200"
+              value=""
+            />
+            <button
+              type="button"
+              onclick="document.getElementById('hyperionIpInput').value='';"
+              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700"
+              aria-label="Clear Hyperion IP"
+            >
+              <span class="material-icons">close</span>
+            </button>
+          </div>
+        </label>
+        <label class="flex flex-col gap-1.5">
+          <span class="text-slate-700 text-sm font-medium leading-normal">Hyperion Port</span>
+          <input
+            id="hyperionPortInput"
+            type="number"
+            class="form-input flex w-full min-w-0 resize-none overflow-hidden rounded-lg text-slate-900 focus:outline-0 focus:ring-2 focus:ring-sky-500 border border-slate-300 bg-white focus:border-sky-500 h-10 placeholder:text-slate-400 px-4 text-base font-normal leading-normal transition-colors"
+            placeholder="8090"
+            value="8090"
+            min="1"
+            max="65535"
+          />
+          <p class="text-xs text-slate-500">
+            Enter the JSON-RPC port (default: 8090)
+          </p>
+        </label>
+      </div>
+
+      <!-- Save Button -->
+      <button
+        id="saveLedConfig"
+        class="flex items-center justify-center gap-2 w-full sm:w-auto cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors"
+      >
+        <span class="material-icons text-lg">save</span>
+        <span class="truncate">Save LED Configuration</span>
+      </button>
     </div>
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">

+ 158 - 40
templates/wled.html

@@ -1,7 +1,7 @@
-{% extends "base.html" %} {% block title %}WLED - {{ app_name or 'Dune Weaver' }}{% endblock %}
+{% extends "base.html" %} {% block title %}LED Control - {{ app_name or 'Dune Weaver' }}{% endblock %}
 
 {% block additional_styles %}
-/* Dark mode styles for WLED page */
+/* Dark mode styles for LED page */
 .dark .bg-white {
   background-color: #262626;
 }
@@ -14,63 +14,181 @@
 .dark .text-gray-700 {
   color: #d1d5db;
 }
+.dark .text-slate-800 {
+  color: #e5e5e5;
+}
+.dark .border-slate-300 {
+  border-color: #404040;
+}
+.dark input[type="range"] {
+  background-color: #404040;
+}
+.dark input[type="color"] {
+  background-color: #262626;
+  border-color: #404040;
+}
 {% endblock %}
 
 {% block content %}
 <div class="layout-content-container flex flex-col w-full max-w-4xl gap-0 pt-2 pb-[75px]">
-  <section class="bg-white rounded-xl shadow-sm overflow-hidden pt-4 sm:pt-0 h-full">
+  <!-- Not Configured State -->
+  <section id="led-not-configured" class="bg-white rounded-xl shadow-sm overflow-hidden pt-4 sm:pt-0 h-full hidden">
     <div class="flex flex-col items-center px-0 py-0 h-full">
       <div class="w-full h-full max-w-5xl flex flex-col overflow-hidden">
-        <div id="wled-status" class="w-full p-8 text-center">
+        <div class="w-full p-8 text-center">
           <div class="flex flex-col items-center gap-4">
             <span class="material-icons text-6xl text-gray-500">lightbulb</span>
-            <h2 class="text-2xl font-semibold text-gray-700">WLED Not Configured</h2>
-            <p class="text-gray-500 max-w-md">Please set up your WLED IP address in the Settings page to control your LED lights.</p>
-            <a href="/settings" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
+            <h2 class="text-2xl font-semibold text-gray-700">LED Controller Not Configured</h2>
+            <p class="text-gray-500 max-w-md">Please configure your LED controller (WLED or Hyperion) in the Settings page.</p>
+            <a href="/settings" class="mt-4 flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-blue-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2">
+              <span class="material-icons">settings</span>
               Go to Settings
             </a>
           </div>
         </div>
+      </div>
+    </div>
+  </section>
+
+  <!-- WLED iframe -->
+  <section id="wled-container" class="bg-white rounded-xl shadow-sm overflow-hidden pt-4 sm:pt-0 h-full hidden">
+    <div class="flex flex-col items-center px-0 py-0 h-full">
+      <div class="w-full h-full max-w-5xl flex flex-col overflow-hidden">
         <iframe id="wled-frame"
           src=""
-          class="h-full w-full rounded-lg border border-slate-200 hidden"
+          class="h-full w-full rounded-lg border border-slate-200"
           frameborder="0"
           allowfullscreen
         ></iframe>
       </div>
     </div>
   </section>
+
+  <!-- Hyperion Controls -->
+  <section id="hyperion-container" class="bg-white rounded-xl shadow-sm overflow-hidden hidden">
+    <div class="px-6 py-5 space-y-6">
+      <h2 class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em]">
+        Hyperion Control
+      </h2>
+
+      <!-- Connection Status -->
+      <div id="hyperion-status" class="p-4 rounded-lg bg-gray-100 border border-slate-200">
+        <div class="flex items-center gap-2">
+          <span class="material-icons text-gray-500">info</span>
+          <span class="text-sm text-gray-700">Checking connection...</span>
+        </div>
+      </div>
+
+      <!-- Power and Brightness Grid -->
+      <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
+        <!-- Power Control -->
+        <div class="flex flex-col gap-3">
+          <h3 class="text-slate-800 text-base font-semibold">Power</h3>
+          <button id="hyperion-power-toggle" class="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">
+            <span class="material-icons">power_settings_new</span>
+            <span id="power-button-text">Turn ON</span>
+          </button>
+        </div>
+
+        <!-- Brightness Control -->
+        <div class="flex flex-col gap-3">
+          <div class="flex items-center justify-between">
+            <h3 class="text-slate-800 text-base font-semibold">Brightness</h3>
+            <span id="brightness-value" class="text-sm font-medium text-slate-600">100%</span>
+          </div>
+          <input type="range" id="hyperion-brightness" min="0" max="100" value="100" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
+        </div>
+      </div>
+
+      <!-- Color Control -->
+      <div class="flex flex-col gap-3">
+        <h3 class="text-slate-800 text-base font-semibold">Color Picker</h3>
+        <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
+          <div class="flex items-center gap-3">
+            <input type="color" id="hyperion-color" value="#ff0000" class="w-20 h-20 rounded-lg border-2 border-slate-300 cursor-pointer">
+            <div class="flex flex-col gap-1">
+              <span id="color-hex-display" class="text-sm font-mono font-semibold text-slate-700">#FF0000</span>
+              <button id="hyperion-set-color" class="flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-md hover:bg-blue-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2">
+                <span class="material-icons text-base">palette</span>
+                <span>Apply</span>
+              </button>
+            </div>
+          </div>
+
+          <!-- Quick Colors -->
+          <div class="flex flex-col gap-2">
+            <span class="text-sm font-medium text-slate-600">Quick Colors</span>
+            <div class="grid grid-cols-4 gap-2">
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#ff0000" style="background-color: #ff0000;" title="Red"></button>
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#00ff00" style="background-color: #00ff00;" title="Green"></button>
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#0000ff" style="background-color: #0000ff;" title="Blue"></button>
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#ffff00" style="background-color: #ffff00;" title="Yellow"></button>
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#ff00ff" style="background-color: #ff00ff;" title="Magenta"></button>
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#00ffff" style="background-color: #00ffff;" title="Cyan"></button>
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#ff8000" style="background-color: #ff8000;" title="Orange"></button>
+              <button class="quick-color h-10 rounded-lg border-2 border-slate-300 hover:scale-105 transition-transform" data-color="#ffffff" style="background-color: #ffffff;" title="White"></button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Effects -->
+      <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
+        <div class="flex flex-col gap-3">
+          <h3 class="text-slate-800 text-base font-semibold">Effects</h3>
+          <select id="hyperion-effect-select" class="form-select w-full rounded-lg border border-slate-300 bg-white px-4 py-3 text-slate-900 focus:outline-0 focus:ring-2 focus:ring-blue-500">
+            <option value="">Loading effects...</option>
+          </select>
+        </div>
+        <div class="flex flex-col gap-3">
+          <h3 class="text-slate-800 text-base font-semibold opacity-0 pointer-events-none">Spacer</h3>
+          <button id="hyperion-set-effect" class="flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-blue-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2">
+            <span class="material-icons">auto_awesome</span>
+            <span>Apply Effect</span>
+          </button>
+        </div>
+      </div>
+
+      <!-- Effect Settings -->
+      <div class="flex flex-col gap-4 pt-4 border-t border-slate-200">
+        <h3 class="text-slate-800 text-base font-semibold">Automation Settings</h3>
+        <p class="text-xs text-slate-500">Configure which effects to show when idle or playing patterns</p>
+
+        <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
+          <!-- Idle Effect -->
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium text-slate-700">Idle Effect</label>
+            <select id="hyperion-idle-effect" class="form-select w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:outline-0 focus:ring-2 focus:ring-blue-500">
+              <option value="">Default (Clear Priority)</option>
+            </select>
+          </div>
+
+          <!-- Playing Effect -->
+          <div class="flex flex-col gap-2">
+            <label class="text-sm font-medium text-slate-700">Playing Effect</label>
+            <select id="hyperion-playing-effect" class="form-select w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:outline-0 focus:ring-2 focus:ring-blue-500">
+              <option value="">Default (Clear Priority)</option>
+            </select>
+          </div>
+        </div>
+
+        <button id="save-hyperion-effects" class="flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-md hover:bg-blue-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2">
+          <span class="material-icons text-base">save</span>
+          <span>Save Effect Settings</span>
+        </button>
+      </div>
+
+      <!-- Reset Control -->
+      <div class="flex items-center gap-4 pt-4 border-t border-slate-200">
+        <button id="hyperion-clear-priority" class="flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-md hover:bg-blue-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2">
+          <span class="material-icons text-base">clear_all</span>
+          <span>Clear & Return to Default</span>
+        </button>
+        <p class="text-xs text-slate-500 flex-1">Clears Dune Weaver's control</p>
+      </div>
+    </div>
+  </section>
 </div>
-<script>
-  async function loadWledIframe() {
-    let wledIp = localStorage.getItem('wled_ip');
-    const status = document.getElementById('wled-status');
-    const frame = document.getElementById('wled-frame');
-    if (!wledIp) {
-      // Try to fetch from server if not in localStorage
-      try {
-        const resp = await fetch('/get_wled_ip');
-        if (resp.ok) {
-          const data = await resp.json();
-          wledIp = data.wled_ip;
-          localStorage.setItem('wled_ip', wledIp);
-        }
-      } catch (e) {}
-    }
-    if (wledIp && frame) {
-      frame.src = `http://${wledIp}`;
-      frame.classList.remove('hidden');
-      status.classList.add('hidden');
-    } else {
-      if (frame) {
-        frame.src = '';
-        frame.classList.add('hidden');
-      }
-      if (status) {
-        status.classList.remove('hidden');
-      }
-    }
-  }
-  document.addEventListener('DOMContentLoaded', loadWledIframe);
-</script>
+
+<script src="/static/js/led-control.js"></script>
 {% endblock %}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor