Add per-layer brightness source to composite CSS and enhance selectors

- Add optional brightness_source_id per composite layer using ValueStreamManager
- Use EntitySelect for composite layer source and brightness dropdowns
- Use IconSelect for composite blend mode and notification filter mode
- Add i18n keys for blend mode and filter mode descriptions (en/ru/zh)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 21:03:58 +03:00
parent c78797ba09
commit d498bb72a9
9 changed files with 181 additions and 17 deletions

View File

@@ -68,7 +68,7 @@ class ColorStripStreamManager:
keyed by ``{css_id}:{consumer_id}``.
"""
def __init__(self, color_strip_store, live_stream_manager, audio_capture_manager=None, audio_source_store=None, audio_template_store=None, sync_clock_manager=None):
def __init__(self, color_strip_store, live_stream_manager, audio_capture_manager=None, audio_source_store=None, audio_template_store=None, sync_clock_manager=None, value_stream_manager=None):
"""
Args:
color_strip_store: ColorStripStore for resolving source configs
@@ -76,6 +76,7 @@ class ColorStripStreamManager:
audio_capture_manager: AudioCaptureManager for audio-reactive sources
audio_source_store: AudioSourceStore for resolving audio source chains
sync_clock_manager: SyncClockManager for acquiring clock runtimes
value_stream_manager: ValueStreamManager for per-layer brightness sources
"""
self._color_strip_store = color_strip_store
self._live_stream_manager = live_stream_manager
@@ -83,6 +84,7 @@ class ColorStripStreamManager:
self._audio_source_store = audio_source_store
self._audio_template_store = audio_template_store
self._sync_clock_manager = sync_clock_manager
self._value_stream_manager = value_stream_manager
self._streams: Dict[str, _ColorStripEntry] = {}
def _inject_clock(self, css_stream, source) -> Optional[str]:
@@ -159,7 +161,7 @@ class ColorStripStreamManager:
css_stream = AudioColorStripStream(source, self._audio_capture_manager, self._audio_source_store, self._audio_template_store)
elif source.source_type == "composite":
from wled_controller.core.processing.composite_stream import CompositeColorStripStream
css_stream = CompositeColorStripStream(source, self)
css_stream = CompositeColorStripStream(source, self, self._value_stream_manager)
elif source.source_type == "mapped":
from wled_controller.core.processing.mapped_stream import MappedColorStripStream
css_stream = MappedColorStripStream(source, self)

View File

@@ -29,12 +29,13 @@ class CompositeColorStripStream(ColorStripStream):
sub-stream's latest colors and blending bottom-to-top.
"""
def __init__(self, source, css_manager):
def __init__(self, source, css_manager, value_stream_manager=None):
self._source_id: str = source.id
self._layers: List[dict] = list(source.layers)
self._led_count: int = source.led_count
self._auto_size: bool = source.led_count == 0
self._css_manager = css_manager
self._value_stream_manager = value_stream_manager
self._fps: int = 30
self._frame_time: float = 1.0 / 30
@@ -45,7 +46,9 @@ class CompositeColorStripStream(ColorStripStream):
# layer_index -> (source_id, consumer_id, stream)
self._sub_streams: Dict[int, tuple] = {}
self._sub_lock = threading.Lock() # guards _sub_streams access across threads
# layer_index -> (vs_id, value_stream)
self._brightness_streams: Dict[int, tuple] = {}
self._sub_lock = threading.Lock() # guards _sub_streams and _brightness_streams
# Pre-allocated scratch (rebuilt when LED count changes)
self._pool_n = 0
@@ -115,9 +118,9 @@ class CompositeColorStripStream(ColorStripStream):
def update_source(self, source) -> None:
"""Hot-update: rebuild sub-streams if layer config changed."""
new_layers = list(source.layers)
old_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"))
old_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"), l.get("brightness_source_id"))
for l in self._layers]
new_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"))
new_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"), l.get("brightness_source_id"))
for l in new_layers]
self._layers = new_layers
@@ -152,6 +155,16 @@ class CompositeColorStripStream(ColorStripStream):
logger.warning(
f"Composite layer {i} (source {src_id}) failed to acquire: {e}"
)
# Acquire brightness value stream if configured
vs_id = layer.get("brightness_source_id")
if vs_id and self._value_stream_manager:
try:
vs = self._value_stream_manager.acquire(vs_id)
self._brightness_streams[i] = (vs_id, vs)
except Exception as e:
logger.warning(
f"Composite layer {i} brightness source {vs_id} failed: {e}"
)
def _release_sub_streams(self) -> None:
for _idx, (src_id, consumer_id, _stream) in list(self._sub_streams.items()):
@@ -160,6 +173,14 @@ class CompositeColorStripStream(ColorStripStream):
except Exception as e:
logger.warning(f"Composite layer release error ({src_id}): {e}")
self._sub_streams.clear()
# Release brightness value streams
if self._value_stream_manager:
for _idx, (vs_id, _vs) in list(self._brightness_streams.items()):
try:
self._value_stream_manager.release(vs_id)
except Exception as e:
logger.warning(f"Composite brightness release error ({vs_id}): {e}")
self._brightness_streams.clear()
# ── Scratch pool ────────────────────────────────────────────
@@ -299,6 +320,13 @@ class CompositeColorStripStream(ColorStripStream):
if len(colors) != target_n:
colors = self._resize_to_target(colors, target_n)
# Apply per-layer brightness from value source
if i in self._brightness_streams:
_vs_id, vs = self._brightness_streams[i]
bri = vs.get_value()
if bri < 1.0:
colors = (colors.astype(np.uint16) * int(bri * 256) >> 8).astype(np.uint8)
opacity = layer.get("opacity", 1.0)
blend_mode = layer.get("blend_mode", _BLEND_NORMAL)
alpha = int(opacity * 256)

View File

@@ -122,6 +122,8 @@ class ProcessorManager:
live_stream_manager=self._live_stream_manager,
audio_template_store=audio_template_store,
) if value_source_store else None
# Wire value stream manager into CSS stream manager for composite layer brightness
self._color_strip_stream_manager._value_stream_manager = self._value_stream_manager
self._overlay_manager = OverlayManager()
self._event_queues: List[asyncio.Queue] = []
self._metrics_history = MetricsHistory(self)