diff --git a/server/src/wled_controller/api/schemas.py b/server/src/wled_controller/api/schemas.py index fa5fbbc..8f25811 100644 --- a/server/src/wled_controller/api/schemas.py +++ b/server/src/wled_controller/api/schemas.py @@ -172,6 +172,8 @@ class ProcessingState(BaseModel): wled_name: Optional[str] = Field(None, description="WLED device name") wled_version: Optional[str] = Field(None, description="WLED firmware version") wled_led_count: Optional[int] = Field(None, description="LED count reported by WLED device") + wled_rgbw: Optional[bool] = Field(None, description="Whether WLED device uses RGBW LEDs") + wled_led_type: Optional[str] = Field(None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)") wled_last_checked: Optional[datetime] = Field(None, description="Last health check time") wled_error: Optional[str] = Field(None, description="Last health check error") diff --git a/server/src/wled_controller/core/processor_manager.py b/server/src/wled_controller/core/processor_manager.py index 198b311..dc149b4 100644 --- a/server/src/wled_controller/core/processor_manager.py +++ b/server/src/wled_controller/core/processor_manager.py @@ -22,6 +22,19 @@ logger = get_logger(__name__) DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds between health checks +# WLED LED bus type codes from const.h → human-readable names +WLED_LED_TYPES: Dict[int, str] = { + 18: "WS2812 1ch", 19: "WS2812 1ch x3", 20: "WS2812 CCT", 21: "WS2812 WWA", + 22: "WS2812B", 23: "GS8608", 24: "WS2811 400kHz", 25: "TM1829", + 26: "UCS8903", 27: "APA106", 28: "FW1906", 29: "UCS8904", + 30: "SK6812 RGBW", 31: "TM1814", 32: "WS2805", 33: "TM1914", 34: "SM16825", + 40: "On/Off", 41: "PWM 1ch", 42: "PWM 2ch", 43: "PWM 3ch", + 44: "PWM 4ch", 45: "PWM 5ch", 46: "PWM 6ch", + 50: "WS2801", 51: "APA102", 52: "LPD8806", 53: "P9813", 54: "LPD6803", + 65: "HUB75 HS", 66: "HUB75 QS", + 80: "DDP RGB", 81: "E1.31", 82: "Art-Net", 88: "DDP RGBW", 89: "Art-Net RGBW", +} + @dataclass class ProcessingSettings: @@ -48,6 +61,8 @@ class DeviceHealth: wled_name: Optional[str] = None wled_version: Optional[str] = None wled_led_count: Optional[int] = None + wled_rgbw: Optional[bool] = None + wled_led_type: Optional[str] = None error: Optional[str] = None @@ -433,6 +448,8 @@ class ProcessorManager: "wled_name": h.wled_name, "wled_version": h.wled_version, "wled_led_count": h.wled_led_count, + "wled_rgbw": h.wled_rgbw, + "wled_led_type": h.wled_led_type, "wled_last_checked": h.last_checked, "wled_error": h.error, "test_mode": state.test_mode_active, @@ -643,7 +660,23 @@ class ProcessorManager: response.raise_for_status() data = response.json() latency = (time.time() - start) * 1000 - wled_led_count = data.get("leds", {}).get("count") + leds_info = data.get("leds", {}) + wled_led_count = leds_info.get("count") + + # Fetch LED type from /json/cfg once (it's static config) + wled_led_type = state.health.wled_led_type + if wled_led_type is None: + try: + cfg = await client.get(f"{url}/json/cfg") + cfg.raise_for_status() + cfg_data = cfg.json() + ins = cfg_data.get("hw", {}).get("led", {}).get("ins", []) + if ins: + type_code = ins[0].get("type", 0) + wled_led_type = WLED_LED_TYPES.get(type_code, f"Type {type_code}") + except Exception as cfg_err: + logger.debug(f"Could not fetch LED type for {device_id}: {cfg_err}") + state.health = DeviceHealth( online=True, latency_ms=round(latency, 1), @@ -651,6 +684,8 @@ class ProcessorManager: wled_name=data.get("name"), wled_version=data.get("ver"), wled_led_count=wled_led_count, + wled_rgbw=leds_info.get("rgbw", False), + wled_led_type=wled_led_type, error=None, ) except Exception as e: @@ -661,6 +696,8 @@ class ProcessorManager: wled_name=state.health.wled_name, wled_version=state.health.wled_version, wled_led_count=state.health.wled_led_count, + wled_rgbw=state.health.wled_rgbw, + wled_led_type=state.health.wled_led_type, error=str(e), ) @@ -684,5 +721,7 @@ class ProcessorManager: "wled_name": h.wled_name, "wled_version": h.wled_version, "wled_led_count": h.wled_led_count, + "wled_rgbw": h.wled_rgbw, + "wled_led_type": h.wled_led_type, "error": h.error, } diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index 2f73b35..41a0c9a 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -327,9 +327,10 @@ function renderDisplayLayout(displays) { const totalWidth = maxX - minX; const totalHeight = maxY - minY; - // Scale factor to fit in canvas (600px wide max, maintain aspect ratio) - const maxCanvasWidth = 600; - const maxCanvasHeight = 350; + // Scale factor to fit in canvas (respect available width, maintain aspect ratio) + const availableWidth = canvas.clientWidth - 60; // account for padding + const maxCanvasWidth = Math.min(600, availableWidth); + const maxCanvasHeight = 450; const scaleX = maxCanvasWidth / totalWidth; const scaleY = maxCanvasHeight / totalHeight; const scale = Math.min(scaleX, scaleY, 0.3); // Max 0.3 scale to keep monitors reasonably sized @@ -489,8 +490,8 @@ function createDeviceCard(device) { healthTitle = `${t('device.health.online')}`; if (wledName) healthTitle += ` - ${wledName}`; if (wledVersion) healthTitle += ` v${wledVersion}`; - healthLabel = wledLatency !== null && wledLatency !== undefined - ? `${Math.round(wledLatency)}ms` : ''; + if (wledLatency !== null && wledLatency !== undefined) healthTitle += ` (${Math.round(wledLatency)}ms)`; + healthLabel = ''; } else { healthClass = 'health-offline'; healthTitle = t('device.health.offline'); @@ -498,21 +499,27 @@ function createDeviceCard(device) { healthLabel = `${t('device.health.offline')}`; } + const displayIndex = settings.display_index !== undefined ? settings.display_index : 0; + const ledCount = state.wled_led_count || device.led_count; + return `
+
${device.name || device.id} - ${wledVersion ? `v${wledVersion}` : ''} ${healthLabel} -
-
- 🖥️${settings.display_index !== undefined ? settings.display_index : 0} - ${state.wled_led_count ? `💡${state.wled_led_count}` : ''} ${isProcessing ? `${t('device.status.processing')}` : ''}
+
+ ${wledVersion ? `v${wledVersion}` : ''} + 🖥️ ${displayIndex} + ${ledCount ? `💡 ${ledCount}` : ''} + ${state.wled_led_type ? `🔌 ${state.wled_led_type.replace(/ RGBW$/, '')}` : ''} + ${state.wled_rgbw ? '' : ''} +
${t('device.url')} @@ -565,9 +572,6 @@ function createDeviceCard(device) { -
`; diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css index 9a9f39d..f3305b2 100644 --- a/server/src/wled_controller/static/style.css +++ b/server/src/wled_controller/static/style.css @@ -173,6 +173,7 @@ section { border: 1px solid var(--border-color); border-radius: 8px; padding: 20px; + position: relative; transition: transform 0.2s, box-shadow 0.2s; } @@ -180,24 +181,35 @@ section { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } +.card-remove-btn { + position: absolute; + top: 10px; + right: 10px; + background: none; + border: none; + color: #777; + font-size: 1rem; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} + +.card-remove-btn:hover { + color: var(--danger-color); + background: rgba(244, 67, 54, 0.1); +} + .card-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 15px; -} - -.card-header-badges { - display: flex; - align-items: center; - gap: 8px; -} - -.display-badge, -.led-count-badge { - font-size: 0.8rem; - color: var(--info-color); - opacity: 0.8; + margin-bottom: 4px; + padding-right: 30px; } .card-title { @@ -206,16 +218,35 @@ section { display: flex; align-items: center; gap: 6px; + flex-wrap: wrap; } -.wled-version { - font-size: 0.65rem; - font-weight: 400; - color: var(--text-secondary); - background: var(--border-color); - padding: 1px 6px; - border-radius: 8px; - letter-spacing: 0.03em; +.card-subtitle { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 15px; + flex-wrap: wrap; +} + +.card-meta { + font-size: 0.8rem; + color: #999; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.channel-indicator { + display: inline-flex; + gap: 2px; + align-items: center; +} + +.channel-indicator .ch { + width: 8px; + height: 10px; + border-radius: 1px; } .badge { @@ -363,10 +394,11 @@ section { border: 2px dashed var(--border-color); border-radius: 8px; padding: 30px; - min-height: 300px; + min-height: 280px; display: flex; align-items: center; justify-content: center; + overflow: hidden; } .layout-container {