Add brightness control to Key Colors targets with HAOS integration
Adds brightness (0.0-1.0) to KeyColorsSettings, applies scaling in the KC processing pipeline after smoothing, exposes a slider on the WebUI target card, and creates a HA number entity for KC target brightness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -246,6 +246,24 @@ class WLEDScreenControllerCoordinator(DataUpdateCoordinator):
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
await self.async_request_refresh()
|
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:
|
async def start_processing(self, target_id: str) -> None:
|
||||||
"""Start processing for a target."""
|
"""Start processing for a target."""
|
||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
|
|||||||
@@ -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
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -32,15 +32,20 @@ async def async_setup_entry(
|
|||||||
for target_id, target_data in coordinator.data["targets"].items():
|
for target_id, target_data in coordinator.data["targets"].items():
|
||||||
info = target_data["info"]
|
info = target_data["info"]
|
||||||
|
|
||||||
# Only LED targets have a device_id
|
|
||||||
if info.get("target_type") == TARGET_TYPE_KEY_COLORS:
|
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
|
continue
|
||||||
|
|
||||||
|
# LED target — brightness lives on the device
|
||||||
device_id = info.get("device_id", "")
|
device_id = info.get("device_id", "")
|
||||||
if not device_id:
|
if not device_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if the device supports brightness control
|
|
||||||
device_data = devices.get(device_id)
|
device_data = devices.get(device_id)
|
||||||
if not device_data:
|
if not device_data:
|
||||||
continue
|
continue
|
||||||
@@ -110,3 +115,55 @@ class WLEDScreenControllerBrightness(CoordinatorEntity, NumberEntity):
|
|||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Set brightness value."""
|
"""Set brightness value."""
|
||||||
await self.coordinator.set_brightness(self._device_id, int(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))
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ def _kc_settings_to_schema(settings: KeyColorsSettings) -> KeyColorsSettingsSche
|
|||||||
interpolation_mode=settings.interpolation_mode,
|
interpolation_mode=settings.interpolation_mode,
|
||||||
smoothing=settings.smoothing,
|
smoothing=settings.smoothing,
|
||||||
pattern_template_id=settings.pattern_template_id,
|
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,
|
interpolation_mode=schema.interpolation_mode,
|
||||||
smoothing=schema.smoothing,
|
smoothing=schema.smoothing,
|
||||||
pattern_template_id=schema.pattern_template_id,
|
pattern_template_id=schema.pattern_template_id,
|
||||||
|
brightness=schema.brightness,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class KeyColorsSettingsSchema(BaseModel):
|
|||||||
interpolation_mode: str = Field(default="average", description="Color mode (average, median, dominant)")
|
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)
|
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")
|
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):
|
class ExtractedColorResponse(BaseModel):
|
||||||
|
|||||||
@@ -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)
|
# 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.
|
"""All CPU-bound work for one KC frame.
|
||||||
|
|
||||||
Returns (colors, colors_arr, timing_ms) where:
|
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:
|
if prev_colors_arr is not None and smoothing > 0:
|
||||||
colors_arr = colors_arr * (1 - smoothing) + prev_colors_arr * smoothing
|
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()
|
t2 = time.perf_counter()
|
||||||
|
|
||||||
# Build output dict
|
# Build output dict
|
||||||
@@ -256,6 +262,7 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
|
|
||||||
target_fps = settings.fps
|
target_fps = settings.fps
|
||||||
smoothing = settings.smoothing
|
smoothing = settings.smoothing
|
||||||
|
brightness = settings.brightness
|
||||||
|
|
||||||
# Select color calculation function
|
# Select color calculation function
|
||||||
calc_fns = {
|
calc_fns = {
|
||||||
@@ -328,7 +335,7 @@ class KCTargetProcessor(TargetProcessor):
|
|||||||
colors, colors_arr, frame_timing = await asyncio.to_thread(
|
colors, colors_arr, frame_timing = await asyncio.to_thread(
|
||||||
_process_kc_frame,
|
_process_kc_frame,
|
||||||
capture, rect_names, rect_bounds, calc_fn,
|
capture, rect_names, rect_bounds, calc_fn,
|
||||||
prev_colors_arr, smoothing,
|
prev_colors_arr, smoothing, brightness,
|
||||||
)
|
)
|
||||||
|
|
||||||
prev_colors_arr = colors_arr
|
prev_colors_arr = colors_arr
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
createKCTargetCard, testKCTarget, toggleKCTestAutoRefresh,
|
createKCTargetCard, testKCTarget, toggleKCTestAutoRefresh,
|
||||||
showKCEditor, closeKCEditorModal, forceCloseKCEditorModal, saveKCEditor,
|
showKCEditor, closeKCEditorModal, forceCloseKCEditorModal, saveKCEditor,
|
||||||
deleteKCTarget, disconnectAllKCWebSockets,
|
deleteKCTarget, disconnectAllKCWebSockets,
|
||||||
|
updateKCBrightnessLabel, saveKCBrightness,
|
||||||
} from './features/kc-targets.js';
|
} from './features/kc-targets.js';
|
||||||
import {
|
import {
|
||||||
createPatternTemplateCard,
|
createPatternTemplateCard,
|
||||||
@@ -210,6 +211,8 @@ Object.assign(window, {
|
|||||||
saveKCEditor,
|
saveKCEditor,
|
||||||
deleteKCTarget,
|
deleteKCTarget,
|
||||||
disconnectAllKCWebSockets,
|
disconnectAllKCWebSockets,
|
||||||
|
updateKCBrightnessLabel,
|
||||||
|
saveKCBrightness,
|
||||||
|
|
||||||
// pattern-templates
|
// pattern-templates
|
||||||
createPatternTemplateCard,
|
createPatternTemplateCard,
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap) {
|
|||||||
const kcSettings = target.key_colors_settings || {};
|
const kcSettings = target.key_colors_settings || {};
|
||||||
|
|
||||||
const isProcessing = state.processing || false;
|
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 source = sourceMap[target.picture_source_id];
|
||||||
const sourceName = source ? source.name : (target.picture_source_id || 'No source');
|
const sourceName = source ? source.name : (target.picture_source_id || 'No source');
|
||||||
@@ -74,11 +76,18 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap) {
|
|||||||
<span class="stream-card-prop" title="${t('kc.pattern_template')}">📄 ${escapeHtml(patternName)}</span>
|
<span class="stream-card-prop" title="${t('kc.pattern_template')}">📄 ${escapeHtml(patternName)}</span>
|
||||||
<span class="stream-card-prop">▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''}</span>
|
<span class="stream-card-prop">▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="brightness-control" data-kc-brightness-wrap="${target.id}">
|
||||||
|
<input type="range" class="brightness-slider" min="0" max="255"
|
||||||
|
value="${brightnessInt}" data-kc-brightness="${target.id}"
|
||||||
|
oninput="updateKCBrightnessLabel('${target.id}', this.value)"
|
||||||
|
onchange="saveKCBrightness('${target.id}', this.value)"
|
||||||
|
title="${Math.round(brightness * 100)}%">
|
||||||
|
</div>
|
||||||
<div id="kc-swatches-${target.id}" class="kc-color-swatches">
|
<div id="kc-swatches-${target.id}" class="kc-color-swatches">
|
||||||
${swatchesHtml}
|
${swatchesHtml}
|
||||||
</div>
|
</div>
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
|
<div class="card-content">
|
||||||
<div class="metrics-grid">
|
<div class="metrics-grid">
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
|
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
|
||||||
@@ -127,8 +136,8 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
<button class="btn btn-icon btn-danger" onclick="stopTargetProcessing('${target.id}')" title="${t('targets.button.stop')}">
|
<button class="btn btn-icon btn-danger" onclick="stopTargetProcessing('${target.id}')" title="${t('targets.button.stop')}">
|
||||||
@@ -521,6 +530,26 @@ export async function deleteKCTarget(targetId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== KC BRIGHTNESS =====
|
||||||
|
|
||||||
|
export function updateKCBrightnessLabel(targetId, value) {
|
||||||
|
const slider = document.querySelector(`[data-kc-brightness="${targetId}"]`);
|
||||||
|
if (slider) slider.title = Math.round(parseInt(value) / 255 * 100) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveKCBrightness(targetId, value) {
|
||||||
|
const brightness = parseInt(value) / 255;
|
||||||
|
try {
|
||||||
|
await fetch(`${API_BASE}/picture-targets/${targetId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getHeaders(),
|
||||||
|
body: JSON.stringify({ key_colors_settings: { brightness } }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save KC brightness:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== KEY COLORS WEBSOCKET =====
|
// ===== KEY COLORS WEBSOCKET =====
|
||||||
|
|
||||||
export function connectKCWebSocket(targetId) {
|
export function connectKCWebSocket(targetId) {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class KeyColorsSettings:
|
|||||||
interpolation_mode: str = "average"
|
interpolation_mode: str = "average"
|
||||||
smoothing: float = 0.3
|
smoothing: float = 0.3
|
||||||
pattern_template_id: str = ""
|
pattern_template_id: str = ""
|
||||||
|
brightness: float = 1.0
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -52,6 +53,7 @@ class KeyColorsSettings:
|
|||||||
"interpolation_mode": self.interpolation_mode,
|
"interpolation_mode": self.interpolation_mode,
|
||||||
"smoothing": self.smoothing,
|
"smoothing": self.smoothing,
|
||||||
"pattern_template_id": self.pattern_template_id,
|
"pattern_template_id": self.pattern_template_id,
|
||||||
|
"brightness": self.brightness,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -61,6 +63,7 @@ class KeyColorsSettings:
|
|||||||
interpolation_mode=data.get("interpolation_mode", "average"),
|
interpolation_mode=data.get("interpolation_mode", "average"),
|
||||||
smoothing=data.get("smoothing", 0.3),
|
smoothing=data.get("smoothing", 0.3),
|
||||||
pattern_template_id=data.get("pattern_template_id", ""),
|
pattern_template_id=data.get("pattern_template_id", ""),
|
||||||
|
brightness=data.get("brightness", 1.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user