Add per-target protocol selection (DDP/HTTP) and reorganize target editor
- Add protocol field (ddp/http) to storage, API schemas, routes, processor - WledTargetProcessor passes protocol to create_led_client(use_ddp=...) - Target editor: protocol dropdown + keepalive in collapsible Specific Settings - FPS, brightness threshold, adaptive FPS moved to main form area - Hide Specific Settings section for serial devices (protocol is WLED-only) - Card badge: show DDP/HTTP for WLED devices, Serial for serial devices Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,7 @@ def _target_to_response(target) -> PictureTargetResponse:
|
|||||||
state_check_interval=target.state_check_interval,
|
state_check_interval=target.state_check_interval,
|
||||||
min_brightness_threshold=target.min_brightness_threshold,
|
min_brightness_threshold=target.min_brightness_threshold,
|
||||||
adaptive_fps=target.adaptive_fps,
|
adaptive_fps=target.adaptive_fps,
|
||||||
|
protocol=target.protocol,
|
||||||
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,
|
||||||
@@ -163,6 +164,7 @@ async def create_target(
|
|||||||
state_check_interval=data.state_check_interval,
|
state_check_interval=data.state_check_interval,
|
||||||
min_brightness_threshold=data.min_brightness_threshold,
|
min_brightness_threshold=data.min_brightness_threshold,
|
||||||
adaptive_fps=data.adaptive_fps,
|
adaptive_fps=data.adaptive_fps,
|
||||||
|
protocol=data.protocol,
|
||||||
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,
|
||||||
@@ -282,6 +284,7 @@ async def update_target(
|
|||||||
state_check_interval=data.state_check_interval,
|
state_check_interval=data.state_check_interval,
|
||||||
min_brightness_threshold=data.min_brightness_threshold,
|
min_brightness_threshold=data.min_brightness_threshold,
|
||||||
adaptive_fps=data.adaptive_fps,
|
adaptive_fps=data.adaptive_fps,
|
||||||
|
protocol=data.protocol,
|
||||||
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,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class PictureTargetCreate(BaseModel):
|
|||||||
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")
|
min_brightness_threshold: int = Field(default=0, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off")
|
||||||
adaptive_fps: bool = Field(default=False, description="Auto-reduce FPS when device is unresponsive")
|
adaptive_fps: bool = Field(default=False, description="Auto-reduce FPS when device is unresponsive")
|
||||||
|
protocol: str = Field(default="ddp", pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)")
|
||||||
# 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)")
|
||||||
@@ -80,6 +81,7 @@ class PictureTargetUpdate(BaseModel):
|
|||||||
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")
|
min_brightness_threshold: Optional[int] = Field(None, ge=0, le=254, description="Min brightness threshold (0=disabled); below this → off")
|
||||||
adaptive_fps: Optional[bool] = Field(None, description="Auto-reduce FPS when device is unresponsive")
|
adaptive_fps: Optional[bool] = Field(None, description="Auto-reduce FPS when device is unresponsive")
|
||||||
|
protocol: Optional[str] = Field(None, pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)")
|
||||||
# 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)")
|
||||||
@@ -102,6 +104,7 @@ class PictureTargetResponse(BaseModel):
|
|||||||
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)")
|
min_brightness_threshold: int = Field(default=0, description="Min brightness threshold (0=disabled)")
|
||||||
adaptive_fps: bool = Field(default=False, description="Auto-reduce FPS when device is unresponsive")
|
adaptive_fps: bool = Field(default=False, description="Auto-reduce FPS when device is unresponsive")
|
||||||
|
protocol: str = Field(default="ddp", description="Send protocol (ddp or http)")
|
||||||
# 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")
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ class ProcessorManager:
|
|||||||
brightness_value_source_id: str = "",
|
brightness_value_source_id: str = "",
|
||||||
min_brightness_threshold: int = 0,
|
min_brightness_threshold: int = 0,
|
||||||
adaptive_fps: bool = False,
|
adaptive_fps: bool = False,
|
||||||
|
protocol: str = "ddp",
|
||||||
):
|
):
|
||||||
"""Register a WLED target processor."""
|
"""Register a WLED target processor."""
|
||||||
if target_id in self._processors:
|
if target_id in self._processors:
|
||||||
@@ -340,6 +341,7 @@ class ProcessorManager:
|
|||||||
brightness_value_source_id=brightness_value_source_id,
|
brightness_value_source_id=brightness_value_source_id,
|
||||||
min_brightness_threshold=min_brightness_threshold,
|
min_brightness_threshold=min_brightness_threshold,
|
||||||
adaptive_fps=adaptive_fps,
|
adaptive_fps=adaptive_fps,
|
||||||
|
protocol=protocol,
|
||||||
ctx=self._build_context(),
|
ctx=self._build_context(),
|
||||||
)
|
)
|
||||||
self._processors[target_id] = proc
|
self._processors[target_id] = proc
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
brightness_value_source_id: str = "",
|
brightness_value_source_id: str = "",
|
||||||
min_brightness_threshold: int = 0,
|
min_brightness_threshold: int = 0,
|
||||||
adaptive_fps: bool = False,
|
adaptive_fps: bool = False,
|
||||||
|
protocol: str = "ddp",
|
||||||
ctx: TargetContext = None,
|
ctx: TargetContext = None,
|
||||||
):
|
):
|
||||||
super().__init__(target_id, ctx)
|
super().__init__(target_id, ctx)
|
||||||
@@ -50,6 +51,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
self._brightness_vs_id = brightness_value_source_id
|
self._brightness_vs_id = brightness_value_source_id
|
||||||
self._min_brightness_threshold = min_brightness_threshold
|
self._min_brightness_threshold = min_brightness_threshold
|
||||||
self._adaptive_fps = adaptive_fps
|
self._adaptive_fps = adaptive_fps
|
||||||
|
self._protocol = protocol
|
||||||
|
|
||||||
# Adaptive FPS / liveness probe runtime state
|
# Adaptive FPS / liveness probe runtime state
|
||||||
self._effective_fps: int = self._target_fps
|
self._effective_fps: int = self._target_fps
|
||||||
@@ -95,7 +97,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
try:
|
try:
|
||||||
self._led_client = create_led_client(
|
self._led_client = create_led_client(
|
||||||
device_info.device_type, device_info.device_url,
|
device_info.device_type, device_info.device_url,
|
||||||
use_ddp=True, led_count=device_info.led_count,
|
use_ddp=(self._protocol == "ddp"), led_count=device_info.led_count,
|
||||||
baud_rate=device_info.baud_rate,
|
baud_rate=device_info.baud_rate,
|
||||||
send_latency_ms=device_info.send_latency_ms,
|
send_latency_ms=device_info.send_latency_ms,
|
||||||
rgbw=device_info.rgbw,
|
rgbw=device_info.rgbw,
|
||||||
@@ -373,6 +375,7 @@ class WledTargetProcessor(TargetProcessor):
|
|||||||
"errors": [metrics.last_error] if metrics.last_error else [],
|
"errors": [metrics.last_error] if metrics.last_error else [],
|
||||||
"device_streaming_reachable": self._device_reachable if self._is_running else None,
|
"device_streaming_reachable": self._device_reachable if self._is_running else None,
|
||||||
"fps_effective": self._effective_fps if self._is_running else None,
|
"fps_effective": self._effective_fps if self._is_running else None,
|
||||||
|
"protocol": self._protocol,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_metrics(self) -> dict:
|
def get_metrics(self) -> dict:
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ class TargetEditorModal extends Modal {
|
|||||||
return {
|
return {
|
||||||
name: document.getElementById('target-editor-name').value,
|
name: document.getElementById('target-editor-name').value,
|
||||||
device: document.getElementById('target-editor-device').value,
|
device: document.getElementById('target-editor-device').value,
|
||||||
|
protocol: document.getElementById('target-editor-protocol').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,
|
brightness_threshold: document.getElementById('target-editor-brightness-threshold').value,
|
||||||
@@ -200,6 +201,14 @@ function _updateKeepaliveVisibility() {
|
|||||||
keepaliveGroup.style.display = caps.includes('standby_required') ? '' : 'none';
|
keepaliveGroup.style.display = caps.includes('standby_required') ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _updateSpecificSettingsVisibility() {
|
||||||
|
const deviceSelect = document.getElementById('target-editor-device');
|
||||||
|
const selectedDevice = _targetEditorDevices.find(d => d.id === deviceSelect.value);
|
||||||
|
const isWled = !selectedDevice || selectedDevice.device_type === 'wled';
|
||||||
|
// Hide entire Specific Settings section for non-WLED devices (protocol + keepalive are WLED-only)
|
||||||
|
document.getElementById('target-editor-device-settings').style.display = isWled ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
function _updateBrightnessThresholdVisibility() {
|
function _updateBrightnessThresholdVisibility() {
|
||||||
// Always visible — threshold considers both brightness source and pixel content
|
// Always visible — threshold considers both brightness source and pixel content
|
||||||
document.getElementById('target-editor-brightness-threshold-group').style.display = '';
|
document.getElementById('target-editor-brightness-threshold-group').style.display = '';
|
||||||
@@ -274,6 +283,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
document.getElementById('target-editor-brightness-threshold-value').textContent = thresh;
|
document.getElementById('target-editor-brightness-threshold-value').textContent = thresh;
|
||||||
|
|
||||||
document.getElementById('target-editor-adaptive-fps').checked = target.adaptive_fps ?? false;
|
document.getElementById('target-editor-adaptive-fps').checked = target.adaptive_fps ?? false;
|
||||||
|
document.getElementById('target-editor-protocol').value = target.protocol || 'ddp';
|
||||||
|
|
||||||
_populateCssDropdown(target.color_strip_source_id || '');
|
_populateCssDropdown(target.color_strip_source_id || '');
|
||||||
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
|
_populateBrightnessVsDropdown(target.brightness_value_source_id || '');
|
||||||
@@ -294,6 +304,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
document.getElementById('target-editor-brightness-threshold-value').textContent = cloneThresh;
|
document.getElementById('target-editor-brightness-threshold-value').textContent = cloneThresh;
|
||||||
|
|
||||||
document.getElementById('target-editor-adaptive-fps').checked = cloneData.adaptive_fps ?? false;
|
document.getElementById('target-editor-adaptive-fps').checked = cloneData.adaptive_fps ?? false;
|
||||||
|
document.getElementById('target-editor-protocol').value = cloneData.protocol || 'ddp';
|
||||||
|
|
||||||
_populateCssDropdown(cloneData.color_strip_source_id || '');
|
_populateCssDropdown(cloneData.color_strip_source_id || '');
|
||||||
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
|
_populateBrightnessVsDropdown(cloneData.brightness_value_source_id || '');
|
||||||
@@ -311,6 +322,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
document.getElementById('target-editor-brightness-threshold-value').textContent = '0';
|
document.getElementById('target-editor-brightness-threshold-value').textContent = '0';
|
||||||
|
|
||||||
document.getElementById('target-editor-adaptive-fps').checked = false;
|
document.getElementById('target-editor-adaptive-fps').checked = false;
|
||||||
|
document.getElementById('target-editor-protocol').value = 'ddp';
|
||||||
|
|
||||||
_populateCssDropdown('');
|
_populateCssDropdown('');
|
||||||
_populateBrightnessVsDropdown('');
|
_populateBrightnessVsDropdown('');
|
||||||
@@ -320,7 +332,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
_targetNameManuallyEdited = !!(targetId || cloneData);
|
_targetNameManuallyEdited = !!(targetId || cloneData);
|
||||||
document.getElementById('target-editor-name').oninput = () => { _targetNameManuallyEdited = true; };
|
document.getElementById('target-editor-name').oninput = () => { _targetNameManuallyEdited = true; };
|
||||||
window._targetAutoName = _autoGenerateTargetName;
|
window._targetAutoName = _autoGenerateTargetName;
|
||||||
deviceSelect.onchange = () => { _updateDeviceInfo(); _updateKeepaliveVisibility(); _updateFpsRecommendation(); _autoGenerateTargetName(); };
|
deviceSelect.onchange = () => { _updateDeviceInfo(); _updateKeepaliveVisibility(); _updateSpecificSettingsVisibility(); _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(); };
|
document.getElementById('target-editor-brightness-vs').onchange = () => { _updateBrightnessThresholdVisibility(); };
|
||||||
if (!targetId && !cloneData) _autoGenerateTargetName();
|
if (!targetId && !cloneData) _autoGenerateTargetName();
|
||||||
@@ -328,6 +340,7 @@ export async function showTargetEditor(targetId = null, cloneData = null) {
|
|||||||
// Show/hide conditional fields
|
// Show/hide conditional fields
|
||||||
_updateDeviceInfo();
|
_updateDeviceInfo();
|
||||||
_updateKeepaliveVisibility();
|
_updateKeepaliveVisibility();
|
||||||
|
_updateSpecificSettingsVisibility();
|
||||||
_updateFpsRecommendation();
|
_updateFpsRecommendation();
|
||||||
_updateBrightnessThresholdVisibility();
|
_updateBrightnessThresholdVisibility();
|
||||||
|
|
||||||
@@ -372,6 +385,7 @@ export async function saveTargetEditor() {
|
|||||||
const minBrightnessThreshold = parseInt(document.getElementById('target-editor-brightness-threshold').value) || 0;
|
const minBrightnessThreshold = parseInt(document.getElementById('target-editor-brightness-threshold').value) || 0;
|
||||||
|
|
||||||
const adaptiveFps = document.getElementById('target-editor-adaptive-fps').checked;
|
const adaptiveFps = document.getElementById('target-editor-adaptive-fps').checked;
|
||||||
|
const protocol = document.getElementById('target-editor-protocol').value;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name,
|
||||||
@@ -382,6 +396,7 @@ export async function saveTargetEditor() {
|
|||||||
fps,
|
fps,
|
||||||
keepalive_interval: standbyInterval,
|
keepalive_interval: standbyInterval,
|
||||||
adaptive_fps: adaptiveFps,
|
adaptive_fps: adaptiveFps,
|
||||||
|
protocol,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -858,6 +873,7 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<div class="stream-card-props">
|
<div class="stream-card-props">
|
||||||
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led','led-devices','data-device-id','${target.device_id}')">${ICON_LED} ${escapeHtml(deviceName)}</span>
|
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led','led-devices','data-device-id','${target.device_id}')">${ICON_LED} ${escapeHtml(deviceName)}</span>
|
||||||
<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>
|
||||||
|
${device?.device_type === 'wled' || !device ? `<span class="stream-card-prop" title="${t('targets.protocol')}">${target.protocol === 'http' ? '🌐' : '📡'} ${(target.protocol || 'ddp').toUpperCase()}</span>` : `<span class="stream-card-prop" title="${t('targets.protocol')}">🔌 ${t('targets.protocol.serial')}</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>` : ''}
|
${target.min_brightness_threshold > 0 ? `<span class="stream-card-prop" title="${t('targets.min_brightness_threshold')}">🔅 <${target.min_brightness_threshold} → off</span>` : ''}
|
||||||
|
|||||||
@@ -367,6 +367,7 @@
|
|||||||
"targets.section.devices": "💡 Devices",
|
"targets.section.devices": "💡 Devices",
|
||||||
"targets.section.color_strips": "🎞️ Color Strip Sources",
|
"targets.section.color_strips": "🎞️ Color Strip Sources",
|
||||||
"targets.section.targets": "⚡ Targets",
|
"targets.section.targets": "⚡ Targets",
|
||||||
|
"targets.section.specific_settings": "Specific Settings",
|
||||||
"targets.add": "Add Target",
|
"targets.add": "Add Target",
|
||||||
"targets.edit": "Edit Target",
|
"targets.edit": "Edit Target",
|
||||||
"targets.loading": "Loading targets...",
|
"targets.loading": "Loading targets...",
|
||||||
@@ -915,6 +916,9 @@
|
|||||||
"targets.min_brightness_threshold.hint": "Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)",
|
"targets.min_brightness_threshold.hint": "Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)",
|
||||||
"targets.adaptive_fps": "Adaptive FPS:",
|
"targets.adaptive_fps": "Adaptive FPS:",
|
||||||
"targets.adaptive_fps.hint": "Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.",
|
"targets.adaptive_fps.hint": "Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.",
|
||||||
|
"targets.protocol": "Protocol:",
|
||||||
|
"targets.protocol.hint": "DDP sends pixels via fast UDP (recommended for most setups). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.",
|
||||||
|
"targets.protocol.serial": "Serial",
|
||||||
|
|
||||||
"search.open": "Search (Ctrl+K)",
|
"search.open": "Search (Ctrl+K)",
|
||||||
"search.placeholder": "Search entities... (Ctrl+K)",
|
"search.placeholder": "Search entities... (Ctrl+K)",
|
||||||
|
|||||||
@@ -367,6 +367,7 @@
|
|||||||
"targets.section.devices": "💡 Устройства",
|
"targets.section.devices": "💡 Устройства",
|
||||||
"targets.section.color_strips": "🎞️ Источники цветовых полос",
|
"targets.section.color_strips": "🎞️ Источники цветовых полос",
|
||||||
"targets.section.targets": "⚡ Цели",
|
"targets.section.targets": "⚡ Цели",
|
||||||
|
"targets.section.specific_settings": "Специальные настройки",
|
||||||
"targets.add": "Добавить Цель",
|
"targets.add": "Добавить Цель",
|
||||||
"targets.edit": "Редактировать Цель",
|
"targets.edit": "Редактировать Цель",
|
||||||
"targets.loading": "Загрузка целей...",
|
"targets.loading": "Загрузка целей...",
|
||||||
@@ -915,6 +916,9 @@
|
|||||||
"targets.min_brightness_threshold.hint": "Если итоговая яркость (яркость пикселей × яркость устройства/источника) ниже этого значения, светодиоды полностью выключаются (0 = отключено)",
|
"targets.min_brightness_threshold.hint": "Если итоговая яркость (яркость пикселей × яркость устройства/источника) ниже этого значения, светодиоды полностью выключаются (0 = отключено)",
|
||||||
"targets.adaptive_fps": "Адаптивный FPS:",
|
"targets.adaptive_fps": "Адаптивный FPS:",
|
||||||
"targets.adaptive_fps.hint": "Автоматически снижает частоту отправки, когда устройство перестаёт отвечать, и постепенно восстанавливает её при стабилизации. Рекомендуется для WiFi-устройств со слабым сигналом.",
|
"targets.adaptive_fps.hint": "Автоматически снижает частоту отправки, когда устройство перестаёт отвечать, и постепенно восстанавливает её при стабилизации. Рекомендуется для WiFi-устройств со слабым сигналом.",
|
||||||
|
"targets.protocol": "Протокол:",
|
||||||
|
"targets.protocol.hint": "DDP отправляет пиксели по быстрому UDP (рекомендуется). HTTP использует JSON API — медленнее, но надёжнее, ограничение ~500 LED.",
|
||||||
|
"targets.protocol.serial": "Serial",
|
||||||
|
|
||||||
"search.open": "Поиск (Ctrl+K)",
|
"search.open": "Поиск (Ctrl+K)",
|
||||||
"search.placeholder": "Поиск... (Ctrl+K)",
|
"search.placeholder": "Поиск... (Ctrl+K)",
|
||||||
|
|||||||
@@ -367,6 +367,7 @@
|
|||||||
"targets.section.devices": "💡 设备",
|
"targets.section.devices": "💡 设备",
|
||||||
"targets.section.color_strips": "🎞️ 色带源",
|
"targets.section.color_strips": "🎞️ 色带源",
|
||||||
"targets.section.targets": "⚡ 目标",
|
"targets.section.targets": "⚡ 目标",
|
||||||
|
"targets.section.specific_settings": "特定设置",
|
||||||
"targets.add": "添加目标",
|
"targets.add": "添加目标",
|
||||||
"targets.edit": "编辑目标",
|
"targets.edit": "编辑目标",
|
||||||
"targets.loading": "正在加载目标...",
|
"targets.loading": "正在加载目标...",
|
||||||
@@ -915,6 +916,9 @@
|
|||||||
"targets.min_brightness_threshold.hint": "当有效输出亮度(像素亮度 × 设备/源亮度)低于此值时,LED完全关闭(0 = 禁用)",
|
"targets.min_brightness_threshold.hint": "当有效输出亮度(像素亮度 × 设备/源亮度)低于此值时,LED完全关闭(0 = 禁用)",
|
||||||
"targets.adaptive_fps": "自适应FPS:",
|
"targets.adaptive_fps": "自适应FPS:",
|
||||||
"targets.adaptive_fps.hint": "当设备无响应时自动降低发送速率,稳定后逐步恢复。推荐用于信号较弱的WiFi设备。",
|
"targets.adaptive_fps.hint": "当设备无响应时自动降低发送速率,稳定后逐步恢复。推荐用于信号较弱的WiFi设备。",
|
||||||
|
"targets.protocol": "协议:",
|
||||||
|
"targets.protocol.hint": "DDP通过快速UDP发送像素(推荐)。HTTP使用JSON API——较慢但可靠,限制约500个LED。",
|
||||||
|
"targets.protocol.serial": "串口",
|
||||||
|
|
||||||
"search.open": "搜索 (Ctrl+K)",
|
"search.open": "搜索 (Ctrl+K)",
|
||||||
"search.placeholder": "搜索实体... (Ctrl+K)",
|
"search.placeholder": "搜索实体... (Ctrl+K)",
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class PictureTargetStore:
|
|||||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL,
|
||||||
min_brightness_threshold: int = 0,
|
min_brightness_threshold: int = 0,
|
||||||
adaptive_fps: bool = False,
|
adaptive_fps: bool = False,
|
||||||
|
protocol: str = "ddp",
|
||||||
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 = "",
|
||||||
@@ -135,6 +136,7 @@ class PictureTargetStore:
|
|||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
min_brightness_threshold=min_brightness_threshold,
|
min_brightness_threshold=min_brightness_threshold,
|
||||||
adaptive_fps=adaptive_fps,
|
adaptive_fps=adaptive_fps,
|
||||||
|
protocol=protocol,
|
||||||
description=description,
|
description=description,
|
||||||
auto_start=auto_start,
|
auto_start=auto_start,
|
||||||
created_at=now,
|
created_at=now,
|
||||||
@@ -173,6 +175,7 @@ class PictureTargetStore:
|
|||||||
state_check_interval: Optional[int] = None,
|
state_check_interval: Optional[int] = None,
|
||||||
min_brightness_threshold: Optional[int] = None,
|
min_brightness_threshold: Optional[int] = None,
|
||||||
adaptive_fps: Optional[bool] = None,
|
adaptive_fps: Optional[bool] = None,
|
||||||
|
protocol: Optional[str] = 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,
|
||||||
@@ -203,6 +206,7 @@ class PictureTargetStore:
|
|||||||
state_check_interval=state_check_interval,
|
state_check_interval=state_check_interval,
|
||||||
min_brightness_threshold=min_brightness_threshold,
|
min_brightness_threshold=min_brightness_threshold,
|
||||||
adaptive_fps=adaptive_fps,
|
adaptive_fps=adaptive_fps,
|
||||||
|
protocol=protocol,
|
||||||
key_colors_settings=key_colors_settings,
|
key_colors_settings=key_colors_settings,
|
||||||
description=description,
|
description=description,
|
||||||
auto_start=auto_start,
|
auto_start=auto_start,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
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)
|
min_brightness_threshold: int = 0 # brightness below this → 0 (disabled when 0)
|
||||||
adaptive_fps: bool = False # auto-reduce FPS when device is unresponsive
|
adaptive_fps: bool = False # auto-reduce FPS when device is unresponsive
|
||||||
|
protocol: str = "ddp" # "ddp" (UDP) or "http" (JSON API)
|
||||||
|
|
||||||
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."""
|
||||||
@@ -35,6 +36,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
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,
|
min_brightness_threshold=self.min_brightness_threshold,
|
||||||
adaptive_fps=self.adaptive_fps,
|
adaptive_fps=self.adaptive_fps,
|
||||||
|
protocol=self.protocol,
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_with_manager(self, manager, *, settings_changed: bool,
|
def sync_with_manager(self, manager, *, settings_changed: bool,
|
||||||
@@ -60,7 +62,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, adaptive_fps=None,
|
min_brightness_threshold=None, adaptive_fps=None, protocol=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)
|
||||||
@@ -80,6 +82,8 @@ class WledPictureTarget(PictureTarget):
|
|||||||
self.min_brightness_threshold = min_brightness_threshold
|
self.min_brightness_threshold = min_brightness_threshold
|
||||||
if adaptive_fps is not None:
|
if adaptive_fps is not None:
|
||||||
self.adaptive_fps = adaptive_fps
|
self.adaptive_fps = adaptive_fps
|
||||||
|
if protocol is not None:
|
||||||
|
self.protocol = protocol
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_picture_source(self) -> bool:
|
def has_picture_source(self) -> bool:
|
||||||
@@ -96,6 +100,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
d["state_check_interval"] = self.state_check_interval
|
d["state_check_interval"] = self.state_check_interval
|
||||||
d["min_brightness_threshold"] = self.min_brightness_threshold
|
d["min_brightness_threshold"] = self.min_brightness_threshold
|
||||||
d["adaptive_fps"] = self.adaptive_fps
|
d["adaptive_fps"] = self.adaptive_fps
|
||||||
|
d["protocol"] = self.protocol
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -123,6 +128,7 @@ class WledPictureTarget(PictureTarget):
|
|||||||
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),
|
min_brightness_threshold=data.get("min_brightness_threshold", 0),
|
||||||
adaptive_fps=data.get("adaptive_fps", False),
|
adaptive_fps=data.get("adaptive_fps", False),
|
||||||
|
protocol=data.get("protocol", "ddp"),
|
||||||
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,18 +44,6 @@
|
|||||||
</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">
|
||||||
@@ -72,16 +60,16 @@
|
|||||||
<small id="target-editor-fps-rec" class="input-hint" style="display:none"></small>
|
<small id="target-editor-fps-rec" class="input-hint" style="display:none"></small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="target-editor-keepalive-group">
|
<div class="form-group" id="target-editor-brightness-threshold-group">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label for="target-editor-keepalive-interval">
|
<label for="target-editor-brightness-threshold">
|
||||||
<span data-i18n="targets.keepalive_interval">Keep Alive Interval:</span>
|
<span data-i18n="targets.min_brightness_threshold">Min Brightness Threshold:</span>
|
||||||
<span id="target-editor-keepalive-interval-value">1.0</span><span>s</span>
|
<span id="target-editor-brightness-threshold-value">0</span>
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="input-hint" style="display:none" data-i18n="targets.keepalive_interval.hint">How often to resend the last frame when the screen is static, to keep the device in live mode (0.5-5.0s)</small>
|
<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-keepalive-interval" min="0.5" max="5.0" step="0.5" value="1.0" oninput="document.getElementById('target-editor-keepalive-interval-value').textContent = this.value">
|
<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>
|
||||||
|
|
||||||
<div class="form-group" id="target-editor-adaptive-fps-group">
|
<div class="form-group" id="target-editor-adaptive-fps-group">
|
||||||
@@ -96,6 +84,35 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details class="form-collapse" id="target-editor-device-settings">
|
||||||
|
<summary data-i18n="targets.section.specific_settings">Specific Settings</summary>
|
||||||
|
<div class="form-collapse-body">
|
||||||
|
<div class="form-group" id="target-editor-protocol-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-protocol" data-i18n="targets.protocol">Protocol:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.protocol.hint">DDP sends pixels via fast UDP (recommended). HTTP uses the JSON API — slower but reliable, limited to ~500 LEDs.</small>
|
||||||
|
<select id="target-editor-protocol">
|
||||||
|
<option value="ddp">DDP (UDP)</option>
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="target-editor-keepalive-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-keepalive-interval">
|
||||||
|
<span data-i18n="targets.keepalive_interval">Keep Alive Interval:</span>
|
||||||
|
<span id="target-editor-keepalive-interval-value">1.0</span><span>s</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.keepalive_interval.hint">How often to resend the last frame when the screen is static, to keep the device in live mode (0.5-5.0s)</small>
|
||||||
|
<input type="range" id="target-editor-keepalive-interval" min="0.5" max="5.0" step="0.5" value="1.0" oninput="document.getElementById('target-editor-keepalive-interval-value').textContent = this.value">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div id="target-editor-error" class="error-message" style="display: none;"></div>
|
<div id="target-editor-error" class="error-message" style="display: none;"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user