diff --git a/server/src/ledgrab/core/processing/color_strip/__init__.py b/server/src/ledgrab/core/processing/color_strip/__init__.py index 6188b68..47fc79a 100644 --- a/server/src/ledgrab/core/processing/color_strip/__init__.py +++ b/server/src/ledgrab/core/processing/color_strip/__init__.py @@ -9,12 +9,12 @@ from .base import ColorStripStream, _SimpleNoise1D, _gradient_noise from .gradient import GradientColorStripStream from .helpers import _compute_gradient_colors from .picture import PictureColorStripStream -from .static import StaticColorStripStream +from .single import SingleColorStripStream __all__ = [ "ColorStripStream", "PictureColorStripStream", - "StaticColorStripStream", + "SingleColorStripStream", "GradientColorStripStream", "_compute_gradient_colors", "_SimpleNoise1D", diff --git a/server/src/ledgrab/core/processing/color_strip/static.py b/server/src/ledgrab/core/processing/color_strip/single.py similarity index 93% rename from server/src/ledgrab/core/processing/color_strip/static.py rename to server/src/ledgrab/core/processing/color_strip/single.py index e59a1a1..eecc26c 100644 --- a/server/src/ledgrab/core/processing/color_strip/static.py +++ b/server/src/ledgrab/core/processing/color_strip/single.py @@ -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 math @@ -18,7 +18,7 @@ from .base import ColorStripStream logger = get_logger(__name__) -class StaticColorStripStream(ColorStripStream): +class SingleColorStripStream(ColorStripStream): """Color strip stream that returns a constant single-color array. When animation is enabled a 30 fps background thread updates _colors with @@ -28,7 +28,7 @@ class StaticColorStripStream(ColorStripStream): def __init__(self, source): """ Args: - source: StaticColorStripSource config + source: SingleColorStripSource config """ self._colors_lock = threading.Lock() 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: self._led_count = device_led_count 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 def target_fps(self) -> int: @@ -98,36 +98,36 @@ class StaticColorStripStream(ColorStripStream): self._running = True self._thread = threading.Thread( target=self._animate_loop, - name="css-static-animate", + name="css-single-animate", daemon=True, ) 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: self._running = False if self._thread: self._thread.join(timeout=5.0) 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 - logger.info("StaticColorStripStream stopped") + logger.info("SingleColorStripStream stopped") def get_latest_colors(self) -> Optional[np.ndarray]: with self._colors_lock: return self._colors 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 self._update_from_source(source) # If we were auto-sized, preserve the runtime LED count across updates if prev_led_count and self._auto_size: self._led_count = prev_led_count self._rebuild_colors() - logger.info("StaticColorStripStream params updated in-place") + logger.info("SingleColorStripStream params updated in-place") def set_clock(self, clock) -> None: """Set or clear the sync clock runtime. Thread-safe (read atomically by loop).""" @@ -266,7 +266,7 @@ class StaticColorStripStream(ColorStripStream): with self._colors_lock: self._colors = buf 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(): sleep_target = frame_time @@ -274,6 +274,6 @@ class StaticColorStripStream(ColorStripStream): sleep_target = 0.25 limiter.wait(sleep_target) 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: self._running = False diff --git a/server/src/ledgrab/core/processing/color_strip_stream.py b/server/src/ledgrab/core/processing/color_strip_stream.py index 0431568..5088c48 100644 --- a/server/src/ledgrab/core/processing/color_strip_stream.py +++ b/server/src/ledgrab/core/processing/color_strip_stream.py @@ -9,7 +9,7 @@ from ledgrab.core.processing.color_strip import ( # noqa: F401 ColorStripStream, GradientColorStripStream, PictureColorStripStream, - StaticColorStripStream, + SingleColorStripStream, _compute_gradient_colors, _gradient_noise, _SimpleNoise1D, @@ -18,7 +18,7 @@ from ledgrab.core.processing.color_strip import ( # noqa: F401 __all__ = [ "ColorStripStream", "PictureColorStripStream", - "StaticColorStripStream", + "SingleColorStripStream", "GradientColorStripStream", "_compute_gradient_colors", "_SimpleNoise1D", diff --git a/server/src/ledgrab/static/js/features/color-strips/cards.ts b/server/src/ledgrab/static/js/features/color-strips/cards.ts index 21d3fdc..ba6d0e8 100644 --- a/server/src/ledgrab/static/js/features/color-strips/cards.ts +++ b/server/src/ledgrab/static/js/features/color-strips/cards.ts @@ -38,7 +38,7 @@ registerIconEntityType('color_strip_source', makeSimpleIconAdapter [`[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 ────────────────────────────────────────────────────── */ @@ -88,7 +88,7 @@ function _gradientEntityStripHTML(stops: Array<{ position: number; color: number /* ── Non-picture types 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', 'math_wave', ]); @@ -96,8 +96,8 @@ const NON_PICTURE_TYPES = new Set([ /* ── Per-type card property renderers ─────────────────────────── */ const CSS_CARD_RENDERERS: Record = { - static: (source, { clockBadge, animBadge }) => { - const colorBadge = _bindableColorBadge(source.color, [255, 255, 255], t('color_strip.static_color')); + single_color: (source, { clockBadge, animBadge }) => { + const colorBadge = _bindableColorBadge(source.color, [255, 255, 255], t('color_strip.single_color')); return ` ${colorBadge} ${animBadge} @@ -312,7 +312,7 @@ function _renderPictureCardProps(source: ColorStripSource, pictureSourceMap: Rec /* ── Main card builder ────────────────────────────────────────── */ const STRIP_BADGE: Record = { - static: 'STRIP · COLOR', + single_color: 'STRIP · COLOR', gradient: 'STRIP · GRD', effect: 'STRIP · FX', composite: 'STRIP · COMP', @@ -336,7 +336,7 @@ export function createColorStripCard(source: ColorStripSource, pictureSourceMap: ? `${ICON_CLOCK} ${escapeHtml(clockObj.name)}` : source.clock_id ? `${ICON_CLOCK} ${source.clock_id}` : ''; - 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 animBadge = anim ? `${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}` diff --git a/server/src/ledgrab/static/js/features/color-strips/gradient.ts b/server/src/ledgrab/static/js/features/color-strips/gradient.ts index 925789e..7cb9580 100644 --- a/server/src/ledgrab/static/js/features/color-strips/gradient.ts +++ b/server/src/ledgrab/static/js/features/color-strips/gradient.ts @@ -194,6 +194,8 @@ export async function closeGradientEditor() { export async function saveGradientEntity() { 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 description = (document.getElementById('gradient-editor-description') as HTMLInputElement).value.trim() || null; const tags = _gradientTagsInput ? _gradientTagsInput.getValue() : []; diff --git a/server/src/ledgrab/static/js/features/color-strips/index.ts b/server/src/ledgrab/static/js/features/color-strips/index.ts index 9298465..07d68d7 100644 --- a/server/src/ledgrab/static/js/features/color-strips/index.ts +++ b/server/src/ledgrab/static/js/features/color-strips/index.ts @@ -127,7 +127,7 @@ class CSSEditorModal extends Modal { if (_candlelightWindWidget) { _candlelightWindWidget.destroy(); _candlelightWindWidget = null; } if (_weatherSpeedWidget) { _weatherSpeedWidget.destroy(); _weatherSpeedWidget = 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 (_apiInputFallbackColorWidget) { _apiInputFallbackColorWidget.destroy(); _apiInputFallbackColorWidget = 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, interpolation: (document.getElementById('css-editor-interpolation') as HTMLInputElement).value, 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, gradient_stops: type === 'gradient' ? JSON.stringify(getGradientStops()) : '[]', 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 _weatherTempInfluenceWidget: BindableScalarWidget | null = null; -let _staticColorWidget: BindableColorWidget | null = null; +let _singleColorWidget: BindableColorWidget | null = null; let _effectColorWidget: BindableColorWidget | null = null; let _apiInputFallbackColorWidget: BindableColorWidget | null = null; let _candlelightColorWidget: BindableColorWidget | null = null; @@ -303,7 +303,7 @@ async function configureKCRegions(sourceId: string): Promise { // ══════════════════════════════════════════════════════════════════ const CSS_TYPE_KEYS = [ - 'picture', 'picture_advanced', 'static', 'gradient', + 'picture', 'picture_advanced', 'single_color', 'gradient', 'effect', 'composite', 'mapped', 'audio', 'api_input', 'notification', 'daylight', 'candlelight', 'weather', 'processed', 'key_colors', 'game_event', 'math_wave', @@ -341,7 +341,7 @@ function _ensureCSSTypeIconSelect() { const CSS_SECTION_MAP: Record = { 'picture': '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', 'effect': 'css-editor-effect-section', 'composite': 'css-editor-composite-section', @@ -399,7 +399,7 @@ export function onCSSTypeChange() { const animSection = document.getElementById('css-editor-animation-section') as HTMLElement; const animTypeSelect = document.getElementById('css-editor-animation-type') as HTMLSelectElement; - if (type === 'static' || type === 'gradient') { + if (type === 'single_color' || type === 'gradient') { animSection.style.display = ''; const opts = type === 'gradient' ? ['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 = 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'; if (clockTypes.includes(type)) _populateClockDropdown(); @@ -726,16 +726,16 @@ function _ensureWeatherTempInfluenceWidget(): BindableScalarWidget { return _weatherTempInfluenceWidget; } -function _ensureStaticColorWidget(): BindableColorWidget { - if (!_staticColorWidget) { - _staticColorWidget = new BindableColorWidget({ +function _ensureSingleColorWidget(): BindableColorWidget { + if (!_singleColorWidget) { + _singleColorWidget = new BindableColorWidget({ container: document.getElementById('css-editor-color-container')!, default: [255, 255, 255], valueSources: () => _cachedValueSources, idPrefix: 'css-editor-color', }); } - return _staticColorWidget; + return _singleColorWidget; } function _ensureEffectColorWidget(): BindableColorWidget { @@ -988,17 +988,17 @@ function _autoGenerateCSSName() { // ══════════════════════════════════════════════════════════════════ const _typeHandlers: Record any; reset: (...args: any[]) => any; getPayload: (name: any) => any }> = { - static: { + single_color: { load(css) { - _ensureStaticColorWidget().setValue(css.color); + _ensureSingleColorWidget().setValue(css.color); _loadAnimationState(css.animation); }, reset() { - _ensureStaticColorWidget().setValue([255, 255, 255]); + _ensureSingleColorWidget().setValue([255, 255, 255]); _loadAnimationState(null); }, getPayload(name) { - return { name, color: _ensureStaticColorWidget().getValue(), animation: _getAnimationPayload() }; + return { name, color: _ensureSingleColorWidget().getValue(), animation: _getAnimationPayload() }; }, }, gradient: { @@ -1417,7 +1417,7 @@ export function getCSSEditorPreviewPayload(sourceType: string): any { const payload = handler.getPayload('__preview__'); if (!payload) return null; 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)) { const clockEl = document.getElementById('css-editor-clock') as HTMLInputElement | null; if (clockEl && clockEl.value) payload.clock_id = clockEl.value; @@ -1559,6 +1559,8 @@ export function isCSSEditorDirty() { return cssEditorModal.isDirty(); } export async function saveCSSEditor() { 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 sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value; @@ -1571,7 +1573,7 @@ export async function saveCSSEditor() { 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)) { payload.clock_id = (document.getElementById('css-editor-clock') as HTMLInputElement).value || null; } diff --git a/server/src/ledgrab/static/js/features/color-strips/test.ts b/server/src/ledgrab/static/js/features/color-strips/test.ts index 6e55651..aca20c0 100644 --- a/server/src/ledgrab/static/js/features/color-strips/test.ts +++ b/server/src/ledgrab/static/js/features/color-strips/test.ts @@ -30,7 +30,7 @@ import { openAuthedWs } from '../../core/ws-auth.ts'; /* ── Preview config builder ───────────────────────────────────── */ 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', ]); @@ -38,8 +38,8 @@ function _collectPreviewConfig() { const sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value; if (!_PREVIEW_TYPES.has(sourceType)) return null; let config: any; - if (sourceType === 'static') { - config = { source_type: 'static', color: hexToRgbArray((document.getElementById('css-editor-color') as HTMLInputElement).value), animation: _getAnimationPayload() }; + if (sourceType === 'single_color') { + config = { source_type: 'single_color', color: hexToRgbArray((document.getElementById('css-editor-color') as HTMLInputElement).value), animation: _getAnimationPayload() }; } else if (sourceType === 'gradient') { const stops = getGradientStops(); if (stops.length < 2) return null; diff --git a/server/src/ledgrab/storage/color_strip_source.py b/server/src/ledgrab/storage/color_strip_source.py index f986bda..10cf4bc 100644 --- a/server/src/ledgrab/storage/color_strip_source.py +++ b/server/src/ledgrab/storage/color_strip_source.py @@ -7,7 +7,7 @@ calibration, color correction, smoothing, and FPS. Current types: PictureColorStripSource — derives LED colors from a single PictureSource (simple 4-edge calibration) 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 AudioColorStripSource — audio-reactive visualization (spectrum, beat pulse, VU meter) ApiInputColorStripSource — receives raw LED colors from external clients via REST/WebSocket @@ -366,8 +366,8 @@ class AdvancedPictureColorStripSource(ColorStripSource): @dataclass -class StaticColorStripSource(ColorStripSource): - """Color strip source that fills all LEDs with a single static color. +class SingleColorStripSource(ColorStripSource): + """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 RGB color. Useful for solid-color accents or as a placeholder while @@ -384,11 +384,11 @@ class StaticColorStripSource(ColorStripSource): return d @classmethod - def from_dict(cls, data: dict) -> "StaticColorStripSource": + def from_dict(cls, data: dict) -> "SingleColorStripSource": common = _parse_css_common(data) return cls( **common, - source_type="static", + source_type="single_color", color=BindableColor.from_raw(data.get("color"), default=[255, 255, 255]), animation=data.get("animation"), ) @@ -412,7 +412,7 @@ class StaticColorStripSource(ColorStripSource): return cls( id=id, name=name, - source_type="static", + source_type="single_color", created_at=created_at, updated_at=updated_at, description=description, @@ -1823,7 +1823,10 @@ class MathWaveColorStripSource(ColorStripSource): _SOURCE_TYPE_MAP: Dict[str, Type[ColorStripSource]] = { "picture": PictureColorStripSource, "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, "effect": EffectColorStripSource, "audio": AudioColorStripSource, diff --git a/server/tests/e2e/test_color_strip_flow.py b/server/tests/e2e/test_color_strip_flow.py index 84c31fd..4de7ddc 100644 --- a/server/tests/e2e/test_color_strip_flow.py +++ b/server/tests/e2e/test_color_strip_flow.py @@ -7,23 +7,23 @@ Tests creating, listing, updating, cloning, and deleting color strip sources. class TestColorStripSourceLifecycle: """A user manages color strip sources for LED effects.""" - def test_static_and_gradient_crud(self, client): - # 1. Create a static color strip source + def test_single_color_and_gradient_crud(self, client): + # 1. Create a single-color strip source resp = client.post( "/api/v1/color-strip-sources", json={ - "name": "Red Static", - "source_type": "static", + "name": "Red Single", + "source_type": "single_color", "color": [255, 0, 0], "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_id = static["id"] - assert static["name"] == "Red Static" - assert static["source_type"] == "static" + assert static["name"] == "Red Single" + assert static["source_type"] == "single_color" assert static["color"] == [255, 0, 0] # 2. Create a gradient color strip source @@ -58,7 +58,7 @@ class TestColorStripSourceLifecycle: # 4. Update the static source -- change color resp = client.put( 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.json()["color"] == [0, 255, 0] @@ -72,8 +72,8 @@ class TestColorStripSourceLifecycle: resp = client.post( "/api/v1/color-strip-sources", json={ - "name": "Cloned Static", - "source_type": "static", + "name": "Cloned Single", + "source_type": "single_color", "color": [0, 255, 0], "led_count": 60, }, @@ -81,7 +81,7 @@ class TestColorStripSourceLifecycle: assert resp.status_code == 201 clone_id = resp.json()["id"] assert clone_id != static_id - assert resp.json()["name"] == "Cloned Static" + assert resp.json()["name"] == "Cloned Single" # 7. Delete all three for sid in [static_id, gradient_id, clone_id]: @@ -99,7 +99,7 @@ class TestColorStripSourceLifecycle: "/api/v1/color-strip-sources", json={ "name": "Original Name", - "source_type": "static", + "source_type": "single_color", "color": [100, 100, 100], "led_count": 10, }, @@ -108,7 +108,7 @@ class TestColorStripSourceLifecycle: resp = client.put( 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.json()["name"] == "New Name" @@ -125,7 +125,7 @@ class TestColorStripSourceLifecycle: """Cannot create two sources with the same name.""" payload = { "name": "Unique Name", - "source_type": "static", + "source_type": "single_color", "color": [0, 0, 0], "led_count": 10, } diff --git a/server/tests/test_composite_nesting.py b/server/tests/test_composite_nesting.py index 9680c59..0530056 100644 --- a/server/tests/test_composite_nesting.py +++ b/server/tests/test_composite_nesting.py @@ -22,8 +22,8 @@ def store(tmp_db): def _create_static(store: ColorStripStore, name: str) -> str: - """Create a static color strip source, return its ID.""" - source = store.create_source(name=name, source_type="static", colors=[[255, 0, 0]]) + """Create a single-color strip source, return its ID.""" + source = store.create_source(name=name, source_type="single_color", colors=[[255, 0, 0]]) return source.id