Add min brightness threshold to LED targets
New per-target property: when effective output brightness (max pixel value × device/source brightness) falls below the threshold, LEDs turn off completely. Useful for cutting dim flicker in audio-reactive and ambient setups. Threshold slider (0–254) in target editor, badge on card, hot-swap to running processors, persisted in storage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
|||||||
fps=target.fps,
|
fps=target.fps,
|
||||||
keepalive_interval=target.keepalive_interval,
|
keepalive_interval=target.keepalive_interval,
|
||||||
state_check_interval=target.state_check_interval,
|
state_check_interval=target.state_check_interval,
|
||||||
|
min_brightness_threshold=target.min_brightness_threshold,
|
||||||
description=target.description,
|
description=target.description,
|
||||||
auto_start=target.auto_start,
|
auto_start=target.auto_start,
|
||||||
created_at=target.created_at,
|
created_at=target.created_at,
|
||||||
@@ -159,6 +160,7 @@ async def create_target(
|
|||||||
fps=data.fps,
|
fps=data.fps,
|
||||||
keepalive_interval=data.keepalive_interval,
|
keepalive_interval=data.keepalive_interval,
|
||||||
state_check_interval=data.state_check_interval,
|
state_check_interval=data.state_check_interval,
|
||||||
|
min_brightness_threshold=data.min_brightness_threshold,
|
||||||
picture_source_id=data.picture_source_id,
|
picture_source_id=data.picture_source_id,
|
||||||
key_colors_settings=kc_settings,
|
key_colors_settings=kc_settings,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
@@ -276,6 +278,7 @@ async def update_target(
|
|||||||
fps=data.fps,
|
fps=data.fps,
|
||||||
keepalive_interval=data.keepalive_interval,
|
keepalive_interval=data.keepalive_interval,
|
||||||
state_check_interval=data.state_check_interval,
|
state_check_interval=data.state_check_interval,
|
||||||
|
min_brightness_threshold=data.min_brightness_threshold,
|
||||||
key_colors_settings=kc_settings,
|
key_colors_settings=kc_settings,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
auto_start=data.auto_start,
|
auto_start=data.auto_start,
|
||||||
@@ -295,6 +298,7 @@ async def update_target(
|
|||||||
settings_changed=(data.fps is not None or
|
settings_changed=(data.fps is not None or
|
||||||
data.keepalive_interval is not None or
|
data.keepalive_interval is not None or
|
||||||
data.state_check_interval is not None or
|
data.state_check_interval is not None or
|
||||||
|
data.min_brightness_threshold is not None or
|
||||||
data.key_colors_settings is not None),
|
data.key_colors_settings is not None),
|
||||||
css_changed=data.color_strip_source_id is not None,
|
css_changed=data.color_strip_source_id is not None,
|
||||||
device_changed=data.device_id is not None,
|
device_changed=data.device_id is not None,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class PictureTargetCreate(BaseModel):
|
|||||||
fps: int = Field(default=30, ge=1, le=90, description="Target send FPS (1-90)")
|
fps: int = Field(default=30, ge=1, le=90, description="Target send FPS (1-90)")
|
||||||
keepalive_interval: float = Field(default=1.0, description="Keepalive send interval when screen is static (0.5-5.0s)", ge=0.5, le=5.0)
|
keepalive_interval: float = Field(default=1.0, description="Keepalive send interval when screen is static (0.5-5.0s)", ge=0.5, le=5.0)
|
||||||
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Device health check interval (5-600s)", ge=5, le=600)
|
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Device health check interval (5-600s)", ge=5, le=600)
|
||||||
|
min_brightness_threshold: int = Field(default=0, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off")
|
||||||
# KC target fields
|
# KC target fields
|
||||||
picture_source_id: str = Field(default="", description="Picture source ID (for key_colors targets)")
|
picture_source_id: str = Field(default="", description="Picture source ID (for key_colors targets)")
|
||||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||||
@@ -76,6 +77,7 @@ class PictureTargetUpdate(BaseModel):
|
|||||||
fps: Optional[int] = Field(None, ge=1, le=90, description="Target send FPS (1-90)")
|
fps: Optional[int] = Field(None, ge=1, le=90, description="Target send FPS (1-90)")
|
||||||
keepalive_interval: Optional[float] = Field(None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0)
|
keepalive_interval: Optional[float] = Field(None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0)
|
||||||
state_check_interval: Optional[int] = Field(None, description="Health check interval (5-600s)", ge=5, le=600)
|
state_check_interval: Optional[int] = Field(None, description="Health check interval (5-600s)", ge=5, le=600)
|
||||||
|
min_brightness_threshold: Optional[int] = Field(None, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off")
|
||||||
# KC target fields
|
# KC target fields
|
||||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors targets)")
|
picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors targets)")
|
||||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
|
||||||
@@ -96,6 +98,7 @@ class PictureTargetResponse(BaseModel):
|
|||||||
fps: Optional[int] = Field(None, description="Target send FPS")
|
fps: Optional[int] = Field(None, description="Target send FPS")
|
||||||
keepalive_interval: float = Field(default=1.0, description="Keepalive interval (s)")
|
keepalive_interval: float = Field(default=1.0, description="Keepalive interval (s)")
|
||||||
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)")
|
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)")
|
||||||
|
min_brightness_threshold: int = Field(default=0, description="Min brightness threshold (0=disabled)")
|
||||||
# KC target fields
|
# KC target fields
|
||||||
picture_source_id: str = Field(default="", description="Picture source ID (key_colors)")
|
picture_source_id: str = Field(default="", description="Picture source ID (key_colors)")
|
||||||
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings")
|
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings")
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ class ProcessorManager:
|
|||||||
keepalive_interval: float = 1.0,
|
keepalive_interval: float = 1.0,
|
||||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
||||||
brightness_value_source_id: str = "",
|
brightness_value_source_id: str = "",
|
||||||
|
min_brightness_threshold: int = 0,
|
||||||
):
|
):
|
||||||
"""Register a WLED target processor."""
|
"""Register a WLED target processor."""
|
||||||
if target_id in self._processors:
|
if target_id in self._processors:
|
||||||
@@ -332,6 +333,7 @@ class ProcessorManager:
|
|||||||
keepalive_interval=keepalive_interval,
|
keepalive_interval=keepalive_interval,
|
||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
brightness_value_source_id=brightness_value_source_id,
|
brightness_value_source_id=brightness_value_source_id,
|
||||||
|
min_brightness_threshold=min_brightness_threshold,
|
||||||
ctx=self._build_context(),
|
ctx=self._build_context(),
|
||||||
)
|
)
|
||||||
self._processors[target_id] = proc
|
self._processors[target_id] = proc
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
keepalive_interval: float = 1.0,
|
keepalive_interval: float = 1.0,
|
||||||
state_check_interval: int = 30,
|
state_check_interval: int = 30,
|
||||||
brightness_value_source_id: str = "",
|
brightness_value_source_id: str = "",
|
||||||
|
min_brightness_threshold: int = 0,
|
||||||
ctx: TargetContext = None,
|
ctx: TargetContext = None,
|
||||||
):
|
):
|
||||||
super().__init__(target_id, ctx)
|
super().__init__(target_id, ctx)
|
||||||
@@ -45,6 +46,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
self._state_check_interval = state_check_interval
|
self._state_check_interval = state_check_interval
|
||||||
self._css_id = color_strip_source_id
|
self._css_id = color_strip_source_id
|
||||||
self._brightness_vs_id = brightness_value_source_id
|
self._brightness_vs_id = brightness_value_source_id
|
||||||
|
self._min_brightness_threshold = min_brightness_threshold
|
||||||
|
|
||||||
# Runtime state (populated on start)
|
# Runtime state (populated on start)
|
||||||
self._led_client: Optional[LEDClient] = None
|
self._led_client: Optional[LEDClient] = None
|
||||||
@@ -210,6 +212,8 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
self._keepalive_interval = settings["keepalive_interval"]
|
self._keepalive_interval = settings["keepalive_interval"]
|
||||||
if "state_check_interval" in settings:
|
if "state_check_interval" in settings:
|
||||||
self._state_check_interval = settings["state_check_interval"]
|
self._state_check_interval = settings["state_check_interval"]
|
||||||
|
if "min_brightness_threshold" in settings:
|
||||||
|
self._min_brightness_threshold = settings["min_brightness_threshold"]
|
||||||
logger.info(f"Updated settings for target {self._target_id}")
|
logger.info(f"Updated settings for target {self._target_id}")
|
||||||
|
|
||||||
def update_device(self, device_id: str) -> None:
|
def update_device(self, device_id: str) -> None:
|
||||||
@@ -581,6 +585,15 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
|
|
||||||
cur_brightness = _effective_brightness(device_info)
|
cur_brightness = _effective_brightness(device_info)
|
||||||
|
|
||||||
|
# Min brightness threshold: combine brightness source
|
||||||
|
# with max pixel value to get effective output brightness.
|
||||||
|
# If below cutoff → snap to 0 (LEDs off).
|
||||||
|
_thresh = self._min_brightness_threshold
|
||||||
|
if _thresh > 0 and cur_brightness > 0:
|
||||||
|
max_pixel = int(np.max(frame))
|
||||||
|
if max_pixel * cur_brightness // 255 < _thresh:
|
||||||
|
cur_brightness = 0
|
||||||
|
|
||||||
# Zero-brightness suppression: if output is black and
|
# Zero-brightness suppression: if output is black and
|
||||||
# the last sent frame was also black, skip sending.
|
# the last sent frame was also black, skip sending.
|
||||||
if cur_brightness <= 1 and _prev_brightness <= 1 and has_any_frame:
|
if cur_brightness <= 1 and _prev_brightness <= 1 and has_any_frame:
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class TargetEditorModal extends Modal {
|
|||||||
device: document.getElementById('target-editor-device').value,
|
device: document.getElementById('target-editor-device').value,
|
||||||
css_source: document.getElementById('target-editor-css-source').value,
|
css_source: document.getElementById('target-editor-css-source').value,
|
||||||
brightness_vs: document.getElementById('target-editor-brightness-vs').value,
|
brightness_vs: document.getElementById('target-editor-brightness-vs').value,
|
||||||
|
brightness_threshold: document.getElementById('target-editor-brightness-threshold').value,
|
||||||
fps: document.getElementById('target-editor-fps').value,
|
fps: document.getElementById('target-editor-fps').value,
|
||||||
keepalive_interval: document.getElementById('target-editor-keepalive-interval').value,
|
keepalive_interval: document.getElementById('target-editor-keepalive-interval').value,
|
||||||
};
|
};
|
||||||
@@ -198,6 +199,11 @@ function _updateKeepaliveVisibility() {
|
|||||||
keepaliveGroup.style.display = caps.includes('standby_required') ? '' : 'none';
|
keepaliveGroup.style.display = caps.includes('standby_required') ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _updateBrightnessThresholdVisibility() {
|
||||||
|
// Always visible — threshold considers both brightness source and pixel content
|
||||||
|
document.getElementById('target-editor-brightness-threshold-group').style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
function _populateCssDropdown(selectedId = '') {
|
function _populateCssDropdown(selectedId = '') {
|
||||||
const select = document.getElementById('target-editor-css-source');
|
const select = document.getElementById('target-editor-css-source');
|
||||||
select.innerHTML = _editorCssSources.map(s =>
|
select.innerHTML = _editorCssSources.map(s =>
|
||||||
@@ -262,6 +268,10 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
document.getElementById('target-editor-keepalive-interval-value').textContent = target.keepalive_interval ?? 1.0;
|
document.getElementById('target-editor-keepalive-interval-value').textContent = target.keepalive_interval ?? 1.0;
|
||||||
document.getElementById('target-editor-title').textContent = t('targets.edit');
|
document.getElementById('target-editor-title').textContent = t('targets.edit');
|
||||||
|
|
||||||
|
const thresh = target.min_brightness_threshold ?? 0;
|
||||||
|
document.getElementById('target-editor-brightness-threshold').value = thresh;
|
||||||
|
document.getElementById('target-editor-brightness-threshold-value').textContent = thresh;
|
||||||
|
|
||||||
_populateCssDropdown(target.color_strip_source_id || '');
|
_populateCssDropdown(target.color_strip_source_id || '');
|
||||||
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
|
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
|
||||||
} else if (cloneData) {
|
} else if (cloneData) {
|
||||||
@@ -276,6 +286,10 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
document.getElementById('target-editor-keepalive-interval-value').textContent = cloneData.keepalive_interval ?? 1.0;
|
document.getElementById('target-editor-keepalive-interval-value').textContent = cloneData.keepalive_interval ?? 1.0;
|
||||||
document.getElementById('target-editor-title').textContent = t('targets.add');
|
document.getElementById('target-editor-title').textContent = t('targets.add');
|
||||||
|
|
||||||
|
const cloneThresh = cloneData.min_brightness_threshold ?? 0;
|
||||||
|
document.getElementById('target-editor-brightness-threshold').value = cloneThresh;
|
||||||
|
document.getElementById('target-editor-brightness-threshold-value').textContent = cloneThresh;
|
||||||
|
|
||||||
_populateCssDropdown(cloneData.color_strip_source_id || '');
|
_populateCssDropdown(cloneData.color_strip_source_id || '');
|
||||||
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
|
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
|
||||||
} else {
|
} else {
|
||||||
@@ -288,6 +302,9 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
document.getElementById('target-editor-keepalive-interval-value').textContent = '1.0';
|
document.getElementById('target-editor-keepalive-interval-value').textContent = '1.0';
|
||||||
document.getElementById('target-editor-title').textContent = t('targets.add');
|
document.getElementById('target-editor-title').textContent = t('targets.add');
|
||||||
|
|
||||||
|
document.getElementById('target-editor-brightness-threshold').value = 0;
|
||||||
|
document.getElementById('target-editor-brightness-threshold-value').textContent = '0';
|
||||||
|
|
||||||
_populateCssDropdown('');
|
_populateCssDropdown('');
|
||||||
_populateBrightnessVsDropdown('');
|
_populateBrightnessVsDropdown('');
|
||||||
}
|
}
|
||||||
@@ -298,12 +315,14 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
window._targetAutoName = _autoGenerateTargetName;
|
window._targetAutoName = _autoGenerateTargetName;
|
||||||
deviceSelect.onchange = () => { _updateDeviceInfo(); _updateKeepaliveVisibility(); _updateFpsRecommendation(); _autoGenerateTargetName(); };
|
deviceSelect.onchange = () => { _updateDeviceInfo(); _updateKeepaliveVisibility(); _updateFpsRecommendation(); _autoGenerateTargetName(); };
|
||||||
document.getElementById('target-editor-css-source').onchange = () => { _autoGenerateTargetName(); };
|
document.getElementById('target-editor-css-source').onchange = () => { _autoGenerateTargetName(); };
|
||||||
|
document.getElementById('target-editor-brightness-vs').onchange = () => { _updateBrightnessThresholdVisibility(); };
|
||||||
if (!targetId && !cloneData) _autoGenerateTargetName();
|
if (!targetId && !cloneData) _autoGenerateTargetName();
|
||||||
|
|
||||||
// Show/hide standby interval based on selected device capabilities
|
// Show/hide conditional fields
|
||||||
_updateDeviceInfo();
|
_updateDeviceInfo();
|
||||||
_updateKeepaliveVisibility();
|
_updateKeepaliveVisibility();
|
||||||
_updateFpsRecommendation();
|
_updateFpsRecommendation();
|
||||||
|
_updateBrightnessThresholdVisibility();
|
||||||
|
|
||||||
targetEditorModal.snapshot();
|
targetEditorModal.snapshot();
|
||||||
targetEditorModal.open();
|
targetEditorModal.open();
|
||||||
@@ -343,12 +362,14 @@ export async function saveTargetEditor() {
|
|||||||
const colorStripSourceId = document.getElementById('target-editor-css-source').value;
|
const colorStripSourceId = document.getElementById('target-editor-css-source').value;
|
||||||
|
|
||||||
const brightnessVsId = document.getElementById('target-editor-brightness-vs').value;
|
const brightnessVsId = document.getElementById('target-editor-brightness-vs').value;
|
||||||
|
const minBrightnessThreshold = parseInt(document.getElementById('target-editor-brightness-threshold').value) || 0;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name,
|
||||||
device_id: deviceId,
|
device_id: deviceId,
|
||||||
color_strip_source_id: colorStripSourceId,
|
color_strip_source_id: colorStripSourceId,
|
||||||
brightness_value_source_id: brightnessVsId,
|
brightness_value_source_id: brightnessVsId,
|
||||||
|
min_brightness_threshold: minBrightnessThreshold,
|
||||||
fps,
|
fps,
|
||||||
keepalive_interval: standbyInterval,
|
keepalive_interval: standbyInterval,
|
||||||
};
|
};
|
||||||
@@ -808,6 +829,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<span class="stream-card-prop" title="${t('targets.fps')}">${ICON_FPS} ${target.fps || 30}</span>
|
<span class="stream-card-prop" title="${t('targets.fps')}">${ICON_FPS} ${target.fps || 30}</span>
|
||||||
<span class="stream-card-prop stream-card-prop-full${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('targets','led','led-css','data-css-id','${cssId}')"` : ''}>🎞️ ${cssSummary}</span>
|
<span class="stream-card-prop stream-card-prop-full${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('targets','led','led-css','data-css-id','${cssId}')"` : ''}>🎞️ ${cssSummary}</span>
|
||||||
${bvs ? `<span class="stream-card-prop stream-card-prop-full stream-card-link" title="${t('targets.brightness_vs')}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')">${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}</span>` : ''}
|
${bvs ? `<span class="stream-card-prop stream-card-prop-full stream-card-link" title="${t('targets.brightness_vs')}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')">${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}</span>` : ''}
|
||||||
|
${target.min_brightness_threshold > 0 ? `<span class="stream-card-prop" title="${t('targets.min_brightness_threshold')}">🔅 <${target.min_brightness_threshold} → off</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
|
|||||||
@@ -896,6 +896,8 @@
|
|||||||
"targets.brightness_vs": "Brightness Source:",
|
"targets.brightness_vs": "Brightness Source:",
|
||||||
"targets.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (overrides device brightness)",
|
"targets.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (overrides device brightness)",
|
||||||
"targets.brightness_vs.none": "None (device brightness)",
|
"targets.brightness_vs.none": "None (device brightness)",
|
||||||
|
"targets.min_brightness_threshold": "Min Brightness Threshold:",
|
||||||
|
"targets.min_brightness_threshold.hint": "Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)",
|
||||||
|
|
||||||
"search.open": "Search (Ctrl+K)",
|
"search.open": "Search (Ctrl+K)",
|
||||||
"search.placeholder": "Search entities... (Ctrl+K)",
|
"search.placeholder": "Search entities... (Ctrl+K)",
|
||||||
|
|||||||
@@ -896,6 +896,8 @@
|
|||||||
"targets.brightness_vs": "Источник яркости:",
|
"targets.brightness_vs": "Источник яркости:",
|
||||||
"targets.brightness_vs.hint": "Необязательный источник значений для динамического управления яркостью каждый кадр (переопределяет яркость устройства)",
|
"targets.brightness_vs.hint": "Необязательный источник значений для динамического управления яркостью каждый кадр (переопределяет яркость устройства)",
|
||||||
"targets.brightness_vs.none": "Нет (яркость устройства)",
|
"targets.brightness_vs.none": "Нет (яркость устройства)",
|
||||||
|
"targets.min_brightness_threshold": "Мин. порог яркости:",
|
||||||
|
"targets.min_brightness_threshold.hint": "Если итоговая яркость (яркость пикселей × яркость устройства/источника) ниже этого значения, светодиоды полностью выключаются (0 = отключено)",
|
||||||
|
|
||||||
"search.open": "Поиск (Ctrl+K)",
|
"search.open": "Поиск (Ctrl+K)",
|
||||||
"search.placeholder": "Поиск... (Ctrl+K)",
|
"search.placeholder": "Поиск... (Ctrl+K)",
|
||||||
|
|||||||
@@ -896,6 +896,8 @@
|
|||||||
"targets.brightness_vs": "亮度源:",
|
"targets.brightness_vs": "亮度源:",
|
||||||
"targets.brightness_vs.hint": "可选的值源,每帧动态控制亮度(覆盖设备亮度)",
|
"targets.brightness_vs.hint": "可选的值源,每帧动态控制亮度(覆盖设备亮度)",
|
||||||
"targets.brightness_vs.none": "无(设备亮度)",
|
"targets.brightness_vs.none": "无(设备亮度)",
|
||||||
|
"targets.min_brightness_threshold": "最低亮度阈值:",
|
||||||
|
"targets.min_brightness_threshold.hint": "当有效输出亮度(像素亮度 × 设备/源亮度)低于此值时,LED完全关闭(0 = 禁用)",
|
||||||
|
|
||||||
"search.open": "搜索 (Ctrl+K)",
|
"search.open": "搜索 (Ctrl+K)",
|
||||||
"search.placeholder": "搜索实体... (Ctrl+K)",
|
"search.placeholder": "搜索实体... (Ctrl+K)",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class PictureTargetStore:
|
|||||||
fps: int = 30,
|
fps: int = 30,
|
||||||
keepalive_interval: float = 1.0,
|
keepalive_interval: float = 1.0,
|
||||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
||||||
|
min_brightness_threshold: int = 0,
|
||||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
picture_source_id: str = "",
|
picture_source_id: str = "",
|
||||||
@@ -138,6 +139,7 @@ class PictureTargetStore:
|
|||||||
fps=fps,
|
fps=fps,
|
||||||
keepalive_interval=keepalive_interval,
|
keepalive_interval=keepalive_interval,
|
||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
|
min_brightness_threshold=min_brightness_threshold,
|
||||||
description=description,
|
description=description,
|
||||||
auto_start=auto_start,
|
auto_start=auto_start,
|
||||||
created_at=now,
|
created_at=now,
|
||||||
@@ -174,6 +176,7 @@ class PictureTargetStore:
|
|||||||
fps: Optional[int] = None,
|
fps: Optional[int] = None,
|
||||||
keepalive_interval: Optional[float] = None,
|
keepalive_interval: Optional[float] = None,
|
||||||
state_check_interval: Optional[int] = None,
|
state_check_interval: Optional[int] = None,
|
||||||
|
min_brightness_threshold: Optional[int] = None,
|
||||||
key_colors_settings: Optional[KeyColorsSettings] = None,
|
key_colors_settings: Optional[KeyColorsSettings] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
auto_start: Optional[bool] = None,
|
auto_start: Optional[bool] = None,
|
||||||
@@ -202,6 +205,7 @@ class PictureTargetStore:
|
|||||||
fps=fps,
|
fps=fps,
|
||||||
keepalive_interval=keepalive_interval,
|
keepalive_interval=keepalive_interval,
|
||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
|
min_brightness_threshold=min_brightness_threshold,
|
||||||
key_colors_settings=key_colors_settings,
|
key_colors_settings=key_colors_settings,
|
||||||
description=description,
|
description=description,
|
||||||
auto_start=auto_start,
|
auto_start=auto_start,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
fps: int = 30 # target send FPS (1-90)
|
fps: int = 30 # target send FPS (1-90)
|
||||||
keepalive_interval: float = 1.0 # seconds between keepalive sends when screen is static
|
keepalive_interval: float = 1.0 # seconds between keepalive sends when screen is static
|
||||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL
|
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL
|
||||||
|
min_brightness_threshold: int = 0 # brightness below this → 0 (disabled when 0)
|
||||||
|
|
||||||
def register_with_manager(self, manager) -> None:
|
def register_with_manager(self, manager) -> None:
|
||||||
"""Register this WLED target with the processor manager."""
|
"""Register this WLED target with the processor manager."""
|
||||||
@@ -31,6 +32,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
keepalive_interval=self.keepalive_interval,
|
keepalive_interval=self.keepalive_interval,
|
||||||
state_check_interval=self.state_check_interval,
|
state_check_interval=self.state_check_interval,
|
||||||
brightness_value_source_id=self.brightness_value_source_id,
|
brightness_value_source_id=self.brightness_value_source_id,
|
||||||
|
min_brightness_threshold=self.min_brightness_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_with_manager(self, manager, *, settings_changed: bool,
|
def sync_with_manager(self, manager, *, settings_changed: bool,
|
||||||
@@ -43,6 +45,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
"fps": self.fps,
|
"fps": self.fps,
|
||||||
"keepalive_interval": self.keepalive_interval,
|
"keepalive_interval": self.keepalive_interval,
|
||||||
"state_check_interval": self.state_check_interval,
|
"state_check_interval": self.state_check_interval,
|
||||||
|
"min_brightness_threshold": self.min_brightness_threshold,
|
||||||
})
|
})
|
||||||
if css_changed:
|
if css_changed:
|
||||||
manager.update_target_css(self.id, self.color_strip_source_id)
|
manager.update_target_css(self.id, self.color_strip_source_id)
|
||||||
@@ -54,6 +57,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
def update_fields(self, *, name=None, device_id=None, color_strip_source_id=None,
|
def update_fields(self, *, name=None, device_id=None, color_strip_source_id=None,
|
||||||
brightness_value_source_id=None,
|
brightness_value_source_id=None,
|
||||||
fps=None, keepalive_interval=None, state_check_interval=None,
|
fps=None, keepalive_interval=None, state_check_interval=None,
|
||||||
|
min_brightness_threshold=None,
|
||||||
description=None, auto_start=None, **_kwargs) -> None:
|
description=None, auto_start=None, **_kwargs) -> None:
|
||||||
"""Apply mutable field updates for WLED targets."""
|
"""Apply mutable field updates for WLED targets."""
|
||||||
super().update_fields(name=name, description=description, auto_start=auto_start)
|
super().update_fields(name=name, description=description, auto_start=auto_start)
|
||||||
@@ -69,6 +73,8 @@ class WledPictureTarget(PictureTarget):
|
|||||||
self.keepalive_interval = keepalive_interval
|
self.keepalive_interval = keepalive_interval
|
||||||
if state_check_interval is not None:
|
if state_check_interval is not None:
|
||||||
self.state_check_interval = state_check_interval
|
self.state_check_interval = state_check_interval
|
||||||
|
if min_brightness_threshold is not None:
|
||||||
|
self.min_brightness_threshold = min_brightness_threshold
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_picture_source(self) -> bool:
|
def has_picture_source(self) -> bool:
|
||||||
@@ -83,6 +89,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
d["fps"] = self.fps
|
d["fps"] = self.fps
|
||||||
d["keepalive_interval"] = self.keepalive_interval
|
d["keepalive_interval"] = self.keepalive_interval
|
||||||
d["state_check_interval"] = self.state_check_interval
|
d["state_check_interval"] = self.state_check_interval
|
||||||
|
d["min_brightness_threshold"] = self.min_brightness_threshold
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -108,6 +115,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
fps=data.get("fps", 30),
|
fps=data.get("fps", 30),
|
||||||
keepalive_interval=data.get("keepalive_interval", data.get("standby_interval", 1.0)),
|
keepalive_interval=data.get("keepalive_interval", data.get("standby_interval", 1.0)),
|
||||||
state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
|
state_check_interval=data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
|
||||||
|
min_brightness_threshold=data.get("min_brightness_threshold", 0),
|
||||||
description=data.get("description"),
|
description=data.get("description"),
|
||||||
auto_start=data.get("auto_start", False),
|
auto_start=data.get("auto_start", False),
|
||||||
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
|
||||||
|
|||||||
@@ -44,6 +44,18 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="target-editor-brightness-threshold-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-brightness-threshold">
|
||||||
|
<span data-i18n="targets.min_brightness_threshold">Min Brightness Threshold:</span>
|
||||||
|
<span id="target-editor-brightness-threshold-value">0</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.min_brightness_threshold.hint">Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)</small>
|
||||||
|
<input type="range" id="target-editor-brightness-threshold" min="0" max="254" value="0" oninput="document.getElementById('target-editor-brightness-threshold-value').textContent = this.value">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="target-editor-fps-group">
|
<div class="form-group" id="target-editor-fps-group">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label for="target-editor-fps">
|
<label for="target-editor-fps">
|
||||||
|
|||||||
Reference in New Issue
Block a user