Add frame-change detection, keepalive, current FPS, and compact metrics UI
Skip redundant processing/DDP sends when screen is static using object identity comparison. Add configurable standby interval to periodically resend last frame keeping WLED in live mode. Track frames skipped, keepalive count, and current FPS (rolling 1-second send count). Always use DDP regardless of LED count. Compact metrics grid with label-value rows and remove Skipped from UI display. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,7 @@ def _settings_to_core(schema: ProcessingSettingsSchema) -> ProcessingSettings:
|
||||
interpolation_mode=schema.interpolation_mode,
|
||||
brightness=schema.brightness,
|
||||
smoothing=schema.smoothing,
|
||||
standby_interval=schema.standby_interval,
|
||||
state_check_interval=schema.state_check_interval,
|
||||
)
|
||||
if schema.color_correction:
|
||||
@@ -87,6 +88,7 @@ def _settings_to_schema(settings: ProcessingSettings) -> ProcessingSettingsSchem
|
||||
interpolation_mode=settings.interpolation_mode,
|
||||
brightness=settings.brightness,
|
||||
smoothing=settings.smoothing,
|
||||
standby_interval=settings.standby_interval,
|
||||
state_check_interval=settings.state_check_interval,
|
||||
color_correction=ColorCorrection(
|
||||
gamma=settings.gamma,
|
||||
@@ -470,6 +472,7 @@ async def update_target_settings(
|
||||
gamma=existing.gamma,
|
||||
saturation=existing.saturation,
|
||||
smoothing=settings.smoothing if 'smoothing' in sent else existing.smoothing,
|
||||
standby_interval=settings.standby_interval if 'standby_interval' in sent else existing.standby_interval,
|
||||
state_check_interval=settings.state_check_interval if 'state_check_interval' in sent else existing.state_check_interval,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class ProcessingSettings(BaseModel):
|
||||
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
|
||||
brightness: float = Field(default=1.0, description="Global brightness (0.0-1.0)", ge=0.0, le=1.0)
|
||||
smoothing: float = Field(default=0.3, description="Temporal smoothing factor (0.0=none, 1.0=full)", ge=0.0, le=1.0)
|
||||
standby_interval: float = Field(default=1.0, description="Seconds between keepalive sends when screen is static (0.5-5.0)", ge=0.5, le=5.0)
|
||||
state_check_interval: int = Field(
|
||||
default=DEFAULT_STATE_CHECK_INTERVAL, ge=5, le=600,
|
||||
description="Seconds between WLED health checks"
|
||||
@@ -125,6 +126,9 @@ class TargetProcessingState(BaseModel):
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
|
||||
fps_potential: Optional[float] = Field(None, description="Potential FPS (processing speed without throttle)")
|
||||
fps_target: int = Field(default=0, description="Target FPS")
|
||||
frames_skipped: Optional[int] = Field(None, description="Frames skipped (no screen change)")
|
||||
frames_keepalive: Optional[int] = Field(None, description="Keepalive frames sent during standby")
|
||||
fps_current: Optional[int] = Field(None, description="Frames sent in the last second")
|
||||
display_index: int = Field(default=0, description="Current display index")
|
||||
last_update: Optional[datetime] = Field(None, description="Last successful update")
|
||||
errors: List[str] = Field(default_factory=list, description="Recent errors")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Processing manager for coordinating screen capture and WLED updates."""
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import json
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
@@ -33,15 +34,16 @@ logger = get_logger(__name__)
|
||||
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds between health checks
|
||||
|
||||
|
||||
def _process_frame(live_stream, border_width, pixel_mapper, previous_colors, smoothing):
|
||||
def _process_frame(capture, border_width, pixel_mapper, previous_colors, smoothing):
|
||||
"""All CPU-bound work for one WLED frame (runs in thread pool).
|
||||
|
||||
Includes get_latest_frame() because ProcessedLiveStream may apply
|
||||
filters (image copy + processing) which should not block the event loop.
|
||||
Args:
|
||||
capture: ScreenCapture from live_stream.get_latest_frame()
|
||||
border_width: Border pixel width for extraction
|
||||
pixel_mapper: PixelMapper for LED mapping
|
||||
previous_colors: Previous frame colors for smoothing
|
||||
smoothing: Smoothing factor (0-1)
|
||||
"""
|
||||
capture = live_stream.get_latest_frame()
|
||||
if capture is None:
|
||||
return None
|
||||
border_pixels = extract_border_pixels(capture, border_width)
|
||||
led_colors = pixel_mapper.map_border_to_leds(border_pixels)
|
||||
if previous_colors and smoothing > 0:
|
||||
@@ -49,15 +51,16 @@ def _process_frame(live_stream, border_width, pixel_mapper, previous_colors, smo
|
||||
return led_colors
|
||||
|
||||
|
||||
def _process_kc_frame(live_stream, rectangles, calc_fn, previous_colors, smoothing):
|
||||
def _process_kc_frame(capture, rectangles, calc_fn, previous_colors, smoothing):
|
||||
"""All CPU-bound work for one KC frame (runs in thread pool).
|
||||
|
||||
Includes get_latest_frame() because ProcessedLiveStream may apply
|
||||
filters which should not block the event loop.
|
||||
Args:
|
||||
capture: ScreenCapture from live_stream.get_latest_frame()
|
||||
rectangles: List of pattern rectangles to extract colors from
|
||||
calc_fn: Color calculation function (average/median/dominant)
|
||||
previous_colors: Previous frame colors for smoothing
|
||||
smoothing: Smoothing factor (0-1)
|
||||
"""
|
||||
capture = live_stream.get_latest_frame()
|
||||
if capture is None:
|
||||
return None
|
||||
img = capture.image
|
||||
h, w = img.shape[:2]
|
||||
colors = {}
|
||||
@@ -110,6 +113,7 @@ class ProcessingSettings:
|
||||
saturation: float = 1.0
|
||||
smoothing: float = 0.3
|
||||
interpolation_mode: str = "average"
|
||||
standby_interval: float = 1.0 # seconds between keepalive sends when screen is static
|
||||
state_check_interval: int = DEFAULT_STATE_CHECK_INTERVAL
|
||||
|
||||
|
||||
@@ -133,12 +137,15 @@ class ProcessingMetrics:
|
||||
"""Metrics for processing performance."""
|
||||
|
||||
frames_processed: int = 0
|
||||
frames_skipped: int = 0
|
||||
frames_keepalive: int = 0
|
||||
errors_count: int = 0
|
||||
last_error: Optional[str] = None
|
||||
last_update: Optional[datetime] = None
|
||||
start_time: Optional[datetime] = None
|
||||
fps_actual: float = 0.0
|
||||
fps_potential: float = 0.0
|
||||
fps_current: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -558,13 +565,10 @@ class ProcessorManager:
|
||||
logger.warning(f"Could not snapshot WLED state: {e}")
|
||||
state.wled_state_before = None
|
||||
|
||||
# Connect to WLED device
|
||||
# Connect to WLED device (always use DDP for low-latency UDP streaming)
|
||||
try:
|
||||
use_ddp = state.led_count > 500
|
||||
state.wled_client = WLEDClient(state.device_url, use_ddp=use_ddp)
|
||||
state.wled_client = WLEDClient(state.device_url, use_ddp=True)
|
||||
await state.wled_client.connect()
|
||||
|
||||
if use_ddp:
|
||||
logger.info(f"Target {target_id} using DDP protocol ({state.led_count} LEDs)")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to WLED device for target {target_id}: {e}")
|
||||
@@ -672,8 +676,12 @@ class ProcessorManager:
|
||||
)
|
||||
|
||||
frame_time = 1.0 / target_fps
|
||||
standby_interval = settings.standby_interval
|
||||
fps_samples = []
|
||||
prev_frame_time_stamp = time.time()
|
||||
prev_capture = None # Track previous ScreenCapture for change detection
|
||||
last_send_time = 0.0 # Timestamp of last DDP send (for keepalive)
|
||||
send_timestamps: collections.deque = collections.deque() # for fps_current
|
||||
|
||||
# Check if the device has test mode active — skip capture while in test mode
|
||||
device_state = self._devices.get(state.device_id)
|
||||
@@ -688,19 +696,47 @@ class ProcessorManager:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Batch all CPU work (frame read + processing) in a single thread call.
|
||||
led_colors = await asyncio.to_thread(
|
||||
_process_frame,
|
||||
state.live_stream, border_width,
|
||||
state.pixel_mapper, state.previous_colors, smoothing,
|
||||
)
|
||||
# get_latest_frame() is a fast lock read (ProcessedLiveStream
|
||||
# pre-computes in a background thread). Safe on asyncio thread.
|
||||
capture = state.live_stream.get_latest_frame()
|
||||
|
||||
if led_colors is None:
|
||||
if capture is None:
|
||||
if state.metrics.frames_processed == 0:
|
||||
logger.info(f"Capture returned None for target {target_id} (no new frame yet)")
|
||||
await asyncio.sleep(frame_time)
|
||||
continue
|
||||
|
||||
# Skip processing + send if the frame hasn't changed
|
||||
if capture is prev_capture:
|
||||
# Keepalive: resend last colors to prevent WLED exiting live mode
|
||||
if state.previous_colors and (loop_start - last_send_time) >= standby_interval:
|
||||
if not state.is_running or state.wled_client is None:
|
||||
break
|
||||
brightness_value = int(wled_brightness * 255)
|
||||
if state.wled_client.use_ddp:
|
||||
state.wled_client.send_pixels_fast(state.previous_colors, brightness=brightness_value)
|
||||
else:
|
||||
await state.wled_client.send_pixels(state.previous_colors, brightness=brightness_value)
|
||||
last_send_time = time.time()
|
||||
send_timestamps.append(last_send_time)
|
||||
state.metrics.frames_keepalive += 1
|
||||
state.metrics.frames_skipped += 1
|
||||
# Update fps_current: count sends in last 1 second
|
||||
now_ts = time.time()
|
||||
while send_timestamps and send_timestamps[0] < now_ts - 1.0:
|
||||
send_timestamps.popleft()
|
||||
state.metrics.fps_current = len(send_timestamps)
|
||||
await asyncio.sleep(frame_time)
|
||||
continue
|
||||
prev_capture = capture
|
||||
|
||||
# CPU-bound work in thread pool
|
||||
led_colors = await asyncio.to_thread(
|
||||
_process_frame,
|
||||
capture, border_width,
|
||||
state.pixel_mapper, state.previous_colors, smoothing,
|
||||
)
|
||||
|
||||
# Send to WLED with device brightness
|
||||
if not state.is_running or state.wled_client is None:
|
||||
break
|
||||
@@ -709,6 +745,8 @@ class ProcessorManager:
|
||||
state.wled_client.send_pixels_fast(led_colors, brightness=brightness_value)
|
||||
else:
|
||||
await state.wled_client.send_pixels(led_colors, brightness=brightness_value)
|
||||
last_send_time = time.time()
|
||||
send_timestamps.append(last_send_time)
|
||||
|
||||
# Update metrics
|
||||
state.metrics.frames_processed += 1
|
||||
@@ -730,6 +768,11 @@ class ProcessorManager:
|
||||
processing_time = now - loop_start
|
||||
state.metrics.fps_potential = 1.0 / processing_time if processing_time > 0 else 0
|
||||
|
||||
# Update fps_current: count sends in last 1 second
|
||||
while send_timestamps and send_timestamps[0] < now - 1.0:
|
||||
send_timestamps.popleft()
|
||||
state.metrics.fps_current = len(send_timestamps)
|
||||
|
||||
except Exception as e:
|
||||
state.metrics.errors_count += 1
|
||||
state.metrics.last_error = str(e)
|
||||
@@ -782,6 +825,9 @@ class ProcessorManager:
|
||||
"fps_actual": metrics.fps_actual if state.is_running else None,
|
||||
"fps_potential": metrics.fps_potential if state.is_running else None,
|
||||
"fps_target": state.settings.fps,
|
||||
"frames_skipped": metrics.frames_skipped if state.is_running else None,
|
||||
"frames_keepalive": metrics.frames_keepalive if state.is_running else None,
|
||||
"fps_current": metrics.fps_current if state.is_running else None,
|
||||
"display_index": state.resolved_display_index if state.resolved_display_index is not None else state.settings.display_index,
|
||||
"last_update": metrics.last_update,
|
||||
"errors": [metrics.last_error] if metrics.last_error else [],
|
||||
@@ -877,8 +923,7 @@ class ProcessorManager:
|
||||
if active_client:
|
||||
await active_client.send_pixels(pixels)
|
||||
else:
|
||||
use_ddp = ds.led_count > WLEDClient.HTTP_MAX_LEDS
|
||||
async with WLEDClient(ds.device_url, use_ddp=use_ddp) as wled:
|
||||
async with WLEDClient(ds.device_url, use_ddp=True) as wled:
|
||||
await wled.send_pixels(pixels)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send test pixels for {device_id}: {e}")
|
||||
@@ -898,8 +943,7 @@ class ProcessorManager:
|
||||
if active_client:
|
||||
await active_client.send_pixels(pixels)
|
||||
else:
|
||||
use_ddp = ds.led_count > WLEDClient.HTTP_MAX_LEDS
|
||||
async with WLEDClient(ds.device_url, use_ddp=use_ddp) as wled:
|
||||
async with WLEDClient(ds.device_url, use_ddp=True) as wled:
|
||||
await wled.send_pixels(pixels)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clear pixels for {device_id}: {e}")
|
||||
@@ -1197,6 +1241,7 @@ class ProcessorManager:
|
||||
|
||||
frame_time = 1.0 / target_fps
|
||||
fps_samples: List[float] = []
|
||||
prev_capture = None # Track previous ScreenCapture for change detection
|
||||
|
||||
rectangles = state._resolved_rectangles
|
||||
|
||||
@@ -1210,16 +1255,27 @@ class ProcessorManager:
|
||||
loop_start = time.time()
|
||||
|
||||
try:
|
||||
# Batch all CPU work in a single thread call
|
||||
colors = await asyncio.to_thread(
|
||||
_process_kc_frame,
|
||||
state.live_stream, rectangles, calc_fn,
|
||||
state.previous_colors, smoothing,
|
||||
)
|
||||
if colors is None:
|
||||
# get_latest_frame() is a fast lock read — safe on asyncio thread
|
||||
capture = state.live_stream.get_latest_frame()
|
||||
|
||||
if capture is None:
|
||||
await asyncio.sleep(frame_time)
|
||||
continue
|
||||
|
||||
# Skip processing if the frame hasn't changed
|
||||
if capture is prev_capture:
|
||||
state.metrics.frames_skipped += 1
|
||||
await asyncio.sleep(frame_time)
|
||||
continue
|
||||
prev_capture = capture
|
||||
|
||||
# CPU-bound work in thread pool
|
||||
colors = await asyncio.to_thread(
|
||||
_process_kc_frame,
|
||||
capture, rectangles, calc_fn,
|
||||
state.previous_colors, smoothing,
|
||||
)
|
||||
|
||||
state.previous_colors = dict(colors)
|
||||
state.latest_colors = dict(colors)
|
||||
|
||||
|
||||
@@ -3821,6 +3821,8 @@ async function showTargetEditor(targetId = null) {
|
||||
document.getElementById('target-editor-interpolation').value = target.settings?.interpolation_mode ?? 'average';
|
||||
document.getElementById('target-editor-smoothing').value = target.settings?.smoothing ?? 0.3;
|
||||
document.getElementById('target-editor-smoothing-value').textContent = target.settings?.smoothing ?? 0.3;
|
||||
document.getElementById('target-editor-standby-interval').value = target.settings?.standby_interval ?? 1.0;
|
||||
document.getElementById('target-editor-standby-interval-value').textContent = target.settings?.standby_interval ?? 1.0;
|
||||
document.getElementById('target-editor-title').textContent = t('targets.edit');
|
||||
} else {
|
||||
// Creating new target
|
||||
@@ -3834,6 +3836,8 @@ async function showTargetEditor(targetId = null) {
|
||||
document.getElementById('target-editor-interpolation').value = 'average';
|
||||
document.getElementById('target-editor-smoothing').value = 0.3;
|
||||
document.getElementById('target-editor-smoothing-value').textContent = '0.3';
|
||||
document.getElementById('target-editor-standby-interval').value = 1.0;
|
||||
document.getElementById('target-editor-standby-interval-value').textContent = '1.0';
|
||||
document.getElementById('target-editor-title').textContent = t('targets.add');
|
||||
}
|
||||
|
||||
@@ -3845,6 +3849,7 @@ async function showTargetEditor(targetId = null) {
|
||||
border_width: document.getElementById('target-editor-border-width').value,
|
||||
interpolation: document.getElementById('target-editor-interpolation').value,
|
||||
smoothing: document.getElementById('target-editor-smoothing').value,
|
||||
standby_interval: document.getElementById('target-editor-standby-interval').value,
|
||||
};
|
||||
|
||||
const modal = document.getElementById('target-editor-modal');
|
||||
@@ -3868,7 +3873,8 @@ function isTargetEditorDirty() {
|
||||
document.getElementById('target-editor-fps').value !== targetEditorInitialValues.fps ||
|
||||
document.getElementById('target-editor-border-width').value !== targetEditorInitialValues.border_width ||
|
||||
document.getElementById('target-editor-interpolation').value !== targetEditorInitialValues.interpolation ||
|
||||
document.getElementById('target-editor-smoothing').value !== targetEditorInitialValues.smoothing
|
||||
document.getElementById('target-editor-smoothing').value !== targetEditorInitialValues.smoothing ||
|
||||
document.getElementById('target-editor-standby-interval').value !== targetEditorInitialValues.standby_interval
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3896,6 +3902,7 @@ async function saveTargetEditor() {
|
||||
const borderWidth = parseInt(document.getElementById('target-editor-border-width').value) || 10;
|
||||
const interpolation = document.getElementById('target-editor-interpolation').value;
|
||||
const smoothing = parseFloat(document.getElementById('target-editor-smoothing').value);
|
||||
const standbyInterval = parseFloat(document.getElementById('target-editor-standby-interval').value);
|
||||
const errorEl = document.getElementById('target-editor-error');
|
||||
|
||||
if (!name) {
|
||||
@@ -3913,6 +3920,7 @@ async function saveTargetEditor() {
|
||||
border_width: borderWidth,
|
||||
interpolation_mode: interpolation,
|
||||
smoothing: smoothing,
|
||||
standby_interval: standbyInterval,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4175,24 +4183,32 @@ function createTargetCard(target, deviceMap, sourceMap) {
|
||||
${isProcessing ? `
|
||||
<div class="metrics-grid">
|
||||
<div class="metric">
|
||||
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
|
||||
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
|
||||
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">${t('device.metrics.current_fps')}</div>
|
||||
<div class="metric-value">${state.fps_current ?? '-'}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">${state.fps_target || 0}</div>
|
||||
<div class="metric-label">${t('device.metrics.target_fps')}</div>
|
||||
<div class="metric-value">${state.fps_target || 0}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">${state.fps_potential?.toFixed(0) || '-'}</div>
|
||||
<div class="metric-label">${t('device.metrics.potential_fps')}</div>
|
||||
<div class="metric-value">${state.fps_potential?.toFixed(0) || '-'}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">${metrics.frames_processed || 0}</div>
|
||||
<div class="metric-label">${t('device.metrics.frames')}</div>
|
||||
<div class="metric-value">${metrics.frames_processed || 0}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">${t('device.metrics.keepalive')}</div>
|
||||
<div class="metric-value">${state.frames_keepalive ?? '-'}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-value">${metrics.errors_count || 0}</div>
|
||||
<div class="metric-label">${t('device.metrics.errors')}</div>
|
||||
<div class="metric-value">${metrics.errors_count || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
@@ -317,6 +317,18 @@
|
||||
<input type="range" id="target-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('target-editor-smoothing-value').textContent = this.value">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="target-editor-standby-interval">
|
||||
<span data-i18n="targets.standby_interval">Standby Interval:</span>
|
||||
<span id="target-editor-standby-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.standby_interval.hint">How often to resend the last frame when the screen is static, to keep WLED in live mode (0.5-5.0s)</small>
|
||||
<input type="range" id="target-editor-standby-interval" min="0.5" max="5.0" step="0.5" value="1.0" oninput="document.getElementById('target-editor-standby-interval-value').textContent = this.value">
|
||||
</div>
|
||||
|
||||
<div id="target-editor-error" class="error-message" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -129,9 +129,12 @@
|
||||
"device.started": "Processing started",
|
||||
"device.stopped": "Processing stopped",
|
||||
"device.metrics.actual_fps": "Actual FPS",
|
||||
"device.metrics.current_fps": "Current FPS",
|
||||
"device.metrics.target_fps": "Target FPS",
|
||||
"device.metrics.potential_fps": "Potential FPS",
|
||||
"device.metrics.frames": "Frames",
|
||||
"device.metrics.frames_skipped": "Skipped",
|
||||
"device.metrics.keepalive": "Keepalive",
|
||||
"device.metrics.errors": "Errors",
|
||||
"device.health.online": "WLED Online",
|
||||
"device.health.offline": "WLED Offline",
|
||||
@@ -330,6 +333,8 @@
|
||||
"targets.interpolation.dominant": "Dominant",
|
||||
"targets.smoothing": "Smoothing:",
|
||||
"targets.smoothing.hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
|
||||
"targets.standby_interval": "Standby Interval:",
|
||||
"targets.standby_interval.hint": "How often to resend the last frame when the screen is static, keeping WLED in live mode (0.5-5.0s)",
|
||||
"targets.created": "Target created successfully",
|
||||
"targets.updated": "Target updated successfully",
|
||||
"targets.deleted": "Target deleted successfully",
|
||||
|
||||
@@ -129,9 +129,12 @@
|
||||
"device.started": "Обработка запущена",
|
||||
"device.stopped": "Обработка остановлена",
|
||||
"device.metrics.actual_fps": "Факт. FPS",
|
||||
"device.metrics.current_fps": "Текущ. FPS",
|
||||
"device.metrics.target_fps": "Целев. FPS",
|
||||
"device.metrics.potential_fps": "Потенц. FPS",
|
||||
"device.metrics.frames": "Кадры",
|
||||
"device.metrics.frames_skipped": "Пропущено",
|
||||
"device.metrics.keepalive": "Keepalive",
|
||||
"device.metrics.errors": "Ошибки",
|
||||
"device.health.online": "WLED Онлайн",
|
||||
"device.health.offline": "WLED Недоступен",
|
||||
@@ -330,6 +333,8 @@
|
||||
"targets.interpolation.dominant": "Доминантный",
|
||||
"targets.smoothing": "Сглаживание:",
|
||||
"targets.smoothing.hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.",
|
||||
"targets.standby_interval": "Интервал ожидания:",
|
||||
"targets.standby_interval.hint": "Как часто повторно отправлять последний кадр при статичном экране для удержания WLED в режиме live (0.5-5.0с)",
|
||||
"targets.created": "Цель успешно создана",
|
||||
"targets.updated": "Цель успешно обновлена",
|
||||
"targets.deleted": "Цель успешно удалена",
|
||||
|
||||
@@ -844,28 +844,29 @@ input:-webkit-autofill:focus {
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 3px 8px;
|
||||
background: var(--bg-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
|
||||
@@ -30,6 +30,7 @@ class WledPictureTarget(PictureTarget):
|
||||
"saturation": self.settings.saturation,
|
||||
"smoothing": self.settings.smoothing,
|
||||
"interpolation_mode": self.settings.interpolation_mode,
|
||||
"standby_interval": self.settings.standby_interval,
|
||||
"state_check_interval": self.settings.state_check_interval,
|
||||
}
|
||||
return d
|
||||
@@ -49,6 +50,7 @@ class WledPictureTarget(PictureTarget):
|
||||
saturation=settings_data.get("saturation", 1.0),
|
||||
smoothing=settings_data.get("smoothing", 0.3),
|
||||
interpolation_mode=settings_data.get("interpolation_mode", "average"),
|
||||
standby_interval=settings_data.get("standby_interval", 1.0),
|
||||
state_check_interval=settings_data.get("state_check_interval", DEFAULT_STATE_CHECK_INTERVAL),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user