diff --git a/custom_components/wled_screen_controller/coordinator.py b/custom_components/wled_screen_controller/coordinator.py index d8ec925..881ae20 100644 --- a/custom_components/wled_screen_controller/coordinator.py +++ b/custom_components/wled_screen_controller/coordinator.py @@ -246,6 +246,24 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator): resp.raise_for_status() await self.async_request_refresh() + async def set_kc_brightness(self, target_id: str, brightness: int) -> None: + """Set brightness for a Key Colors target (0-255 mapped to 0.0-1.0).""" + brightness_float = round(brightness / 255, 4) + async with self.session.put( + f"{self.server_url}/api/v1/picture-targets/{target_id}", + headers={**self._auth_headers, "Content-Type": "application/json"}, + json={"key_colors_settings": {"brightness": brightness_float}}, + timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), + ) as resp: + if resp.status != 200: + body = await resp.text() + _LOGGER.error( + "Failed to set KC brightness for target %s: %s %s", + target_id, resp.status, body, + ) + resp.raise_for_status() + await self.async_request_refresh() + async def start_processing(self, target_id: str) -> None: """Start processing for a target.""" async with self.session.post( diff --git a/custom_components/wled_screen_controller/number.py b/custom_components/wled_screen_controller/number.py index 30062e9..f7b7602 100644 --- a/custom_components/wled_screen_controller/number.py +++ b/custom_components/wled_screen_controller/number.py @@ -1,4 +1,4 @@ -"""Number platform for LED Screen Controller (device brightness).""" +"""Number platform for LED Screen Controller (device & KC target brightness).""" from __future__ import annotations import logging @@ -32,15 +32,20 @@ async def async_setup_entry( for target_id, target_data in coordinator.data["targets"].items(): info = target_data["info"] - # Only LED targets have a device_id if info.get("target_type") == TARGET_TYPE_KEY_COLORS: + # KC target — brightness lives in key_colors_settings + entities.append( + WLEDScreenControllerKCBrightness( + coordinator, target_id, entry.entry_id, + ) + ) continue + # LED target — brightness lives on the device device_id = info.get("device_id", "") if not device_id: continue - # Check if the device supports brightness control device_data = devices.get(device_id) if not device_data: continue @@ -110,3 +115,55 @@ class WLEDScreenControllerBrightness(CoordinatorEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set brightness value.""" await self.coordinator.set_brightness(self._device_id, int(value)) + + +class WLEDScreenControllerKCBrightness(CoordinatorEntity, NumberEntity): + """Brightness control for a Key Colors target.""" + + _attr_has_entity_name = True + _attr_native_min_value = 0 + _attr_native_max_value = 255 + _attr_native_step = 1 + _attr_mode = NumberMode.SLIDER + _attr_icon = "mdi:brightness-6" + + def __init__( + self, + coordinator: WLEDScreenControllerCoordinator, + target_id: str, + entry_id: str, + ) -> None: + """Initialize the KC brightness number.""" + super().__init__(coordinator) + self._target_id = target_id + self._entry_id = entry_id + self._attr_unique_id = f"{target_id}_brightness" + self._attr_translation_key = "brightness" + + @property + def device_info(self) -> dict[str, Any]: + """Return device information.""" + return {"identifiers": {(DOMAIN, self._target_id)}} + + @property + def native_value(self) -> float | None: + """Return the current brightness value (0-255).""" + if not self.coordinator.data: + return None + target_data = self.coordinator.data.get("targets", {}).get(self._target_id) + if not target_data: + return None + kc_settings = target_data.get("info", {}).get("key_colors_settings") or {} + brightness_float = kc_settings.get("brightness", 1.0) + return round(brightness_float * 255) + + @property + def available(self) -> bool: + """Return if entity is available.""" + if not self.coordinator.data: + return False + return self._target_id in self.coordinator.data.get("targets", {}) + + async def async_set_native_value(self, value: float) -> None: + """Set brightness value.""" + await self.coordinator.set_kc_brightness(self._target_id, int(value)) diff --git a/server/src/wled_controller/api/routes/picture_targets.py b/server/src/wled_controller/api/routes/picture_targets.py index a8b5091..c32db81 100644 --- a/server/src/wled_controller/api/routes/picture_targets.py +++ b/server/src/wled_controller/api/routes/picture_targets.py @@ -106,6 +106,7 @@ def _kc_settings_to_schema(settings: KeyColorsSettings) -> KeyColorsSettingsSche interpolation_mode=settings.interpolation_mode, smoothing=settings.smoothing, pattern_template_id=settings.pattern_template_id, + brightness=settings.brightness, ) @@ -116,6 +117,7 @@ def _kc_schema_to_settings(schema: KeyColorsSettingsSchema) -> KeyColorsSettings interpolation_mode=schema.interpolation_mode, smoothing=schema.smoothing, pattern_template_id=schema.pattern_template_id, + brightness=schema.brightness, ) diff --git a/server/src/wled_controller/api/schemas/picture_targets.py b/server/src/wled_controller/api/schemas/picture_targets.py index 842e90b..3de8520 100644 --- a/server/src/wled_controller/api/schemas/picture_targets.py +++ b/server/src/wled_controller/api/schemas/picture_targets.py @@ -52,6 +52,7 @@ class KeyColorsSettingsSchema(BaseModel): interpolation_mode: str = Field(default="average", description="Color mode (average, median, dominant)") smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0) pattern_template_id: str = Field(default="", description="Pattern template ID for rectangle layout") + brightness: float = Field(default=1.0, description="Output brightness (0.0-1.0)", ge=0.0, le=1.0) class ExtractedColorResponse(BaseModel): diff --git a/server/src/wled_controller/core/processing/kc_target_processor.py b/server/src/wled_controller/core/processing/kc_target_processor.py index eb3b5f1..682a122 100644 --- a/server/src/wled_controller/core/processing/kc_target_processor.py +++ b/server/src/wled_controller/core/processing/kc_target_processor.py @@ -34,7 +34,7 @@ KC_WORK_SIZE = (160, 90) # (width, height) — small enough for fast color calc # CPU-bound frame processing (runs in thread pool via asyncio.to_thread) # --------------------------------------------------------------------------- -def _process_kc_frame(capture, rect_names, rect_bounds, calc_fn, prev_colors_arr, smoothing): +def _process_kc_frame(capture, rect_names, rect_bounds, calc_fn, prev_colors_arr, smoothing, brightness): """All CPU-bound work for one KC frame. Returns (colors, colors_arr, timing_ms) where: @@ -59,7 +59,13 @@ def _process_kc_frame(capture, rect_names, rect_bounds, calc_fn, prev_colors_arr if prev_colors_arr is not None and smoothing > 0: colors_arr = colors_arr * (1 - smoothing) + prev_colors_arr * smoothing - colors_u8 = np.clip(colors_arr, 0, 255).astype(np.uint8) + # Apply brightness scaling + if brightness < 1.0: + output_arr = colors_arr * brightness + else: + output_arr = colors_arr + + colors_u8 = np.clip(output_arr, 0, 255).astype(np.uint8) t2 = time.perf_counter() # Build output dict @@ -256,6 +262,7 @@ class KCTargetProcessor(TargetProcessor): target_fps = settings.fps smoothing = settings.smoothing + brightness = settings.brightness # Select color calculation function calc_fns = { @@ -328,7 +335,7 @@ class KCTargetProcessor(TargetProcessor): colors, colors_arr, frame_timing = await asyncio.to_thread( _process_kc_frame, capture, rect_names, rect_bounds, calc_fn, - prev_colors_arr, smoothing, + prev_colors_arr, smoothing, brightness, ) prev_colors_arr = colors_arr diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index cfa99f5..aa879e8 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -58,6 +58,7 @@ import { createKCTargetCard, testKCTarget, toggleKCTestAutoRefresh, showKCEditor, closeKCEditorModal, forceCloseKCEditorModal, saveKCEditor, deleteKCTarget, disconnectAllKCWebSockets, + updateKCBrightnessLabel, saveKCBrightness, } from './features/kc-targets.js'; import { createPatternTemplateCard, @@ -210,6 +211,8 @@ Object.assign(window, { saveKCEditor, deleteKCTarget, disconnectAllKCWebSockets, + updateKCBrightnessLabel, + saveKCBrightness, // pattern-templates createPatternTemplateCard, diff --git a/server/src/wled_controller/static/js/features/kc-targets.js b/server/src/wled_controller/static/js/features/kc-targets.js index 1dd986c..901d4b4 100644 --- a/server/src/wled_controller/static/js/features/kc-targets.js +++ b/server/src/wled_controller/static/js/features/kc-targets.js @@ -39,6 +39,8 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap) { const kcSettings = target.key_colors_settings || {}; const isProcessing = state.processing || false; + const brightness = kcSettings.brightness ?? 1.0; + const brightnessInt = Math.round(brightness * 255); const source = sourceMap[target.picture_source_id]; const sourceName = source ? source.name : (target.picture_source_id || 'No source'); @@ -74,11 +76,18 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap) { 📄 ${escapeHtml(patternName)} ▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''} +
+ +
+
+ ${swatchesHtml} +
+ ${isProcessing ? `
-
- ${swatchesHtml} -
- ${isProcessing ? `
${t('device.metrics.actual_fps')}
@@ -127,8 +136,8 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap) {
` : ''} - ` : ''}
+ ` : ''}
${isProcessing ? `