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:
@@ -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",
|
||||||
|
|||||||
+13
-13
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user