feat: HA light output targets — cast LED colors to Home Assistant lights
Lint & Test / test (push) Has been cancelled
Lint & Test / test (push) Has been cancelled
New output target type `ha_light` that sends averaged LED colors to HA light entities via WebSocket service calls (light.turn_on/turn_off): Backend: - HARuntime.call_service(): fire-and-forget WS service calls - HALightOutputTarget: data model with light mappings, update rate, transition - HALightTargetProcessor: processing loop with delta detection, rate limiting - ProcessorManager.add_ha_light_target(): registration - API schemas/routes updated for ha_light target type Frontend: - HA Light Targets section in Targets tab tree nav - Modal editor: HA source picker, CSS source picker, light entity mappings - Target cards with start/stop/clone/edit actions - i18n keys for all new UI strings
This commit is contained in:
@@ -25,6 +25,11 @@ from wled_controller.storage.key_colors_output_target import (
|
||||
KeyColorsSettings,
|
||||
KeyColorsOutputTarget,
|
||||
)
|
||||
from wled_controller.storage.ha_light_output_target import (
|
||||
HALightMapping,
|
||||
HALightOutputTarget,
|
||||
)
|
||||
from wled_controller.api.schemas.output_targets import HALightMappingSchema
|
||||
from wled_controller.storage.output_target_store import OutputTargetStore
|
||||
from wled_controller.utils import get_logger
|
||||
from wled_controller.storage.base_store import EntityNotFoundError
|
||||
@@ -76,7 +81,6 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
protocol=target.protocol,
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -89,7 +93,31 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
key_colors_settings=_kc_settings_to_schema(target.settings),
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
elif isinstance(target, HALightOutputTarget):
|
||||
return OutputTargetResponse(
|
||||
id=target.id,
|
||||
name=target.name,
|
||||
target_type=target.target_type,
|
||||
ha_source_id=target.ha_source_id,
|
||||
color_strip_source_id=target.color_strip_source_id,
|
||||
ha_light_mappings=[
|
||||
HALightMappingSchema(
|
||||
entity_id=m.entity_id,
|
||||
led_start=m.led_start,
|
||||
led_end=m.led_end,
|
||||
brightness_scale=m.brightness_scale,
|
||||
)
|
||||
for m in target.light_mappings
|
||||
],
|
||||
update_rate=target.update_rate,
|
||||
ha_transition=target.transition,
|
||||
color_tolerance=target.color_tolerance,
|
||||
min_brightness_threshold=target.min_brightness_threshold,
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -100,7 +128,6 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
target_type=target.target_type,
|
||||
description=target.description,
|
||||
tags=target.tags,
|
||||
|
||||
created_at=target.created_at,
|
||||
updated_at=target.updated_at,
|
||||
)
|
||||
@@ -108,7 +135,10 @@ def _target_to_response(target) -> OutputTargetResponse:
|
||||
|
||||
# ===== CRUD ENDPOINTS =====
|
||||
|
||||
@router.post("/api/v1/output-targets", response_model=OutputTargetResponse, tags=["Targets"], status_code=201)
|
||||
|
||||
@router.post(
|
||||
"/api/v1/output-targets", response_model=OutputTargetResponse, tags=["Targets"], status_code=201
|
||||
)
|
||||
async def create_target(
|
||||
data: OutputTargetCreate,
|
||||
_auth: AuthRequired,
|
||||
@@ -125,7 +155,22 @@ async def create_target(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=422, detail=f"Device {data.device_id} not found")
|
||||
|
||||
kc_settings = _kc_schema_to_settings(data.key_colors_settings) if data.key_colors_settings else None
|
||||
kc_settings = (
|
||||
_kc_schema_to_settings(data.key_colors_settings) if data.key_colors_settings else None
|
||||
)
|
||||
ha_mappings = (
|
||||
[
|
||||
HALightMapping(
|
||||
entity_id=m.entity_id,
|
||||
led_start=m.led_start,
|
||||
led_end=m.led_end,
|
||||
brightness_scale=m.brightness_scale,
|
||||
)
|
||||
for m in data.ha_light_mappings
|
||||
]
|
||||
if data.ha_light_mappings
|
||||
else None
|
||||
)
|
||||
|
||||
# Create in store
|
||||
target = target_store.create_target(
|
||||
@@ -144,6 +189,11 @@ async def create_target(
|
||||
key_colors_settings=kc_settings,
|
||||
description=data.description,
|
||||
tags=data.tags,
|
||||
ha_source_id=data.ha_source_id,
|
||||
ha_light_mappings=ha_mappings,
|
||||
update_rate=data.update_rate,
|
||||
transition=data.transition,
|
||||
color_tolerance=data.color_tolerance,
|
||||
)
|
||||
|
||||
# Register in processor manager
|
||||
@@ -196,7 +246,9 @@ async def batch_target_metrics(
|
||||
return {"metrics": manager.get_all_target_metrics()}
|
||||
|
||||
|
||||
@router.get("/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"])
|
||||
@router.get(
|
||||
"/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"]
|
||||
)
|
||||
async def get_target(
|
||||
target_id: str,
|
||||
_auth: AuthRequired,
|
||||
@@ -210,7 +262,9 @@ async def get_target(
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"])
|
||||
@router.put(
|
||||
"/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"]
|
||||
)
|
||||
async def update_target(
|
||||
target_id: str,
|
||||
data: OutputTargetUpdate,
|
||||
@@ -246,7 +300,9 @@ async def update_target(
|
||||
smoothing=incoming.get("smoothing", ex.smoothing),
|
||||
pattern_template_id=incoming.get("pattern_template_id", ex.pattern_template_id),
|
||||
brightness=incoming.get("brightness", ex.brightness),
|
||||
brightness_value_source_id=incoming.get("brightness_value_source_id", ex.brightness_value_source_id),
|
||||
brightness_value_source_id=incoming.get(
|
||||
"brightness_value_source_id", ex.brightness_value_source_id
|
||||
),
|
||||
)
|
||||
kc_settings = _kc_schema_to_settings(merged)
|
||||
else:
|
||||
@@ -282,14 +338,18 @@ async def update_target(
|
||||
await asyncio.to_thread(
|
||||
target.sync_with_manager,
|
||||
manager,
|
||||
settings_changed=(data.fps is not None or
|
||||
data.keepalive_interval is not None or
|
||||
data.state_check_interval is not None or
|
||||
data.min_brightness_threshold is not None or
|
||||
data.adaptive_fps is not None or
|
||||
data.key_colors_settings is not None),
|
||||
settings_changed=(
|
||||
data.fps is not None
|
||||
or data.keepalive_interval is not None
|
||||
or data.state_check_interval is not None
|
||||
or data.min_brightness_threshold is not None
|
||||
or data.adaptive_fps is not None
|
||||
or data.key_colors_settings is not None
|
||||
),
|
||||
css_changed=data.color_strip_source_id is not None,
|
||||
brightness_vs_changed=(data.brightness_value_source_id is not None or kc_brightness_vs_changed),
|
||||
brightness_vs_changed=(
|
||||
data.brightness_value_source_id is not None or kc_brightness_vs_changed
|
||||
),
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.debug("Processor config update skipped for target %s: %s", target_id, e)
|
||||
|
||||
@@ -10,6 +10,8 @@ import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import os
|
||||
|
||||
import psutil
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
@@ -52,8 +54,10 @@ from wled_controller.api.routes.system_settings import load_external_url # noqa
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Prime psutil CPU counter (first call always returns 0.0)
|
||||
# Prime psutil CPU counters (first call always returns 0.0)
|
||||
psutil.cpu_percent(interval=None)
|
||||
_process = psutil.Process(os.getpid())
|
||||
_process.cpu_percent(interval=None) # prime process-level counter
|
||||
|
||||
# GPU monitoring (initialized once in utils.gpu, shared with metrics_history)
|
||||
from wled_controller.utils.gpu import ( # noqa: E402
|
||||
@@ -264,18 +268,36 @@ def get_system_performance(_: AuthRequired):
|
||||
"""
|
||||
mem = psutil.virtual_memory()
|
||||
|
||||
# App-level metrics
|
||||
proc_mem = _process.memory_info()
|
||||
app_cpu = _process.cpu_percent(interval=None)
|
||||
app_ram_mb = round(proc_mem.rss / 1024 / 1024, 1)
|
||||
|
||||
gpu = None
|
||||
if _nvml_available:
|
||||
try:
|
||||
util = _nvml.nvmlDeviceGetUtilizationRates(_nvml_handle)
|
||||
mem_info = _nvml.nvmlDeviceGetMemoryInfo(_nvml_handle)
|
||||
temp = _nvml.nvmlDeviceGetTemperature(_nvml_handle, _nvml.NVML_TEMPERATURE_GPU)
|
||||
|
||||
# App GPU memory: sum memory used by this process on the GPU
|
||||
app_gpu_mem: float | None = None
|
||||
try:
|
||||
pid = os.getpid()
|
||||
for proc_info in _nvml.nvmlDeviceGetComputeRunningProcesses(_nvml_handle):
|
||||
if proc_info.pid == pid and proc_info.usedGpuMemory:
|
||||
app_gpu_mem = round(proc_info.usedGpuMemory / 1024 / 1024, 1)
|
||||
break
|
||||
except Exception:
|
||||
pass # not all drivers support per-process queries
|
||||
|
||||
gpu = GpuInfo(
|
||||
name=_nvml.nvmlDeviceGetName(_nvml_handle),
|
||||
utilization=float(util.gpu),
|
||||
memory_used_mb=round(mem_info.used / 1024 / 1024, 1),
|
||||
memory_total_mb=round(mem_info.total / 1024 / 1024, 1),
|
||||
temperature_c=float(temp),
|
||||
app_memory_mb=app_gpu_mem,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("NVML query failed: %s", e)
|
||||
@@ -286,6 +308,8 @@ def get_system_performance(_: AuthRequired):
|
||||
ram_used_mb=round(mem.used / 1024 / 1024, 1),
|
||||
ram_total_mb=round(mem.total / 1024 / 1024, 1),
|
||||
ram_percent=mem.percent,
|
||||
app_cpu_percent=app_cpu,
|
||||
app_ram_mb=app_ram_mb,
|
||||
gpu=gpu,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
@@ -22,10 +22,18 @@ class KeyColorsSettingsSchema(BaseModel):
|
||||
"""Settings for key colors extraction."""
|
||||
|
||||
fps: int = Field(default=10, description="Extraction rate (1-60)", ge=1, le=60)
|
||||
interpolation_mode: str = Field(default="average", description="Color mode (average, median, dominant)")
|
||||
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
|
||||
pattern_template_id: str = Field(default="", description="Pattern template ID for rectangle layout")
|
||||
brightness: float = Field(default=1.0, description="Output brightness (0.0-1.0)", ge=0.0, le=1.0)
|
||||
interpolation_mode: str = Field(
|
||||
default="average", description="Color mode (average, median, dominant)"
|
||||
)
|
||||
smoothing: float = Field(
|
||||
default=0.3, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0
|
||||
)
|
||||
pattern_template_id: str = Field(
|
||||
default="", description="Pattern template ID for rectangle layout"
|
||||
)
|
||||
brightness: float = Field(
|
||||
default=1.0, description="Output brightness (0.0-1.0)", ge=0.0, le=1.0
|
||||
)
|
||||
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
|
||||
|
||||
|
||||
@@ -46,24 +54,79 @@ class KeyColorsResponse(BaseModel):
|
||||
timestamp: Optional[datetime] = Field(None, description="Extraction timestamp")
|
||||
|
||||
|
||||
class HALightMappingSchema(BaseModel):
|
||||
"""Maps an LED range to one HA light entity."""
|
||||
|
||||
entity_id: str = Field(description="HA light entity ID (e.g. 'light.living_room')")
|
||||
led_start: int = Field(default=0, ge=0, description="Start LED index (0-based)")
|
||||
led_end: int = Field(default=-1, description="End LED index (-1 = last)")
|
||||
brightness_scale: float = Field(
|
||||
default=1.0, ge=0.0, le=1.0, description="Brightness multiplier"
|
||||
)
|
||||
|
||||
|
||||
class OutputTargetCreate(BaseModel):
|
||||
"""Request to create an output target."""
|
||||
|
||||
name: str = Field(description="Target name", min_length=1, max_length=100)
|
||||
target_type: str = Field(default="led", description="Target type (led, key_colors)")
|
||||
target_type: str = Field(default="led", description="Target type (led, key_colors, ha_light)")
|
||||
# LED target fields
|
||||
device_id: str = Field(default="", description="LED device ID")
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
|
||||
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)
|
||||
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")
|
||||
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)")
|
||||
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,
|
||||
)
|
||||
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"
|
||||
)
|
||||
protocol: str = Field(
|
||||
default="ddp",
|
||||
pattern="^(ddp|http)$",
|
||||
description="Send protocol: ddp (UDP) or http (JSON API)",
|
||||
)
|
||||
# KC target fields
|
||||
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)")
|
||||
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)"
|
||||
)
|
||||
# HA light target fields
|
||||
ha_source_id: str = Field(
|
||||
default="", description="Home Assistant source ID (for ha_light targets)"
|
||||
)
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
None, description="LED-to-light mappings (for ha_light targets)"
|
||||
)
|
||||
update_rate: float = Field(
|
||||
default=2.0, ge=0.5, le=5.0, description="Service call rate in Hz (for ha_light targets)"
|
||||
)
|
||||
transition: float = Field(
|
||||
default=0.5, ge=0.0, le=10.0, description="HA transition seconds (for ha_light targets)"
|
||||
)
|
||||
color_tolerance: int = Field(
|
||||
default=5,
|
||||
ge=0,
|
||||
le=50,
|
||||
description="Skip service call if RGB delta < this (for ha_light targets)",
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
|
||||
@@ -75,16 +138,48 @@ class OutputTargetUpdate(BaseModel):
|
||||
# LED target fields
|
||||
device_id: Optional[str] = Field(None, description="LED device ID")
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
brightness_value_source_id: Optional[str] = Field(None, description="Brightness value source ID")
|
||||
brightness_value_source_id: Optional[str] = Field(
|
||||
None, description="Brightness value source ID"
|
||||
)
|
||||
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)
|
||||
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")
|
||||
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)")
|
||||
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
|
||||
)
|
||||
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"
|
||||
)
|
||||
protocol: Optional[str] = Field(
|
||||
None, pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)"
|
||||
)
|
||||
# KC target fields
|
||||
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)")
|
||||
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)"
|
||||
)
|
||||
# HA light target fields
|
||||
ha_source_id: Optional[str] = Field(
|
||||
None, description="Home Assistant source ID (for ha_light targets)"
|
||||
)
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
None, description="LED-to-light mappings (for ha_light targets)"
|
||||
)
|
||||
update_rate: Optional[float] = Field(
|
||||
None, ge=0.5, le=5.0, description="Service call rate Hz (for ha_light targets)"
|
||||
)
|
||||
transition: Optional[float] = Field(
|
||||
None, ge=0.0, le=10.0, description="HA transition seconds (for ha_light targets)"
|
||||
)
|
||||
color_tolerance: Optional[int] = Field(
|
||||
None, ge=0, le=50, description="RGB delta tolerance (for ha_light targets)"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
|
||||
@@ -101,13 +196,29 @@ class OutputTargetResponse(BaseModel):
|
||||
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
|
||||
fps: Optional[int] = Field(None, description="Target send FPS")
|
||||
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)")
|
||||
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")
|
||||
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)"
|
||||
)
|
||||
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
|
||||
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"
|
||||
)
|
||||
# HA light target fields
|
||||
ha_source_id: str = Field(default="", description="Home Assistant source ID (ha_light)")
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
None, description="LED-to-light mappings (ha_light)"
|
||||
)
|
||||
update_rate: Optional[float] = Field(None, description="Service call rate Hz (ha_light)")
|
||||
ha_transition: Optional[float] = Field(None, description="HA transition seconds (ha_light)")
|
||||
color_tolerance: Optional[int] = Field(None, description="RGB delta tolerance (ha_light)")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
@@ -129,23 +240,39 @@ class TargetProcessingState(BaseModel):
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
|
||||
fps_potential: Optional[float] = Field(None, description="Potential FPS (processing speed without throttle)")
|
||||
fps_potential: Optional[float] = Field(
|
||||
None, description="Potential FPS (processing speed without throttle)"
|
||||
)
|
||||
fps_target: Optional[int] = Field(None, 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")
|
||||
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")
|
||||
timing_send_ms: Optional[float] = Field(None, description="DDP send time (ms)")
|
||||
timing_extract_ms: Optional[float] = Field(None, description="Border pixel extraction time (ms)")
|
||||
timing_extract_ms: Optional[float] = Field(
|
||||
None, description="Border pixel extraction time (ms)"
|
||||
)
|
||||
timing_map_leds_ms: Optional[float] = Field(None, description="LED color mapping time (ms)")
|
||||
timing_smooth_ms: Optional[float] = Field(None, description="Temporal smoothing time (ms)")
|
||||
timing_total_ms: Optional[float] = Field(None, description="Total processing time per frame (ms)")
|
||||
timing_total_ms: Optional[float] = Field(
|
||||
None, description="Total processing time per frame (ms)"
|
||||
)
|
||||
timing_audio_read_ms: Optional[float] = Field(None, description="Audio device read time (ms)")
|
||||
timing_audio_fft_ms: Optional[float] = Field(None, description="Audio FFT analysis time (ms)")
|
||||
timing_audio_render_ms: Optional[float] = Field(None, description="Audio visualization render time (ms)")
|
||||
timing_calc_colors_ms: Optional[float] = Field(None, description="Color calculation time (ms, KC targets)")
|
||||
timing_broadcast_ms: Optional[float] = Field(None, description="WebSocket broadcast time (ms, KC targets)")
|
||||
timing_audio_render_ms: Optional[float] = Field(
|
||||
None, description="Audio visualization render time (ms)"
|
||||
)
|
||||
timing_calc_colors_ms: Optional[float] = Field(
|
||||
None, description="Color calculation time (ms, KC targets)"
|
||||
)
|
||||
timing_broadcast_ms: Optional[float] = Field(
|
||||
None, description="WebSocket broadcast time (ms, KC targets)"
|
||||
)
|
||||
display_index: Optional[int] = Field(None, description="Current display index")
|
||||
overlay_active: bool = Field(default=False, description="Whether visualization overlay is active")
|
||||
overlay_active: bool = Field(
|
||||
default=False, description="Whether visualization overlay is active"
|
||||
)
|
||||
last_update: Optional[datetime] = Field(None, description="Last successful update")
|
||||
errors: List[str] = Field(default_factory=list, description="Recent errors")
|
||||
device_online: bool = Field(default=False, description="Whether device is reachable")
|
||||
@@ -154,11 +281,17 @@ class TargetProcessingState(BaseModel):
|
||||
device_version: Optional[str] = Field(None, description="Firmware version")
|
||||
device_led_count: Optional[int] = Field(None, description="LED count reported by device")
|
||||
device_rgbw: Optional[bool] = Field(None, description="Whether device uses RGBW LEDs")
|
||||
device_led_type: Optional[str] = Field(None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)")
|
||||
device_fps: Optional[int] = Field(None, description="Device-reported FPS (WLED internal refresh rate)")
|
||||
device_led_type: Optional[str] = Field(
|
||||
None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)"
|
||||
)
|
||||
device_fps: Optional[int] = Field(
|
||||
None, description="Device-reported FPS (WLED internal refresh rate)"
|
||||
)
|
||||
device_last_checked: Optional[datetime] = Field(None, description="Last health check time")
|
||||
device_error: Optional[str] = Field(None, description="Last health check error")
|
||||
device_streaming_reachable: Optional[bool] = Field(None, description="Device reachable during streaming (HTTP probe)")
|
||||
device_streaming_reachable: Optional[bool] = Field(
|
||||
None, description="Device reachable during streaming (HTTP probe)"
|
||||
)
|
||||
fps_effective: Optional[int] = Field(None, description="Effective FPS after adaptive reduction")
|
||||
|
||||
|
||||
@@ -186,9 +319,15 @@ class BulkTargetRequest(BaseModel):
|
||||
class BulkTargetResponse(BaseModel):
|
||||
"""Response for bulk start/stop operations."""
|
||||
|
||||
started: List[str] = Field(default_factory=list, description="IDs that were successfully started")
|
||||
stopped: List[str] = Field(default_factory=list, description="IDs that were successfully stopped")
|
||||
errors: Dict[str, str] = Field(default_factory=dict, description="Map of target ID to error message for failures")
|
||||
started: List[str] = Field(
|
||||
default_factory=list, description="IDs that were successfully started"
|
||||
)
|
||||
stopped: List[str] = Field(
|
||||
default_factory=list, description="IDs that were successfully stopped"
|
||||
)
|
||||
errors: Dict[str, str] = Field(
|
||||
default_factory=dict, description="Map of target ID to error message for failures"
|
||||
)
|
||||
|
||||
|
||||
class KCTestRectangleResponse(BaseModel):
|
||||
@@ -206,6 +345,8 @@ class KCTestResponse(BaseModel):
|
||||
"""Response from testing a KC target."""
|
||||
|
||||
image: str = Field(description="Base64 data URI of the captured frame")
|
||||
rectangles: List[KCTestRectangleResponse] = Field(description="Rectangles with extracted colors")
|
||||
rectangles: List[KCTestRectangleResponse] = Field(
|
||||
description="Rectangles with extracted colors"
|
||||
)
|
||||
interpolation_mode: str = Field(description="Color extraction mode used")
|
||||
pattern_template_name: str = Field(description="Pattern template name")
|
||||
|
||||
@@ -64,6 +64,9 @@ class GpuInfo(BaseModel):
|
||||
memory_used_mb: float | None = Field(default=None, description="GPU memory used in MB")
|
||||
memory_total_mb: float | None = Field(default=None, description="GPU total memory in MB")
|
||||
temperature_c: float | None = Field(default=None, description="GPU temperature in Celsius")
|
||||
app_memory_mb: float | None = Field(
|
||||
default=None, description="GPU memory used by this app in MB"
|
||||
)
|
||||
|
||||
|
||||
class PerformanceResponse(BaseModel):
|
||||
@@ -74,6 +77,8 @@ class PerformanceResponse(BaseModel):
|
||||
ram_used_mb: float = Field(description="RAM used in MB")
|
||||
ram_total_mb: float = Field(description="RAM total in MB")
|
||||
ram_percent: float = Field(description="RAM usage percent")
|
||||
app_cpu_percent: float = Field(description="App process CPU usage percent")
|
||||
app_ram_mb: float = Field(description="App process resident memory in MB")
|
||||
gpu: GpuInfo | None = Field(default=None, description="GPU info (null if unavailable)")
|
||||
timestamp: datetime = Field(description="Measurement timestamp")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user