Add LED type detection, improve device card layout
Some checks failed
Validate / validate (push) Failing after 9s
Some checks failed
Validate / validate (push) Failing after 9s
- Detect LED chip type (WS2812B, SK6812, etc.) from WLED /json/cfg - Show RGBW/RGB channel indicator with colored squares on device card - Move version, display index, LED count to subtitle row under device name - Add remove button as × icon in top-right corner of card - Hide latency from card display (still visible in health dot tooltip) - Fix display layout overflow on narrow viewports - Add wled_rgbw and wled_led_type to ProcessingState API schema Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
? `<span class="health-latency">${Math.round(wledLatency)}ms</span>` : '';
|
||||
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 = `<span class="health-latency offline">${t('device.health.offline')}</span>`;
|
||||
}
|
||||
|
||||
const displayIndex = settings.display_index !== undefined ? settings.display_index : 0;
|
||||
const ledCount = state.wled_led_count || device.led_count;
|
||||
|
||||
return `
|
||||
<div class="card" data-device-id="${device.id}">
|
||||
<button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">✕</button>
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
||||
${device.name || device.id}
|
||||
${wledVersion ? `<span class="wled-version">v${wledVersion}</span>` : ''}
|
||||
${healthLabel}
|
||||
</div>
|
||||
<div class="card-header-badges">
|
||||
<span class="display-badge" title="${t('device.display')} ${settings.display_index !== undefined ? settings.display_index : 0}">🖥️${settings.display_index !== undefined ? settings.display_index : 0}</span>
|
||||
${state.wled_led_count ? `<span class="led-count-badge" title="${t('device.led_count')} ${state.wled_led_count}">💡${state.wled_led_count}</span>` : ''}
|
||||
${isProcessing ? `<span class="badge processing">${t('device.status.processing')}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-subtitle">
|
||||
${wledVersion ? `<span class="card-meta">v${wledVersion}</span>` : ''}
|
||||
<span class="card-meta" title="${t('device.display')}">🖥️ ${displayIndex}</span>
|
||||
${ledCount ? `<span class="card-meta" title="${t('device.led_count')}">💡 ${ledCount}</span>` : ''}
|
||||
${state.wled_led_type ? `<span class="card-meta">🔌 ${state.wled_led_type.replace(/ RGBW$/, '')}</span>` : ''}
|
||||
<span class="card-meta" title="${state.wled_rgbw ? 'RGBW' : 'RGB'}"><span class="channel-indicator"><span class="ch" style="background:#e53935"></span><span class="ch" style="background:#43a047"></span><span class="ch" style="background:#1e88e5"></span>${state.wled_rgbw ? '<span class="ch" style="background:#eee"></span>' : ''}</span></span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="info-row">
|
||||
<span class="info-label">${t('device.url')}</span>
|
||||
@@ -565,9 +572,6 @@ function createDeviceCard(device) {
|
||||
<button class="btn btn-icon btn-secondary" onclick="showPixelPreview('${device.id}')" title="${t('preview.button')}">
|
||||
👁️
|
||||
</button>
|
||||
<button class="btn btn-icon btn-danger" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user