Add dynamic brightness value source support for KC targets, fix subtab selector collision

Extend value source brightness modulation to Key Colors targets (matching LED target support).
Also fix stream subtab CSS selector collision that broke target subtab selection, and use 🔢 emoji
for value source UI elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 12:42:00 +03:00
parent ef474fe275
commit 8f79b77fe4
10 changed files with 131 additions and 13 deletions

View File

@@ -96,9 +96,11 @@ class KCTargetProcessor(TargetProcessor):
):
super().__init__(target_id, ctx, picture_source_id)
self._settings = settings
self._brightness_vs_id = settings.brightness_value_source_id if settings else ""
# Runtime state
self._live_stream: Optional[LiveStream] = None
self._value_stream = None # active brightness value stream
self._previous_colors: Optional[Dict[str, Tuple[int, int, int]]] = None
self._latest_colors: Optional[Dict[str, Tuple[int, int, int]]] = None
self._ws_clients: List = []
@@ -156,6 +158,16 @@ class KCTargetProcessor(TargetProcessor):
logger.error(f"Failed to initialize live stream for KC target {self._target_id}: {e}")
raise RuntimeError(f"Failed to initialize live stream: {e}")
# Acquire value stream for brightness modulation (if configured)
if self._brightness_vs_id and self._ctx.value_stream_manager:
try:
self._value_stream = self._ctx.value_stream_manager.acquire(
self._brightness_vs_id, self._target_id
)
except Exception as e:
logger.warning(f"Failed to acquire value stream {self._brightness_vs_id}: {e}")
self._value_stream = None
# Reset metrics
self._metrics = ProcessingMetrics(start_time=datetime.utcnow())
self._previous_colors = None
@@ -192,6 +204,14 @@ class KCTargetProcessor(TargetProcessor):
logger.warning(f"Error releasing live stream for KC target: {e}")
self._live_stream = None
# Release value stream
if self._value_stream is not None and self._ctx.value_stream_manager:
try:
self._ctx.value_stream_manager.release(self._brightness_vs_id, self._target_id)
except Exception as e:
logger.warning(f"Error releasing value stream: {e}")
self._value_stream = None
logger.info(f"Stopped KC processing for target {self._target_id}")
self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": False})
@@ -199,8 +219,37 @@ class KCTargetProcessor(TargetProcessor):
def update_settings(self, settings) -> None:
self._settings = settings
# Keep _brightness_vs_id in sync (hot-swap handled separately)
self._brightness_vs_id = settings.brightness_value_source_id if settings else ""
logger.info(f"Updated KC target settings: {self._target_id}")
def update_brightness_value_source(self, vs_id: str) -> None:
"""Hot-swap the brightness value source for a running KC target."""
old_vs_id = self._brightness_vs_id
self._brightness_vs_id = vs_id
vs_mgr = self._ctx.value_stream_manager
if not self._is_running or vs_mgr is None:
return
# Release old stream
if self._value_stream is not None and old_vs_id:
try:
vs_mgr.release(old_vs_id, self._target_id)
except Exception as e:
logger.warning(f"Error releasing old value stream {old_vs_id}: {e}")
self._value_stream = None
# Acquire new stream
if vs_id:
try:
self._value_stream = vs_mgr.acquire(vs_id, self._target_id)
except Exception as e:
logger.warning(f"Failed to acquire value stream {vs_id}: {e}")
self._value_stream = None
logger.info(f"Hot-swapped brightness VS for KC target {self._target_id}: {old_vs_id} -> {vs_id}")
# ----- State / Metrics -----
def get_state(self) -> dict:
@@ -220,6 +269,7 @@ class KCTargetProcessor(TargetProcessor):
"timing_total_ms": round(metrics.timing_total_ms, 1) if self._is_running else None,
"last_update": metrics.last_update,
"errors": [metrics.last_error] if metrics.last_error else [],
"brightness_value_source_id": self._brightness_vs_id,
}
def get_metrics(self) -> dict:
@@ -333,11 +383,17 @@ class KCTargetProcessor(TargetProcessor):
s = self._settings
calc_fn = calc_fns.get(s.interpolation_mode, calculate_average_color)
# Effective brightness: static setting * value stream
eff_brightness = s.brightness
vs = self._value_stream
if vs is not None:
eff_brightness *= vs.get_value()
# CPU-bound work in thread pool
colors, colors_arr, frame_timing = await asyncio.to_thread(
_process_kc_frame,
capture, rect_names, rect_bounds, calc_fn,
prev_colors_arr, s.smoothing, s.brightness,
prev_colors_arr, s.smoothing, eff_brightness,
)
prev_colors_arr = colors_arr