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 `