refactor(color-strip): rename static -> single + frontend follow-through

The "static" source kind always rendered a SINGLE color and the name
confused new code paths. Rename the module + kind to "single". Storage
keeps backward-compatible serialisation. Frontend color-strip cards /
gradient / index / test modules and the affected tests follow the new
name.
This commit is contained in:
2026-05-23 00:49:00 +03:00
parent 737fd72b73
commit 826e680f37
10 changed files with 74 additions and 67 deletions
@@ -9,12 +9,12 @@ from .base import ColorStripStream, _SimpleNoise1D, _gradient_noise
from .gradient import GradientColorStripStream from .gradient import GradientColorStripStream
from .helpers import _compute_gradient_colors from .helpers import _compute_gradient_colors
from .picture import PictureColorStripStream from .picture import PictureColorStripStream
from .static import StaticColorStripStream from .single import SingleColorStripStream
__all__ = [ __all__ = [
"ColorStripStream", "ColorStripStream",
"PictureColorStripStream", "PictureColorStripStream",
"StaticColorStripStream", "SingleColorStripStream",
"GradientColorStripStream", "GradientColorStripStream",
"_compute_gradient_colors", "_compute_gradient_colors",
"_SimpleNoise1D", "_SimpleNoise1D",
@@ -1,4 +1,4 @@
"""Static color strip stream — solid color with optional animation.""" """Single color strip stream — solid color with optional animation."""
import colorsys import colorsys
import math import math
@@ -18,7 +18,7 @@ from .base import ColorStripStream
logger = get_logger(__name__) logger = get_logger(__name__)
class StaticColorStripStream(ColorStripStream): class SingleColorStripStream(ColorStripStream):
"""Color strip stream that returns a constant single-color array. """Color strip stream that returns a constant single-color array.
When animation is enabled a 30 fps background thread updates _colors with When animation is enabled a 30 fps background thread updates _colors with
@@ -28,7 +28,7 @@ class StaticColorStripStream(ColorStripStream):
def __init__(self, source): def __init__(self, source):
""" """
Args: Args:
source: StaticColorStripSource config source: SingleColorStripSource config
""" """
self._colors_lock = threading.Lock() self._colors_lock = threading.Lock()
self._running = False self._running = False
@@ -64,7 +64,7 @@ class StaticColorStripStream(ColorStripStream):
if self._auto_size and device_led_count > 0 and device_led_count != self._led_count: if self._auto_size and device_led_count > 0 and device_led_count != self._led_count:
self._led_count = device_led_count self._led_count = device_led_count
self._rebuild_colors() self._rebuild_colors()
logger.debug(f"StaticColorStripStream auto-sized to {device_led_count} LEDs") logger.debug(f"SingleColorStripStream auto-sized to {device_led_count} LEDs")
@property @property
def target_fps(self) -> int: def target_fps(self) -> int:
@@ -98,36 +98,36 @@ class StaticColorStripStream(ColorStripStream):
self._running = True self._running = True
self._thread = threading.Thread( self._thread = threading.Thread(
target=self._animate_loop, target=self._animate_loop,
name="css-static-animate", name="css-single-animate",
daemon=True, daemon=True,
) )
self._thread.start() self._thread.start()
logger.info(f"StaticColorStripStream started (leds={self._led_count})") logger.info(f"SingleColorStripStream started (leds={self._led_count})")
def stop(self) -> None: def stop(self) -> None:
self._running = False self._running = False
if self._thread: if self._thread:
self._thread.join(timeout=5.0) self._thread.join(timeout=5.0)
if self._thread.is_alive(): if self._thread.is_alive():
logger.warning("StaticColorStripStream animate thread did not terminate within 5s") logger.warning("SingleColorStripStream animate thread did not terminate within 5s")
self._thread = None self._thread = None
logger.info("StaticColorStripStream stopped") logger.info("SingleColorStripStream stopped")
def get_latest_colors(self) -> Optional[np.ndarray]: def get_latest_colors(self) -> Optional[np.ndarray]:
with self._colors_lock: with self._colors_lock:
return self._colors return self._colors
def update_source(self, source) -> None: def update_source(self, source) -> None:
from ledgrab.storage.color_strip_source import StaticColorStripSource from ledgrab.storage.color_strip_source import SingleColorStripSource
if isinstance(source, StaticColorStripSource): if isinstance(source, SingleColorStripSource):
prev_led_count = self._led_count if self._auto_size else None prev_led_count = self._led_count if self._auto_size else None
self._update_from_source(source) self._update_from_source(source)
# If we were auto-sized, preserve the runtime LED count across updates # If we were auto-sized, preserve the runtime LED count across updates
if prev_led_count and self._auto_size: if prev_led_count and self._auto_size:
self._led_count = prev_led_count self._led_count = prev_led_count
self._rebuild_colors() self._rebuild_colors()
logger.info("StaticColorStripStream params updated in-place") logger.info("SingleColorStripStream params updated in-place")
def set_clock(self, clock) -> None: def set_clock(self, clock) -> None:
"""Set or clear the sync clock runtime. Thread-safe (read atomically by loop).""" """Set or clear the sync clock runtime. Thread-safe (read atomically by loop)."""
@@ -266,7 +266,7 @@ class StaticColorStripStream(ColorStripStream):
with self._colors_lock: with self._colors_lock:
self._colors = buf self._colors = buf
except Exception as e: except Exception as e:
logger.error(f"StaticColorStripStream animation error: {e}") logger.error(f"SingleColorStripStream animation error: {e}")
if (anim and anim.get("enabled")) or self._is_color_bound(): if (anim and anim.get("enabled")) or self._is_color_bound():
sleep_target = frame_time sleep_target = frame_time
@@ -274,6 +274,6 @@ class StaticColorStripStream(ColorStripStream):
sleep_target = 0.25 sleep_target = 0.25
limiter.wait(sleep_target) limiter.wait(sleep_target)
except Exception as e: except Exception as e:
logger.error(f"Fatal StaticColorStripStream loop error: {e}", exc_info=True) logger.error(f"Fatal SingleColorStripStream loop error: {e}", exc_info=True)
finally: finally:
self._running = False self._running = False
@@ -9,7 +9,7 @@ from ledgrab.core.processing.color_strip import ( # noqa: F401
ColorStripStream, ColorStripStream,
GradientColorStripStream, GradientColorStripStream,
PictureColorStripStream, PictureColorStripStream,
StaticColorStripStream, SingleColorStripStream,
_compute_gradient_colors, _compute_gradient_colors,
_gradient_noise, _gradient_noise,
_SimpleNoise1D, _SimpleNoise1D,
@@ -18,7 +18,7 @@ from ledgrab.core.processing.color_strip import ( # noqa: F401
__all__ = [ __all__ = [
"ColorStripStream", "ColorStripStream",
"PictureColorStripStream", "PictureColorStripStream",
"StaticColorStripStream", "SingleColorStripStream",
"GradientColorStripStream", "GradientColorStripStream",
"_compute_gradient_colors", "_compute_gradient_colors",
"_SimpleNoise1D", "_SimpleNoise1D",
@@ -38,7 +38,7 @@ registerIconEntityType('color_strip_source', makeSimpleIconAdapter<ColorStripSou
typeLabelKey: 'device.icon.entity.color_strip_source', typeLabelKey: 'device.icon.entity.color_strip_source',
typeLabelFallback: 'Color strip', typeLabelFallback: 'Color strip',
cardSelectors: (id) => [`[data-css-id="${CSS.escape(id)}"]`], cardSelectors: (id) => [`[data-css-id="${CSS.escape(id)}"]`],
bodyExtras: (rec) => ({ source_type: (rec as any)?.source_type ?? 'static' }), bodyExtras: (rec) => ({ source_type: (rec as any)?.source_type ?? 'single_color' }),
})); }));
/* ── Types ────────────────────────────────────────────────────── */ /* ── Types ────────────────────────────────────────────────────── */
@@ -88,7 +88,7 @@ function _gradientEntityStripHTML(stops: Array<{ position: number; color: number
/* ── Non-picture types set ────────────────────────────────────── */ /* ── Non-picture types set ────────────────────────────────────── */
const NON_PICTURE_TYPES = new Set([ const NON_PICTURE_TYPES = new Set([
'static', 'gradient', 'effect', 'composite', 'mapped', 'single_color', 'gradient', 'effect', 'composite', 'mapped',
'audio', 'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors', 'audio', 'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors',
'math_wave', 'math_wave',
]); ]);
@@ -96,8 +96,8 @@ const NON_PICTURE_TYPES = new Set([
/* ── Per-type card property renderers ─────────────────────────── */ /* ── Per-type card property renderers ─────────────────────────── */
const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = { const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
static: (source, { clockBadge, animBadge }) => { single_color: (source, { clockBadge, animBadge }) => {
const colorBadge = _bindableColorBadge(source.color, [255, 255, 255], t('color_strip.static_color')); const colorBadge = _bindableColorBadge(source.color, [255, 255, 255], t('color_strip.single_color'));
return ` return `
${colorBadge} ${colorBadge}
${animBadge} ${animBadge}
@@ -312,7 +312,7 @@ function _renderPictureCardProps(source: ColorStripSource, pictureSourceMap: Rec
/* ── Main card builder ────────────────────────────────────────── */ /* ── Main card builder ────────────────────────────────────────── */
const STRIP_BADGE: Record<string, string> = { const STRIP_BADGE: Record<string, string> = {
static: 'STRIP · COLOR', single_color: 'STRIP · COLOR',
gradient: 'STRIP · GRD', gradient: 'STRIP · GRD',
effect: 'STRIP · FX', effect: 'STRIP · FX',
composite: 'STRIP · COMP', composite: 'STRIP · COMP',
@@ -336,7 +336,7 @@ export function createColorStripCard(source: ColorStripSource, pictureSourceMap:
? `<span class="stream-card-prop stream-card-link" title="${t('color_strip.clock')}" onclick="event.stopPropagation(); navigateToCard('streams','sync','sync-clocks','data-id','${source.clock_id}')">${ICON_CLOCK} ${escapeHtml(clockObj.name)}</span>` ? `<span class="stream-card-prop stream-card-link" title="${t('color_strip.clock')}" onclick="event.stopPropagation(); navigateToCard('streams','sync','sync-clocks','data-id','${source.clock_id}')">${ICON_CLOCK} ${escapeHtml(clockObj.name)}</span>`
: source.clock_id ? `<span class="stream-card-prop">${ICON_CLOCK} ${source.clock_id}</span>` : ''; : source.clock_id ? `<span class="stream-card-prop">${ICON_CLOCK} ${source.clock_id}</span>` : '';
const isAnimatable = source.source_type === 'static' || source.source_type === 'gradient'; const isAnimatable = source.source_type === 'single_color' || source.source_type === 'gradient';
const anim = isAnimatable && source.animation && source.animation.enabled ? source.animation : null; const anim = isAnimatable && source.animation && source.animation.enabled ? source.animation : null;
const animBadge = anim const animBadge = anim
? `<span class="stream-card-prop" title="${t('color_strip.animation')}">${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}</span>` ? `<span class="stream-card-prop" title="${t('color_strip.animation')}">${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}</span>`
@@ -194,6 +194,8 @@ export async function closeGradientEditor() {
export async function saveGradientEntity() { export async function saveGradientEntity() {
const id = (document.getElementById('gradient-editor-id') as HTMLInputElement).value; const id = (document.getElementById('gradient-editor-id') as HTMLInputElement).value;
if (gradientEditorModal.closeIfPristine(id)) return;
const name = (document.getElementById('gradient-editor-name') as HTMLInputElement).value.trim(); const name = (document.getElementById('gradient-editor-name') as HTMLInputElement).value.trim();
const description = (document.getElementById('gradient-editor-description') as HTMLInputElement).value.trim() || null; const description = (document.getElementById('gradient-editor-description') as HTMLInputElement).value.trim() || null;
const tags = _gradientTagsInput ? _gradientTagsInput.getValue() : []; const tags = _gradientTagsInput ? _gradientTagsInput.getValue() : [];
@@ -127,7 +127,7 @@ class CSSEditorModal extends Modal {
if (_candlelightWindWidget) { _candlelightWindWidget.destroy(); _candlelightWindWidget = null; } if (_candlelightWindWidget) { _candlelightWindWidget.destroy(); _candlelightWindWidget = null; }
if (_weatherSpeedWidget) { _weatherSpeedWidget.destroy(); _weatherSpeedWidget = null; } if (_weatherSpeedWidget) { _weatherSpeedWidget.destroy(); _weatherSpeedWidget = null; }
if (_weatherTempInfluenceWidget) { _weatherTempInfluenceWidget.destroy(); _weatherTempInfluenceWidget = null; } if (_weatherTempInfluenceWidget) { _weatherTempInfluenceWidget.destroy(); _weatherTempInfluenceWidget = null; }
if (_staticColorWidget) { _staticColorWidget.destroy(); _staticColorWidget = null; } if (_singleColorWidget) { _singleColorWidget.destroy(); _singleColorWidget = null; }
if (_effectColorWidget) { _effectColorWidget.destroy(); _effectColorWidget = null; } if (_effectColorWidget) { _effectColorWidget.destroy(); _effectColorWidget = null; }
if (_apiInputFallbackColorWidget) { _apiInputFallbackColorWidget.destroy(); _apiInputFallbackColorWidget = null; } if (_apiInputFallbackColorWidget) { _apiInputFallbackColorWidget.destroy(); _apiInputFallbackColorWidget = null; }
if (_candlelightColorWidget) { _candlelightColorWidget.destroy(); _candlelightColorWidget = null; } if (_candlelightColorWidget) { _candlelightColorWidget.destroy(); _candlelightColorWidget = null; }
@@ -150,7 +150,7 @@ class CSSEditorModal extends Modal {
picture_source: (document.getElementById('css-editor-picture-source') as HTMLInputElement).value, picture_source: (document.getElementById('css-editor-picture-source') as HTMLInputElement).value,
interpolation: (document.getElementById('css-editor-interpolation') as HTMLInputElement).value, interpolation: (document.getElementById('css-editor-interpolation') as HTMLInputElement).value,
smoothing: _smoothingWidget ? JSON.stringify(_smoothingWidget.getValue()) : '0.3', smoothing: _smoothingWidget ? JSON.stringify(_smoothingWidget.getValue()) : '0.3',
color: _staticColorWidget ? JSON.stringify(_staticColorWidget.getValue()) : '[]', color: _singleColorWidget ? JSON.stringify(_singleColorWidget.getValue()) : '[]',
led_count: (document.getElementById('css-editor-led-count') as HTMLInputElement).value, led_count: (document.getElementById('css-editor-led-count') as HTMLInputElement).value,
gradient_stops: type === 'gradient' ? JSON.stringify(getGradientStops()) : '[]', gradient_stops: type === 'gradient' ? JSON.stringify(getGradientStops()) : '[]',
animation_type: (document.getElementById('css-editor-animation-type') as HTMLInputElement).value, animation_type: (document.getElementById('css-editor-animation-type') as HTMLInputElement).value,
@@ -223,7 +223,7 @@ let _candlelightWindWidget: BindableScalarWidget | null = null;
let _weatherSpeedWidget: BindableScalarWidget | null = null; let _weatherSpeedWidget: BindableScalarWidget | null = null;
let _weatherTempInfluenceWidget: BindableScalarWidget | null = null; let _weatherTempInfluenceWidget: BindableScalarWidget | null = null;
let _staticColorWidget: BindableColorWidget | null = null; let _singleColorWidget: BindableColorWidget | null = null;
let _effectColorWidget: BindableColorWidget | null = null; let _effectColorWidget: BindableColorWidget | null = null;
let _apiInputFallbackColorWidget: BindableColorWidget | null = null; let _apiInputFallbackColorWidget: BindableColorWidget | null = null;
let _candlelightColorWidget: BindableColorWidget | null = null; let _candlelightColorWidget: BindableColorWidget | null = null;
@@ -303,7 +303,7 @@ async function configureKCRegions(sourceId: string): Promise<void> {
// ══════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════
const CSS_TYPE_KEYS = [ const CSS_TYPE_KEYS = [
'picture', 'picture_advanced', 'static', 'gradient', 'picture', 'picture_advanced', 'single_color', 'gradient',
'effect', 'composite', 'mapped', 'audio', 'effect', 'composite', 'mapped', 'audio',
'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors', 'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors',
'game_event', 'math_wave', 'game_event', 'math_wave',
@@ -341,7 +341,7 @@ function _ensureCSSTypeIconSelect() {
const CSS_SECTION_MAP: Record<string, string> = { const CSS_SECTION_MAP: Record<string, string> = {
'picture': 'css-editor-picture-section', 'picture': 'css-editor-picture-section',
'picture_advanced': 'css-editor-picture-section', 'picture_advanced': 'css-editor-picture-section',
'static': 'css-editor-static-section', 'single_color': 'css-editor-single-color-section',
'gradient': 'css-editor-gradient-section', 'gradient': 'css-editor-gradient-section',
'effect': 'css-editor-effect-section', 'effect': 'css-editor-effect-section',
'composite': 'css-editor-composite-section', 'composite': 'css-editor-composite-section',
@@ -399,7 +399,7 @@ export function onCSSTypeChange() {
const animSection = document.getElementById('css-editor-animation-section') as HTMLElement; const animSection = document.getElementById('css-editor-animation-section') as HTMLElement;
const animTypeSelect = document.getElementById('css-editor-animation-type') as HTMLSelectElement; const animTypeSelect = document.getElementById('css-editor-animation-type') as HTMLSelectElement;
if (type === 'static' || type === 'gradient') { if (type === 'single_color' || type === 'gradient') {
animSection.style.display = ''; animSection.style.display = '';
const opts = type === 'gradient' const opts = type === 'gradient'
? ['none','breathing','gradient_shift','wave','noise_perturb','hue_rotate','strobe','sparkle','pulse','candle','rainbow_fade'] ? ['none','breathing','gradient_shift','wave','noise_perturb','hue_rotate','strobe','sparkle','pulse','candle','rainbow_fade']
@@ -417,7 +417,7 @@ export function onCSSTypeChange() {
(document.getElementById('css-editor-led-count-group') as HTMLElement).style.display = (document.getElementById('css-editor-led-count-group') as HTMLElement).style.display =
hasLedCount.includes(type) ? '' : 'none'; hasLedCount.includes(type) ? '' : 'none';
const clockTypes = ['static', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave']; const clockTypes = ['single_color', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
(document.getElementById('css-editor-clock-group') as HTMLElement).style.display = clockTypes.includes(type) ? '' : 'none'; (document.getElementById('css-editor-clock-group') as HTMLElement).style.display = clockTypes.includes(type) ? '' : 'none';
if (clockTypes.includes(type)) _populateClockDropdown(); if (clockTypes.includes(type)) _populateClockDropdown();
@@ -726,16 +726,16 @@ function _ensureWeatherTempInfluenceWidget(): BindableScalarWidget {
return _weatherTempInfluenceWidget; return _weatherTempInfluenceWidget;
} }
function _ensureStaticColorWidget(): BindableColorWidget { function _ensureSingleColorWidget(): BindableColorWidget {
if (!_staticColorWidget) { if (!_singleColorWidget) {
_staticColorWidget = new BindableColorWidget({ _singleColorWidget = new BindableColorWidget({
container: document.getElementById('css-editor-color-container')!, container: document.getElementById('css-editor-color-container')!,
default: [255, 255, 255], default: [255, 255, 255],
valueSources: () => _cachedValueSources, valueSources: () => _cachedValueSources,
idPrefix: 'css-editor-color', idPrefix: 'css-editor-color',
}); });
} }
return _staticColorWidget; return _singleColorWidget;
} }
function _ensureEffectColorWidget(): BindableColorWidget { function _ensureEffectColorWidget(): BindableColorWidget {
@@ -988,17 +988,17 @@ function _autoGenerateCSSName() {
// ══════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════
const _typeHandlers: Record<string, { load: (...args: any[]) => any; reset: (...args: any[]) => any; getPayload: (name: any) => any }> = { const _typeHandlers: Record<string, { load: (...args: any[]) => any; reset: (...args: any[]) => any; getPayload: (name: any) => any }> = {
static: { single_color: {
load(css) { load(css) {
_ensureStaticColorWidget().setValue(css.color); _ensureSingleColorWidget().setValue(css.color);
_loadAnimationState(css.animation); _loadAnimationState(css.animation);
}, },
reset() { reset() {
_ensureStaticColorWidget().setValue([255, 255, 255]); _ensureSingleColorWidget().setValue([255, 255, 255]);
_loadAnimationState(null); _loadAnimationState(null);
}, },
getPayload(name) { getPayload(name) {
return { name, color: _ensureStaticColorWidget().getValue(), animation: _getAnimationPayload() }; return { name, color: _ensureSingleColorWidget().getValue(), animation: _getAnimationPayload() };
}, },
}, },
gradient: { gradient: {
@@ -1417,7 +1417,7 @@ export function getCSSEditorPreviewPayload(sourceType: string): any {
const payload = handler.getPayload('__preview__'); const payload = handler.getPayload('__preview__');
if (!payload) return null; if (!payload) return null;
payload.source_type = sourceType; payload.source_type = sourceType;
const clockTypes = ['static', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave']; const clockTypes = ['single_color', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
if (clockTypes.includes(sourceType)) { if (clockTypes.includes(sourceType)) {
const clockEl = document.getElementById('css-editor-clock') as HTMLInputElement | null; const clockEl = document.getElementById('css-editor-clock') as HTMLInputElement | null;
if (clockEl && clockEl.value) payload.clock_id = clockEl.value; if (clockEl && clockEl.value) payload.clock_id = clockEl.value;
@@ -1559,6 +1559,8 @@ export function isCSSEditorDirty() { return cssEditorModal.isDirty(); }
export async function saveCSSEditor() { export async function saveCSSEditor() {
const cssId = (document.getElementById('css-editor-id') as HTMLInputElement).value; const cssId = (document.getElementById('css-editor-id') as HTMLInputElement).value;
if (cssEditorModal.closeIfPristine(cssId)) return;
const name = (document.getElementById('css-editor-name') as HTMLInputElement).value.trim(); const name = (document.getElementById('css-editor-name') as HTMLInputElement).value.trim();
const sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value; const sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value;
@@ -1571,7 +1573,7 @@ export async function saveCSSEditor() {
payload.source_type = knownType ? sourceType : 'picture'; payload.source_type = knownType ? sourceType : 'picture';
const clockTypes = ['static', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave']; const clockTypes = ['single_color', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
if (clockTypes.includes(sourceType)) { if (clockTypes.includes(sourceType)) {
payload.clock_id = (document.getElementById('css-editor-clock') as HTMLInputElement).value || null; payload.clock_id = (document.getElementById('css-editor-clock') as HTMLInputElement).value || null;
} }
@@ -30,7 +30,7 @@ import { openAuthedWs } from '../../core/ws-auth.ts';
/* ── Preview config builder ───────────────────────────────────── */ /* ── Preview config builder ───────────────────────────────────── */
const _PREVIEW_TYPES = new Set([ const _PREVIEW_TYPES = new Set([
'static', 'gradient', 'effect', 'daylight', 'candlelight', 'notification', 'audio', 'math_wave', 'single_color', 'gradient', 'effect', 'daylight', 'candlelight', 'notification', 'audio', 'math_wave',
'weather', 'game_event', 'api_input', 'mapped', 'composite', 'processed', 'weather', 'game_event', 'api_input', 'mapped', 'composite', 'processed',
]); ]);
@@ -38,8 +38,8 @@ function _collectPreviewConfig() {
const sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value; const sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value;
if (!_PREVIEW_TYPES.has(sourceType)) return null; if (!_PREVIEW_TYPES.has(sourceType)) return null;
let config: any; let config: any;
if (sourceType === 'static') { if (sourceType === 'single_color') {
config = { source_type: 'static', color: hexToRgbArray((document.getElementById('css-editor-color') as HTMLInputElement).value), animation: _getAnimationPayload() }; config = { source_type: 'single_color', color: hexToRgbArray((document.getElementById('css-editor-color') as HTMLInputElement).value), animation: _getAnimationPayload() };
} else if (sourceType === 'gradient') { } else if (sourceType === 'gradient') {
const stops = getGradientStops(); const stops = getGradientStops();
if (stops.length < 2) return null; if (stops.length < 2) return null;
@@ -7,7 +7,7 @@ calibration, color correction, smoothing, and FPS.
Current types: Current types:
PictureColorStripSource — derives LED colors from a single PictureSource (simple 4-edge calibration) PictureColorStripSource — derives LED colors from a single PictureSource (simple 4-edge calibration)
AdvancedPictureColorStripSource — line-based calibration across multiple PictureSources AdvancedPictureColorStripSource — line-based calibration across multiple PictureSources
StaticColorStripSource — constant solid color fills all LEDs SingleColorStripSource — constant solid color fills all LEDs
GradientColorStripSource — linear gradient across all LEDs from user-defined color stops GradientColorStripSource — linear gradient across all LEDs from user-defined color stops
AudioColorStripSource — audio-reactive visualization (spectrum, beat pulse, VU meter) AudioColorStripSource — audio-reactive visualization (spectrum, beat pulse, VU meter)
ApiInputColorStripSource — receives raw LED colors from external clients via REST/WebSocket ApiInputColorStripSource — receives raw LED colors from external clients via REST/WebSocket
@@ -366,8 +366,8 @@ class AdvancedPictureColorStripSource(ColorStripSource):
@dataclass @dataclass
class StaticColorStripSource(ColorStripSource): class SingleColorStripSource(ColorStripSource):
"""Color strip source that fills all LEDs with a single static color. """Color strip source that fills all LEDs with a single solid color.
No capture or processing -- the entire LED strip is set to one constant No capture or processing -- the entire LED strip is set to one constant
RGB color. Useful for solid-color accents or as a placeholder while RGB color. Useful for solid-color accents or as a placeholder while
@@ -384,11 +384,11 @@ class StaticColorStripSource(ColorStripSource):
return d return d
@classmethod @classmethod
def from_dict(cls, data: dict) -> "StaticColorStripSource": def from_dict(cls, data: dict) -> "SingleColorStripSource":
common = _parse_css_common(data) common = _parse_css_common(data)
return cls( return cls(
**common, **common,
source_type="static", source_type="single_color",
color=BindableColor.from_raw(data.get("color"), default=[255, 255, 255]), color=BindableColor.from_raw(data.get("color"), default=[255, 255, 255]),
animation=data.get("animation"), animation=data.get("animation"),
) )
@@ -412,7 +412,7 @@ class StaticColorStripSource(ColorStripSource):
return cls( return cls(
id=id, id=id,
name=name, name=name,
source_type="static", source_type="single_color",
created_at=created_at, created_at=created_at,
updated_at=updated_at, updated_at=updated_at,
description=description, description=description,
@@ -1823,7 +1823,10 @@ class MathWaveColorStripSource(ColorStripSource):
_SOURCE_TYPE_MAP: Dict[str, Type[ColorStripSource]] = { _SOURCE_TYPE_MAP: Dict[str, Type[ColorStripSource]] = {
"picture": PictureColorStripSource, "picture": PictureColorStripSource,
"picture_advanced": AdvancedPictureColorStripSource, "picture_advanced": AdvancedPictureColorStripSource,
"static": StaticColorStripSource, "single_color": SingleColorStripSource,
# Legacy alias: pre-rename rows used "static". Kept so old DBs deserialize;
# ColorStripStore migrates the on-disk source_type to "single_color" on startup.
"static": SingleColorStripSource,
"gradient": GradientColorStripSource, "gradient": GradientColorStripSource,
"effect": EffectColorStripSource, "effect": EffectColorStripSource,
"audio": AudioColorStripSource, "audio": AudioColorStripSource,
+15 -15
View File
@@ -7,23 +7,23 @@ Tests creating, listing, updating, cloning, and deleting color strip sources.
class TestColorStripSourceLifecycle: class TestColorStripSourceLifecycle:
"""A user manages color strip sources for LED effects.""" """A user manages color strip sources for LED effects."""
def test_static_and_gradient_crud(self, client): def test_single_color_and_gradient_crud(self, client):
# 1. Create a static color strip source # 1. Create a single-color strip source
resp = client.post( resp = client.post(
"/api/v1/color-strip-sources", "/api/v1/color-strip-sources",
json={ json={
"name": "Red Static", "name": "Red Single",
"source_type": "static", "source_type": "single_color",
"color": [255, 0, 0], "color": [255, 0, 0],
"led_count": 60, "led_count": 60,
"tags": ["e2e", "static"], "tags": ["e2e", "single_color"],
}, },
) )
assert resp.status_code == 201, f"Create static failed: {resp.text}" assert resp.status_code == 201, f"Create single_color failed: {resp.text}"
static = resp.json() static = resp.json()
static_id = static["id"] static_id = static["id"]
assert static["name"] == "Red Static" assert static["name"] == "Red Single"
assert static["source_type"] == "static" assert static["source_type"] == "single_color"
assert static["color"] == [255, 0, 0] assert static["color"] == [255, 0, 0]
# 2. Create a gradient color strip source # 2. Create a gradient color strip source
@@ -58,7 +58,7 @@ class TestColorStripSourceLifecycle:
# 4. Update the static source -- change color # 4. Update the static source -- change color
resp = client.put( resp = client.put(
f"/api/v1/color-strip-sources/{static_id}", f"/api/v1/color-strip-sources/{static_id}",
json={"source_type": "static", "color": [0, 255, 0]}, json={"source_type": "single_color", "color": [0, 255, 0]},
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json()["color"] == [0, 255, 0] assert resp.json()["color"] == [0, 255, 0]
@@ -72,8 +72,8 @@ class TestColorStripSourceLifecycle:
resp = client.post( resp = client.post(
"/api/v1/color-strip-sources", "/api/v1/color-strip-sources",
json={ json={
"name": "Cloned Static", "name": "Cloned Single",
"source_type": "static", "source_type": "single_color",
"color": [0, 255, 0], "color": [0, 255, 0],
"led_count": 60, "led_count": 60,
}, },
@@ -81,7 +81,7 @@ class TestColorStripSourceLifecycle:
assert resp.status_code == 201 assert resp.status_code == 201
clone_id = resp.json()["id"] clone_id = resp.json()["id"]
assert clone_id != static_id assert clone_id != static_id
assert resp.json()["name"] == "Cloned Static" assert resp.json()["name"] == "Cloned Single"
# 7. Delete all three # 7. Delete all three
for sid in [static_id, gradient_id, clone_id]: for sid in [static_id, gradient_id, clone_id]:
@@ -99,7 +99,7 @@ class TestColorStripSourceLifecycle:
"/api/v1/color-strip-sources", "/api/v1/color-strip-sources",
json={ json={
"name": "Original Name", "name": "Original Name",
"source_type": "static", "source_type": "single_color",
"color": [100, 100, 100], "color": [100, 100, 100],
"led_count": 10, "led_count": 10,
}, },
@@ -108,7 +108,7 @@ class TestColorStripSourceLifecycle:
resp = client.put( resp = client.put(
f"/api/v1/color-strip-sources/{source_id}", f"/api/v1/color-strip-sources/{source_id}",
json={"source_type": "static", "name": "New Name"}, json={"source_type": "single_color", "name": "New Name"},
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json()["name"] == "New Name" assert resp.json()["name"] == "New Name"
@@ -125,7 +125,7 @@ class TestColorStripSourceLifecycle:
"""Cannot create two sources with the same name.""" """Cannot create two sources with the same name."""
payload = { payload = {
"name": "Unique Name", "name": "Unique Name",
"source_type": "static", "source_type": "single_color",
"color": [0, 0, 0], "color": [0, 0, 0],
"led_count": 10, "led_count": 10,
} }
+2 -2
View File
@@ -22,8 +22,8 @@ def store(tmp_db):
def _create_static(store: ColorStripStore, name: str) -> str: def _create_static(store: ColorStripStore, name: str) -> str:
"""Create a static color strip source, return its ID.""" """Create a single-color strip source, return its ID."""
source = store.create_source(name=name, source_type="static", colors=[[255, 0, 0]]) source = store.create_source(name=name, source_type="single_color", colors=[[255, 0, 0]])
return source.id return source.id