diff --git a/server/src/wled_controller/api/routes/value_sources.py b/server/src/wled_controller/api/routes/value_sources.py index c6a67bd..5fd2579 100644 --- a/server/src/wled_controller/api/routes/value_sources.py +++ b/server/src/wled_controller/api/routes/value_sources.py @@ -47,6 +47,7 @@ from wled_controller.storage.value_source import ( from wled_controller.storage.value_source_store import ValueSourceStore from wled_controller.storage.output_target_store import OutputTargetStore from wled_controller.core.processing.processor_manager import ProcessorManager +from wled_controller.core.processing.value_stream import ValueStream from wled_controller.utils import get_logger from wled_controller.storage.base_store import EntityNotFoundError @@ -381,10 +382,21 @@ async def test_value_source_ws( await websocket.accept() logger.info(f"Value source test WebSocket connected for {source_id}") + # Detect if this stream produces colors + _is_color_stream = ( + hasattr(stream, "get_color") and type(stream).get_color is not ValueStream.get_color + ) + try: while True: value = stream.get_value() msg: dict = {"value": round(value, 4)} + if _is_color_stream: + try: + r, g, b = stream.get_color() + msg["color"] = [int(r), int(g), int(b)] + except NotImplementedError: + pass if hasattr(stream, "get_raw_value"): raw = stream.get_raw_value() if raw is not None: diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css index 8602dd7..73b120b 100644 --- a/server/src/wled_controller/static/css/modal.css +++ b/server/src/wled_controller/static/css/modal.css @@ -82,6 +82,17 @@ border-radius: 6px; } +.vs-test-color-swatch { + margin-top: 8px; +} + +.vs-test-color-canvas { + display: block; + width: 100%; + height: 32px; + border-radius: 6px; +} + .vs-test-stats { display: flex; gap: 20px; diff --git a/server/src/wled_controller/static/js/features/value-sources.ts b/server/src/wled_controller/static/js/features/value-sources.ts index 03e27a7..2c47cee 100644 --- a/server/src/wled_controller/static/js/features/value-sources.ts +++ b/server/src/wled_controller/static/js/features/value-sources.ts @@ -760,6 +760,8 @@ let _testVsMinObserved = Infinity; let _testVsMaxObserved = -Infinity; let _testVsRawLatest: number | null = null; let _testVsRawRange: [number, number] | null = null; +let _testVsColorLatest: [number, number, number] | null = null; +let _testVsColorHistory: [number, number, number][] = []; const testVsModal = new Modal('test-value-source-modal', { backdrop: true, lock: true }); @@ -777,11 +779,17 @@ export function testValueSource(sourceId: any) { _testVsMaxObserved = -Infinity; _testVsRawLatest = null; _testVsRawRange = null; + _testVsColorLatest = null; + _testVsColorHistory = []; + + // Hide color swatch until color data arrives + const swatchEl = document.getElementById('vs-test-color-swatch'); + if (swatchEl) swatchEl.style.display = 'none'; const currentEl = document.getElementById('vs-test-current'); const minEl = document.getElementById('vs-test-min'); const maxEl = document.getElementById('vs-test-max'); - if (currentEl) currentEl.textContent = '---'; + if (currentEl) { currentEl.textContent = '---'; currentEl.style.color = ''; } if (minEl) minEl.textContent = '---'; if (maxEl) maxEl.textContent = '---'; @@ -816,6 +824,13 @@ export function testValueSource(sourceId: any) { _testVsRawLatest = data.raw_value; if (data.raw_range) _testVsRawRange = data.raw_range; } + if (data.color) { + _testVsColorLatest = data.color; + _testVsColorHistory.push(data.color); + if (_testVsColorHistory.length > VS_HISTORY_SIZE) { + _testVsColorHistory.shift(); + } + } } catch (e) { console.error('Value source test WS parse error:', e); return; @@ -869,6 +884,7 @@ function _sizeVsCanvas(canvas: HTMLCanvasElement) { function _renderVsTestLoop() { _renderVsChart(); + _renderVsColorSwatch(); if (testVsModal.isOpen) { _testVsAnimFrame = requestAnimationFrame(_renderVsTestLoop); } @@ -923,7 +939,9 @@ function _renderVsChart() { } ctx.lineTo((startOffset + history.length - 1) * stepX, h); ctx.closePath(); - ctx.fillStyle = 'rgba(76, 175, 80, 0.15)'; + ctx.fillStyle = _testVsColorLatest + ? `rgba(${_testVsColorLatest[0]},${_testVsColorLatest[1]},${_testVsColorLatest[2]},0.15)` + : 'rgba(76, 175, 80, 0.15)'; ctx.fill(); // Draw the line @@ -934,7 +952,10 @@ function _renderVsChart() { if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } - ctx.strokeStyle = '#4caf50'; + const lineColor = _testVsColorLatest + ? `rgb(${_testVsColorLatest[0]},${_testVsColorLatest[1]},${_testVsColorLatest[2]})` + : '#4caf50'; + ctx.strokeStyle = lineColor; ctx.lineWidth = 2; ctx.stroke(); @@ -993,6 +1014,51 @@ function _fmtRaw(v: number): string { return Number.isInteger(v) ? String(v) : v.toFixed(1); } +function _renderVsColorSwatch() { + const swatchEl = document.getElementById('vs-test-color-swatch'); + const canvas = document.getElementById('vs-test-color-canvas') as HTMLCanvasElement | null; + if (!canvas || !swatchEl) return; + + if (_testVsColorHistory.length === 0) { + swatchEl.style.display = 'none'; + return; + } + + swatchEl.style.display = ''; + + const dpr = window.devicePixelRatio || 1; + const rect = canvas.parentElement!.getBoundingClientRect(); + const w = rect.width; + const h = 32; + canvas.width = w * dpr; + canvas.height = h * dpr; + canvas.style.height = h + 'px'; + + const ctx = canvas.getContext('2d')!; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + const colors = _testVsColorHistory; + const stepX = w / VS_HISTORY_SIZE; + const barW = Math.max(stepX, 1); + const startOffset = VS_HISTORY_SIZE - colors.length; + + for (let i = 0; i < colors.length; i++) { + const [r, g, b] = colors[i]; + ctx.fillStyle = `rgb(${r},${g},${b})`; + ctx.fillRect((startOffset + i) * stepX, 0, barW + 0.5, h); + } + + // Update current color stat display + if (_testVsColorLatest) { + const [r, g, b] = _testVsColorLatest; + const curEl = document.getElementById('vs-test-current'); + if (curEl) { + curEl.textContent = `rgb(${r}, ${g}, ${b})`; + curEl.style.color = `rgb(${r},${g},${b})`; + } + } +} + // ── Card rendering (used by streams.js) ─────────────────────── export function createValueSourceCard(src: ValueSource) { diff --git a/server/src/wled_controller/templates/modals/test-value-source.html b/server/src/wled_controller/templates/modals/test-value-source.html index 20e4dc4..f1bbfaa 100644 --- a/server/src/wled_controller/templates/modals/test-value-source.html +++ b/server/src/wled_controller/templates/modals/test-value-source.html @@ -7,6 +7,9 @@