refactor(types): PEP-604 union sweep + UP007/UP045 enforcement
ruff --select UP007,UP045 --fix converted ~1760 sites across the backend: `Optional[T]` → `T | None`, `Union[X, Y]` → `X | Y`. The remaining module-level alias targets that ruff conservatively skips (BindableFloatInput, ColorList, DeviceConfig) were converted by hand earlier in the pass. black -formatted the result so the wider unions fit cleanly under the 100-char line budget. pyproject.toml now sets [tool.ruff.lint] extend-select = ["UP007", "UP045"] so future legacy imports fire CI on every push. The pre-commit ruff hook was bumped from v0.8.0 -> v0.15.12 to recognise UP045 (split off from UP007 in v0.13).
This commit is contained in:
@@ -6,7 +6,10 @@ repos:
|
||||
args: [--line-length=100, --target-version=py311]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.0
|
||||
# Bumped from v0.8.0 so the hook recognises UP045
|
||||
# (non-pep604-annotation-optional), which the v0.13+ ruff split off
|
||||
# from UP007. Pyproject.toml extend-selects both rules.
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--line-length=100, --target-version=py311]
|
||||
|
||||
@@ -117,3 +117,11 @@ target-version = ['py311']
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# E + F are ruff's defaults; UP007 + UP045 enforce PEP-604 `X | Y` and
|
||||
# `T | None` style so we don't drift back to the legacy `Union[X, Y]` /
|
||||
# `Optional[T]` imports the REVIEW_TODO mechanical sweep removed.
|
||||
# Recent ruff versions split the rule — UP007 covers `Union`, UP045
|
||||
# covers `Optional`.
|
||||
extend-select = ["UP007", "UP045"]
|
||||
|
||||
@@ -8,11 +8,11 @@ inside an Android application. Sets up Android-specific paths
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
_server_thread: Optional[threading.Thread] = None
|
||||
_server: Optional[Any] = None # uvicorn.Server
|
||||
_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
_server_thread: threading.Thread | None = None
|
||||
_server: Any | None = None # uvicorn.Server
|
||||
_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
|
||||
def start_server(data_dir: str, port: int = 8080) -> None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
from starlette.websockets import WebSocket
|
||||
@@ -61,8 +61,8 @@ async def stream_capture_test(
|
||||
websocket: WebSocket,
|
||||
engine_factory: Callable,
|
||||
duration: float,
|
||||
pp_filters: Optional[list] = None,
|
||||
preview_width: Optional[int] = None,
|
||||
pp_filters: list | None = None,
|
||||
preview_width: int | None = None,
|
||||
) -> None:
|
||||
"""Run a capture test, streaming intermediate thumbnails and a final full-res frame.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Audio source routes: CRUD for audio sources + real-time test WebSocket."""
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||
from starlette.websockets import WebSocket, WebSocketDisconnect
|
||||
@@ -91,7 +91,7 @@ def _to_response(source: AudioSource) -> AudioSourceResponse:
|
||||
@router.get("/api/v1/audio-sources", response_model=AudioSourceListResponse, tags=["Audio Sources"])
|
||||
async def list_audio_sources(
|
||||
_auth: AuthRequired,
|
||||
source_type: Optional[str] = Query(
|
||||
source_type: str | None = Query(
|
||||
None, description="Filter by source_type: capture or processed"
|
||||
),
|
||||
store: AudioSourceStore = Depends(get_audio_source_store),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Output target routes: CRUD endpoints and batch state/metrics queries."""
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Depends
|
||||
|
||||
@@ -421,7 +421,7 @@ async def get_target(
|
||||
|
||||
|
||||
def _resolve_effective_color_vs_id(
|
||||
target_store: OutputTargetStore, target_id: str, payload_id: Optional[str]
|
||||
target_store: OutputTargetStore, target_id: str, payload_id: str | None
|
||||
) -> str:
|
||||
if payload_id is not None:
|
||||
return payload_id
|
||||
|
||||
@@ -9,7 +9,6 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import os
|
||||
|
||||
@@ -190,7 +189,7 @@ async def list_all_tags(_: AuthRequired):
|
||||
@router.get("/api/v1/config/displays", response_model=DisplayListResponse, tags=["Config"])
|
||||
async def get_displays(
|
||||
_: AuthRequired,
|
||||
engine_type: Optional[str] = Query(None, description="Engine type to get displays for"),
|
||||
engine_type: str | None = Query(None, description="Engine type to get displays for"),
|
||||
):
|
||||
"""Get list of available displays.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Value source routes: CRUD for value sources."""
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
@@ -289,7 +289,7 @@ def _to_response(source: ValueSource) -> ValueSourceResponse:
|
||||
@router.get("/api/v1/value-sources", response_model=ValueSourceListResponse, tags=["Value Sources"])
|
||||
async def list_value_sources(
|
||||
_auth: AuthRequired,
|
||||
source_type: Optional[str] = Query(
|
||||
source_type: str | None = Query(
|
||||
None,
|
||||
description="Filter by source_type: static, animated, audio, adaptive_time, or adaptive_scene",
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Asset schemas (CRUD)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -9,15 +9,15 @@ from pydantic import BaseModel, Field
|
||||
class AssetUpdate(BaseModel):
|
||||
"""Request to update asset metadata."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100, description="Display name")
|
||||
description: Optional[str] = Field(None, max_length=500, description="Optional description")
|
||||
tags: Optional[List[str]] = Field(None, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, min_length=1, max_length=100, description="Display name")
|
||||
description: str | None = Field(None, max_length=500, description="Optional description")
|
||||
tags: List[str] | None = Field(None, description="User-defined tags")
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -33,15 +33,15 @@ class AssetResponse(BaseModel):
|
||||
mime_type: str = Field(description="MIME type")
|
||||
asset_type: str = Field(description="Asset type: sound, image, video, other")
|
||||
size_bytes: int = Field(description="File size in bytes")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
prebuilt: bool = Field(False, description="Whether this is a shipped prebuilt asset")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Audio processing template schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -15,14 +15,14 @@ class AudioProcessingTemplateCreate(BaseModel):
|
||||
filters: List[FilterInstanceSchema] = Field(
|
||||
default_factory=list, description="Ordered list of audio filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -32,18 +32,18 @@ class AudioProcessingTemplateCreate(BaseModel):
|
||||
class AudioProcessingTemplateUpdate(BaseModel):
|
||||
"""Request to update an audio processing template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
||||
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: List[FilterInstanceSchema] | None = Field(
|
||||
None, description="Ordered list of audio filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -61,13 +61,13 @@ class AudioProcessingTemplateResponse(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description")
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Audio source schemas — discriminated unions per source type."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, List, Literal, Optional, Union
|
||||
from typing import Annotated, List, Literal
|
||||
|
||||
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||
|
||||
@@ -15,16 +15,16 @@ class _AudioSourceResponseBase(BaseModel):
|
||||
|
||||
id: str = Field(description="Source ID")
|
||||
name: str = Field(description="Source name")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -35,7 +35,7 @@ class CaptureAudioSourceResponse(_AudioSourceResponseBase):
|
||||
source_type: Literal["capture"] = "capture"
|
||||
device_index: int = Field(description="Audio device index (-1 = default)")
|
||||
is_loopback: bool = Field(description="WASAPI loopback mode")
|
||||
audio_template_id: Optional[str] = Field(None, description="Audio capture template ID")
|
||||
audio_template_id: str | None = Field(None, description="Audio capture template ID")
|
||||
|
||||
|
||||
class ProcessedAudioSourceResponse(_AudioSourceResponseBase):
|
||||
@@ -45,10 +45,8 @@ class ProcessedAudioSourceResponse(_AudioSourceResponseBase):
|
||||
|
||||
|
||||
AudioSourceResponse = Annotated[
|
||||
Union[
|
||||
Annotated[CaptureAudioSourceResponse, Tag("capture")],
|
||||
Annotated[ProcessedAudioSourceResponse, Tag("processed")],
|
||||
],
|
||||
Annotated[CaptureAudioSourceResponse, Tag("capture")]
|
||||
| Annotated[ProcessedAudioSourceResponse, Tag("processed")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -61,14 +59,14 @@ class _AudioSourceCreateBase(BaseModel):
|
||||
"""Shared fields for all audio source create requests."""
|
||||
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -79,7 +77,7 @@ class CaptureAudioSourceCreate(_AudioSourceCreateBase):
|
||||
source_type: Literal["capture"] = "capture"
|
||||
device_index: int = Field(-1, description="Audio device index (-1 = default)")
|
||||
is_loopback: bool = Field(True, description="True for system audio (WASAPI loopback)")
|
||||
audio_template_id: Optional[str] = Field(None, description="Audio capture template ID")
|
||||
audio_template_id: str | None = Field(None, description="Audio capture template ID")
|
||||
|
||||
|
||||
class ProcessedAudioSourceCreate(_AudioSourceCreateBase):
|
||||
@@ -89,10 +87,8 @@ class ProcessedAudioSourceCreate(_AudioSourceCreateBase):
|
||||
|
||||
|
||||
AudioSourceCreate = Annotated[
|
||||
Union[
|
||||
Annotated[CaptureAudioSourceCreate, Tag("capture")],
|
||||
Annotated[ProcessedAudioSourceCreate, Tag("processed")],
|
||||
],
|
||||
Annotated[CaptureAudioSourceCreate, Tag("capture")]
|
||||
| Annotated[ProcessedAudioSourceCreate, Tag("processed")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -104,15 +100,15 @@ AudioSourceCreate = Annotated[
|
||||
class _AudioSourceUpdateBase(BaseModel):
|
||||
"""Shared fields for all audio source update requests."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -121,24 +117,22 @@ class _AudioSourceUpdateBase(BaseModel):
|
||||
|
||||
class CaptureAudioSourceUpdate(_AudioSourceUpdateBase):
|
||||
source_type: Literal["capture"] = "capture"
|
||||
device_index: Optional[int] = Field(None, description="Audio device index (-1 = default)")
|
||||
is_loopback: Optional[bool] = Field(None, description="True for system audio (WASAPI loopback)")
|
||||
audio_template_id: Optional[str] = Field(None, description="Audio capture template ID")
|
||||
device_index: int | None = Field(None, description="Audio device index (-1 = default)")
|
||||
is_loopback: bool | None = Field(None, description="True for system audio (WASAPI loopback)")
|
||||
audio_template_id: str | None = Field(None, description="Audio capture template ID")
|
||||
|
||||
|
||||
class ProcessedAudioSourceUpdate(_AudioSourceUpdateBase):
|
||||
source_type: Literal["processed"] = "processed"
|
||||
audio_source_id: Optional[str] = Field(None, description="Input audio source ID")
|
||||
audio_processing_template_id: Optional[str] = Field(
|
||||
audio_source_id: str | None = Field(None, description="Input audio source ID")
|
||||
audio_processing_template_id: str | None = Field(
|
||||
None, description="Audio processing template ID"
|
||||
)
|
||||
|
||||
|
||||
AudioSourceUpdate = Annotated[
|
||||
Union[
|
||||
Annotated[CaptureAudioSourceUpdate, Tag("capture")],
|
||||
Annotated[ProcessedAudioSourceUpdate, Tag("processed")],
|
||||
],
|
||||
Annotated[CaptureAudioSourceUpdate, Tag("capture")]
|
||||
| Annotated[ProcessedAudioSourceUpdate, Tag("processed")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Audio capture template and engine schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -14,14 +14,14 @@ class AudioTemplateCreate(BaseModel):
|
||||
description="Audio engine type (e.g., 'wasapi', 'sounddevice')", min_length=1
|
||||
)
|
||||
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -31,17 +31,17 @@ class AudioTemplateCreate(BaseModel):
|
||||
class AudioTemplateUpdate(BaseModel):
|
||||
"""Request to update an audio template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
engine_type: Optional[str] = Field(None, description="Audio engine type")
|
||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
engine_type: str | None = Field(None, description="Audio engine type")
|
||||
engine_config: Dict | None = Field(None, description="Engine-specific configuration")
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -58,13 +58,13 @@ class AudioTemplateResponse(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description")
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Automation-related schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -11,57 +11,53 @@ class RuleSchema(BaseModel):
|
||||
|
||||
rule_type: str = Field(description="Rule type discriminator (e.g. 'application')")
|
||||
# Application rule fields
|
||||
apps: Optional[List[str]] = Field(None, description="Process names (for application rule)")
|
||||
match_type: Optional[str] = Field(
|
||||
apps: List[str] | None = Field(None, description="Process names (for application rule)")
|
||||
match_type: str | None = Field(
|
||||
None, description="'running' or 'topmost' (for application rule)"
|
||||
)
|
||||
# Time-of-day rule fields
|
||||
start_time: Optional[str] = Field(None, description="Start time HH:MM (for time_of_day rule)")
|
||||
end_time: Optional[str] = Field(None, description="End time HH:MM (for time_of_day rule)")
|
||||
start_time: str | None = Field(None, description="Start time HH:MM (for time_of_day rule)")
|
||||
end_time: str | None = Field(None, description="End time HH:MM (for time_of_day rule)")
|
||||
# System idle rule fields
|
||||
idle_minutes: Optional[int] = Field(
|
||||
idle_minutes: int | None = Field(
|
||||
None, description="Idle timeout in minutes (for system_idle rule)"
|
||||
)
|
||||
when_idle: Optional[bool] = Field(
|
||||
None, description="True=active when idle (for system_idle rule)"
|
||||
)
|
||||
when_idle: bool | None = Field(None, description="True=active when idle (for system_idle rule)")
|
||||
# Display state rule fields
|
||||
state: Optional[str] = Field(None, description="'on' or 'off' (for display_state rule)")
|
||||
state: str | None = Field(None, description="'on' or 'off' (for display_state rule)")
|
||||
# MQTT rule fields
|
||||
mqtt_source_id: Optional[str] = Field(None, description="MQTT source ID (for mqtt rule)")
|
||||
topic: Optional[str] = Field(None, description="MQTT topic to watch (for mqtt rule)")
|
||||
payload: Optional[str] = Field(None, description="Expected payload value (for mqtt rule)")
|
||||
match_mode: Optional[str] = Field(
|
||||
mqtt_source_id: str | None = Field(None, description="MQTT source ID (for mqtt rule)")
|
||||
topic: str | None = Field(None, description="MQTT topic to watch (for mqtt rule)")
|
||||
payload: str | None = Field(None, description="Expected payload value (for mqtt rule)")
|
||||
match_mode: str | None = Field(
|
||||
None, description="'exact', 'contains', or 'regex' (for mqtt rule)"
|
||||
)
|
||||
# Webhook rule fields
|
||||
token: Optional[str] = Field(
|
||||
None, description="Secret token for webhook URL (for webhook rule)"
|
||||
)
|
||||
token: str | None = Field(None, description="Secret token for webhook URL (for webhook rule)")
|
||||
# Home Assistant rule fields
|
||||
ha_source_id: Optional[str] = Field(
|
||||
ha_source_id: str | None = Field(
|
||||
None, description="Home Assistant source ID (for home_assistant rule)"
|
||||
)
|
||||
entity_id: Optional[str] = Field(
|
||||
entity_id: str | None = Field(
|
||||
None,
|
||||
description="HA entity ID, e.g. 'binary_sensor.front_door' (for home_assistant rule)",
|
||||
)
|
||||
# HTTP poll rule fields
|
||||
value_source_id: Optional[str] = Field(
|
||||
value_source_id: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Value source ID (for http_poll rule). The referenced "
|
||||
"ValueSource must be of source_type='http'."
|
||||
),
|
||||
)
|
||||
operator: Optional[str] = Field(
|
||||
operator: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Comparison operator for http_poll rule: "
|
||||
"'equals', 'not_equals', 'contains', 'regex', 'gt', 'lt', 'exists'."
|
||||
),
|
||||
)
|
||||
value: Optional[str] = Field(
|
||||
value: str | None = Field(
|
||||
None, description="Expected value (for http_poll rule; ignored for 'exists')"
|
||||
)
|
||||
|
||||
@@ -77,20 +73,20 @@ class AutomationCreate(BaseModel):
|
||||
enabled: bool = Field(default=True, description="Whether the automation is enabled")
|
||||
rule_logic: str = Field(default="or", description="How rules combine: 'or' or 'and'")
|
||||
rules: List[RuleSchema] = Field(default_factory=list, description="List of rules")
|
||||
scene_preset_id: Optional[str] = Field(None, description="Scene preset to activate")
|
||||
scene_preset_id: str | None = Field(None, description="Scene preset to activate")
|
||||
deactivation_mode: str = Field(
|
||||
default="none", description="'none', 'revert', or 'fallback_scene'"
|
||||
)
|
||||
deactivation_scene_preset_id: Optional[str] = Field(
|
||||
deactivation_scene_preset_id: str | None = Field(
|
||||
None, description="Scene preset for fallback deactivation"
|
||||
)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -100,24 +96,22 @@ class AutomationCreate(BaseModel):
|
||||
class AutomationUpdate(BaseModel):
|
||||
"""Request to update an automation."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Automation name", min_length=1, max_length=100)
|
||||
enabled: Optional[bool] = Field(None, description="Whether the automation is enabled")
|
||||
rule_logic: Optional[str] = Field(None, description="How rules combine: 'or' or 'and'")
|
||||
rules: Optional[List[RuleSchema]] = Field(None, description="List of rules")
|
||||
scene_preset_id: Optional[str] = Field(None, description="Scene preset to activate")
|
||||
deactivation_mode: Optional[str] = Field(
|
||||
None, description="'none', 'revert', or 'fallback_scene'"
|
||||
)
|
||||
deactivation_scene_preset_id: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Automation name", min_length=1, max_length=100)
|
||||
enabled: bool | None = Field(None, description="Whether the automation is enabled")
|
||||
rule_logic: str | None = Field(None, description="How rules combine: 'or' or 'and'")
|
||||
rules: List[RuleSchema] | None = Field(None, description="List of rules")
|
||||
scene_preset_id: str | None = Field(None, description="Scene preset to activate")
|
||||
deactivation_mode: str | None = Field(None, description="'none', 'revert', or 'fallback_scene'")
|
||||
deactivation_scene_preset_id: str | None = Field(
|
||||
None, description="Scene preset for fallback deactivation"
|
||||
)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -132,26 +126,26 @@ class AutomationResponse(BaseModel):
|
||||
enabled: bool = Field(description="Whether the automation is enabled")
|
||||
rule_logic: str = Field(description="Rule combination logic")
|
||||
rules: List[RuleSchema] = Field(description="List of rules")
|
||||
scene_preset_id: Optional[str] = Field(None, description="Scene preset to activate")
|
||||
scene_preset_id: str | None = Field(None, description="Scene preset to activate")
|
||||
deactivation_mode: str = Field(default="none", description="Deactivation behavior")
|
||||
deactivation_scene_preset_id: Optional[str] = Field(None, description="Fallback scene preset")
|
||||
deactivation_scene_preset_id: str | None = Field(None, description="Fallback scene preset")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
webhook_url: Optional[str] = Field(
|
||||
webhook_url: str | None = Field(
|
||||
None, description="Webhook URL for the first webhook rule (if any)"
|
||||
)
|
||||
is_active: bool = Field(default=False, description="Whether the automation is currently active")
|
||||
last_activated_at: Optional[datetime] = Field(
|
||||
last_activated_at: datetime | None = Field(
|
||||
None, description="Last time this automation was activated"
|
||||
)
|
||||
last_deactivated_at: Optional[datetime] = Field(
|
||||
last_deactivated_at: datetime | None = Field(
|
||||
None, description="Last time this automation was deactivated"
|
||||
)
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Color strip processing template schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -15,14 +15,14 @@ class ColorStripProcessingTemplateCreate(BaseModel):
|
||||
filters: List[FilterInstanceSchema] = Field(
|
||||
default_factory=list, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -32,18 +32,18 @@ class ColorStripProcessingTemplateCreate(BaseModel):
|
||||
class ColorStripProcessingTemplateUpdate(BaseModel):
|
||||
"""Request to update a color strip processing template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
||||
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: List[FilterInstanceSchema] | None = Field(
|
||||
None, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -59,13 +59,13 @@ class ColorStripProcessingTemplateResponse(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description")
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"""Color strip source schemas — discriminated unions per source type."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
||||
from typing import Annotated, Any, Dict, List, Literal
|
||||
|
||||
from pydantic import BaseModel, Discriminator, Field, Tag, model_validator
|
||||
|
||||
from ledgrab.api.schemas.devices import Calibration
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Helper models (unchanged)
|
||||
# =====================================================================
|
||||
@@ -16,10 +15,10 @@ from ledgrab.api.schemas.devices import Calibration
|
||||
class AppSoundOverride(BaseModel):
|
||||
"""Per-application sound override for notification sources."""
|
||||
|
||||
sound_asset_id: Optional[str] = Field(
|
||||
sound_asset_id: str | None = Field(
|
||||
None, description="Asset ID for the sound (None = mute this app)"
|
||||
)
|
||||
volume: Optional[float] = Field(
|
||||
volume: float | None = Field(
|
||||
None, ge=0.0, le=1.0, description="Volume override (None = use global)"
|
||||
)
|
||||
|
||||
@@ -39,7 +38,7 @@ class ColorStop(BaseModel):
|
||||
description="Relative position along the strip (0.0-1.0)", ge=0.0, le=1.0
|
||||
)
|
||||
color: List[int] = Field(description="Primary RGB color [R, G, B] (0-255 each)")
|
||||
color_right: Optional[List[int]] = Field(
|
||||
color_right: List[int] | None = Field(
|
||||
None,
|
||||
description="Optional right-side RGB color for a hard edge (bidirectional stop)",
|
||||
)
|
||||
@@ -54,10 +53,10 @@ class CompositeLayer(BaseModel):
|
||||
)
|
||||
opacity: float = Field(default=1.0, ge=0.0, le=1.0, description="Layer opacity 0.0-1.0")
|
||||
enabled: bool = Field(default=True, description="Whether this layer is active")
|
||||
brightness_source_id: Optional[str] = Field(
|
||||
brightness_source_id: str | None = Field(
|
||||
None, description="Optional value source ID for dynamic brightness"
|
||||
)
|
||||
processing_template_id: Optional[str] = Field(
|
||||
processing_template_id: str | None = Field(
|
||||
None, description="Optional color strip processing template ID"
|
||||
)
|
||||
start: int = Field(default=0, ge=0, description="First LED index for range (0 = full strip)")
|
||||
@@ -86,21 +85,21 @@ class _CSSResponseBase(BaseModel):
|
||||
|
||||
id: str = Field(description="Source ID")
|
||||
name: str = Field(description="Source name")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
led_count: int = Field(0, description="Total LED count (0 = auto)")
|
||||
overlay_active: bool = Field(
|
||||
False, description="Whether the screen overlay is currently active"
|
||||
)
|
||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
||||
clock_id: str | None = Field(None, description="Optional sync clock ID")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -112,40 +111,40 @@ class PictureCSSResponse(_CSSResponseBase):
|
||||
picture_source_id: str = Field(description="Picture source ID")
|
||||
smoothing: Any = Field(description="Temporal smoothing")
|
||||
interpolation_mode: str = Field(description="Interpolation mode")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||
|
||||
|
||||
class PictureAdvancedCSSResponse(_CSSResponseBase):
|
||||
source_type: Literal["picture_advanced"] = "picture_advanced"
|
||||
smoothing: Any = Field(description="Temporal smoothing")
|
||||
interpolation_mode: str = Field(description="Interpolation mode")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||
|
||||
|
||||
class SingleColorCSSResponse(_CSSResponseBase):
|
||||
source_type: Literal["single_color"] = "single_color"
|
||||
color: Any = Field(description="Solid RGB color")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
||||
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||
|
||||
|
||||
class GradientCSSResponse(_CSSResponseBase):
|
||||
source_type: Literal["gradient"] = "gradient"
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
||||
stops: List[ColorStop] | None = Field(None, description="Color stops")
|
||||
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||
easing: str = Field(description="Gradient interpolation easing")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
|
||||
|
||||
class EffectCSSResponse(_CSSResponseBase):
|
||||
source_type: Literal["effect"] = "effect"
|
||||
effect_type: str = Field(description="Effect algorithm")
|
||||
palette: str = Field(description="Named palette")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
color: Any = Field(description="Primary color")
|
||||
intensity: Any = Field(description="Effect intensity")
|
||||
scale: Any = Field(description="Spatial scale")
|
||||
mirror: bool = Field(description="Mirror/bounce mode")
|
||||
custom_palette: Optional[List[List[float]]] = Field(None, description="Custom palette stops")
|
||||
custom_palette: List[List[float]] | None = Field(None, description="Custom palette stops")
|
||||
|
||||
|
||||
class CompositeCSSResponse(_CSSResponseBase):
|
||||
@@ -165,7 +164,7 @@ class AudioCSSResponse(_CSSResponseBase):
|
||||
sensitivity: Any = Field(description="Audio sensitivity")
|
||||
smoothing: Any = Field(description="Temporal smoothing")
|
||||
palette: str = Field(description="Named palette")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
color: Any = Field(description="Primary color")
|
||||
color_peak: Any = Field(description="Peak color")
|
||||
mirror: bool = Field(description="Mirror mode")
|
||||
@@ -188,7 +187,7 @@ class NotificationCSSResponse(_CSSResponseBase):
|
||||
app_filter_mode: str = Field(description="App filter mode")
|
||||
app_filter_list: List[str] = Field(default_factory=list, description="App names for filter")
|
||||
os_listener: bool = Field(description="Whether to listen for OS notifications")
|
||||
sound_asset_id: Optional[str] = Field(None, description="Global notification sound asset ID")
|
||||
sound_asset_id: str | None = Field(None, description="Global notification sound asset ID")
|
||||
sound_volume: Any = Field(description="Global notification sound volume")
|
||||
app_sounds: Dict[str, dict] = Field(default_factory=dict, description="Per-app sound overrides")
|
||||
|
||||
@@ -237,7 +236,7 @@ class MathWaveCSSResponse(_CSSResponseBase):
|
||||
source_type: Literal["math_wave"] = "math_wave"
|
||||
waves: List[dict] = Field(description="Wave layer definitions")
|
||||
speed: Any = Field(description="Global speed multiplier (bindable)")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID for color mapping")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID for color mapping")
|
||||
|
||||
|
||||
class GameEventCSSResponse(_CSSResponseBase):
|
||||
@@ -248,25 +247,23 @@ class GameEventCSSResponse(_CSSResponseBase):
|
||||
|
||||
|
||||
ColorStripSourceResponse = Annotated[
|
||||
Union[
|
||||
Annotated[PictureCSSResponse, Tag("picture")],
|
||||
Annotated[PictureAdvancedCSSResponse, Tag("picture_advanced")],
|
||||
Annotated[SingleColorCSSResponse, Tag("single_color")],
|
||||
Annotated[GradientCSSResponse, Tag("gradient")],
|
||||
Annotated[EffectCSSResponse, Tag("effect")],
|
||||
Annotated[CompositeCSSResponse, Tag("composite")],
|
||||
Annotated[MappedCSSResponse, Tag("mapped")],
|
||||
Annotated[AudioCSSResponse, Tag("audio")],
|
||||
Annotated[ApiInputCSSResponse, Tag("api_input")],
|
||||
Annotated[NotificationCSSResponse, Tag("notification")],
|
||||
Annotated[DaylightCSSResponse, Tag("daylight")],
|
||||
Annotated[CandlelightCSSResponse, Tag("candlelight")],
|
||||
Annotated[ProcessedCSSResponse, Tag("processed")],
|
||||
Annotated[WeatherCSSResponse, Tag("weather")],
|
||||
Annotated[KeyColorsCSSResponse, Tag("key_colors")],
|
||||
Annotated[MathWaveCSSResponse, Tag("math_wave")],
|
||||
Annotated[GameEventCSSResponse, Tag("game_event")],
|
||||
],
|
||||
Annotated[PictureCSSResponse, Tag("picture")]
|
||||
| Annotated[PictureAdvancedCSSResponse, Tag("picture_advanced")]
|
||||
| Annotated[SingleColorCSSResponse, Tag("single_color")]
|
||||
| Annotated[GradientCSSResponse, Tag("gradient")]
|
||||
| Annotated[EffectCSSResponse, Tag("effect")]
|
||||
| Annotated[CompositeCSSResponse, Tag("composite")]
|
||||
| Annotated[MappedCSSResponse, Tag("mapped")]
|
||||
| Annotated[AudioCSSResponse, Tag("audio")]
|
||||
| Annotated[ApiInputCSSResponse, Tag("api_input")]
|
||||
| Annotated[NotificationCSSResponse, Tag("notification")]
|
||||
| Annotated[DaylightCSSResponse, Tag("daylight")]
|
||||
| Annotated[CandlelightCSSResponse, Tag("candlelight")]
|
||||
| Annotated[ProcessedCSSResponse, Tag("processed")]
|
||||
| Annotated[WeatherCSSResponse, Tag("weather")]
|
||||
| Annotated[KeyColorsCSSResponse, Tag("key_colors")]
|
||||
| Annotated[MathWaveCSSResponse, Tag("math_wave")]
|
||||
| Annotated[GameEventCSSResponse, Tag("game_event")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -281,15 +278,15 @@ class _CSSCreateBase(BaseModel):
|
||||
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
led_count: int = Field(default=0, description="Total LED count (0 = auto)", ge=0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
clock_id: str | None = Field(None, description="Optional sync clock ID")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -301,63 +298,63 @@ class PictureCSSCreate(_CSSCreateBase):
|
||||
picture_source_id: str = Field(default="", description="Picture source ID")
|
||||
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
||||
interpolation_mode: str = Field(default="average", description="Interpolation mode")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||
|
||||
|
||||
class PictureAdvancedCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["picture_advanced"] = "picture_advanced"
|
||||
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
||||
interpolation_mode: str = Field(default="average", description="Interpolation mode")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||
|
||||
|
||||
class SingleColorCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["single_color"] = "single_color"
|
||||
color: Any = Field(default=None, description="Solid RGB color [R,G,B]")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
||||
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||
|
||||
|
||||
class GradientCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["gradient"] = "gradient"
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
||||
easing: Optional[str] = Field(None, description="Gradient easing")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
stops: List[ColorStop] | None = Field(None, description="Color stops")
|
||||
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||
easing: str | None = Field(None, description="Gradient easing")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
|
||||
|
||||
class EffectCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["effect"] = "effect"
|
||||
effect_type: Optional[str] = Field(None, description="Effect algorithm")
|
||||
palette: Optional[str] = Field(None, description="Named palette")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
effect_type: str | None = Field(None, description="Effect algorithm")
|
||||
palette: str | None = Field(None, description="Named palette")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
color: Any = Field(default=None, description="Primary color")
|
||||
intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
|
||||
scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
|
||||
mirror: Optional[bool] = Field(None, description="Mirror/bounce mode")
|
||||
custom_palette: Optional[List[List[float]]] = Field(None, description="Custom palette stops")
|
||||
mirror: bool | None = Field(None, description="Mirror/bounce mode")
|
||||
custom_palette: List[List[float]] | None = Field(None, description="Custom palette stops")
|
||||
|
||||
|
||||
class CompositeCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["composite"] = "composite"
|
||||
layers: Optional[List[CompositeLayer]] = Field(None, description="Layers for composite type")
|
||||
layers: List[CompositeLayer] | None = Field(None, description="Layers for composite type")
|
||||
|
||||
|
||||
class MappedCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["mapped"] = "mapped"
|
||||
zones: Optional[List[MappedZone]] = Field(None, description="Zones for mapped type")
|
||||
zones: List[MappedZone] | None = Field(None, description="Zones for mapped type")
|
||||
|
||||
|
||||
class AudioCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["audio"] = "audio"
|
||||
visualization_mode: Optional[str] = Field(None, description="Audio visualization mode")
|
||||
audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
|
||||
visualization_mode: str | None = Field(None, description="Audio visualization mode")
|
||||
audio_source_id: str | None = Field(None, description="Mono audio source ID")
|
||||
sensitivity: Any = Field(default=None, description="Audio sensitivity (0.1-5.0)")
|
||||
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
||||
palette: Optional[str] = Field(None, description="Named palette")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
palette: str | None = Field(None, description="Named palette")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
color: Any = Field(default=None, description="Primary color")
|
||||
color_peak: Any = Field(default=None, description="Peak color [R,G,B]")
|
||||
mirror: Optional[bool] = Field(None, description="Mirror mode")
|
||||
mirror: bool | None = Field(None, description="Mirror mode")
|
||||
beat_decay: Any = Field(
|
||||
default=None, description="Beat pulse decay rate (music modes, 0.01-0.5)"
|
||||
)
|
||||
@@ -367,23 +364,23 @@ class ApiInputCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["api_input"] = "api_input"
|
||||
fallback_color: Any = Field(default=None, description="Fallback RGB color [R,G,B]")
|
||||
timeout: Any = Field(default=None, description="Timeout before fallback (0.0-300.0)")
|
||||
interpolation: Optional[str] = Field(None, description="LED count interpolation mode")
|
||||
interpolation: str | None = Field(None, description="LED count interpolation mode")
|
||||
|
||||
|
||||
class NotificationCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["notification"] = "notification"
|
||||
notification_effect: Optional[str] = Field(None, description="Notification effect")
|
||||
notification_effect: str | None = Field(None, description="Notification effect")
|
||||
duration_ms: Any = Field(default=None, description="Effect duration in milliseconds")
|
||||
default_color: Optional[Union[List[int], Dict[str, Any], str]] = Field(
|
||||
default_color: List[int] | Dict[str, Any] | str | None = Field(
|
||||
None, description="Default color"
|
||||
)
|
||||
app_colors: Optional[Dict[str, str]] = Field(None, description="Per-app hex colors")
|
||||
app_filter_mode: Optional[str] = Field(None, description="App filter mode")
|
||||
app_filter_list: Optional[List[str]] = Field(None, description="App names for filter")
|
||||
os_listener: Optional[bool] = Field(None, description="Listen for OS notifications")
|
||||
sound_asset_id: Optional[str] = Field(None, description="Global notification sound asset ID")
|
||||
app_colors: Dict[str, str] | None = Field(None, description="Per-app hex colors")
|
||||
app_filter_mode: str | None = Field(None, description="App filter mode")
|
||||
app_filter_list: List[str] | None = Field(None, description="App names for filter")
|
||||
os_listener: bool | None = Field(None, description="Listen for OS notifications")
|
||||
sound_asset_id: str | None = Field(None, description="Global notification sound asset ID")
|
||||
sound_volume: Any = Field(default=None, description="Global notification sound volume")
|
||||
app_sounds: Optional[Dict[str, AppSoundOverride]] = Field(
|
||||
app_sounds: Dict[str, AppSoundOverride] | None = Field(
|
||||
None, description="Per-app sound overrides"
|
||||
)
|
||||
|
||||
@@ -391,9 +388,9 @@ class NotificationCSSCreate(_CSSCreateBase):
|
||||
class DaylightCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["daylight"] = "daylight"
|
||||
speed: Any = Field(default=None, description="Cycle speed multiplier (0.1-10.0)")
|
||||
use_real_time: Optional[bool] = Field(None, description="Use wall-clock time")
|
||||
latitude: Optional[float] = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||
longitude: Optional[float] = Field(
|
||||
use_real_time: bool | None = Field(None, description="Use wall-clock time")
|
||||
latitude: float | None = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||
longitude: float | None = Field(
|
||||
None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
|
||||
)
|
||||
|
||||
@@ -402,23 +399,23 @@ class CandlelightCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["candlelight"] = "candlelight"
|
||||
color: Any = Field(default=None, description="Candle color [R,G,B]")
|
||||
intensity: Any = Field(default=None, description="Candle intensity (0.1-2.0)")
|
||||
num_candles: Optional[int] = Field(
|
||||
num_candles: int | None = Field(
|
||||
None, description="Number of candle sources (1-20)", ge=1, le=20
|
||||
)
|
||||
speed: Any = Field(default=None, description="Flicker speed (0.1-10.0)")
|
||||
wind_strength: Any = Field(default=None, description="Wind strength (0.0-2.0)")
|
||||
candle_type: Optional[str] = Field(None, description="Candle type preset")
|
||||
candle_type: str | None = Field(None, description="Candle type preset")
|
||||
|
||||
|
||||
class ProcessedCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["processed"] = "processed"
|
||||
input_source_id: Optional[str] = Field(None, description="Input color strip source ID")
|
||||
processing_template_id: Optional[str] = Field(None, description="Processing template ID")
|
||||
input_source_id: str | None = Field(None, description="Input color strip source ID")
|
||||
processing_template_id: str | None = Field(None, description="Processing template ID")
|
||||
|
||||
|
||||
class WeatherCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["weather"] = "weather"
|
||||
weather_source_id: Optional[str] = Field(None, description="Weather source entity ID")
|
||||
weather_source_id: str | None = Field(None, description="Weather source entity ID")
|
||||
speed: Any = Field(default=None, description="Speed multiplier (0.1-10.0)")
|
||||
temperature_influence: Any = Field(default=None, description="Temperature influence (0.0-1.0)")
|
||||
|
||||
@@ -426,49 +423,47 @@ class WeatherCSSCreate(_CSSCreateBase):
|
||||
class KeyColorsCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["key_colors"] = "key_colors"
|
||||
picture_source_id: str = Field(default="", description="Picture source ID")
|
||||
rectangles: Optional[List[dict]] = Field(None, description="Named screen regions")
|
||||
rectangles: List[dict] | None = Field(None, description="Named screen regions")
|
||||
interpolation_mode: str = Field(default="average", description="Interpolation mode")
|
||||
smoothing: Any = Field(default=0.3, description="Temporal smoothing (0.0-1.0)")
|
||||
brightness: Any = Field(default=None, description="Brightness (0.0-1.0)")
|
||||
brightness_value_source_id: Optional[str] = Field(
|
||||
brightness_value_source_id: str | None = Field(
|
||||
None, description="Dynamic brightness value source ID"
|
||||
)
|
||||
|
||||
|
||||
class MathWaveCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["math_wave"] = "math_wave"
|
||||
waves: Optional[List[dict]] = Field(None, description="Wave layer definitions")
|
||||
waves: List[dict] | None = Field(None, description="Wave layer definitions")
|
||||
speed: Any = Field(default=None, description="Global speed multiplier (bindable, 0.1-10.0)")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID for color mapping")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID for color mapping")
|
||||
|
||||
|
||||
class GameEventCSSCreate(_CSSCreateBase):
|
||||
source_type: Literal["game_event"] = "game_event"
|
||||
game_integration_id: Optional[str] = Field(None, description="Game integration entity ID")
|
||||
game_integration_id: str | None = Field(None, description="Game integration entity ID")
|
||||
idle_color: Any = Field(default=None, description="Idle RGB color [R,G,B] (bindable)")
|
||||
event_mappings: Optional[List[dict]] = Field(None, description="Event-to-effect mappings")
|
||||
event_mappings: List[dict] | None = Field(None, description="Event-to-effect mappings")
|
||||
|
||||
|
||||
ColorStripSourceCreate = Annotated[
|
||||
Union[
|
||||
Annotated[PictureCSSCreate, Tag("picture")],
|
||||
Annotated[PictureAdvancedCSSCreate, Tag("picture_advanced")],
|
||||
Annotated[SingleColorCSSCreate, Tag("single_color")],
|
||||
Annotated[GradientCSSCreate, Tag("gradient")],
|
||||
Annotated[EffectCSSCreate, Tag("effect")],
|
||||
Annotated[CompositeCSSCreate, Tag("composite")],
|
||||
Annotated[MappedCSSCreate, Tag("mapped")],
|
||||
Annotated[AudioCSSCreate, Tag("audio")],
|
||||
Annotated[ApiInputCSSCreate, Tag("api_input")],
|
||||
Annotated[NotificationCSSCreate, Tag("notification")],
|
||||
Annotated[DaylightCSSCreate, Tag("daylight")],
|
||||
Annotated[CandlelightCSSCreate, Tag("candlelight")],
|
||||
Annotated[ProcessedCSSCreate, Tag("processed")],
|
||||
Annotated[WeatherCSSCreate, Tag("weather")],
|
||||
Annotated[KeyColorsCSSCreate, Tag("key_colors")],
|
||||
Annotated[MathWaveCSSCreate, Tag("math_wave")],
|
||||
Annotated[GameEventCSSCreate, Tag("game_event")],
|
||||
],
|
||||
Annotated[PictureCSSCreate, Tag("picture")]
|
||||
| Annotated[PictureAdvancedCSSCreate, Tag("picture_advanced")]
|
||||
| Annotated[SingleColorCSSCreate, Tag("single_color")]
|
||||
| Annotated[GradientCSSCreate, Tag("gradient")]
|
||||
| Annotated[EffectCSSCreate, Tag("effect")]
|
||||
| Annotated[CompositeCSSCreate, Tag("composite")]
|
||||
| Annotated[MappedCSSCreate, Tag("mapped")]
|
||||
| Annotated[AudioCSSCreate, Tag("audio")]
|
||||
| Annotated[ApiInputCSSCreate, Tag("api_input")]
|
||||
| Annotated[NotificationCSSCreate, Tag("notification")]
|
||||
| Annotated[DaylightCSSCreate, Tag("daylight")]
|
||||
| Annotated[CandlelightCSSCreate, Tag("candlelight")]
|
||||
| Annotated[ProcessedCSSCreate, Tag("processed")]
|
||||
| Annotated[WeatherCSSCreate, Tag("weather")]
|
||||
| Annotated[KeyColorsCSSCreate, Tag("key_colors")]
|
||||
| Annotated[MathWaveCSSCreate, Tag("math_wave")]
|
||||
| Annotated[GameEventCSSCreate, Tag("game_event")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -481,17 +476,17 @@ ColorStripSourceCreate = Annotated[
|
||||
class _CSSUpdateBase(BaseModel):
|
||||
"""Shared fields for all color strip source update requests."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
led_count: Optional[int] = Field(None, description="Total LED count (0 = auto)", ge=0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
clock_id: Optional[str] = Field(None, description="Optional sync clock ID")
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
led_count: int | None = Field(None, description="Total LED count (0 = auto)", ge=0)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
clock_id: str | None = Field(None, description="Optional sync clock ID")
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -500,66 +495,66 @@ class _CSSUpdateBase(BaseModel):
|
||||
|
||||
class PictureCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["picture"] = "picture"
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
picture_source_id: str | None = Field(None, description="Picture source ID")
|
||||
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
interpolation_mode: str | None = Field(None, description="Interpolation mode")
|
||||
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||
|
||||
|
||||
class PictureAdvancedCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["picture_advanced"] = "picture_advanced"
|
||||
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
||||
calibration: Optional[Calibration] = Field(None, description="LED calibration")
|
||||
interpolation_mode: str | None = Field(None, description="Interpolation mode")
|
||||
calibration: Calibration | None = Field(None, description="LED calibration")
|
||||
|
||||
|
||||
class SingleColorCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["single_color"] = "single_color"
|
||||
color: Any = Field(default=None, description="Solid RGB color [R,G,B]")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
||||
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||
|
||||
|
||||
class GradientCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["gradient"] = "gradient"
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config")
|
||||
easing: Optional[str] = Field(None, description="Gradient easing")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
stops: List[ColorStop] | None = Field(None, description="Color stops")
|
||||
animation: AnimationConfig | None = Field(None, description="Procedural animation config")
|
||||
easing: str | None = Field(None, description="Gradient easing")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
|
||||
|
||||
class EffectCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["effect"] = "effect"
|
||||
effect_type: Optional[str] = Field(None, description="Effect algorithm")
|
||||
palette: Optional[str] = Field(None, description="Named palette")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
effect_type: str | None = Field(None, description="Effect algorithm")
|
||||
palette: str | None = Field(None, description="Named palette")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
color: Any = Field(default=None, description="Primary color")
|
||||
intensity: Any = Field(default=None, description="Effect intensity (0.1-2.0)")
|
||||
scale: Any = Field(default=None, description="Spatial scale (0.5-5.0)")
|
||||
mirror: Optional[bool] = Field(None, description="Mirror/bounce mode")
|
||||
custom_palette: Optional[List[List[float]]] = Field(None, description="Custom palette stops")
|
||||
mirror: bool | None = Field(None, description="Mirror/bounce mode")
|
||||
custom_palette: List[List[float]] | None = Field(None, description="Custom palette stops")
|
||||
|
||||
|
||||
class CompositeCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["composite"] = "composite"
|
||||
layers: Optional[List[CompositeLayer]] = Field(None, description="Layers for composite type")
|
||||
layers: List[CompositeLayer] | None = Field(None, description="Layers for composite type")
|
||||
|
||||
|
||||
class MappedCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["mapped"] = "mapped"
|
||||
zones: Optional[List[MappedZone]] = Field(None, description="Zones for mapped type")
|
||||
zones: List[MappedZone] | None = Field(None, description="Zones for mapped type")
|
||||
|
||||
|
||||
class AudioCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["audio"] = "audio"
|
||||
visualization_mode: Optional[str] = Field(None, description="Audio visualization mode")
|
||||
audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
|
||||
visualization_mode: str | None = Field(None, description="Audio visualization mode")
|
||||
audio_source_id: str | None = Field(None, description="Mono audio source ID")
|
||||
sensitivity: Any = Field(default=None, description="Audio sensitivity (0.1-5.0)")
|
||||
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||
palette: Optional[str] = Field(None, description="Named palette")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
palette: str | None = Field(None, description="Named palette")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
color: Any = Field(default=None, description="Primary color")
|
||||
color_peak: Any = Field(default=None, description="Peak color [R,G,B]")
|
||||
mirror: Optional[bool] = Field(None, description="Mirror mode")
|
||||
mirror: bool | None = Field(None, description="Mirror mode")
|
||||
beat_decay: Any = Field(default=None, description="Beat pulse decay rate (music modes)")
|
||||
|
||||
|
||||
@@ -567,23 +562,23 @@ class ApiInputCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["api_input"] = "api_input"
|
||||
fallback_color: Any = Field(default=None, description="Fallback RGB color [R,G,B]")
|
||||
timeout: Any = Field(default=None, description="Timeout before fallback (0.0-300.0)")
|
||||
interpolation: Optional[str] = Field(None, description="LED count interpolation mode")
|
||||
interpolation: str | None = Field(None, description="LED count interpolation mode")
|
||||
|
||||
|
||||
class NotificationCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["notification"] = "notification"
|
||||
notification_effect: Optional[str] = Field(None, description="Notification effect")
|
||||
notification_effect: str | None = Field(None, description="Notification effect")
|
||||
duration_ms: Any = Field(default=None, description="Effect duration in milliseconds")
|
||||
default_color: Optional[Union[List[int], Dict[str, Any], str]] = Field(
|
||||
default_color: List[int] | Dict[str, Any] | str | None = Field(
|
||||
None, description="Default color"
|
||||
)
|
||||
app_colors: Optional[Dict[str, str]] = Field(None, description="Per-app hex colors")
|
||||
app_filter_mode: Optional[str] = Field(None, description="App filter mode")
|
||||
app_filter_list: Optional[List[str]] = Field(None, description="App names for filter")
|
||||
os_listener: Optional[bool] = Field(None, description="Listen for OS notifications")
|
||||
sound_asset_id: Optional[str] = Field(None, description="Global notification sound asset ID")
|
||||
app_colors: Dict[str, str] | None = Field(None, description="Per-app hex colors")
|
||||
app_filter_mode: str | None = Field(None, description="App filter mode")
|
||||
app_filter_list: List[str] | None = Field(None, description="App names for filter")
|
||||
os_listener: bool | None = Field(None, description="Listen for OS notifications")
|
||||
sound_asset_id: str | None = Field(None, description="Global notification sound asset ID")
|
||||
sound_volume: Any = Field(default=None, description="Global notification sound volume")
|
||||
app_sounds: Optional[Dict[str, AppSoundOverride]] = Field(
|
||||
app_sounds: Dict[str, AppSoundOverride] | None = Field(
|
||||
None, description="Per-app sound overrides"
|
||||
)
|
||||
|
||||
@@ -591,9 +586,9 @@ class NotificationCSSUpdate(_CSSUpdateBase):
|
||||
class DaylightCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["daylight"] = "daylight"
|
||||
speed: Any = Field(default=None, description="Cycle speed multiplier (0.1-10.0)")
|
||||
use_real_time: Optional[bool] = Field(None, description="Use wall-clock time")
|
||||
latitude: Optional[float] = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||
longitude: Optional[float] = Field(
|
||||
use_real_time: bool | None = Field(None, description="Use wall-clock time")
|
||||
latitude: float | None = Field(None, description="Latitude (-90 to 90)", ge=-90.0, le=90.0)
|
||||
longitude: float | None = Field(
|
||||
None, description="Longitude (-180 to 180)", ge=-180.0, le=180.0
|
||||
)
|
||||
|
||||
@@ -602,73 +597,71 @@ class CandlelightCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["candlelight"] = "candlelight"
|
||||
color: Any = Field(default=None, description="Candle color [R,G,B]")
|
||||
intensity: Any = Field(default=None, description="Candle intensity (0.1-2.0)")
|
||||
num_candles: Optional[int] = Field(
|
||||
num_candles: int | None = Field(
|
||||
None, description="Number of candle sources (1-20)", ge=1, le=20
|
||||
)
|
||||
speed: Any = Field(default=None, description="Flicker speed (0.1-10.0)")
|
||||
wind_strength: Any = Field(default=None, description="Wind strength (0.0-2.0)")
|
||||
candle_type: Optional[str] = Field(None, description="Candle type preset")
|
||||
candle_type: str | None = Field(None, description="Candle type preset")
|
||||
|
||||
|
||||
class ProcessedCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["processed"] = "processed"
|
||||
input_source_id: Optional[str] = Field(None, description="Input color strip source ID")
|
||||
processing_template_id: Optional[str] = Field(None, description="Processing template ID")
|
||||
input_source_id: str | None = Field(None, description="Input color strip source ID")
|
||||
processing_template_id: str | None = Field(None, description="Processing template ID")
|
||||
|
||||
|
||||
class WeatherCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["weather"] = "weather"
|
||||
weather_source_id: Optional[str] = Field(None, description="Weather source entity ID")
|
||||
weather_source_id: str | None = Field(None, description="Weather source entity ID")
|
||||
speed: Any = Field(default=None, description="Speed multiplier (0.1-10.0)")
|
||||
temperature_influence: Any = Field(default=None, description="Temperature influence (0.0-1.0)")
|
||||
|
||||
|
||||
class KeyColorsCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["key_colors"] = "key_colors"
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
rectangles: Optional[List[dict]] = Field(None, description="Named screen regions")
|
||||
interpolation_mode: Optional[str] = Field(None, description="Interpolation mode")
|
||||
picture_source_id: str | None = Field(None, description="Picture source ID")
|
||||
rectangles: List[dict] | None = Field(None, description="Named screen regions")
|
||||
interpolation_mode: str | None = Field(None, description="Interpolation mode")
|
||||
smoothing: Any = Field(default=None, description="Temporal smoothing (0.0-1.0)")
|
||||
brightness: Any = Field(default=None, description="Brightness (0.0-1.0)")
|
||||
brightness_value_source_id: Optional[str] = Field(
|
||||
brightness_value_source_id: str | None = Field(
|
||||
None, description="Dynamic brightness value source ID"
|
||||
)
|
||||
|
||||
|
||||
class MathWaveCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["math_wave"] = "math_wave"
|
||||
waves: Optional[List[dict]] = Field(None, description="Wave layer definitions")
|
||||
waves: List[dict] | None = Field(None, description="Wave layer definitions")
|
||||
speed: Any = Field(default=None, description="Global speed multiplier (bindable)")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID for color mapping")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID for color mapping")
|
||||
|
||||
|
||||
class GameEventCSSUpdate(_CSSUpdateBase):
|
||||
source_type: Literal["game_event"] = "game_event"
|
||||
game_integration_id: Optional[str] = Field(None, description="Game integration entity ID")
|
||||
game_integration_id: str | None = Field(None, description="Game integration entity ID")
|
||||
idle_color: Any = Field(default=None, description="Idle RGB color [R,G,B] (bindable)")
|
||||
event_mappings: Optional[List[dict]] = Field(None, description="Event-to-effect mappings")
|
||||
event_mappings: List[dict] | None = Field(None, description="Event-to-effect mappings")
|
||||
|
||||
|
||||
ColorStripSourceUpdate = Annotated[
|
||||
Union[
|
||||
Annotated[PictureCSSUpdate, Tag("picture")],
|
||||
Annotated[PictureAdvancedCSSUpdate, Tag("picture_advanced")],
|
||||
Annotated[SingleColorCSSUpdate, Tag("single_color")],
|
||||
Annotated[GradientCSSUpdate, Tag("gradient")],
|
||||
Annotated[EffectCSSUpdate, Tag("effect")],
|
||||
Annotated[CompositeCSSUpdate, Tag("composite")],
|
||||
Annotated[MappedCSSUpdate, Tag("mapped")],
|
||||
Annotated[AudioCSSUpdate, Tag("audio")],
|
||||
Annotated[ApiInputCSSUpdate, Tag("api_input")],
|
||||
Annotated[NotificationCSSUpdate, Tag("notification")],
|
||||
Annotated[DaylightCSSUpdate, Tag("daylight")],
|
||||
Annotated[CandlelightCSSUpdate, Tag("candlelight")],
|
||||
Annotated[ProcessedCSSUpdate, Tag("processed")],
|
||||
Annotated[WeatherCSSUpdate, Tag("weather")],
|
||||
Annotated[KeyColorsCSSUpdate, Tag("key_colors")],
|
||||
Annotated[MathWaveCSSUpdate, Tag("math_wave")],
|
||||
Annotated[GameEventCSSUpdate, Tag("game_event")],
|
||||
],
|
||||
Annotated[PictureCSSUpdate, Tag("picture")]
|
||||
| Annotated[PictureAdvancedCSSUpdate, Tag("picture_advanced")]
|
||||
| Annotated[SingleColorCSSUpdate, Tag("single_color")]
|
||||
| Annotated[GradientCSSUpdate, Tag("gradient")]
|
||||
| Annotated[EffectCSSUpdate, Tag("effect")]
|
||||
| Annotated[CompositeCSSUpdate, Tag("composite")]
|
||||
| Annotated[MappedCSSUpdate, Tag("mapped")]
|
||||
| Annotated[AudioCSSUpdate, Tag("audio")]
|
||||
| Annotated[ApiInputCSSUpdate, Tag("api_input")]
|
||||
| Annotated[NotificationCSSUpdate, Tag("notification")]
|
||||
| Annotated[DaylightCSSUpdate, Tag("daylight")]
|
||||
| Annotated[CandlelightCSSUpdate, Tag("candlelight")]
|
||||
| Annotated[ProcessedCSSUpdate, Tag("processed")]
|
||||
| Annotated[WeatherCSSUpdate, Tag("weather")]
|
||||
| Annotated[KeyColorsCSSUpdate, Tag("key_colors")]
|
||||
| Annotated[MathWaveCSSUpdate, Tag("math_wave")]
|
||||
| Annotated[GameEventCSSUpdate, Tag("game_event")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -699,17 +692,17 @@ class SegmentPayload(BaseModel):
|
||||
``color`` therefore fills the entire strip.
|
||||
"""
|
||||
|
||||
start: Optional[int] = Field(
|
||||
start: int | None = Field(
|
||||
None, ge=0, description="Starting LED index (default 0 = beginning of strip)"
|
||||
)
|
||||
length: Optional[int] = Field(
|
||||
length: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
description="Number of LEDs in segment (default = led_count - start)",
|
||||
)
|
||||
mode: Literal["solid", "per_pixel", "gradient"] = Field(description="Fill mode")
|
||||
color: Optional[List[int]] = Field(None, description="RGB for solid mode [R,G,B]")
|
||||
colors: Optional[List[List[int]]] = Field(
|
||||
color: List[int] | None = Field(None, description="RGB for solid mode [R,G,B]")
|
||||
colors: List[List[int]] | None = Field(
|
||||
None, description="Colors for per_pixel/gradient [[R,G,B],...]"
|
||||
)
|
||||
|
||||
@@ -742,12 +735,10 @@ class ColorPushRequest(BaseModel):
|
||||
At least one must be provided.
|
||||
"""
|
||||
|
||||
colors: Optional[List[List[int]]] = Field(
|
||||
colors: List[List[int]] | None = Field(
|
||||
None, description="LED color array [[R,G,B], ...] (0-255 each)"
|
||||
)
|
||||
segments: Optional[List[SegmentPayload]] = Field(
|
||||
None, description="Segment-based color updates"
|
||||
)
|
||||
segments: List[SegmentPayload] | None = Field(None, description="Segment-based color updates")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_colors_or_segments(self) -> "ColorPushRequest":
|
||||
@@ -759,8 +750,8 @@ class ColorPushRequest(BaseModel):
|
||||
class NotifyRequest(BaseModel):
|
||||
"""Request to trigger a notification on a notification color strip source."""
|
||||
|
||||
app: Optional[str] = Field(None, description="App name for color lookup")
|
||||
color: Optional[str] = Field(None, description="Hex color override (#RRGGBB)")
|
||||
app: str | None = Field(None, description="App name for color lookup")
|
||||
color: str | None = Field(None, description="Hex color override (#RRGGBB)")
|
||||
|
||||
|
||||
class CSSCalibrationTestRequest(BaseModel):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Shared schemas used across multiple route modules."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -11,7 +11,7 @@ class ErrorResponse(BaseModel):
|
||||
|
||||
error: str = Field(description="Error type")
|
||||
message: str = Field(description="Error message")
|
||||
detail: Optional[Dict] = Field(None, description="Additional error details")
|
||||
detail: Dict | None = Field(None, description="Additional error details")
|
||||
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Error timestamp")
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ class CaptureImage(BaseModel):
|
||||
"""Captured image with metadata."""
|
||||
|
||||
image: str = Field(description="Base64-encoded thumbnail image data")
|
||||
full_image: Optional[str] = Field(None, description="Base64-encoded full-resolution image data")
|
||||
full_image: str | None = Field(None, description="Base64-encoded full-resolution image data")
|
||||
width: int = Field(description="Original image width in pixels")
|
||||
height: int = Field(description="Original image height in pixels")
|
||||
thumbnail_width: Optional[int] = Field(None, description="Thumbnail width (if resized)")
|
||||
thumbnail_height: Optional[int] = Field(None, description="Thumbnail height (if resized)")
|
||||
thumbnail_width: int | None = Field(None, description="Thumbnail width (if resized)")
|
||||
thumbnail_height: int | None = Field(None, description="Thumbnail height (if resized)")
|
||||
|
||||
|
||||
class BorderExtraction(BaseModel):
|
||||
@@ -48,7 +48,7 @@ class TemplateTestResponse(BaseModel):
|
||||
"""Response from template test."""
|
||||
|
||||
full_capture: CaptureImage = Field(description="Full screen capture with thumbnail")
|
||||
border_extraction: Optional[BorderExtraction] = Field(
|
||||
border_extraction: BorderExtraction | None = Field(
|
||||
None, description="Extracted border images (deprecated)"
|
||||
)
|
||||
performance: PerformanceMetrics = Field(description="Performance metrics")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Device-related schemas (CRUD, calibration, device state)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Literal, Optional
|
||||
from typing import Dict, List, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -10,149 +10,145 @@ class DeviceCreate(BaseModel):
|
||||
"""Request to create/attach an LED device."""
|
||||
|
||||
name: str = Field(description="Device name", min_length=1, max_length=100)
|
||||
url: Optional[str] = Field(
|
||||
url: str | None = Field(
|
||||
None,
|
||||
description="Device URL (e.g., http://192.168.1.100 or COM3). Not required for group devices.",
|
||||
)
|
||||
device_type: str = Field(default="wled", description="LED device type (e.g., wled, adalight)")
|
||||
led_count: Optional[int] = Field(
|
||||
led_count: int | None = Field(
|
||||
None, ge=1, le=10000, description="Number of LEDs (required for adalight)"
|
||||
)
|
||||
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
|
||||
auto_shutdown: Optional[bool] = Field(
|
||||
baud_rate: int | None = Field(None, description="Serial baud rate (for adalight devices)")
|
||||
auto_shutdown: bool | None = Field(
|
||||
default=None,
|
||||
description="Turn off device when server stops (defaults to true for adalight)",
|
||||
)
|
||||
send_latency_ms: Optional[int] = Field(
|
||||
send_latency_ms: int | None = Field(
|
||||
None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)"
|
||||
)
|
||||
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
|
||||
zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate")
|
||||
rgbw: bool | None = Field(None, description="RGBW mode (mock devices)")
|
||||
zone_mode: str | None = Field(None, description="OpenRGB zone mode: combined or separate")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
# DMX (Art-Net / sACN) fields
|
||||
dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn")
|
||||
dmx_start_universe: Optional[int] = Field(
|
||||
None, ge=0, le=32767, description="DMX start universe"
|
||||
)
|
||||
dmx_start_channel: Optional[int] = Field(
|
||||
dmx_protocol: str | None = Field(None, description="DMX protocol: artnet or sacn")
|
||||
dmx_start_universe: int | None = Field(None, ge=0, le=32767, description="DMX start universe")
|
||||
dmx_start_channel: int | None = Field(
|
||||
None, ge=1, le=512, description="DMX start channel (1-512)"
|
||||
)
|
||||
# DDP fields
|
||||
ddp_port: Optional[int] = Field(
|
||||
ddp_port: int | None = Field(
|
||||
None, ge=0, le=65535, description="DDP UDP port (0 = protocol default 4048)"
|
||||
)
|
||||
ddp_destination_id: Optional[int] = Field(
|
||||
ddp_destination_id: int | None = Field(
|
||||
None, ge=0, le=255, description="DDP destination ID (default 1 = display)"
|
||||
)
|
||||
ddp_color_order: Optional[int] = Field(
|
||||
ddp_color_order: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=5,
|
||||
description="DDP color order: 0=GRB 1=RGB 2=BRG 3=RBG 4=BGR 5=GBR (most receivers expect RGB)",
|
||||
)
|
||||
# ESP-NOW fields
|
||||
espnow_peer_mac: Optional[str] = Field(
|
||||
espnow_peer_mac: str | None = Field(
|
||||
None, description="ESP-NOW peer MAC address (e.g. AA:BB:CC:DD:EE:FF)"
|
||||
)
|
||||
espnow_channel: Optional[int] = Field(
|
||||
None, ge=1, le=14, description="ESP-NOW WiFi channel (1-14)"
|
||||
)
|
||||
espnow_channel: int | None = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel (1-14)")
|
||||
# Philips Hue fields
|
||||
hue_username: Optional[str] = Field(None, description="Hue bridge username (from pairing)")
|
||||
hue_client_key: Optional[str] = Field(None, description="Hue entertainment client key (hex)")
|
||||
hue_entertainment_group_id: Optional[str] = Field(
|
||||
hue_username: str | None = Field(None, description="Hue bridge username (from pairing)")
|
||||
hue_client_key: str | None = Field(None, description="Hue entertainment client key (hex)")
|
||||
hue_entertainment_group_id: str | None = Field(
|
||||
None, description="Hue entertainment group/zone ID"
|
||||
)
|
||||
# Yeelight fields
|
||||
yeelight_min_interval_ms: Optional[int] = Field(
|
||||
yeelight_min_interval_ms: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=10000,
|
||||
description="Yeelight client-side rate limit between commands in ms (default 500)",
|
||||
)
|
||||
# WiZ fields
|
||||
wiz_min_interval_ms: Optional[int] = Field(
|
||||
wiz_min_interval_ms: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=10000,
|
||||
description="WiZ client-side rate limit between commands in ms (default 50)",
|
||||
)
|
||||
# LIFX fields
|
||||
lifx_min_interval_ms: Optional[int] = Field(
|
||||
lifx_min_interval_ms: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=10000,
|
||||
description="LIFX client-side rate limit between commands in ms (default 50)",
|
||||
)
|
||||
# Govee fields
|
||||
govee_min_interval_ms: Optional[int] = Field(
|
||||
govee_min_interval_ms: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=10000,
|
||||
description="Govee client-side rate limit between commands in ms (default 50)",
|
||||
)
|
||||
# OPC fields
|
||||
opc_channel: Optional[int] = Field(
|
||||
opc_channel: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=255,
|
||||
description="OPC channel (0 = broadcast to all channels on the server)",
|
||||
)
|
||||
# Nanoleaf fields
|
||||
nanoleaf_token: Optional[str] = Field(
|
||||
nanoleaf_token: str | None = Field(
|
||||
None,
|
||||
max_length=512,
|
||||
description="Nanoleaf auth token returned by the pairing handshake",
|
||||
)
|
||||
nanoleaf_min_interval_ms: Optional[int] = Field(
|
||||
nanoleaf_min_interval_ms: int | None = Field(
|
||||
None,
|
||||
ge=0,
|
||||
le=10000,
|
||||
description="Nanoleaf client-side rate limit between commands in ms (default 100)",
|
||||
)
|
||||
# SPI Direct fields
|
||||
spi_speed_hz: Optional[int] = Field(
|
||||
spi_speed_hz: int | None = Field(
|
||||
None, ge=100000, le=4000000, description="SPI clock speed in Hz"
|
||||
)
|
||||
spi_led_type: Optional[str] = Field(
|
||||
spi_led_type: str | None = Field(
|
||||
None, description="LED chipset: WS2812, WS2812B, WS2811, SK6812, SK6812_RGBW"
|
||||
)
|
||||
# Razer Chroma fields
|
||||
chroma_device_type: Optional[str] = Field(
|
||||
chroma_device_type: str | None = Field(
|
||||
None,
|
||||
description="Chroma peripheral type: keyboard, mouse, mousepad, headset, chromalink, keypad",
|
||||
)
|
||||
# SteelSeries GameSense fields
|
||||
gamesense_device_type: Optional[str] = Field(
|
||||
gamesense_device_type: str | None = Field(
|
||||
None, description="GameSense device type: keyboard, mouse, headset, mousepad, indicator"
|
||||
)
|
||||
# BLE controller fields
|
||||
ble_family: Optional[str] = Field(
|
||||
ble_family: str | None = Field(
|
||||
None,
|
||||
description="BLE protocol family: sp110e, triones, zengge, govee",
|
||||
)
|
||||
ble_govee_key: Optional[str] = Field(
|
||||
ble_govee_key: str | None = Field(
|
||||
None,
|
||||
description="Govee AES key (hex) — required for encrypted Govee firmware",
|
||||
)
|
||||
default_css_processing_template_id: Optional[str] = Field(
|
||||
default_css_processing_template_id: str | None = Field(
|
||||
None, description="Default color strip processing template ID"
|
||||
)
|
||||
# Group device fields
|
||||
group_device_ids: Optional[List[str]] = Field(
|
||||
group_device_ids: List[str] | None = Field(
|
||||
None, description="Ordered list of child device IDs (for group device type)"
|
||||
)
|
||||
group_mode: Optional[str] = Field(
|
||||
group_mode: str | None = Field(
|
||||
None,
|
||||
description="Group mode: sequence (LEDs concatenated) or independent (each child gets full strip resampled)",
|
||||
)
|
||||
# Custom card icon (frontend display only)
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library (e.g. 'mouse', 'motherboard'). Empty/null hides the plate.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the card's channel accent.",
|
||||
@@ -162,86 +158,80 @@ class DeviceCreate(BaseModel):
|
||||
class DeviceUpdate(BaseModel):
|
||||
"""Request to update device information."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Device name", min_length=1, max_length=100)
|
||||
url: Optional[str] = Field(None, description="Device URL or serial port")
|
||||
enabled: Optional[bool] = Field(None, description="Whether device is enabled")
|
||||
led_count: Optional[int] = Field(
|
||||
name: str | None = Field(None, description="Device name", min_length=1, max_length=100)
|
||||
url: str | None = Field(None, description="Device URL or serial port")
|
||||
enabled: bool | None = Field(None, description="Whether device is enabled")
|
||||
led_count: int | None = Field(
|
||||
None,
|
||||
ge=1,
|
||||
le=10000,
|
||||
description="Number of LEDs (for devices with manual_led_count capability)",
|
||||
)
|
||||
baud_rate: Optional[int] = Field(None, description="Serial baud rate (for adalight devices)")
|
||||
auto_shutdown: Optional[bool] = Field(None, description="Turn off device when server stops")
|
||||
send_latency_ms: Optional[int] = Field(
|
||||
baud_rate: int | None = Field(None, description="Serial baud rate (for adalight devices)")
|
||||
auto_shutdown: bool | None = Field(None, description="Turn off device when server stops")
|
||||
send_latency_ms: int | None = Field(
|
||||
None, ge=0, le=5000, description="Simulated send latency in ms (mock devices)"
|
||||
)
|
||||
rgbw: Optional[bool] = Field(None, description="RGBW mode (mock devices)")
|
||||
zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate")
|
||||
tags: Optional[List[str]] = None
|
||||
dmx_protocol: Optional[str] = Field(None, description="DMX protocol: artnet or sacn")
|
||||
dmx_start_universe: Optional[int] = Field(
|
||||
None, ge=0, le=32767, description="DMX start universe"
|
||||
)
|
||||
dmx_start_channel: Optional[int] = Field(
|
||||
rgbw: bool | None = Field(None, description="RGBW mode (mock devices)")
|
||||
zone_mode: str | None = Field(None, description="OpenRGB zone mode: combined or separate")
|
||||
tags: List[str] | None = None
|
||||
dmx_protocol: str | None = Field(None, description="DMX protocol: artnet or sacn")
|
||||
dmx_start_universe: int | None = Field(None, ge=0, le=32767, description="DMX start universe")
|
||||
dmx_start_channel: int | None = Field(
|
||||
None, ge=1, le=512, description="DMX start channel (1-512)"
|
||||
)
|
||||
ddp_port: Optional[int] = Field(
|
||||
ddp_port: int | None = Field(
|
||||
None, ge=0, le=65535, description="DDP UDP port (0 = protocol default 4048)"
|
||||
)
|
||||
ddp_destination_id: Optional[int] = Field(None, ge=0, le=255, description="DDP destination ID")
|
||||
ddp_color_order: Optional[int] = Field(None, ge=0, le=5, description="DDP color order code")
|
||||
espnow_peer_mac: Optional[str] = Field(None, description="ESP-NOW peer MAC address")
|
||||
espnow_channel: Optional[int] = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel")
|
||||
hue_username: Optional[str] = Field(None, description="Hue bridge username")
|
||||
hue_client_key: Optional[str] = Field(None, description="Hue entertainment client key")
|
||||
hue_entertainment_group_id: Optional[str] = Field(
|
||||
None, description="Hue entertainment group ID"
|
||||
)
|
||||
yeelight_min_interval_ms: Optional[int] = Field(
|
||||
ddp_destination_id: int | None = Field(None, ge=0, le=255, description="DDP destination ID")
|
||||
ddp_color_order: int | None = Field(None, ge=0, le=5, description="DDP color order code")
|
||||
espnow_peer_mac: str | None = Field(None, description="ESP-NOW peer MAC address")
|
||||
espnow_channel: int | None = Field(None, ge=1, le=14, description="ESP-NOW WiFi channel")
|
||||
hue_username: str | None = Field(None, description="Hue bridge username")
|
||||
hue_client_key: str | None = Field(None, description="Hue entertainment client key")
|
||||
hue_entertainment_group_id: str | None = Field(None, description="Hue entertainment group ID")
|
||||
yeelight_min_interval_ms: int | None = Field(
|
||||
None, ge=0, le=10000, description="Yeelight client-side rate limit in ms"
|
||||
)
|
||||
wiz_min_interval_ms: Optional[int] = Field(
|
||||
wiz_min_interval_ms: int | None = Field(
|
||||
None, ge=0, le=10000, description="WiZ client-side rate limit in ms"
|
||||
)
|
||||
lifx_min_interval_ms: Optional[int] = Field(
|
||||
lifx_min_interval_ms: int | None = Field(
|
||||
None, ge=0, le=10000, description="LIFX client-side rate limit in ms"
|
||||
)
|
||||
govee_min_interval_ms: Optional[int] = Field(
|
||||
govee_min_interval_ms: int | None = Field(
|
||||
None, ge=0, le=10000, description="Govee client-side rate limit in ms"
|
||||
)
|
||||
opc_channel: Optional[int] = Field(
|
||||
None, ge=0, le=255, description="OPC channel (0 = broadcast)"
|
||||
)
|
||||
nanoleaf_token: Optional[str] = Field(None, max_length=512, description="Nanoleaf auth token")
|
||||
nanoleaf_min_interval_ms: Optional[int] = Field(
|
||||
opc_channel: int | None = Field(None, ge=0, le=255, description="OPC channel (0 = broadcast)")
|
||||
nanoleaf_token: str | None = Field(None, max_length=512, description="Nanoleaf auth token")
|
||||
nanoleaf_min_interval_ms: int | None = Field(
|
||||
None, ge=0, le=10000, description="Nanoleaf client-side rate limit in ms"
|
||||
)
|
||||
spi_speed_hz: Optional[int] = Field(None, ge=100000, le=4000000, description="SPI clock speed")
|
||||
spi_led_type: Optional[str] = Field(None, description="LED chipset type")
|
||||
chroma_device_type: Optional[str] = Field(None, description="Chroma peripheral type")
|
||||
gamesense_device_type: Optional[str] = Field(None, description="GameSense device type")
|
||||
ble_family: Optional[str] = Field(
|
||||
spi_speed_hz: int | None = Field(None, ge=100000, le=4000000, description="SPI clock speed")
|
||||
spi_led_type: str | None = Field(None, description="LED chipset type")
|
||||
chroma_device_type: str | None = Field(None, description="Chroma peripheral type")
|
||||
gamesense_device_type: str | None = Field(None, description="GameSense device type")
|
||||
ble_family: str | None = Field(
|
||||
None, description="BLE protocol family: sp110e, triones, zengge, govee"
|
||||
)
|
||||
ble_govee_key: Optional[str] = Field(
|
||||
ble_govee_key: str | None = Field(
|
||||
None, description="Govee AES key (hex) — required for encrypted Govee firmware"
|
||||
)
|
||||
default_css_processing_template_id: Optional[str] = Field(
|
||||
default_css_processing_template_id: str | None = Field(
|
||||
None, description="Default color strip processing template ID"
|
||||
)
|
||||
# Group device fields
|
||||
group_device_ids: Optional[List[str]] = Field(
|
||||
group_device_ids: List[str] | None = Field(
|
||||
None, description="Ordered list of child device IDs (for group device type)"
|
||||
)
|
||||
group_mode: Optional[str] = Field(None, description="Group mode: sequence or independent")
|
||||
group_mode: str | None = Field(None, description="Group mode: sequence or independent")
|
||||
# Custom card icon
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
@@ -294,7 +284,7 @@ class Calibration(BaseModel):
|
||||
description="Calibration mode: simple (4-edge) or advanced (multi-source lines)",
|
||||
)
|
||||
# Advanced mode: ordered list of lines
|
||||
lines: Optional[List[CalibrationLineSchema]] = Field(
|
||||
lines: List[CalibrationLineSchema] | None = Field(
|
||||
default=None, description="Line list for advanced mode (ignored in simple mode)"
|
||||
)
|
||||
# Simple mode fields
|
||||
@@ -388,7 +378,7 @@ class DeviceResponse(BaseModel):
|
||||
device_type: str = Field(default="wled", description="LED device type")
|
||||
led_count: int = Field(description="Total number of LEDs")
|
||||
enabled: bool = Field(description="Whether device is enabled")
|
||||
baud_rate: Optional[int] = Field(None, description="Serial baud rate")
|
||||
baud_rate: int | None = Field(None, description="Serial baud rate")
|
||||
auto_shutdown: bool = Field(
|
||||
default=False, description="Restore device to idle state when targets stop"
|
||||
)
|
||||
@@ -473,19 +463,19 @@ class DeviceStateResponse(BaseModel):
|
||||
device_id: str = Field(description="Device ID")
|
||||
device_type: str = Field(default="wled", description="LED device type")
|
||||
device_online: bool = Field(default=False, description="Whether device is reachable")
|
||||
device_latency_ms: Optional[float] = Field(None, description="Health check latency in ms")
|
||||
device_name: Optional[str] = Field(None, description="Device name reported by firmware")
|
||||
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(
|
||||
device_latency_ms: float | None = Field(None, description="Health check latency in ms")
|
||||
device_name: str | None = Field(None, description="Device name reported by firmware")
|
||||
device_version: str | None = Field(None, description="Firmware version")
|
||||
device_led_count: int | None = Field(None, description="LED count reported by device")
|
||||
device_rgbw: bool | None = Field(None, description="Whether device uses RGBW LEDs")
|
||||
device_led_type: str | None = Field(
|
||||
None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)"
|
||||
)
|
||||
device_fps: Optional[int] = Field(
|
||||
device_fps: int | None = 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_last_checked: datetime | None = Field(None, description="Last health check time")
|
||||
device_error: str | None = Field(None, description="Last health check error")
|
||||
test_mode: bool = Field(default=False, description="Whether calibration test mode is active")
|
||||
test_mode_edges: List[str] = Field(
|
||||
default_factory=list, description="Currently lit edges in test mode"
|
||||
@@ -500,9 +490,9 @@ class DiscoveredDeviceResponse(BaseModel):
|
||||
device_type: str = Field(default="wled", description="Device type")
|
||||
ip: str = Field(description="IP address")
|
||||
mac: str = Field(default="", description="MAC address")
|
||||
led_count: Optional[int] = Field(None, description="LED count (if reachable)")
|
||||
version: Optional[str] = Field(None, description="Firmware version")
|
||||
ble_family: Optional[str] = Field(
|
||||
led_count: int | None = Field(None, description="LED count (if reachable)")
|
||||
version: str | None = Field(None, description="Firmware version")
|
||||
ble_family: str | None = Field(
|
||||
None, description="Detected BLE protocol family (sp110e/triones/zengge/govee)"
|
||||
)
|
||||
already_added: bool = Field(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Filter-related schemas."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -22,10 +22,10 @@ class FilterOptionDefSchema(BaseModel):
|
||||
min_value: Any = Field(description="Minimum value")
|
||||
max_value: Any = Field(description="Maximum value")
|
||||
step: Any = Field(description="Step increment")
|
||||
choices: Optional[List[Dict[str, str]]] = Field(
|
||||
choices: List[Dict[str, str]] | None = Field(
|
||||
default=None, description="Available choices for select type"
|
||||
)
|
||||
max_length: Optional[int] = Field(
|
||||
max_length: int | None = Field(
|
||||
default=None, description="Maximum string length for string type"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""Pydantic schemas for game integration API endpoints."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ── Event Mapping ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -40,14 +39,14 @@ class GameIntegrationCreate(BaseModel):
|
||||
event_mappings: List[EventMappingSchema] = Field(
|
||||
default_factory=list, description="Event-to-effect mappings"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
||||
description: str | None = Field(None, description="Integration description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
@@ -57,21 +56,21 @@ class GameIntegrationCreate(BaseModel):
|
||||
class GameIntegrationUpdate(BaseModel):
|
||||
"""Request to update a game integration config."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Integration name", min_length=1, max_length=100)
|
||||
adapter_type: Optional[str] = Field(None, description="Adapter type identifier", min_length=1)
|
||||
enabled: Optional[bool] = Field(None, description="Whether integration is active")
|
||||
adapter_config: Optional[Dict[str, Any]] = Field(None, description="Adapter-specific settings")
|
||||
event_mappings: Optional[List[EventMappingSchema]] = Field(
|
||||
name: str | None = Field(None, description="Integration name", min_length=1, max_length=100)
|
||||
adapter_type: str | None = Field(None, description="Adapter type identifier", min_length=1)
|
||||
enabled: bool | None = Field(None, description="Whether integration is active")
|
||||
adapter_config: Dict[str, Any] | None = Field(None, description="Adapter-specific settings")
|
||||
event_mappings: List[EventMappingSchema] | None = Field(
|
||||
None, description="Event-to-effect mappings"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Integration description", max_length=500)
|
||||
tags: Optional[List[str]] = Field(None, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Integration description", max_length=500)
|
||||
tags: List[str] | None = Field(None, description="User-defined tags")
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
@@ -89,14 +88,14 @@ class GameIntegrationResponse(BaseModel):
|
||||
event_mappings: List[EventMappingSchema] = Field(description="Event-to-effect mappings")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Integration description")
|
||||
description: str | None = Field(None, description="Integration description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
@@ -158,7 +157,7 @@ class GameIntegrationStatusResponse(BaseModel):
|
||||
integration_id: str = Field(description="Integration ID")
|
||||
enabled: bool = Field(description="Whether integration is active")
|
||||
connected: bool = Field(description="Whether adapter is currently receiving data")
|
||||
last_event_time: Optional[float] = Field(None, description="Monotonic timestamp of last event")
|
||||
last_event_time: float | None = Field(None, description="Monotonic timestamp of last event")
|
||||
event_count: int = Field(default=0, description="Total events received")
|
||||
event_counts_by_type: Dict[str, int] = Field(
|
||||
default_factory=dict, description="Event counts per event type"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Gradient schemas (CRUD)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -18,14 +18,14 @@ class GradientCreate(BaseModel):
|
||||
|
||||
name: str = Field(description="Gradient name", min_length=1, max_length=100)
|
||||
stops: List[GradientStopSchema] = Field(description="Color stops", min_length=2)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -35,16 +35,16 @@ class GradientCreate(BaseModel):
|
||||
class GradientUpdate(BaseModel):
|
||||
"""Request to update a gradient."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Gradient name", min_length=1, max_length=100)
|
||||
stops: Optional[List[GradientStopSchema]] = Field(None, description="Color stops", min_length=2)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Gradient name", min_length=1, max_length=100)
|
||||
stops: List[GradientStopSchema] | None = Field(None, description="Color stops", min_length=2)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -58,16 +58,16 @@ class GradientResponse(BaseModel):
|
||||
name: str = Field(description="Gradient name")
|
||||
stops: List[GradientStopSchema] = Field(description="Color stops")
|
||||
is_builtin: bool = Field(description="Whether this is a built-in gradient")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Home Assistant source schemas (CRUD + test + entities)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -16,14 +16,14 @@ class HomeAssistantSourceCreate(BaseModel):
|
||||
entity_filters: List[str] = Field(
|
||||
default_factory=list, description="Entity ID filter patterns (e.g. ['sensor.*'])"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
@@ -33,19 +33,19 @@ class HomeAssistantSourceCreate(BaseModel):
|
||||
class HomeAssistantSourceUpdate(BaseModel):
|
||||
"""Request to update a Home Assistant source."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
host: Optional[str] = Field(None, description="HA host:port", min_length=1)
|
||||
token: Optional[str] = Field(None, description="Long-Lived Access Token", min_length=1)
|
||||
use_ssl: Optional[bool] = Field(None, description="Use wss://")
|
||||
entity_filters: Optional[List[str]] = Field(None, description="Entity ID filter patterns")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
host: str | None = Field(None, description="HA host:port", min_length=1)
|
||||
token: str | None = Field(None, description="Long-Lived Access Token", min_length=1)
|
||||
use_ssl: bool | None = Field(None, description="Use wss://")
|
||||
entity_filters: List[str] | None = Field(None, description="Entity ID filter patterns")
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
@@ -62,21 +62,21 @@ class HomeAssistantSourceResponse(BaseModel):
|
||||
entity_filters: List[str] = Field(default_factory=list, description="Entity filter patterns")
|
||||
connected: bool = Field(default=False, description="Whether the WebSocket connection is active")
|
||||
entity_count: int = Field(default=0, description="Number of cached entities")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
)
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
token: Optional[str] = Field(
|
||||
token: str | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"Long-Lived Access Token. Redacted as '***' unless the request "
|
||||
@@ -112,9 +112,9 @@ class HomeAssistantTestResponse(BaseModel):
|
||||
"""Connection test result."""
|
||||
|
||||
success: bool = Field(description="Whether connection and auth succeeded")
|
||||
ha_version: Optional[str] = Field(None, description="Home Assistant version")
|
||||
ha_version: str | None = Field(None, description="Home Assistant version")
|
||||
entity_count: int = Field(default=0, description="Number of entities found")
|
||||
error: Optional[str] = Field(None, description="Error message if connection failed")
|
||||
error: str | None = Field(None, description="Error message if connection failed")
|
||||
|
||||
|
||||
class HomeAssistantConnectionStatus(BaseModel):
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
from typing import Any, Dict, List, Literal
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
# RFC 7230 token chars for header names + reject any control character in values.
|
||||
_HEADER_NAME_RE = re.compile(r"^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$")
|
||||
_HEADER_CONTROL_CHARS_RE = re.compile(r"[\x00-\x1f\x7f]")
|
||||
@@ -64,10 +63,10 @@ class HTTPEndpointCreate(BaseModel):
|
||||
)
|
||||
headers: Dict[str, str] = Field(default_factory=dict)
|
||||
timeout_s: float = Field(default=10.0, gt=0)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
description: str | None = Field(None, max_length=500)
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
icon: Optional[str] = Field(None, max_length=64)
|
||||
icon_color: Optional[str] = Field(None, max_length=32)
|
||||
icon: str | None = Field(None, max_length=64)
|
||||
icon_color: str | None = Field(None, max_length=32)
|
||||
|
||||
@field_validator("headers")
|
||||
@classmethod
|
||||
@@ -88,16 +87,16 @@ class HTTPEndpointUpdate(BaseModel):
|
||||
field (or send ``null``) to keep it.
|
||||
"""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
url: Optional[str] = Field(None, min_length=1)
|
||||
method: Optional[Literal["GET", "HEAD"]] = None
|
||||
auth_token: Optional[str] = Field(None, description="null = keep existing; '' = clear.")
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
timeout_s: Optional[float] = Field(None, gt=0)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(None, max_length=64)
|
||||
icon_color: Optional[str] = Field(None, max_length=32)
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
url: str | None = Field(None, min_length=1)
|
||||
method: Literal["GET", "HEAD"] | None = None
|
||||
auth_token: str | None = Field(None, description="null = keep existing; '' = clear.")
|
||||
headers: Dict[str, str] | None = None
|
||||
timeout_s: float | None = Field(None, gt=0)
|
||||
description: str | None = Field(None, max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(None, max_length=64)
|
||||
icon_color: str | None = Field(None, max_length=32)
|
||||
|
||||
@field_validator("headers")
|
||||
@classmethod
|
||||
@@ -125,10 +124,10 @@ class HTTPEndpointResponse(BaseModel):
|
||||
auth_token_set: bool = False
|
||||
headers: Dict[str, str] = Field(default_factory=dict)
|
||||
timeout_s: float
|
||||
description: Optional[str] = None
|
||||
description: str | None = None
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
icon: Optional[str] = Field(None, max_length=64)
|
||||
icon_color: Optional[str] = Field(None, max_length=32)
|
||||
icon: str | None = Field(None, max_length=64)
|
||||
icon_color: str | None = Field(None, max_length=32)
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -160,7 +159,7 @@ class HTTPTestRequest(BaseModel):
|
||||
|
||||
class HTTPTestResponse(BaseModel):
|
||||
success: bool
|
||||
status_code: Optional[int] = None
|
||||
body_preview: Optional[str] = Field(None, description="First 500 chars of the body")
|
||||
status_code: int | None = None
|
||||
body_preview: str | None = Field(None, description="First 500 chars of the body")
|
||||
body_json: Any = None
|
||||
error: Optional[str] = None
|
||||
error: str | None = None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""MQTT source schemas (CRUD + test + status)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -16,14 +16,14 @@ class MQTTSourceCreate(BaseModel):
|
||||
password: str = Field(default="", description="Broker password (optional)")
|
||||
client_id: str = Field(default="ledgrab", description="MQTT client ID")
|
||||
base_topic: str = Field(default="ledgrab", description="Base topic prefix")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
@@ -33,21 +33,21 @@ class MQTTSourceCreate(BaseModel):
|
||||
class MQTTSourceUpdate(BaseModel):
|
||||
"""Request to update an MQTT source."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
broker_host: Optional[str] = Field(None, description="MQTT broker hostname or IP", min_length=1)
|
||||
broker_port: Optional[int] = Field(None, description="MQTT broker port", ge=1, le=65535)
|
||||
username: Optional[str] = Field(None, description="Broker username")
|
||||
password: Optional[str] = Field(None, description="Broker password")
|
||||
client_id: Optional[str] = Field(None, description="MQTT client ID")
|
||||
base_topic: Optional[str] = Field(None, description="Base topic prefix")
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
broker_host: str | None = Field(None, description="MQTT broker hostname or IP", min_length=1)
|
||||
broker_port: int | None = Field(None, description="MQTT broker port", ge=1, le=65535)
|
||||
username: str | None = Field(None, description="Broker username")
|
||||
password: str | None = Field(None, description="Broker password")
|
||||
client_id: str | None = Field(None, description="MQTT client ID")
|
||||
base_topic: str | None = Field(None, description="Base topic prefix")
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
@@ -66,14 +66,14 @@ class MQTTSourceResponse(BaseModel):
|
||||
client_id: str = Field(description="MQTT client ID")
|
||||
base_topic: str = Field(description="Base topic prefix")
|
||||
connected: bool = Field(default=False, description="Whether the broker connection is active")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
@@ -93,7 +93,7 @@ class MQTTTestResponse(BaseModel):
|
||||
"""Connection test result."""
|
||||
|
||||
success: bool = Field(description="Whether broker connection succeeded")
|
||||
error: Optional[str] = Field(None, description="Error message if connection failed")
|
||||
error: str | None = Field(None, description="Error message if connection failed")
|
||||
|
||||
|
||||
class MQTTConnectionStatus(BaseModel):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Output target schemas — discriminated unions per target type."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
||||
from typing import Annotated, Any, Dict, List, Literal
|
||||
|
||||
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||
|
||||
@@ -11,7 +11,7 @@ DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds between health checks
|
||||
# BindableFloat — accepts plain number OR {value, source_id} dict
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BindableFloatInput = Union[float, int, Dict[str, Any]]
|
||||
BindableFloatInput = float | int | Dict[str, Any]
|
||||
"""API input type: a plain number (static) or {"value": float, "source_id": str}."""
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class HALightMappingSchema(BaseModel):
|
||||
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: Optional[BindableFloatInput] = Field(
|
||||
brightness_scale: BindableFloatInput | None = Field(
|
||||
default=1.0, description="Brightness multiplier (bindable)"
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ class Z2MLightMappingSchema(BaseModel):
|
||||
)
|
||||
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: Optional[BindableFloatInput] = Field(
|
||||
brightness_scale: BindableFloatInput | None = Field(
|
||||
default=1.0, description="Brightness multiplier (bindable)"
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ class _OutputTargetResponseBase(BaseModel):
|
||||
|
||||
id: str = Field(description="Target ID")
|
||||
name: str = Field(description="Target name")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: str = Field(default="", description="Custom icon id from the curated icon library")
|
||||
icon_color: str = Field(default="", description="Optional CSS color override for the icon")
|
||||
@@ -79,13 +79,13 @@ class LedOutputTargetResponse(_OutputTargetResponseBase):
|
||||
target_type: Literal["led"] = "led"
|
||||
device_id: str = Field(default="", description="LED device ID")
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
fps: Optional[BindableFloatInput] = Field(None, description="Target send FPS (bindable)")
|
||||
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||
fps: BindableFloatInput | None = Field(None, description="Target send FPS (bindable)")
|
||||
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: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
default=0, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
adaptive_fps: bool = Field(
|
||||
@@ -110,20 +110,20 @@ class HALightOutputTargetResponse(_OutputTargetResponseBase):
|
||||
description="Colour value source ID (used when source_kind='color_vs'); "
|
||||
"must reference a value source whose return_type='color'.",
|
||||
)
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||
ha_light_mappings: List[HALightMappingSchema] | None = Field(
|
||||
None, description="LED-to-light mappings"
|
||||
)
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
update_rate: BindableFloatInput | None = Field(
|
||||
None, description="Service call rate Hz (bindable)"
|
||||
)
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
transition: BindableFloatInput | None = Field(
|
||||
None, description="HA transition seconds (bindable)"
|
||||
)
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
color_tolerance: BindableFloatInput | None = Field(
|
||||
None, description="RGB delta tolerance (bindable)"
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
default=0, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
stop_action: Literal["none", "turn_off", "restore"] = Field(
|
||||
@@ -151,24 +151,24 @@ class Z2MLightOutputTargetResponse(_OutputTargetResponseBase):
|
||||
default="",
|
||||
description="Colour value source ID (used when source_kind='color_vs').",
|
||||
)
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
z2m_light_mappings: Optional[List[Z2MLightMappingSchema]] = Field(
|
||||
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||
z2m_light_mappings: List[Z2MLightMappingSchema] | None = Field(
|
||||
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
||||
)
|
||||
base_topic: str = Field(
|
||||
default="zigbee2mqtt",
|
||||
description="Z2M MQTT base topic prefix (override if your Z2M instance is non-default).",
|
||||
)
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
update_rate: BindableFloatInput | None = Field(
|
||||
None, description="Publish rate Hz (bindable; 0.5-10)"
|
||||
)
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
transition: BindableFloatInput | None = Field(
|
||||
None, description="Z2M transition seconds (bindable)"
|
||||
)
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
color_tolerance: BindableFloatInput | None = Field(
|
||||
None, description="RGB delta tolerance (bindable)"
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
default=0, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
stop_action: Literal["none", "turn_off"] = Field(
|
||||
@@ -179,11 +179,9 @@ class Z2MLightOutputTargetResponse(_OutputTargetResponseBase):
|
||||
|
||||
|
||||
OutputTargetResponse = Annotated[
|
||||
Union[
|
||||
Annotated[LedOutputTargetResponse, Tag("led")],
|
||||
Annotated[HALightOutputTargetResponse, Tag("ha_light")],
|
||||
Annotated[Z2MLightOutputTargetResponse, Tag("z2m_light")],
|
||||
],
|
||||
Annotated[LedOutputTargetResponse, Tag("led")]
|
||||
| Annotated[HALightOutputTargetResponse, Tag("ha_light")]
|
||||
| Annotated[Z2MLightOutputTargetResponse, Tag("z2m_light")],
|
||||
Discriminator("target_type"),
|
||||
]
|
||||
|
||||
@@ -196,12 +194,12 @@ class _OutputTargetCreateBase(BaseModel):
|
||||
"""Shared fields for all output target create requests."""
|
||||
|
||||
name: str = Field(description="Target name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None, max_length=64, description="Custom icon id from the curated icon library"
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None, max_length=32, description="Optional CSS color override for the icon"
|
||||
)
|
||||
|
||||
@@ -210,10 +208,8 @@ class LedOutputTargetCreate(_OutputTargetCreateBase):
|
||||
target_type: Literal["led"] = "led"
|
||||
device_id: str = Field(default="", description="LED device ID")
|
||||
color_strip_source_id: str = Field(default="", description="Color strip source ID")
|
||||
brightness: Optional[BindableFloatInput] = Field(
|
||||
default=1.0, description="Brightness (bindable)"
|
||||
)
|
||||
fps: Optional[BindableFloatInput] = Field(
|
||||
brightness: BindableFloatInput | None = Field(default=1.0, description="Brightness (bindable)")
|
||||
fps: BindableFloatInput | None = Field(
|
||||
default=30, description="Target send FPS (bindable, 1-90)"
|
||||
)
|
||||
keepalive_interval: float = Field(
|
||||
@@ -228,7 +224,7 @@ class LedOutputTargetCreate(_OutputTargetCreateBase):
|
||||
ge=5,
|
||||
le=600,
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
default=0,
|
||||
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
||||
)
|
||||
@@ -257,22 +253,20 @@ class HALightOutputTargetCreate(_OutputTargetCreateBase):
|
||||
default="",
|
||||
description="Colour value source ID (used when source_kind='color_vs').",
|
||||
)
|
||||
brightness: Optional[BindableFloatInput] = Field(
|
||||
default=1.0, description="Brightness (bindable)"
|
||||
)
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
brightness: BindableFloatInput | None = Field(default=1.0, description="Brightness (bindable)")
|
||||
ha_light_mappings: List[HALightMappingSchema] | None = Field(
|
||||
None, description="LED-to-light mappings"
|
||||
)
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
update_rate: BindableFloatInput | None = Field(
|
||||
default=2.0, description="Service call rate in Hz (bindable)"
|
||||
)
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
transition: BindableFloatInput | None = Field(
|
||||
default=0.5, description="HA transition seconds (bindable)"
|
||||
)
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
color_tolerance: BindableFloatInput | None = Field(
|
||||
default=5, description="RGB delta tolerance (bindable)"
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
default=0,
|
||||
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
||||
)
|
||||
@@ -299,10 +293,8 @@ class Z2MLightOutputTargetCreate(_OutputTargetCreateBase):
|
||||
default="",
|
||||
description="Colour value source ID (used when source_kind='color_vs').",
|
||||
)
|
||||
brightness: Optional[BindableFloatInput] = Field(
|
||||
default=1.0, description="Brightness (bindable)"
|
||||
)
|
||||
z2m_light_mappings: Optional[List[Z2MLightMappingSchema]] = Field(
|
||||
brightness: BindableFloatInput | None = Field(default=1.0, description="Brightness (bindable)")
|
||||
z2m_light_mappings: List[Z2MLightMappingSchema] | None = Field(
|
||||
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
||||
)
|
||||
base_topic: str = Field(
|
||||
@@ -310,16 +302,16 @@ class Z2MLightOutputTargetCreate(_OutputTargetCreateBase):
|
||||
max_length=128,
|
||||
description="Z2M MQTT base topic prefix.",
|
||||
)
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
update_rate: BindableFloatInput | None = Field(
|
||||
default=5.0, description="Publish rate in Hz (bindable; 0.5-10)"
|
||||
)
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
transition: BindableFloatInput | None = Field(
|
||||
default=0.3, description="Z2M transition seconds (bindable)"
|
||||
)
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
color_tolerance: BindableFloatInput | None = Field(
|
||||
default=5, description="RGB delta tolerance (bindable)"
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
default=0,
|
||||
description="Min brightness threshold (bindable, 0=disabled); below this -> off",
|
||||
)
|
||||
@@ -330,11 +322,9 @@ class Z2MLightOutputTargetCreate(_OutputTargetCreateBase):
|
||||
|
||||
|
||||
OutputTargetCreate = Annotated[
|
||||
Union[
|
||||
Annotated[LedOutputTargetCreate, Tag("led")],
|
||||
Annotated[HALightOutputTargetCreate, Tag("ha_light")],
|
||||
Annotated[Z2MLightOutputTargetCreate, Tag("z2m_light")],
|
||||
],
|
||||
Annotated[LedOutputTargetCreate, Tag("led")]
|
||||
| Annotated[HALightOutputTargetCreate, Tag("ha_light")]
|
||||
| Annotated[Z2MLightOutputTargetCreate, Tag("z2m_light")],
|
||||
Discriminator("target_type"),
|
||||
]
|
||||
|
||||
@@ -346,15 +336,15 @@ OutputTargetCreate = Annotated[
|
||||
class _OutputTargetUpdateBase(BaseModel):
|
||||
"""Shared fields for all output target update requests."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Target name", min_length=1, max_length=100)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Custom icon id; pass empty string to clear and inherit from device.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon; empty string clears.",
|
||||
@@ -363,103 +353,99 @@ class _OutputTargetUpdateBase(BaseModel):
|
||||
|
||||
class LedOutputTargetUpdate(_OutputTargetUpdateBase):
|
||||
target_type: Literal["led"] = "led"
|
||||
device_id: Optional[str] = Field(None, description="LED device ID")
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
fps: Optional[BindableFloatInput] = Field(None, description="Target send FPS (bindable, 1-90)")
|
||||
keepalive_interval: Optional[float] = Field(
|
||||
device_id: str | None = Field(None, description="LED device ID")
|
||||
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||
fps: BindableFloatInput | None = Field(None, description="Target send FPS (bindable, 1-90)")
|
||||
keepalive_interval: float | None = Field(
|
||||
None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0
|
||||
)
|
||||
state_check_interval: Optional[int] = Field(
|
||||
state_check_interval: int | None = Field(
|
||||
None, description="Health check interval (5-600s)", ge=5, le=600
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
None, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
adaptive_fps: Optional[bool] = Field(
|
||||
adaptive_fps: bool | None = Field(
|
||||
None, description="Auto-reduce FPS when device is unresponsive"
|
||||
)
|
||||
protocol: Optional[str] = Field(
|
||||
protocol: str | None = Field(
|
||||
None, pattern="^(ddp|http)$", description="Send protocol: ddp (UDP) or http (JSON API)"
|
||||
)
|
||||
|
||||
|
||||
class HALightOutputTargetUpdate(_OutputTargetUpdateBase):
|
||||
target_type: Literal["ha_light"] = "ha_light"
|
||||
ha_source_id: Optional[str] = Field(None, description="Home Assistant source ID")
|
||||
source_kind: Optional[Literal["css", "color_vs"]] = Field(
|
||||
ha_source_id: str | None = Field(None, description="Home Assistant source ID")
|
||||
source_kind: Literal["css", "color_vs"] | None = Field(
|
||||
None,
|
||||
description="Colour source kind: 'css' or 'color_vs'.",
|
||||
)
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
color_value_source_id: Optional[str] = Field(
|
||||
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||
color_value_source_id: str | None = Field(
|
||||
None,
|
||||
description="Colour value source ID (used when source_kind='color_vs').",
|
||||
)
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
ha_light_mappings: Optional[List[HALightMappingSchema]] = Field(
|
||||
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||
ha_light_mappings: List[HALightMappingSchema] | None = Field(
|
||||
None, description="LED-to-light mappings"
|
||||
)
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
update_rate: BindableFloatInput | None = Field(
|
||||
None, description="Service call rate Hz (bindable)"
|
||||
)
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
transition: BindableFloatInput | None = Field(
|
||||
None, description="HA transition seconds (bindable)"
|
||||
)
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
color_tolerance: BindableFloatInput | None = Field(
|
||||
None, description="RGB delta tolerance (bindable)"
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
None, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
stop_action: Optional[Literal["none", "turn_off", "restore"]] = Field(
|
||||
stop_action: Literal["none", "turn_off", "restore"] | None = Field(
|
||||
None, description="Finalization on stop: 'none', 'turn_off', or 'restore'."
|
||||
)
|
||||
|
||||
|
||||
class Z2MLightOutputTargetUpdate(_OutputTargetUpdateBase):
|
||||
target_type: Literal["z2m_light"] = "z2m_light"
|
||||
mqtt_source_id: Optional[str] = Field(
|
||||
mqtt_source_id: str | None = Field(
|
||||
None,
|
||||
description="MQTT source (broker) id. Empty string clears the binding.",
|
||||
)
|
||||
source_kind: Optional[Literal["css", "color_vs"]] = Field(
|
||||
source_kind: Literal["css", "color_vs"] | None = Field(
|
||||
None, description="Colour source kind: 'css' or 'color_vs'."
|
||||
)
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
color_value_source_id: Optional[str] = Field(
|
||||
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||
color_value_source_id: str | None = Field(
|
||||
None, description="Colour value source ID (used when source_kind='color_vs')."
|
||||
)
|
||||
brightness: Optional[BindableFloatInput] = Field(None, description="Brightness (bindable)")
|
||||
z2m_light_mappings: Optional[List[Z2MLightMappingSchema]] = Field(
|
||||
brightness: BindableFloatInput | None = Field(None, description="Brightness (bindable)")
|
||||
z2m_light_mappings: List[Z2MLightMappingSchema] | None = Field(
|
||||
None, description="LED-to-bulb mappings (by Z2M friendly_name)"
|
||||
)
|
||||
base_topic: Optional[str] = Field(
|
||||
None, max_length=128, description="Z2M MQTT base topic prefix."
|
||||
)
|
||||
update_rate: Optional[BindableFloatInput] = Field(
|
||||
base_topic: str | None = Field(None, max_length=128, description="Z2M MQTT base topic prefix.")
|
||||
update_rate: BindableFloatInput | None = Field(
|
||||
None, description="Publish rate Hz (bindable; 0.5-10)"
|
||||
)
|
||||
transition: Optional[BindableFloatInput] = Field(
|
||||
transition: BindableFloatInput | None = Field(
|
||||
None, description="Z2M transition seconds (bindable)"
|
||||
)
|
||||
color_tolerance: Optional[BindableFloatInput] = Field(
|
||||
color_tolerance: BindableFloatInput | None = Field(
|
||||
None, description="RGB delta tolerance (bindable)"
|
||||
)
|
||||
min_brightness_threshold: Optional[BindableFloatInput] = Field(
|
||||
min_brightness_threshold: BindableFloatInput | None = Field(
|
||||
None, description="Min brightness threshold (bindable, 0=disabled)"
|
||||
)
|
||||
stop_action: Optional[Literal["none", "turn_off"]] = Field(
|
||||
stop_action: Literal["none", "turn_off"] | None = Field(
|
||||
None, description="Finalization on stop: 'none' or 'turn_off'."
|
||||
)
|
||||
|
||||
|
||||
OutputTargetUpdate = Annotated[
|
||||
Union[
|
||||
Annotated[LedOutputTargetUpdate, Tag("led")],
|
||||
Annotated[HALightOutputTargetUpdate, Tag("ha_light")],
|
||||
Annotated[Z2MLightOutputTargetUpdate, Tag("z2m_light")],
|
||||
],
|
||||
Annotated[LedOutputTargetUpdate, Tag("led")]
|
||||
| Annotated[HALightOutputTargetUpdate, Tag("ha_light")]
|
||||
| Annotated[Z2MLightOutputTargetUpdate, Tag("z2m_light")],
|
||||
Discriminator("target_type"),
|
||||
]
|
||||
|
||||
@@ -479,75 +465,69 @@ class TargetProcessingState(BaseModel):
|
||||
"""Processing state for an output target."""
|
||||
|
||||
target_id: str = Field(description="Target ID")
|
||||
device_id: Optional[str] = Field(None, description="Device ID")
|
||||
device_id: str | None = Field(None, description="Device ID")
|
||||
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(
|
||||
fps_actual: float | None = Field(None, description="Actual FPS achieved")
|
||||
fps_potential: float | None = Field(
|
||||
None, description="Potential FPS (processing speed without throttle)"
|
||||
)
|
||||
fps_target: Optional[int] = Field(None, description="Target FPS")
|
||||
fps_capture: Optional[int] = Field(
|
||||
fps_target: int | None = Field(None, description="Target FPS")
|
||||
fps_capture: int | None = Field(
|
||||
None, description="Configured capture-side FPS for the underlying color strip stream"
|
||||
)
|
||||
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")
|
||||
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_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_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(
|
||||
frames_skipped: int | None = Field(None, description="Frames skipped (no screen change)")
|
||||
frames_keepalive: int | None = Field(None, description="Keepalive frames sent during standby")
|
||||
fps_current: int | None = Field(None, description="Frames sent in the last second")
|
||||
timing_send_ms: float | None = Field(None, description="DDP send time (ms)")
|
||||
timing_extract_ms: float | None = Field(None, description="Border pixel extraction time (ms)")
|
||||
timing_map_leds_ms: float | None = Field(None, description="LED color mapping time (ms)")
|
||||
timing_smooth_ms: float | None = Field(None, description="Temporal smoothing time (ms)")
|
||||
timing_total_ms: float | None = Field(None, description="Total processing time per frame (ms)")
|
||||
timing_audio_read_ms: float | None = Field(None, description="Audio device read time (ms)")
|
||||
timing_audio_fft_ms: float | None = Field(None, description="Audio FFT analysis time (ms)")
|
||||
timing_audio_render_ms: float | None = Field(
|
||||
None, description="Audio visualization render time (ms)"
|
||||
)
|
||||
display_index: Optional[int] = Field(None, description="Current display index")
|
||||
display_index: int | None = Field(None, description="Current display index")
|
||||
overlay_active: bool = Field(
|
||||
default=False, description="Whether visualization overlay is active"
|
||||
)
|
||||
last_update: Optional[datetime] = Field(None, description="Last successful update")
|
||||
last_update: datetime | None = 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")
|
||||
device_latency_ms: Optional[float] = Field(None, description="Health check latency in ms")
|
||||
device_name: Optional[str] = Field(None, description="Device name reported by firmware")
|
||||
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(
|
||||
device_latency_ms: float | None = Field(None, description="Health check latency in ms")
|
||||
device_name: str | None = Field(None, description="Device name reported by firmware")
|
||||
device_version: str | None = Field(None, description="Firmware version")
|
||||
device_led_count: int | None = Field(None, description="LED count reported by device")
|
||||
device_rgbw: bool | None = Field(None, description="Whether device uses RGBW LEDs")
|
||||
device_led_type: str | None = Field(
|
||||
None, description="LED chip type (e.g. WS2812B, SK6812 RGBW)"
|
||||
)
|
||||
device_fps: Optional[int] = Field(
|
||||
device_fps: int | None = 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(
|
||||
device_last_checked: datetime | None = Field(None, description="Last health check time")
|
||||
device_error: str | None = Field(None, description="Last health check error")
|
||||
device_streaming_reachable: bool | None = Field(
|
||||
None, description="Device reachable during streaming (HTTP probe)"
|
||||
)
|
||||
fps_effective: Optional[int] = Field(None, description="Effective FPS after adaptive reduction")
|
||||
fps_effective: int | None = Field(None, description="Effective FPS after adaptive reduction")
|
||||
|
||||
|
||||
class TargetMetricsResponse(BaseModel):
|
||||
"""Target metrics response."""
|
||||
|
||||
target_id: str = Field(description="Target ID")
|
||||
device_id: Optional[str] = Field(None, description="Device ID")
|
||||
device_id: str | None = Field(None, description="Device ID")
|
||||
processing: bool = Field(description="Whether processing is active")
|
||||
fps_actual: Optional[float] = Field(None, description="Actual FPS")
|
||||
fps_target: Optional[int] = Field(None, description="Target FPS")
|
||||
fps_actual: float | None = Field(None, description="Actual FPS")
|
||||
fps_target: int | None = Field(None, description="Target FPS")
|
||||
uptime_seconds: float = Field(description="Processing uptime in seconds")
|
||||
frames_processed: int = Field(description="Total frames processed")
|
||||
errors_count: int = Field(description="Total error count")
|
||||
last_error: Optional[str] = Field(None, description="Last error message")
|
||||
last_update: Optional[datetime] = Field(None, description="Last update timestamp")
|
||||
last_error: str | None = Field(None, description="Last error message")
|
||||
last_update: datetime | None = Field(None, description="Last update timestamp")
|
||||
|
||||
|
||||
class BulkTargetRequest(BaseModel):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Pydantic schemas for pattern template API."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -15,14 +15,14 @@ class PatternTemplateCreate(BaseModel):
|
||||
rectangles: List[KeyColorRectangleSchema] = Field(
|
||||
default_factory=list, description="List of named rectangles"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -32,18 +32,18 @@ class PatternTemplateCreate(BaseModel):
|
||||
class PatternTemplateUpdate(BaseModel):
|
||||
"""Request to update a pattern template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
rectangles: Optional[List[KeyColorRectangleSchema]] = Field(
|
||||
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
rectangles: List[KeyColorRectangleSchema] | None = Field(
|
||||
None, description="List of named rectangles"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -59,13 +59,13 @@ class PatternTemplateResponse(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description")
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Picture source schemas — discriminated unions per stream type."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, List, Literal, Optional, Union
|
||||
from typing import Annotated, List, Literal
|
||||
|
||||
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||
|
||||
@@ -15,16 +15,16 @@ class _PictureSourceResponseBase(BaseModel):
|
||||
|
||||
id: str = Field(description="Stream ID")
|
||||
name: str = Field(description="Stream name")
|
||||
description: Optional[str] = Field(None, description="Stream description")
|
||||
description: str | None = Field(None, description="Stream description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -46,28 +46,26 @@ class ProcessedPictureSourceResponse(_PictureSourceResponseBase):
|
||||
|
||||
class StaticImagePictureSourceResponse(_PictureSourceResponseBase):
|
||||
stream_type: Literal["static_image"] = "static_image"
|
||||
image_asset_id: Optional[str] = Field(None, description="Image asset ID")
|
||||
image_asset_id: str | None = Field(None, description="Image asset ID")
|
||||
|
||||
|
||||
class VideoPictureSourceResponse(_PictureSourceResponseBase):
|
||||
stream_type: Literal["video"] = "video"
|
||||
video_asset_id: Optional[str] = Field(None, description="Video asset ID")
|
||||
video_asset_id: str | None = Field(None, description="Video asset ID")
|
||||
loop: bool = Field(True, description="Loop video playback")
|
||||
playback_speed: float = Field(1.0, description="Playback speed multiplier")
|
||||
start_time: Optional[float] = Field(None, description="Trim start time in seconds")
|
||||
end_time: Optional[float] = Field(None, description="Trim end time in seconds")
|
||||
resolution_limit: Optional[int] = Field(None, description="Max width for decode")
|
||||
clock_id: Optional[str] = Field(None, description="Sync clock ID")
|
||||
start_time: float | None = Field(None, description="Trim start time in seconds")
|
||||
end_time: float | None = Field(None, description="Trim end time in seconds")
|
||||
resolution_limit: int | None = Field(None, description="Max width for decode")
|
||||
clock_id: str | None = Field(None, description="Sync clock ID")
|
||||
target_fps: int = Field(30, description="Target FPS")
|
||||
|
||||
|
||||
PictureSourceResponse = Annotated[
|
||||
Union[
|
||||
Annotated[RawPictureSourceResponse, Tag("raw")],
|
||||
Annotated[ProcessedPictureSourceResponse, Tag("processed")],
|
||||
Annotated[StaticImagePictureSourceResponse, Tag("static_image")],
|
||||
Annotated[VideoPictureSourceResponse, Tag("video")],
|
||||
],
|
||||
Annotated[RawPictureSourceResponse, Tag("raw")]
|
||||
| Annotated[ProcessedPictureSourceResponse, Tag("processed")]
|
||||
| Annotated[StaticImagePictureSourceResponse, Tag("static_image")]
|
||||
| Annotated[VideoPictureSourceResponse, Tag("video")],
|
||||
Discriminator("stream_type"),
|
||||
]
|
||||
|
||||
@@ -80,14 +78,14 @@ class _PictureSourceCreateBase(BaseModel):
|
||||
"""Shared fields for all picture source create requests."""
|
||||
|
||||
name: str = Field(description="Stream name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
||||
description: str | None = Field(None, description="Stream description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -117,22 +115,20 @@ class VideoPictureSourceCreate(_PictureSourceCreateBase):
|
||||
video_asset_id: str = Field(description="Video asset ID")
|
||||
loop: bool = Field(True, description="Loop video playback")
|
||||
playback_speed: float = Field(1.0, description="Playback speed multiplier", ge=0.1, le=10.0)
|
||||
start_time: Optional[float] = Field(None, description="Trim start time in seconds", ge=0)
|
||||
end_time: Optional[float] = Field(None, description="Trim end time in seconds", ge=0)
|
||||
resolution_limit: Optional[int] = Field(
|
||||
start_time: float | None = Field(None, description="Trim start time in seconds", ge=0)
|
||||
end_time: float | None = Field(None, description="Trim end time in seconds", ge=0)
|
||||
resolution_limit: int | None = Field(
|
||||
None, description="Max width in pixels for decode downscale", ge=64, le=7680
|
||||
)
|
||||
clock_id: Optional[str] = Field(None, description="Sync clock ID for frame-accurate timing")
|
||||
clock_id: str | None = Field(None, description="Sync clock ID for frame-accurate timing")
|
||||
target_fps: int = Field(30, description="Target FPS", ge=1, le=90)
|
||||
|
||||
|
||||
PictureSourceCreate = Annotated[
|
||||
Union[
|
||||
Annotated[RawPictureSourceCreate, Tag("raw")],
|
||||
Annotated[ProcessedPictureSourceCreate, Tag("processed")],
|
||||
Annotated[StaticImagePictureSourceCreate, Tag("static_image")],
|
||||
Annotated[VideoPictureSourceCreate, Tag("video")],
|
||||
],
|
||||
Annotated[RawPictureSourceCreate, Tag("raw")]
|
||||
| Annotated[ProcessedPictureSourceCreate, Tag("processed")]
|
||||
| Annotated[StaticImagePictureSourceCreate, Tag("static_image")]
|
||||
| Annotated[VideoPictureSourceCreate, Tag("video")],
|
||||
Discriminator("stream_type"),
|
||||
]
|
||||
|
||||
@@ -144,15 +140,15 @@ PictureSourceCreate = Annotated[
|
||||
class _PictureSourceUpdateBase(BaseModel):
|
||||
"""Shared fields for all picture source update requests."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Stream name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Stream description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Stream name", min_length=1, max_length=100)
|
||||
description: str | None = Field(None, description="Stream description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -161,47 +157,43 @@ class _PictureSourceUpdateBase(BaseModel):
|
||||
|
||||
class RawPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||
stream_type: Literal["raw"] = "raw"
|
||||
display_index: Optional[int] = Field(None, description="Display index", ge=0)
|
||||
capture_template_id: Optional[str] = Field(None, description="Capture template ID")
|
||||
target_fps: Optional[int] = Field(None, description="Target FPS", ge=1, le=90)
|
||||
display_index: int | None = Field(None, description="Display index", ge=0)
|
||||
capture_template_id: str | None = Field(None, description="Capture template ID")
|
||||
target_fps: int | None = Field(None, description="Target FPS", ge=1, le=90)
|
||||
|
||||
|
||||
class ProcessedPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||
stream_type: Literal["processed"] = "processed"
|
||||
source_stream_id: Optional[str] = Field(None, description="Source stream ID")
|
||||
postprocessing_template_id: Optional[str] = Field(
|
||||
None, description="Postprocessing template ID"
|
||||
)
|
||||
source_stream_id: str | None = Field(None, description="Source stream ID")
|
||||
postprocessing_template_id: str | None = Field(None, description="Postprocessing template ID")
|
||||
|
||||
|
||||
class StaticImagePictureSourceUpdate(_PictureSourceUpdateBase):
|
||||
stream_type: Literal["static_image"] = "static_image"
|
||||
image_asset_id: Optional[str] = Field(None, description="Image asset ID")
|
||||
image_asset_id: str | None = Field(None, description="Image asset ID")
|
||||
|
||||
|
||||
class VideoPictureSourceUpdate(_PictureSourceUpdateBase):
|
||||
stream_type: Literal["video"] = "video"
|
||||
video_asset_id: Optional[str] = Field(None, description="Video asset ID")
|
||||
loop: Optional[bool] = Field(None, description="Loop video playback")
|
||||
playback_speed: Optional[float] = Field(
|
||||
video_asset_id: str | None = Field(None, description="Video asset ID")
|
||||
loop: bool | None = Field(None, description="Loop video playback")
|
||||
playback_speed: float | None = Field(
|
||||
None, description="Playback speed multiplier", ge=0.1, le=10.0
|
||||
)
|
||||
start_time: Optional[float] = Field(None, description="Trim start time in seconds", ge=0)
|
||||
end_time: Optional[float] = Field(None, description="Trim end time in seconds", ge=0)
|
||||
resolution_limit: Optional[int] = Field(
|
||||
start_time: float | None = Field(None, description="Trim start time in seconds", ge=0)
|
||||
end_time: float | None = Field(None, description="Trim end time in seconds", ge=0)
|
||||
resolution_limit: int | None = Field(
|
||||
None, description="Max width in pixels for decode downscale", ge=64, le=7680
|
||||
)
|
||||
clock_id: Optional[str] = Field(None, description="Sync clock ID for frame-accurate timing")
|
||||
target_fps: Optional[int] = Field(None, description="Target FPS", ge=1, le=90)
|
||||
clock_id: str | None = Field(None, description="Sync clock ID for frame-accurate timing")
|
||||
target_fps: int | None = Field(None, description="Target FPS", ge=1, le=90)
|
||||
|
||||
|
||||
PictureSourceUpdate = Annotated[
|
||||
Union[
|
||||
Annotated[RawPictureSourceUpdate, Tag("raw")],
|
||||
Annotated[ProcessedPictureSourceUpdate, Tag("processed")],
|
||||
Annotated[StaticImagePictureSourceUpdate, Tag("static_image")],
|
||||
Annotated[VideoPictureSourceUpdate, Tag("video")],
|
||||
],
|
||||
Annotated[RawPictureSourceUpdate, Tag("raw")]
|
||||
| Annotated[ProcessedPictureSourceUpdate, Tag("processed")]
|
||||
| Annotated[StaticImagePictureSourceUpdate, Tag("static_image")]
|
||||
| Annotated[VideoPictureSourceUpdate, Tag("video")],
|
||||
Discriminator("stream_type"),
|
||||
]
|
||||
|
||||
@@ -246,7 +238,7 @@ class ImageValidateResponse(BaseModel):
|
||||
"""Response from image validation."""
|
||||
|
||||
valid: bool = Field(description="Whether the image source is accessible and valid")
|
||||
width: Optional[int] = Field(None, description="Image width in pixels")
|
||||
height: Optional[int] = Field(None, description="Image height in pixels")
|
||||
preview: Optional[str] = Field(None, description="Base64-encoded JPEG thumbnail")
|
||||
error: Optional[str] = Field(None, description="Error message if invalid")
|
||||
width: int | None = Field(None, description="Image width in pixels")
|
||||
height: int | None = Field(None, description="Image height in pixels")
|
||||
preview: str | None = Field(None, description="Base64-encoded JPEG thumbnail")
|
||||
error: str | None = Field(None, description="Error message if invalid")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Postprocessing template schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -15,14 +15,14 @@ class PostprocessingTemplateCreate(BaseModel):
|
||||
filters: List[FilterInstanceSchema] = Field(
|
||||
default_factory=list, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -32,18 +32,18 @@ class PostprocessingTemplateCreate(BaseModel):
|
||||
class PostprocessingTemplateUpdate(BaseModel):
|
||||
"""Request to update a postprocessing template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: Optional[List[FilterInstanceSchema]] = Field(
|
||||
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
filters: List[FilterInstanceSchema] | None = Field(
|
||||
None, description="Ordered list of filter instances"
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -59,13 +59,13 @@ class PostprocessingTemplateResponse(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description")
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Scene preset API schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -19,16 +19,14 @@ class ScenePresetCreate(BaseModel):
|
||||
|
||||
name: str = Field(description="Preset name", min_length=1, max_length=100)
|
||||
description: str = Field(default="", max_length=500)
|
||||
target_ids: Optional[List[str]] = Field(
|
||||
None, description="Target IDs to capture (all if omitted)"
|
||||
)
|
||||
target_ids: List[str] | None = Field(None, description="Target IDs to capture (all if omitted)")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -38,20 +36,20 @@ class ScenePresetCreate(BaseModel):
|
||||
class ScenePresetUpdate(BaseModel):
|
||||
"""Update scene preset metadata and optionally change which targets are included."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
order: Optional[int] = None
|
||||
target_ids: Optional[List[str]] = Field(
|
||||
name: str | None = Field(None, min_length=1, max_length=100)
|
||||
description: str | None = Field(None, max_length=500)
|
||||
order: int | None = None
|
||||
target_ids: List[str] | None = Field(
|
||||
None,
|
||||
description="Update target list: keep state for existing, capture fresh for new, drop removed",
|
||||
)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -67,12 +65,12 @@ class ScenePresetResponse(BaseModel):
|
||||
targets: List[TargetSnapshotSchema]
|
||||
order: int
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Sync clock schemas (CRUD + control)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -11,14 +11,14 @@ class SyncClockCreate(BaseModel):
|
||||
|
||||
name: str = Field(description="Clock name", min_length=1, max_length=100)
|
||||
speed: float = Field(default=1.0, description="Speed multiplier (0.1–10.0)", ge=0.1, le=10.0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
@@ -28,16 +28,16 @@ class SyncClockCreate(BaseModel):
|
||||
class SyncClockUpdate(BaseModel):
|
||||
"""Request to update a synchronization clock."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Clock name", min_length=1, max_length=100)
|
||||
speed: Optional[float] = Field(None, description="Speed multiplier (0.1–10.0)", ge=0.1, le=10.0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Clock name", min_length=1, max_length=100)
|
||||
speed: float | None = Field(None, description="Speed multiplier (0.1–10.0)", ge=0.1, le=10.0)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
@@ -50,14 +50,14 @@ class SyncClockResponse(BaseModel):
|
||||
id: str = Field(description="Clock ID")
|
||||
name: str = Field(description="Clock name")
|
||||
speed: float = Field(description="Speed multiplier")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Capture template and engine schemas."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -12,14 +12,14 @@ class TemplateCreate(BaseModel):
|
||||
name: str = Field(description="Template name", min_length=1, max_length=100)
|
||||
engine_type: str = Field(description="Engine type (e.g., 'mss', 'dxcam', 'wgc')", min_length=1)
|
||||
engine_config: Dict = Field(default_factory=dict, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -29,17 +29,17 @@ class TemplateCreate(BaseModel):
|
||||
class TemplateUpdate(BaseModel):
|
||||
"""Request to update a template."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
engine_type: Optional[str] = Field(None, description="Capture engine type (mss, dxcam, wgc)")
|
||||
engine_config: Optional[Dict] = Field(None, description="Engine-specific configuration")
|
||||
description: Optional[str] = Field(None, description="Template description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Template name", min_length=1, max_length=100)
|
||||
engine_type: str | None = Field(None, description="Capture engine type (mss, dxcam, wgc)")
|
||||
engine_config: Dict | None = Field(None, description="Engine-specific configuration")
|
||||
description: str | None = Field(None, description="Template description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Empty/null inherits the channel accent.",
|
||||
@@ -56,11 +56,11 @@ class TemplateResponse(BaseModel):
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
description: Optional[str] = Field(None, description="Template description")
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Template description")
|
||||
icon: str | None = Field(
|
||||
None, max_length=64, description="Icon id from the curated icon library."
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None, max_length=32, description="Optional CSS color override for the icon."
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Value source schemas — discriminated unions per source type."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated, List, Literal, Optional, Union
|
||||
from typing import Annotated, List, Literal
|
||||
|
||||
from pydantic import BaseModel, Discriminator, Field, Tag
|
||||
|
||||
@@ -15,14 +15,14 @@ class _ValueSourceResponseBase(BaseModel):
|
||||
|
||||
id: str = Field(description="Source ID")
|
||||
name: str = Field(description="Source name")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
@@ -100,7 +100,7 @@ class AnimatedColorValueSourceResponse(_ValueSourceResponseBase):
|
||||
colors: List[List[int]] = Field(description="Color list [[R,G,B], ...]")
|
||||
speed: float = Field(description="Cycles per minute (ignored when clock_id is set)")
|
||||
easing: str = Field(description="Color easing: linear|step|ease_in|ease_out|ease_in_out|sine")
|
||||
clock_id: Optional[str] = Field(
|
||||
clock_id: str | None = Field(
|
||||
None, description="Optional sync clock ID for shared timing (overrides speed)"
|
||||
)
|
||||
|
||||
@@ -163,22 +163,20 @@ class HTTPValueSourceResponse(_ValueSourceResponseBase):
|
||||
|
||||
|
||||
ValueSourceResponse = Annotated[
|
||||
Union[
|
||||
Annotated[StaticValueSourceResponse, Tag("static")],
|
||||
Annotated[AnimatedValueSourceResponse, Tag("animated")],
|
||||
Annotated[AudioValueSourceResponse, Tag("audio")],
|
||||
Annotated[AdaptiveTimeValueSourceResponse, Tag("adaptive_time")],
|
||||
Annotated[AdaptiveSceneValueSourceResponse, Tag("adaptive_scene")],
|
||||
Annotated[DaylightValueSourceResponse, Tag("daylight")],
|
||||
Annotated[StaticColorValueSourceResponse, Tag("static_color")],
|
||||
Annotated[AnimatedColorValueSourceResponse, Tag("animated_color")],
|
||||
Annotated[AdaptiveTimeColorValueSourceResponse, Tag("adaptive_time_color")],
|
||||
Annotated[HAEntityValueSourceResponse, Tag("ha_entity")],
|
||||
Annotated[GradientMapValueSourceResponse, Tag("gradient_map")],
|
||||
Annotated[CSSExtractValueSourceResponse, Tag("css_extract")],
|
||||
Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")],
|
||||
Annotated[HTTPValueSourceResponse, Tag("http")],
|
||||
],
|
||||
Annotated[StaticValueSourceResponse, Tag("static")]
|
||||
| Annotated[AnimatedValueSourceResponse, Tag("animated")]
|
||||
| Annotated[AudioValueSourceResponse, Tag("audio")]
|
||||
| Annotated[AdaptiveTimeValueSourceResponse, Tag("adaptive_time")]
|
||||
| Annotated[AdaptiveSceneValueSourceResponse, Tag("adaptive_scene")]
|
||||
| Annotated[DaylightValueSourceResponse, Tag("daylight")]
|
||||
| Annotated[StaticColorValueSourceResponse, Tag("static_color")]
|
||||
| Annotated[AnimatedColorValueSourceResponse, Tag("animated_color")]
|
||||
| Annotated[AdaptiveTimeColorValueSourceResponse, Tag("adaptive_time_color")]
|
||||
| Annotated[HAEntityValueSourceResponse, Tag("ha_entity")]
|
||||
| Annotated[GradientMapValueSourceResponse, Tag("gradient_map")]
|
||||
| Annotated[CSSExtractValueSourceResponse, Tag("css_extract")]
|
||||
| Annotated[SystemMetricsValueSourceResponse, Tag("system_metrics")]
|
||||
| Annotated[HTTPValueSourceResponse, Tag("http")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -191,14 +189,14 @@ class _ValueSourceCreateBase(BaseModel):
|
||||
"""Shared fields for all value source create requests."""
|
||||
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
@@ -276,7 +274,7 @@ class AnimatedColorValueSourceCreate(_ValueSourceCreateBase):
|
||||
easing: str = Field(
|
||||
"linear", description="Color easing: linear|step|ease_in|ease_out|ease_in_out|sine"
|
||||
)
|
||||
clock_id: Optional[str] = Field(
|
||||
clock_id: str | None = Field(
|
||||
None, description="Optional sync clock ID (overrides speed when set)"
|
||||
)
|
||||
|
||||
@@ -333,22 +331,20 @@ class HTTPValueSourceCreate(_ValueSourceCreateBase):
|
||||
|
||||
|
||||
ValueSourceCreate = Annotated[
|
||||
Union[
|
||||
Annotated[StaticValueSourceCreate, Tag("static")],
|
||||
Annotated[AnimatedValueSourceCreate, Tag("animated")],
|
||||
Annotated[AudioValueSourceCreate, Tag("audio")],
|
||||
Annotated[AdaptiveTimeValueSourceCreate, Tag("adaptive_time")],
|
||||
Annotated[AdaptiveSceneValueSourceCreate, Tag("adaptive_scene")],
|
||||
Annotated[DaylightValueSourceCreate, Tag("daylight")],
|
||||
Annotated[StaticColorValueSourceCreate, Tag("static_color")],
|
||||
Annotated[AnimatedColorValueSourceCreate, Tag("animated_color")],
|
||||
Annotated[AdaptiveTimeColorValueSourceCreate, Tag("adaptive_time_color")],
|
||||
Annotated[HAEntityValueSourceCreate, Tag("ha_entity")],
|
||||
Annotated[GradientMapValueSourceCreate, Tag("gradient_map")],
|
||||
Annotated[CSSExtractValueSourceCreate, Tag("css_extract")],
|
||||
Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")],
|
||||
Annotated[HTTPValueSourceCreate, Tag("http")],
|
||||
],
|
||||
Annotated[StaticValueSourceCreate, Tag("static")]
|
||||
| Annotated[AnimatedValueSourceCreate, Tag("animated")]
|
||||
| Annotated[AudioValueSourceCreate, Tag("audio")]
|
||||
| Annotated[AdaptiveTimeValueSourceCreate, Tag("adaptive_time")]
|
||||
| Annotated[AdaptiveSceneValueSourceCreate, Tag("adaptive_scene")]
|
||||
| Annotated[DaylightValueSourceCreate, Tag("daylight")]
|
||||
| Annotated[StaticColorValueSourceCreate, Tag("static_color")]
|
||||
| Annotated[AnimatedColorValueSourceCreate, Tag("animated_color")]
|
||||
| Annotated[AdaptiveTimeColorValueSourceCreate, Tag("adaptive_time_color")]
|
||||
| Annotated[HAEntityValueSourceCreate, Tag("ha_entity")]
|
||||
| Annotated[GradientMapValueSourceCreate, Tag("gradient_map")]
|
||||
| Annotated[CSSExtractValueSourceCreate, Tag("css_extract")]
|
||||
| Annotated[SystemMetricsValueSourceCreate, Tag("system_metrics")]
|
||||
| Annotated[HTTPValueSourceCreate, Tag("http")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
@@ -360,15 +356,15 @@ ValueSourceCreate = Annotated[
|
||||
class _ValueSourceUpdateBase(BaseModel):
|
||||
"""Shared fields for all value source update requests."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
@@ -377,142 +373,138 @@ class _ValueSourceUpdateBase(BaseModel):
|
||||
|
||||
class StaticValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["static"] = "static"
|
||||
value: Optional[float] = Field(None, description="Constant value (0.0-1.0)", ge=0.0, le=1.0)
|
||||
value: float | None = Field(None, description="Constant value (0.0-1.0)", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class AnimatedValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["animated"] = "animated"
|
||||
waveform: Optional[str] = Field(None, description="Waveform: sine|triangle|square|sawtooth")
|
||||
speed: Optional[float] = Field(None, description="Cycles per minute", ge=0.1, le=120.0)
|
||||
min_value: Optional[float] = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: Optional[float] = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
waveform: str | None = Field(None, description="Waveform: sine|triangle|square|sawtooth")
|
||||
speed: float | None = Field(None, description="Cycles per minute", ge=0.1, le=120.0)
|
||||
min_value: float | None = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class AudioValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["audio"] = "audio"
|
||||
audio_source_id: Optional[str] = Field(None, description="Mono audio source ID")
|
||||
mode: Optional[str] = Field(None, description="Audio mode: rms|peak|beat")
|
||||
sensitivity: Optional[float] = Field(None, description="Gain multiplier", ge=0.1, le=20.0)
|
||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing", ge=0.0, le=1.0)
|
||||
min_value: Optional[float] = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: Optional[float] = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
auto_gain: Optional[bool] = Field(None, description="Auto-normalize audio levels")
|
||||
audio_source_id: str | None = Field(None, description="Mono audio source ID")
|
||||
mode: str | None = Field(None, description="Audio mode: rms|peak|beat")
|
||||
sensitivity: float | None = Field(None, description="Gain multiplier", ge=0.1, le=20.0)
|
||||
smoothing: float | None = Field(None, description="Temporal smoothing", ge=0.0, le=1.0)
|
||||
min_value: float | None = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
auto_gain: bool | None = Field(None, description="Auto-normalize audio levels")
|
||||
|
||||
|
||||
class AdaptiveTimeValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["adaptive_time"] = "adaptive_time"
|
||||
schedule: Optional[list] = Field(None, description="Time-of-day schedule")
|
||||
min_value: Optional[float] = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: Optional[float] = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
schedule: list | None = Field(None, description="Time-of-day schedule")
|
||||
min_value: float | None = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class AdaptiveSceneValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["adaptive_scene"] = "adaptive_scene"
|
||||
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
|
||||
scene_behavior: Optional[str] = Field(None, description="Scene behavior")
|
||||
sensitivity: Optional[float] = Field(None, description="Gain multiplier", ge=0.1, le=20.0)
|
||||
smoothing: Optional[float] = Field(None, description="Temporal smoothing", ge=0.0, le=1.0)
|
||||
min_value: Optional[float] = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: Optional[float] = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
picture_source_id: str | None = Field(None, description="Picture source ID")
|
||||
scene_behavior: str | None = Field(None, description="Scene behavior")
|
||||
sensitivity: float | None = Field(None, description="Gain multiplier", ge=0.1, le=20.0)
|
||||
smoothing: float | None = Field(None, description="Temporal smoothing", ge=0.0, le=1.0)
|
||||
min_value: float | None = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class DaylightValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["daylight"] = "daylight"
|
||||
speed: Optional[float] = Field(None, description="Simulation speed", ge=0.1, le=120.0)
|
||||
use_real_time: Optional[bool] = Field(None, description="Use wall-clock time")
|
||||
latitude: Optional[float] = Field(None, description="Geographic latitude", ge=-90.0, le=90.0)
|
||||
longitude: Optional[float] = Field(
|
||||
None, description="Geographic longitude", ge=-180.0, le=180.0
|
||||
)
|
||||
min_value: Optional[float] = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: Optional[float] = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
speed: float | None = Field(None, description="Simulation speed", ge=0.1, le=120.0)
|
||||
use_real_time: bool | None = Field(None, description="Use wall-clock time")
|
||||
latitude: float | None = Field(None, description="Geographic latitude", ge=-90.0, le=90.0)
|
||||
longitude: float | None = Field(None, description="Geographic longitude", ge=-180.0, le=180.0)
|
||||
min_value: float | None = Field(None, description="Minimum output", ge=0.0, le=1.0)
|
||||
max_value: float | None = Field(None, description="Maximum output", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class StaticColorValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["static_color"] = "static_color"
|
||||
color: Optional[List[int]] = Field(None, description="Static RGB color [R,G,B]")
|
||||
color: List[int] | None = Field(None, description="Static RGB color [R,G,B]")
|
||||
|
||||
|
||||
class AnimatedColorValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["animated_color"] = "animated_color"
|
||||
colors: Optional[List[List[int]]] = Field(None, description="Color list [[R,G,B], ...]")
|
||||
speed: Optional[float] = Field(None, description="Cycles per minute", ge=0.1, le=120.0)
|
||||
easing: Optional[str] = Field(
|
||||
colors: List[List[int]] | None = Field(None, description="Color list [[R,G,B], ...]")
|
||||
speed: float | None = Field(None, description="Cycles per minute", ge=0.1, le=120.0)
|
||||
easing: str | None = Field(
|
||||
None, description="Color easing: linear|step|ease_in|ease_out|ease_in_out|sine"
|
||||
)
|
||||
clock_id: Optional[str] = Field(
|
||||
clock_id: str | None = Field(
|
||||
None, description="Optional sync clock ID (empty string clears, null leaves unchanged)"
|
||||
)
|
||||
|
||||
|
||||
class AdaptiveTimeColorValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["adaptive_time_color"] = "adaptive_time_color"
|
||||
schedule: Optional[list] = Field(None, description="Color schedule")
|
||||
schedule: list | None = Field(None, description="Color schedule")
|
||||
|
||||
|
||||
class HAEntityValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["ha_entity"] = "ha_entity"
|
||||
ha_source_id: Optional[str] = Field(None, description="Home Assistant source ID")
|
||||
entity_id: Optional[str] = Field(None, description="HA entity ID")
|
||||
attribute: Optional[str] = Field(None, description="Attribute name")
|
||||
min_ha_value: Optional[float] = Field(None, description="Min HA value")
|
||||
max_ha_value: Optional[float] = Field(None, description="Max HA value")
|
||||
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||
ha_source_id: str | None = Field(None, description="Home Assistant source ID")
|
||||
entity_id: str | None = Field(None, description="HA entity ID")
|
||||
attribute: str | None = Field(None, description="Attribute name")
|
||||
min_ha_value: float | None = Field(None, description="Min HA value")
|
||||
max_ha_value: float | None = Field(None, description="Max HA value")
|
||||
smoothing: float | None = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class GradientMapValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["gradient_map"] = "gradient_map"
|
||||
value_source_id: Optional[str] = Field(None, description="Input value source ID")
|
||||
gradient_id: Optional[str] = Field(None, description="Gradient entity ID")
|
||||
easing: Optional[str] = Field(None, description="Interpolation mode")
|
||||
value_source_id: str | None = Field(None, description="Input value source ID")
|
||||
gradient_id: str | None = Field(None, description="Gradient entity ID")
|
||||
easing: str | None = Field(None, description="Interpolation mode")
|
||||
|
||||
|
||||
class CSSExtractValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["css_extract"] = "css_extract"
|
||||
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
|
||||
led_start: Optional[int] = Field(None, description="LED range start", ge=0)
|
||||
led_end: Optional[int] = Field(None, description="LED range end")
|
||||
color_strip_source_id: str | None = Field(None, description="Color strip source ID")
|
||||
led_start: int | None = Field(None, description="LED range start", ge=0)
|
||||
led_end: int | None = Field(None, description="LED range end")
|
||||
|
||||
|
||||
class SystemMetricsValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["system_metrics"] = "system_metrics"
|
||||
metric: Optional[str] = Field(None, description="System metric")
|
||||
min_value: Optional[float] = Field(None, description="Min value")
|
||||
max_value: Optional[float] = Field(None, description="Max value")
|
||||
max_rate: Optional[float] = Field(None, description="Max rate bytes/sec")
|
||||
disk_path: Optional[str] = Field(None, description="Disk path")
|
||||
sensor_label: Optional[str] = Field(None, description="Sensor label")
|
||||
poll_interval: Optional[float] = Field(None, description="Poll interval", ge=0.1, le=60.0)
|
||||
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||
metric: str | None = Field(None, description="System metric")
|
||||
min_value: float | None = Field(None, description="Min value")
|
||||
max_value: float | None = Field(None, description="Max value")
|
||||
max_rate: float | None = Field(None, description="Max rate bytes/sec")
|
||||
disk_path: str | None = Field(None, description="Disk path")
|
||||
sensor_label: str | None = Field(None, description="Sensor label")
|
||||
poll_interval: float | None = Field(None, description="Poll interval", ge=0.1, le=60.0)
|
||||
smoothing: float | None = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
class HTTPValueSourceUpdate(_ValueSourceUpdateBase):
|
||||
source_type: Literal["http"] = "http"
|
||||
http_endpoint_id: Optional[str] = Field(None, description="HTTP endpoint ID")
|
||||
json_path: Optional[str] = Field(None, description="Dot-path into the response")
|
||||
interval_s: Optional[int] = Field(None, description="Polling cadence (seconds)", ge=1)
|
||||
min_value: Optional[float] = Field(None, description="Raw value mapped to 0.0")
|
||||
max_value: Optional[float] = Field(None, description="Raw value mapped to 1.0")
|
||||
smoothing: Optional[float] = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||
http_endpoint_id: str | None = Field(None, description="HTTP endpoint ID")
|
||||
json_path: str | None = Field(None, description="Dot-path into the response")
|
||||
interval_s: int | None = Field(None, description="Polling cadence (seconds)", ge=1)
|
||||
min_value: float | None = Field(None, description="Raw value mapped to 0.0")
|
||||
max_value: float | None = Field(None, description="Raw value mapped to 1.0")
|
||||
smoothing: float | None = Field(None, description="EMA smoothing", ge=0.0, le=1.0)
|
||||
|
||||
|
||||
ValueSourceUpdate = Annotated[
|
||||
Union[
|
||||
Annotated[StaticValueSourceUpdate, Tag("static")],
|
||||
Annotated[AnimatedValueSourceUpdate, Tag("animated")],
|
||||
Annotated[AudioValueSourceUpdate, Tag("audio")],
|
||||
Annotated[AdaptiveTimeValueSourceUpdate, Tag("adaptive_time")],
|
||||
Annotated[AdaptiveSceneValueSourceUpdate, Tag("adaptive_scene")],
|
||||
Annotated[DaylightValueSourceUpdate, Tag("daylight")],
|
||||
Annotated[StaticColorValueSourceUpdate, Tag("static_color")],
|
||||
Annotated[AnimatedColorValueSourceUpdate, Tag("animated_color")],
|
||||
Annotated[AdaptiveTimeColorValueSourceUpdate, Tag("adaptive_time_color")],
|
||||
Annotated[HAEntityValueSourceUpdate, Tag("ha_entity")],
|
||||
Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")],
|
||||
Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")],
|
||||
Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")],
|
||||
Annotated[HTTPValueSourceUpdate, Tag("http")],
|
||||
],
|
||||
Annotated[StaticValueSourceUpdate, Tag("static")]
|
||||
| Annotated[AnimatedValueSourceUpdate, Tag("animated")]
|
||||
| Annotated[AudioValueSourceUpdate, Tag("audio")]
|
||||
| Annotated[AdaptiveTimeValueSourceUpdate, Tag("adaptive_time")]
|
||||
| Annotated[AdaptiveSceneValueSourceUpdate, Tag("adaptive_scene")]
|
||||
| Annotated[DaylightValueSourceUpdate, Tag("daylight")]
|
||||
| Annotated[StaticColorValueSourceUpdate, Tag("static_color")]
|
||||
| Annotated[AnimatedColorValueSourceUpdate, Tag("animated_color")]
|
||||
| Annotated[AdaptiveTimeColorValueSourceUpdate, Tag("adaptive_time_color")]
|
||||
| Annotated[HAEntityValueSourceUpdate, Tag("ha_entity")]
|
||||
| Annotated[GradientMapValueSourceUpdate, Tag("gradient_map")]
|
||||
| Annotated[CSSExtractValueSourceUpdate, Tag("css_extract")]
|
||||
| Annotated[SystemMetricsValueSourceUpdate, Tag("system_metrics")]
|
||||
| Annotated[HTTPValueSourceUpdate, Tag("http")],
|
||||
Discriminator("source_type"),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Weather source schemas (CRUD)."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Literal, Optional
|
||||
from typing import Dict, List, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -13,7 +13,7 @@ class WeatherSourceCreate(BaseModel):
|
||||
provider: Literal["open_meteo"] = Field(
|
||||
default="open_meteo", description="Weather data provider"
|
||||
)
|
||||
provider_config: Optional[Dict] = Field(None, description="Provider-specific configuration")
|
||||
provider_config: Dict | None = Field(None, description="Provider-specific configuration")
|
||||
latitude: float = Field(
|
||||
default=50.0, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
|
||||
)
|
||||
@@ -23,14 +23,14 @@ class WeatherSourceCreate(BaseModel):
|
||||
update_interval: int = Field(
|
||||
default=600, description="API poll interval in seconds (60-3600)", ge=60, le=3600
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon (e.g. '#4CAF50'). Empty/null inherits the channel accent.",
|
||||
@@ -40,26 +40,26 @@ class WeatherSourceCreate(BaseModel):
|
||||
class WeatherSourceUpdate(BaseModel):
|
||||
"""Request to update a weather source."""
|
||||
|
||||
name: Optional[str] = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
provider: Optional[Literal["open_meteo"]] = Field(None, description="Weather data provider")
|
||||
provider_config: Optional[Dict] = Field(None, description="Provider-specific configuration")
|
||||
latitude: Optional[float] = Field(
|
||||
name: str | None = Field(None, description="Source name", min_length=1, max_length=100)
|
||||
provider: Literal["open_meteo"] | None = Field(None, description="Weather data provider")
|
||||
provider_config: Dict | None = Field(None, description="Provider-specific configuration")
|
||||
latitude: float | None = Field(
|
||||
None, description="Geographic latitude (-90 to 90)", ge=-90.0, le=90.0
|
||||
)
|
||||
longitude: Optional[float] = Field(
|
||||
longitude: float | None = Field(
|
||||
None, description="Geographic longitude (-180 to 180)", ge=-180.0, le=180.0
|
||||
)
|
||||
update_interval: Optional[int] = Field(
|
||||
update_interval: int | None = Field(
|
||||
None, description="API poll interval in seconds (60-3600)", ge=60, le=3600
|
||||
)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
tags: Optional[List[str]] = None
|
||||
icon: Optional[str] = Field(
|
||||
description: str | None = Field(None, description="Optional description", max_length=500)
|
||||
tags: List[str] | None = None
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library. Pass empty string to clear.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon. Pass empty string to inherit the channel accent.",
|
||||
@@ -78,14 +78,14 @@ class WeatherSourceResponse(BaseModel):
|
||||
latitude: float = Field(description="Geographic latitude")
|
||||
longitude: float = Field(description="Geographic longitude")
|
||||
update_interval: int = Field(description="API poll interval in seconds")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
description: str | None = Field(None, description="Description")
|
||||
tags: List[str] = Field(default_factory=list, description="User-defined tags")
|
||||
icon: Optional[str] = Field(
|
||||
icon: str | None = Field(
|
||||
None,
|
||||
max_length=64,
|
||||
description="Icon id from the curated icon library.",
|
||||
)
|
||||
icon_color: Optional[str] = Field(
|
||||
icon_color: str | None = Field(
|
||||
None,
|
||||
max_length=32,
|
||||
description="Optional CSS color override for the icon.",
|
||||
|
||||
@@ -11,7 +11,7 @@ capture stream (WASAPI, sounddevice, etc.).
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from ledgrab.core.audio.analysis import (
|
||||
AudioAnalysis,
|
||||
@@ -49,7 +49,7 @@ class ManagedAudioStream:
|
||||
engine_type: str,
|
||||
device_index: int,
|
||||
is_loopback: bool,
|
||||
engine_config: Optional[Dict[str, Any]] = None,
|
||||
engine_config: Dict[str, Any] | None = None,
|
||||
):
|
||||
self._engine_type = engine_type
|
||||
self._device_index = device_index
|
||||
@@ -57,9 +57,9 @@ class ManagedAudioStream:
|
||||
self._engine_config = engine_config or {}
|
||||
|
||||
self._running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._lock = threading.Lock()
|
||||
self._latest: Optional[AudioAnalysis] = None
|
||||
self._latest: AudioAnalysis | None = None
|
||||
self._last_timing: dict = {}
|
||||
|
||||
def start(self) -> None:
|
||||
@@ -90,7 +90,7 @@ class ManagedAudioStream:
|
||||
f"device={self._device_index}"
|
||||
)
|
||||
|
||||
def get_latest_analysis(self) -> Optional[AudioAnalysis]:
|
||||
def get_latest_analysis(self) -> AudioAnalysis | None:
|
||||
with self._lock:
|
||||
return self._latest
|
||||
|
||||
@@ -98,7 +98,7 @@ class ManagedAudioStream:
|
||||
return dict(self._last_timing)
|
||||
|
||||
def _capture_loop(self) -> None:
|
||||
stream: Optional[AudioCaptureStreamBase] = None
|
||||
stream: AudioCaptureStreamBase | None = None
|
||||
try:
|
||||
stream = AudioEngineRegistry.create_stream(
|
||||
self._engine_type,
|
||||
@@ -178,8 +178,8 @@ class AudioCaptureManager:
|
||||
self,
|
||||
device_index: int,
|
||||
is_loopback: bool,
|
||||
engine_type: Optional[str] = None,
|
||||
engine_config: Optional[Dict[str, Any]] = None,
|
||||
engine_type: str | None = None,
|
||||
engine_config: Dict[str, Any] | None = None,
|
||||
) -> ManagedAudioStream:
|
||||
"""Get or create a ManagedAudioStream for the given device.
|
||||
|
||||
@@ -220,7 +220,7 @@ class AudioCaptureManager:
|
||||
self,
|
||||
device_index: int,
|
||||
is_loopback: bool,
|
||||
engine_type: Optional[str] = None,
|
||||
engine_type: str | None = None,
|
||||
) -> None:
|
||||
"""Release a reference to a ManagedAudioStream."""
|
||||
if engine_type is None:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -83,7 +83,7 @@ class AudioCaptureStreamBase(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read_chunk(self) -> Optional[np.ndarray]:
|
||||
def read_chunk(self) -> np.ndarray | None:
|
||||
"""Read one chunk of raw audio data.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Demo audio engine — virtual audio devices with synthetic audio data."""
|
||||
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -62,7 +62,7 @@ class DemoAudioCaptureStream(AudioCaptureStreamBase):
|
||||
self._initialized = False
|
||||
logger.info(f"Demo audio stream cleaned up (device={self.device_index})")
|
||||
|
||||
def read_chunk(self) -> Optional[np.ndarray]:
|
||||
def read_chunk(self) -> np.ndarray | None:
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Engine registry and factory for audio capture engines."""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
from typing import Any, Dict, List, Type
|
||||
|
||||
from ledgrab.core.audio.base import AudioCaptureEngine, AudioCaptureStreamBase
|
||||
from ledgrab.config import is_demo_mode
|
||||
@@ -82,7 +82,7 @@ class AudioEngineRegistry:
|
||||
return available
|
||||
|
||||
@classmethod
|
||||
def get_best_available_engine(cls) -> Optional[str]:
|
||||
def get_best_available_engine(cls) -> str | None:
|
||||
"""Get the highest-priority available engine type.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -8,7 +8,6 @@ from ledgrab.core.audio.filters.base import AudioFilter, AudioFilterOptionDef
|
||||
from ledgrab.core.audio.filters.registry import AudioFilterRegistry
|
||||
from ledgrab.core.audio.band_filter import apply_band_filter, compute_band_mask
|
||||
|
||||
|
||||
# Preset frequency ranges
|
||||
_PRESETS = {
|
||||
"bass": (20.0, 250.0),
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from ledgrab.core.audio.analysis import AudioAnalysis
|
||||
|
||||
@@ -20,8 +20,8 @@ class AudioFilterOptionDef:
|
||||
min_value: Any
|
||||
max_value: Any
|
||||
step: Any
|
||||
choices: Optional[List[Dict[str, str]]] = None # for "select": [{value, label}]
|
||||
max_length: Optional[int] = None # for "string" type
|
||||
choices: List[Dict[str, str]] | None = None # for "select": [{value, label}]
|
||||
max_length: int | None = None # for "string" type
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Sounddevice audio capture engine (cross-platform, via PortAudio)."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -79,7 +79,7 @@ class SounddeviceCaptureStream(AudioCaptureStreamBase):
|
||||
self._sd_stream = None
|
||||
self._initialized = False
|
||||
|
||||
def read_chunk(self) -> Optional[np.ndarray]:
|
||||
def read_chunk(self) -> np.ndarray | None:
|
||||
if self._sd_stream is None:
|
||||
return None
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""WASAPI audio capture engine (Windows only, via PyAudioWPatch)."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -98,7 +98,7 @@ class WasapiCaptureStream(AudioCaptureStreamBase):
|
||||
self._pa = None
|
||||
self._initialized = False
|
||||
|
||||
def read_chunk(self) -> Optional[np.ndarray]:
|
||||
def read_chunk(self) -> np.ndarray | None:
|
||||
if self._stream is None:
|
||||
return None
|
||||
try:
|
||||
@@ -109,7 +109,7 @@ class WasapiCaptureStream(AudioCaptureStreamBase):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find_loopback_device(pa, output_device_index: int) -> Optional[dict]:
|
||||
def _find_loopback_device(pa, output_device_index: int) -> dict | None:
|
||||
"""Find the PyAudioWPatch loopback device for a given output device."""
|
||||
try:
|
||||
first_loopback = None
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Callable, Dict, Optional, Set
|
||||
from typing import Callable, Dict, Set
|
||||
|
||||
from ledgrab.core.automations.platform_detector import PlatformDetector
|
||||
from ledgrab.storage.automation import (
|
||||
@@ -38,11 +38,11 @@ class _RuleEvalContext:
|
||||
"""
|
||||
|
||||
running_procs: Set[str]
|
||||
topmost_proc: Optional[str]
|
||||
topmost_proc: str | None
|
||||
topmost_fullscreen: bool
|
||||
fullscreen_procs: Set[str]
|
||||
idle_seconds: Optional[float]
|
||||
display_state: Optional[str]
|
||||
idle_seconds: float | None
|
||||
display_state: str | None
|
||||
|
||||
|
||||
def _apply_operator(operator: str, extracted, expected: str) -> bool:
|
||||
@@ -101,7 +101,7 @@ class AutomationEngine:
|
||||
self._target_store = target_store
|
||||
self._device_store = device_store
|
||||
self._ha_manager = ha_manager
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._task: asyncio.Task | None = None
|
||||
self._eval_lock = asyncio.Lock()
|
||||
|
||||
# Runtime state (not persisted)
|
||||
@@ -420,11 +420,11 @@ class AutomationEngine:
|
||||
self,
|
||||
automation: Automation,
|
||||
running_procs: Set[str],
|
||||
topmost_proc: Optional[str],
|
||||
topmost_proc: str | None,
|
||||
topmost_fullscreen: bool,
|
||||
fullscreen_procs: Set[str],
|
||||
idle_seconds: Optional[float],
|
||||
display_state: Optional[str],
|
||||
idle_seconds: float | None,
|
||||
display_state: str | None,
|
||||
) -> bool:
|
||||
results = [
|
||||
self._evaluate_rule(
|
||||
@@ -453,11 +453,11 @@ class AutomationEngine:
|
||||
self,
|
||||
rule: Rule,
|
||||
running_procs: Set[str],
|
||||
topmost_proc: Optional[str],
|
||||
topmost_proc: str | None,
|
||||
topmost_fullscreen: bool,
|
||||
fullscreen_procs: Set[str],
|
||||
idle_seconds: Optional[float],
|
||||
display_state: Optional[str],
|
||||
idle_seconds: float | None,
|
||||
display_state: str | None,
|
||||
) -> bool:
|
||||
ctx = _RuleEvalContext(
|
||||
running_procs=running_procs,
|
||||
@@ -531,14 +531,14 @@ class AutomationEngine:
|
||||
return current >= start or current <= end
|
||||
|
||||
@staticmethod
|
||||
def _evaluate_idle(rule: SystemIdleRule, idle_seconds: Optional[float]) -> bool:
|
||||
def _evaluate_idle(rule: SystemIdleRule, idle_seconds: float | None) -> bool:
|
||||
if idle_seconds is None:
|
||||
return False
|
||||
is_idle = idle_seconds >= (rule.idle_minutes * 60)
|
||||
return is_idle if rule.when_idle else not is_idle
|
||||
|
||||
@staticmethod
|
||||
def _evaluate_display_state(rule: DisplayStateRule, display_state: Optional[str]) -> bool:
|
||||
def _evaluate_display_state(rule: DisplayStateRule, display_state: str | None) -> bool:
|
||||
if display_state is None:
|
||||
return False
|
||||
return display_state == rule.state
|
||||
@@ -612,7 +612,7 @@ class AutomationEngine:
|
||||
self,
|
||||
rule: ApplicationRule,
|
||||
running_procs: Set[str],
|
||||
topmost_proc: Optional[str],
|
||||
topmost_proc: str | None,
|
||||
topmost_fullscreen: bool,
|
||||
fullscreen_procs: Set[str],
|
||||
) -> bool:
|
||||
|
||||
@@ -9,7 +9,7 @@ import ctypes
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from typing import Optional, Set
|
||||
from typing import Set
|
||||
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
@@ -164,7 +164,7 @@ class PlatformDetector:
|
||||
except Exception as e:
|
||||
logger.error(f"Display power listener failed: {e}")
|
||||
|
||||
def _get_display_power_state_sync(self) -> Optional[str]:
|
||||
def _get_display_power_state_sync(self) -> str | None:
|
||||
"""Get display power state: 'on' or 'off'. Returns None if unavailable."""
|
||||
if not _IS_WINDOWS:
|
||||
return None
|
||||
@@ -172,7 +172,7 @@ class PlatformDetector:
|
||||
|
||||
# ---- System idle detection ----
|
||||
|
||||
def _get_idle_seconds_sync(self) -> Optional[float]:
|
||||
def _get_idle_seconds_sync(self) -> float | None:
|
||||
"""Get system idle time in seconds (keyboard/mouse inactivity).
|
||||
|
||||
Returns None if detection is unavailable.
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from ledgrab.storage.database import Database
|
||||
from ledgrab.utils import get_logger
|
||||
@@ -33,8 +33,8 @@ class AutoBackupEngine:
|
||||
):
|
||||
self._backup_dir = Path(backup_dir)
|
||||
self._db = db
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
self._last_backup_time: Optional[datetime] = None
|
||||
self._task: asyncio.Task | None = None
|
||||
self._last_backup_time: datetime | None = None
|
||||
|
||||
self._settings = self._load_settings()
|
||||
self._backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Pixel processing utilities for color correction and manipulation."""
|
||||
|
||||
from typing import List, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
import numpy as np
|
||||
|
||||
from ledgrab.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
ColorList = Union[List[Tuple[int, int, int]], np.ndarray]
|
||||
ColorList = List[Tuple[int, int, int]] | np.ndarray
|
||||
|
||||
|
||||
def _as_array(colors: ColorList) -> np.ndarray:
|
||||
|
||||
@@ -6,7 +6,7 @@ import colorsys
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import tkinter as tk
|
||||
@@ -41,8 +41,8 @@ class OverlayWindow:
|
||||
self.calibration = calibration
|
||||
self.target_id = target_id
|
||||
self.target_name = target_name or target_id
|
||||
self._window: Optional[tk.Toplevel] = None
|
||||
self._canvas: Optional[tk.Canvas] = None
|
||||
self._window: tk.Toplevel | None = None
|
||||
self._canvas: tk.Canvas | None = None
|
||||
self.running = False
|
||||
|
||||
# ----- Lifecycle (must run in Tk thread) -----
|
||||
@@ -352,8 +352,8 @@ class OverlayManager:
|
||||
def __init__(self):
|
||||
self._overlays: Dict[str, OverlayWindow] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._tk_root: Optional[tk.Tk] = None
|
||||
self._tk_thread: Optional[threading.Thread] = None
|
||||
self._tk_root: tk.Tk | None = None
|
||||
self._tk_thread: threading.Thread | None = None
|
||||
self._tk_ready = threading.Event()
|
||||
self._start_tk_thread()
|
||||
|
||||
@@ -386,7 +386,7 @@ class OverlayManager:
|
||||
if self._tk_root is None:
|
||||
raise RuntimeError("Tkinter root not available")
|
||||
done = threading.Event()
|
||||
exc_box: List[Optional[BaseException]] = [None]
|
||||
exc_box: List[BaseException | None] = [None]
|
||||
|
||||
def wrapper():
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -70,7 +70,7 @@ class CaptureStream(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
"""Capture one frame from the bound display.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
from ledgrab.core.capture_engines.base import (
|
||||
@@ -104,7 +104,7 @@ class BetterCamCaptureStream(CaptureStream):
|
||||
logger.error(f"BetterCam reinit failed (display={self.display_index}): {reinit_err}")
|
||||
return False
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import platform
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any, Dict, List, Set
|
||||
|
||||
# OpenCV's MSMF backend on Windows often fails to open the device
|
||||
# ("cap.isOpened() == False" right after VideoCapture returns) when
|
||||
@@ -50,7 +50,7 @@ _RESOLUTION_CHOICES: List[str] = [
|
||||
]
|
||||
|
||||
|
||||
def _parse_resolution(value: Any) -> Optional[tuple[int, int]]:
|
||||
def _parse_resolution(value: Any) -> tuple[int, int] | None:
|
||||
"""Parse a 'WxH' string into (width, height). Returns None for 'auto' or invalid."""
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
@@ -101,7 +101,7 @@ _BUILDINFO_LABELS: Dict[str, str] = {
|
||||
"avfoundation": "AVFoundation",
|
||||
}
|
||||
|
||||
_compiled_backends_cache: Optional[Set[str]] = None
|
||||
_compiled_backends_cache: Set[str] | None = None
|
||||
|
||||
|
||||
def _get_compiled_backends() -> Set[str]:
|
||||
@@ -169,7 +169,7 @@ def _get_supported_backends() -> List[str]:
|
||||
return ["auto", *(b for b in candidates if b in compiled)]
|
||||
|
||||
|
||||
def _cv2_backend_id(backend_name: str) -> Optional[int]:
|
||||
def _cv2_backend_id(backend_name: str) -> int | None:
|
||||
"""Convert a backend name string to cv2 API preference constant."""
|
||||
return _CV2_BACKENDS.get(backend_name)
|
||||
|
||||
@@ -307,7 +307,7 @@ def _get_camera_friendly_names() -> Dict[int, str]:
|
||||
return {}
|
||||
|
||||
|
||||
_camera_cache: Optional[List[Dict[str, Any]]] = None
|
||||
_camera_cache: List[Dict[str, Any]] | None = None
|
||||
_camera_cache_time: float = 0
|
||||
_CAMERA_CACHE_TTL = 30.0 # seconds
|
||||
|
||||
@@ -428,7 +428,7 @@ class CameraCaptureStream(CaptureStream):
|
||||
def __init__(self, display_index: int, config: Dict[str, Any]):
|
||||
super().__init__(display_index, config)
|
||||
self._cap = None
|
||||
self._cv2_index: Optional[int] = None
|
||||
self._cv2_index: int | None = None
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self._initialized:
|
||||
@@ -531,7 +531,7 @@ class CameraCaptureStream(CaptureStream):
|
||||
f"(camera={camera['name']}, cv2_idx={cv2_index}, {w}x{h})"
|
||||
)
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Demo capture engine — virtual displays with animated test patterns."""
|
||||
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -65,7 +65,7 @@ class DemoCaptureStream(CaptureStream):
|
||||
self._initialized = False
|
||||
logger.info(f"Demo capture stream cleaned up (display={self.display_index})")
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
from ledgrab.core.capture_engines.base import (
|
||||
@@ -102,7 +102,7 @@ class DXcamCaptureStream(CaptureStream):
|
||||
logger.error(f"DXcam reinit failed (display={self.display_index}): {reinit_err}")
|
||||
return False
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Engine registry and factory for screen capture engines."""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
from typing import Any, Dict, List, Type
|
||||
|
||||
from ledgrab.core.capture_engines.base import CaptureEngine, CaptureStream
|
||||
from ledgrab.config import is_demo_mode
|
||||
@@ -83,7 +83,7 @@ class EngineRegistry:
|
||||
return available
|
||||
|
||||
@classmethod
|
||||
def get_best_available_engine(cls) -> Optional[str]:
|
||||
def get_best_available_engine(cls) -> str | None:
|
||||
"""Get the highest-priority available engine type.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -29,7 +29,7 @@ logger = get_logger(__name__)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_frame_queue: queue.Queue["ScreenCapture"] = queue.Queue(maxsize=2)
|
||||
_display_info: Optional[DisplayInfo] = None
|
||||
_display_info: DisplayInfo | None = None
|
||||
_active = False
|
||||
_frames_received = 0
|
||||
_frames_consumed = 0
|
||||
@@ -141,7 +141,7 @@ class MediaProjectionCaptureStream(CaptureStream):
|
||||
self._initialized = True
|
||||
logger.info("MediaProjection capture stream initialized")
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
# Prefer fresh frames from the queue; fall back to the last
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""MSS-based screen capture engine (cross-platform)."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import mss
|
||||
import numpy as np
|
||||
@@ -41,7 +41,7 @@ class MSSCaptureStream(CaptureStream):
|
||||
self._rgb_idx: int = 0
|
||||
self._rgb_shape: tuple = (0, 0)
|
||||
# Cheap hash of the previous .raw bytes, for change detection.
|
||||
self._prev_hash: Optional[int] = None
|
||||
self._prev_hash: int | None = None
|
||||
|
||||
def initialize(self) -> None:
|
||||
try:
|
||||
@@ -59,7 +59,7 @@ class MSSCaptureStream(CaptureStream):
|
||||
self._prev_hash = None
|
||||
logger.info(f"MSS capture stream cleaned up (display={self.display_index})")
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ logger = get_logger(__name__)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_frame_queue: queue.Queue["ScreenCapture"] = queue.Queue(maxsize=2)
|
||||
_display_info: Optional[DisplayInfo] = None
|
||||
_display_info: DisplayInfo | None = None
|
||||
_active = False
|
||||
_frames_received = 0
|
||||
# screenrecord emits a full bitstream every frame (keyframes aside), so
|
||||
@@ -123,7 +123,7 @@ class RootScreenrecordCaptureStream(CaptureStream):
|
||||
self._initialized = True
|
||||
logger.info("Root screenrecord capture stream initialized")
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
try:
|
||||
|
||||
@@ -14,7 +14,7 @@ video stream. No APK installation, no root.
|
||||
"""
|
||||
|
||||
import threading
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -96,12 +96,12 @@ class ScrcpyClientCaptureStream(CaptureStream):
|
||||
|
||||
def __init__(self, display_index: int, config: Dict[str, Any]):
|
||||
super().__init__(display_index, config)
|
||||
self._client: Optional["scrcpy.Client"] = None
|
||||
self._latest_frame: Optional[ScreenCapture] = None
|
||||
self._client: "scrcpy.Client" | None = None
|
||||
self._latest_frame: ScreenCapture | None = None
|
||||
self._frame_lock = threading.Lock()
|
||||
self._frame_event = threading.Event()
|
||||
self._client_thread: Optional[threading.Thread] = None
|
||||
self._device_serial: Optional[str] = None
|
||||
self._client_thread: threading.Thread | None = None
|
||||
self._device_serial: str | None = None
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self._initialized:
|
||||
@@ -189,7 +189,7 @@ class ScrcpyClientCaptureStream(CaptureStream):
|
||||
)
|
||||
self._frame_event.set()
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -78,7 +78,7 @@ def _find_adb() -> str:
|
||||
return "adb" # last resort — will fail with FileNotFoundError
|
||||
|
||||
|
||||
_adb_path: Optional[str] = None
|
||||
_adb_path: str | None = None
|
||||
|
||||
|
||||
def _get_adb() -> str:
|
||||
@@ -158,7 +158,7 @@ def _list_adb_devices() -> List[Dict[str, Any]]:
|
||||
return devices
|
||||
|
||||
|
||||
def _screencap_once(adb: str, serial: str) -> Optional[np.ndarray]:
|
||||
def _screencap_once(adb: str, serial: str) -> np.ndarray | None:
|
||||
"""Capture a single PNG screenshot and return it as an RGB NumPy array."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -190,12 +190,12 @@ class ScrcpyCaptureStream(CaptureStream):
|
||||
|
||||
def __init__(self, display_index: int, config: Dict[str, Any]):
|
||||
super().__init__(display_index, config)
|
||||
self._capture_thread: Optional[threading.Thread] = None
|
||||
self._latest_frame: Optional[ScreenCapture] = None
|
||||
self._capture_thread: threading.Thread | None = None
|
||||
self._latest_frame: ScreenCapture | None = None
|
||||
self._frame_lock = threading.Lock()
|
||||
self._frame_event = threading.Event()
|
||||
self._running = False
|
||||
self._device_serial: Optional[str] = None
|
||||
self._device_serial: str | None = None
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self._initialized:
|
||||
@@ -281,7 +281,7 @@ class ScrcpyCaptureStream(CaptureStream):
|
||||
if poll_interval > 0:
|
||||
time.sleep(poll_interval)
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import gc
|
||||
import sys
|
||||
import threading
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -199,7 +199,7 @@ class WGCCaptureStream(CaptureStream):
|
||||
gc.collect(0)
|
||||
logger.info(f"WGC capture stream cleaned up (display={self.display_index})")
|
||||
|
||||
def capture_frame(self) -> Optional[ScreenCapture]:
|
||||
def capture_frame(self) -> ScreenCapture | None:
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Tuple
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -41,7 +41,7 @@ def _build_adalight_header(led_count: int) -> bytes:
|
||||
class AdalightClient(LEDClient):
|
||||
"""LED client for Arduino Adalight serial devices."""
|
||||
|
||||
def __init__(self, url: str, led_count: int = 0, baud_rate: Optional[int] = None, **kwargs):
|
||||
def __init__(self, url: str, led_count: int = 0, baud_rate: int | None = None, **kwargs):
|
||||
"""Initialize Adalight client.
|
||||
|
||||
Args:
|
||||
@@ -62,11 +62,11 @@ class AdalightClient(LEDClient):
|
||||
# Pre-allocated wire buffer (header + RGB payload). Resized on the
|
||||
# first frame and reused thereafter so the hot path performs no
|
||||
# allocations — only a single memcpy of the pixel bytes.
|
||||
self._frame_buf: Optional[bytearray] = None
|
||||
self._frame_buf: bytearray | None = None
|
||||
self._frame_buf_n: int = 0
|
||||
# Scratch uint8 array used to coerce non-uint8 / non-contiguous input
|
||||
# without allocating a fresh array per frame.
|
||||
self._u8_scratch: Optional[np.ndarray] = None
|
||||
self._u8_scratch: np.ndarray | None = None
|
||||
self._u8_scratch_n: int = 0
|
||||
# Dedicated single-worker executor for serial writes. Using
|
||||
# ``loop.run_in_executor`` against this avoids the per-call
|
||||
@@ -74,7 +74,7 @@ class AdalightClient(LEDClient):
|
||||
# that ``asyncio.to_thread`` incurs (~5–10 µs per call), and
|
||||
# guarantees FIFO ordering of writes from this client even when
|
||||
# other tasks are using the default executor.
|
||||
self._tx_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
||||
self._tx_executor: concurrent.futures.ThreadPoolExecutor | None = None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Open serial port and wait for Arduino reset."""
|
||||
@@ -245,7 +245,7 @@ class AdalightClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check if the serial port exists without opening it.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ so ``BLEClient`` can treat both backends identically.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from ledgrab.core.devices.ble_transport import DiscoveredBLEDevice
|
||||
from ledgrab.utils import get_logger
|
||||
@@ -49,7 +49,7 @@ async def android_ble_scan(timeout: float = 4.0) -> List[DiscoveredBLEDevice]:
|
||||
continue
|
||||
address, name, rssi_str = parts
|
||||
try:
|
||||
rssi: Optional[int] = int(rssi_str)
|
||||
rssi: int | None = int(rssi_str)
|
||||
except ValueError:
|
||||
rssi = None
|
||||
devices.append(DiscoveredBLEDevice(address=address, name=name or address, rssi=rssi))
|
||||
@@ -80,7 +80,7 @@ class AndroidBLETransport:
|
||||
self._address = address
|
||||
self._write_char_uuid = write_char_uuid
|
||||
self._write_with_response = write_with_response
|
||||
self._handle: Optional[int] = None
|
||||
self._handle: int | None = None
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
@@ -120,7 +120,7 @@ class AndroidBLETransport:
|
||||
except Exception as exc:
|
||||
logger.warning("Android BLE disconnect of %s raised: %s", self._address, exc)
|
||||
|
||||
async def write(self, data: bytes, char_uuid: Optional[str] = None) -> None:
|
||||
async def write(self, data: bytes, char_uuid: str | None = None) -> None:
|
||||
"""Write bytes to a characteristic on the connected peripheral.
|
||||
|
||||
Serialised through an internal lock — BLE stacks do not tolerate
|
||||
|
||||
@@ -8,7 +8,7 @@ optional ``@baud`` suffix).
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import List, Tuple
|
||||
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.utils.platform import is_android
|
||||
@@ -38,7 +38,7 @@ def _bridge():
|
||||
class _UsbAddress:
|
||||
vendor_id: int
|
||||
product_id: int
|
||||
serial: Optional[str]
|
||||
serial: str | None
|
||||
|
||||
@classmethod
|
||||
def parse(cls, device: str) -> "_UsbAddress":
|
||||
@@ -59,7 +59,7 @@ class _UsbAddress:
|
||||
return cls(vid, pid, serial)
|
||||
|
||||
|
||||
def _format_url(vid: int, pid: int, serial: Optional[str]) -> str:
|
||||
def _format_url(vid: int, pid: int, serial: str | None) -> str:
|
||||
base = f"usb:{vid:04x}:{pid:04x}"
|
||||
return f"{base}:{serial}" if serial else base
|
||||
|
||||
@@ -101,7 +101,7 @@ class AndroidSerialTransport:
|
||||
self._url = device
|
||||
self._addr = _UsbAddress.parse(device)
|
||||
self._baud_rate = baud_rate
|
||||
self._handle: Optional[int] = None # opaque token from the bridge
|
||||
self._handle: int | None = None # opaque token from the bridge
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -92,7 +92,7 @@ class BLEClient(LEDClient):
|
||||
write_with_response=self._protocol.write_with_response,
|
||||
)
|
||||
# AES key for Govee encrypted firmware — 16 raw bytes or None.
|
||||
self._aes_key: Optional[bytes] = None
|
||||
self._aes_key: bytes | None = None
|
||||
if ble_govee_key and ble_family == "govee":
|
||||
try:
|
||||
import binascii
|
||||
@@ -104,7 +104,7 @@ class BLEClient(LEDClient):
|
||||
except Exception as exc:
|
||||
logger.warning("Invalid Govee AES key — ignoring: %s", exc)
|
||||
self._last_write_at: float = 0.0
|
||||
self._last_color: Optional[Tuple[int, int, int, int]] = None
|
||||
self._last_color: Tuple[int, int, int, int] | None = None
|
||||
self._connected = False
|
||||
# Throttle "not connected" warnings so the send loop doesn't spam logs
|
||||
# at frame rate when a BLE connection drops silently.
|
||||
@@ -161,12 +161,12 @@ class BLEClient(LEDClient):
|
||||
return self._connected and self._transport.is_connected
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Average the strip to one color and write it — BLE protocols are whole-strip only."""
|
||||
@@ -249,7 +249,7 @@ class BLEClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client, # noqa: ARG003 — unused; kept for the LEDClient contract
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""BLE health isn't a passive check — a full GATT connect is the only signal.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, List, Tuple
|
||||
|
||||
from ledgrab.core.devices.ble_client import BLEClient, _strip_ble_scheme
|
||||
from ledgrab.core.devices.ble_protocols import (
|
||||
@@ -165,7 +165,7 @@ class BLEDeviceProvider(LEDDeviceProvider):
|
||||
]
|
||||
|
||||
|
||||
def get_ble_provider() -> Optional["BLEDeviceProvider"]:
|
||||
def get_ble_provider() -> "BLEDeviceProvider" | None:
|
||||
"""Return the registered BLE provider, or ``None`` if not registered."""
|
||||
from ledgrab.core.devices.led_client import get_provider
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.utils.platform import is_android
|
||||
@@ -65,7 +65,7 @@ class DiscoveredBLEDevice:
|
||||
|
||||
address: str
|
||||
name: str
|
||||
rssi: Optional[int]
|
||||
rssi: int | None
|
||||
service_uuids: tuple = ()
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ class BLETransport:
|
||||
except Exception as exc:
|
||||
logger.warning("BLE disconnect of %s raised: %s", self._address, exc)
|
||||
|
||||
async def write(self, data: bytes, char_uuid: Optional[str] = None) -> None:
|
||||
async def write(self, data: bytes, char_uuid: str | None = None) -> None:
|
||||
"""Send bytes to a GATT write characteristic.
|
||||
|
||||
Serialised through an internal lock — BLE stacks do not like
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -48,9 +48,9 @@ class ChromaClient(LEDClient):
|
||||
self._base_url = url or CHROMA_SDK_URL
|
||||
self._led_count = led_count
|
||||
self._chroma_device_type = chroma_device_type
|
||||
self._session_url: Optional[str] = None
|
||||
self._session_url: str | None = None
|
||||
self._connected = False
|
||||
self._heartbeat_task: Optional[asyncio.Task] = None
|
||||
self._heartbeat_task: asyncio.Task | None = None
|
||||
self._http_client = None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
@@ -135,7 +135,7 @@ class ChromaClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self.is_connected or not self._http_client:
|
||||
@@ -204,7 +204,7 @@ class ChromaClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check if Chroma SDK is running."""
|
||||
base = url or CHROMA_SDK_URL
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -58,12 +58,12 @@ class DDPClient:
|
||||
self._sequence = 0
|
||||
self._buses: List[BusConfig] = []
|
||||
# Pre-allocated RGBW buffer (resized on demand)
|
||||
self._rgbw_buf: Optional[np.ndarray] = None
|
||||
self._rgbw_buf: np.ndarray | None = None
|
||||
self._rgbw_buf_n: int = 0
|
||||
# Pre-allocated send buffer (header + payload). Sized lazily on first
|
||||
# send so we never allocate fresh bytes per frame on the hot path.
|
||||
self._send_buf: Optional[bytearray] = None
|
||||
self._send_view: Optional[memoryview] = None
|
||||
self._send_buf: bytearray | None = None
|
||||
self._send_view: memoryview | None = None
|
||||
|
||||
async def connect(self):
|
||||
"""Establish UDP connection."""
|
||||
|
||||
@@ -13,7 +13,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import socket
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import numpy as np
|
||||
@@ -69,7 +69,7 @@ class DDPLEDClient(LEDClient):
|
||||
led_count: int = 0,
|
||||
*,
|
||||
rgbw: bool = False,
|
||||
port: Optional[int] = None,
|
||||
port: int | None = None,
|
||||
destination_id: int = DEFAULT_DESTINATION_ID,
|
||||
color_order: int = DEFAULT_COLOR_ORDER,
|
||||
):
|
||||
@@ -80,7 +80,7 @@ class DDPLEDClient(LEDClient):
|
||||
self._rgbw = rgbw
|
||||
self._destination_id = destination_id & 0xFF
|
||||
self._color_order = color_order
|
||||
self._ddp: Optional[DDPClient] = None
|
||||
self._ddp: DDPClient | None = None
|
||||
self._connected = False
|
||||
|
||||
@property
|
||||
@@ -92,7 +92,7 @@ class DDPLEDClient(LEDClient):
|
||||
return self._port
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
@property
|
||||
@@ -151,7 +151,7 @@ class DDPLEDClient(LEDClient):
|
||||
# uint16 scratch avoids overflow; integer divide keeps everything in uint8.
|
||||
return ((pixels.astype(np.uint16) * brightness) // 255).astype(np.uint8)
|
||||
|
||||
def _as_numpy(self, pixels: Union[List[Tuple[int, int, int]], np.ndarray]) -> np.ndarray:
|
||||
def _as_numpy(self, pixels: List[Tuple[int, int, int]] | np.ndarray) -> np.ndarray:
|
||||
if isinstance(pixels, np.ndarray):
|
||||
arr = pixels
|
||||
else:
|
||||
@@ -164,7 +164,7 @@ class DDPLEDClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self.is_connected:
|
||||
@@ -176,7 +176,7 @@ class DDPLEDClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
if not self.is_connected or self._ddp is None:
|
||||
@@ -189,7 +189,7 @@ class DDPLEDClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""DDP is connectionless UDP — health = host resolves + port reachable.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ that maps the flat Device storage model to the right typed config.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Literal, Optional, Union
|
||||
from typing import List, Literal
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -43,13 +43,13 @@ class DDPConfig(BaseDeviceConfig):
|
||||
@dataclass(frozen=True)
|
||||
class AdalightConfig(BaseDeviceConfig):
|
||||
device_type: Literal["adalight"] = "adalight"
|
||||
baud_rate: Optional[int] = None
|
||||
baud_rate: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmbiLEDConfig(BaseDeviceConfig):
|
||||
device_type: Literal["ambiled"] = "ambiled"
|
||||
baud_rate: Optional[int] = None
|
||||
baud_rate: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -63,7 +63,7 @@ class DMXConfig(BaseDeviceConfig):
|
||||
@dataclass(frozen=True)
|
||||
class ESPNowConfig(BaseDeviceConfig):
|
||||
device_type: Literal["espnow"] = "espnow"
|
||||
baud_rate: Optional[int] = None
|
||||
baud_rate: int | None = None
|
||||
espnow_peer_mac: str = ""
|
||||
espnow_channel: int = 1
|
||||
|
||||
@@ -217,29 +217,29 @@ class USBHIDConfig(BaseDeviceConfig):
|
||||
device_type: Literal["usbhid"] = "usbhid"
|
||||
|
||||
|
||||
DeviceConfig = Union[
|
||||
WLEDConfig,
|
||||
DDPConfig,
|
||||
YeelightConfig,
|
||||
WiZConfig,
|
||||
LIFXConfig,
|
||||
GoveeConfig,
|
||||
OPCConfig,
|
||||
NanoleafConfig,
|
||||
AdalightConfig,
|
||||
AmbiLEDConfig,
|
||||
DMXConfig,
|
||||
ESPNowConfig,
|
||||
HueConfig,
|
||||
SPIConfig,
|
||||
ChromaConfig,
|
||||
GameSenseConfig,
|
||||
BLEConfig,
|
||||
GroupConfig,
|
||||
MQTTConfig,
|
||||
WSConfig,
|
||||
USBHIDConfig,
|
||||
OpenRGBConfig,
|
||||
MockConfig,
|
||||
DemoConfig,
|
||||
]
|
||||
DeviceConfig = (
|
||||
WLEDConfig
|
||||
| DDPConfig
|
||||
| YeelightConfig
|
||||
| WiZConfig
|
||||
| LIFXConfig
|
||||
| GoveeConfig
|
||||
| OPCConfig
|
||||
| NanoleafConfig
|
||||
| AdalightConfig
|
||||
| AmbiLEDConfig
|
||||
| DMXConfig
|
||||
| ESPNowConfig
|
||||
| HueConfig
|
||||
| SPIConfig
|
||||
| ChromaConfig
|
||||
| GameSenseConfig
|
||||
| BLEConfig
|
||||
| GroupConfig
|
||||
| MQTTConfig
|
||||
| WSConfig
|
||||
| USBHIDConfig
|
||||
| OpenRGBConfig
|
||||
| MockConfig
|
||||
| DemoConfig
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Callable, Dict
|
||||
|
||||
from zeroconf import ServiceStateChange
|
||||
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
|
||||
@@ -74,9 +74,9 @@ class DiscoveryWatcher:
|
||||
self._device_store = device_store
|
||||
self._fire_event = fire_event
|
||||
|
||||
self._aiozc: Optional[AsyncZeroconf] = None
|
||||
self._browser: Optional[AsyncServiceBrowser] = None
|
||||
self._serial_task: Optional[asyncio.Task] = None
|
||||
self._aiozc: AsyncZeroconf | None = None
|
||||
self._browser: AsyncServiceBrowser | None = None
|
||||
self._serial_task: asyncio.Task | None = None
|
||||
self._running = False
|
||||
self._started_at: float = 0.0
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import struct
|
||||
import uuid
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -40,7 +40,7 @@ class DMXClient(LEDClient):
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: Optional[int] = None,
|
||||
port: int | None = None,
|
||||
led_count: int = 1,
|
||||
protocol: str = "artnet",
|
||||
start_universe: int = 0,
|
||||
@@ -123,7 +123,7 @@ class DMXClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self._transport:
|
||||
@@ -133,7 +133,7 @@ class DMXClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
if not self._transport:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -59,7 +59,7 @@ class ESPNowClient(LEDClient):
|
||||
self,
|
||||
url: str = "",
|
||||
led_count: int = 0,
|
||||
baud_rate: Optional[int] = None,
|
||||
baud_rate: int | None = None,
|
||||
espnow_peer_mac: str = "FF:FF:FF:FF:FF:FF",
|
||||
espnow_channel: int = 1,
|
||||
**kwargs,
|
||||
@@ -109,7 +109,7 @@ class ESPNowClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
if not self.is_connected:
|
||||
@@ -129,7 +129,7 @@ class ESPNowClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self.is_connected:
|
||||
@@ -146,7 +146,7 @@ class ESPNowClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check if the serial port is available without opening it."""
|
||||
port, _baud = parse_serial_url(url)
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import os
|
||||
import platform
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -19,7 +19,7 @@ GAME_DISPLAY_NAME = "LedGrab"
|
||||
EVENT_NAME = "PIXEL_DATA"
|
||||
|
||||
|
||||
def _get_gamesense_address() -> Optional[str]:
|
||||
def _get_gamesense_address() -> str | None:
|
||||
"""Discover the SteelSeries GameSense address from coreProps.json."""
|
||||
if platform.system() == "Windows":
|
||||
props_path = os.path.join(
|
||||
@@ -77,7 +77,7 @@ class GameSenseClient(LEDClient):
|
||||
self._gs_device_type = gamesense_device_type
|
||||
self._connected = False
|
||||
self._http_client = None
|
||||
self._base_url: Optional[str] = None
|
||||
self._base_url: str | None = None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
import httpx
|
||||
@@ -174,7 +174,7 @@ class GameSenseClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self.is_connected or not self._http_client:
|
||||
@@ -227,7 +227,7 @@ class GameSenseClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check if SteelSeries Engine is running."""
|
||||
address = url.replace("gamesense://", "").strip() if url else None
|
||||
|
||||
@@ -21,7 +21,7 @@ import json
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import numpy as np
|
||||
@@ -105,8 +105,8 @@ class GoveeClient(LEDClient):
|
||||
self._port = GOVEE_CONTROL_PORT
|
||||
self._led_count = led_count
|
||||
self._min_interval_s = max(0.0, min_interval_s)
|
||||
self._transport: Optional[asyncio.DatagramTransport] = None
|
||||
self._protocol: Optional[_GoveeProtocol] = None
|
||||
self._transport: asyncio.DatagramTransport | None = None
|
||||
self._protocol: _GoveeProtocol | None = None
|
||||
self._connected = False
|
||||
self._next_tx_at: float = 0.0
|
||||
|
||||
@@ -123,7 +123,7 @@ class GoveeClient(LEDClient):
|
||||
return self._connected and self._transport is not None
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
@@ -185,7 +185,7 @@ class GoveeClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Average the strip → colorwc with the resulting RGB."""
|
||||
@@ -206,7 +206,7 @@ class GoveeClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
"""Synchronous variant for the hot loop."""
|
||||
@@ -248,7 +248,7 @@ class GoveeClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Send devStatus and wait briefly for a reply on port 4002.
|
||||
|
||||
@@ -298,7 +298,7 @@ class GoveeClient(LEDClient):
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def _parse_scan_reply(raw: bytes) -> Optional[dict]:
|
||||
def _parse_scan_reply(raw: bytes) -> dict | None:
|
||||
"""Parse a Govee scan reply into a flat metadata dict.
|
||||
|
||||
Govee sends ``{"msg": {"cmd": "scan", "data": {"ip": ..., "device": ...,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -82,14 +82,14 @@ class GroupLEDClient(LEDClient):
|
||||
return self._connected and all(c.is_connected for c, _ in self._children)
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
if self._group_mode == "sequence":
|
||||
return self._total_led_count
|
||||
return None # independent mode uses user-specified led_count
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self._children:
|
||||
@@ -150,7 +150,7 @@ class GroupLEDClient(LEDClient):
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
return all(r is True for r in results if not isinstance(r, Exception))
|
||||
|
||||
async def snapshot_device_state(self) -> Optional[dict]:
|
||||
async def snapshot_device_state(self) -> dict | None:
|
||||
"""Snapshot all children's states."""
|
||||
states = {}
|
||||
for i, (client, _) in enumerate(self._children):
|
||||
@@ -159,7 +159,7 @@ class GroupLEDClient(LEDClient):
|
||||
states[i] = state
|
||||
return states if states else None
|
||||
|
||||
async def restore_device_state(self, state: Optional[dict]) -> None:
|
||||
async def restore_device_state(self, state: dict | None) -> None:
|
||||
"""Restore all children's states."""
|
||||
if not state:
|
||||
return
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import socket
|
||||
import struct
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -79,7 +79,7 @@ class HueClient(LEDClient):
|
||||
self._username = hue_username
|
||||
self._client_key = hue_client_key
|
||||
self._group_id = hue_entertainment_group_id
|
||||
self._sock: Optional[socket.socket] = None
|
||||
self._sock: socket.socket | None = None
|
||||
self._connected = False
|
||||
self._sequence = 0
|
||||
self._dtls_sock = None
|
||||
@@ -173,7 +173,7 @@ class HueClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
if not self._connected:
|
||||
@@ -197,7 +197,7 @@ class HueClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self._connected:
|
||||
@@ -210,7 +210,7 @@ class HueClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check if the Hue bridge is reachable."""
|
||||
bridge_ip = url.replace("hue://", "").rstrip("/")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -17,7 +17,7 @@ class ProviderDeps:
|
||||
"""Runtime dependencies injected into every provider.create_client() call."""
|
||||
|
||||
device_store: Optional["DeviceStore"] = None
|
||||
mqtt_manager: Optional[object] = None # MQTTManager (avoid circular import)
|
||||
mqtt_manager: object | None = None # MQTTManager (avoid circular import)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -25,16 +25,16 @@ class DeviceHealth:
|
||||
"""Health check result for an LED device."""
|
||||
|
||||
online: bool = False
|
||||
latency_ms: Optional[float] = None
|
||||
last_checked: Optional[datetime] = None
|
||||
latency_ms: float | None = None
|
||||
last_checked: datetime | None = None
|
||||
# Device-reported metadata (populated by type-specific health check)
|
||||
device_name: Optional[str] = None
|
||||
device_version: Optional[str] = None
|
||||
device_led_count: Optional[int] = None
|
||||
device_rgbw: Optional[bool] = None
|
||||
device_led_type: Optional[str] = None
|
||||
device_fps: Optional[int] = None
|
||||
error: Optional[str] = None
|
||||
device_name: str | None = None
|
||||
device_version: str | None = None
|
||||
device_led_count: int | None = None
|
||||
device_rgbw: bool | None = None
|
||||
device_led_type: str | None = None
|
||||
device_fps: int | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class PairingNotReady(Exception):
|
||||
@@ -55,12 +55,12 @@ class DiscoveredDevice:
|
||||
device_type: str
|
||||
ip: str
|
||||
mac: str
|
||||
led_count: Optional[int]
|
||||
version: Optional[str]
|
||||
led_count: int | None
|
||||
version: str | None
|
||||
# Optional provider-specific detected protocol identifier (e.g. BLE family
|
||||
# like "sp110e" / "triones" / "zengge" / "govee"). Surfaced so the UI can
|
||||
# preselect the right sub-type when the user adds a discovered device.
|
||||
ble_family: Optional[str] = None
|
||||
ble_family: str | None = None
|
||||
|
||||
|
||||
class LEDClient(ABC):
|
||||
@@ -99,7 +99,7 @@ class LEDClient(ABC):
|
||||
@abstractmethod
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Send pixel colors to the LED device (async).
|
||||
@@ -127,11 +127,11 @@ class LEDClient(ABC):
|
||||
raise NotImplementedError("send_pixels_fast not supported for this device type")
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
"""Actual LED count discovered after connect(). None if not available."""
|
||||
return None
|
||||
|
||||
async def snapshot_device_state(self) -> Optional[dict]:
|
||||
async def snapshot_device_state(self) -> dict | None:
|
||||
"""Snapshot device state before streaming starts.
|
||||
|
||||
Override in subclasses that need to save/restore state around streaming.
|
||||
@@ -139,7 +139,7 @@ class LEDClient(ABC):
|
||||
"""
|
||||
return None
|
||||
|
||||
async def restore_device_state(self, state: Optional[dict]) -> None:
|
||||
async def restore_device_state(self, state: dict | None) -> None:
|
||||
"""Restore device state after streaming stops.
|
||||
|
||||
Args:
|
||||
@@ -152,7 +152,7 @@ class LEDClient(ABC):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check device health without a full client connection.
|
||||
|
||||
@@ -309,7 +309,7 @@ async def check_device_health(
|
||||
device_type: str,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Factory: dispatch health check to the right provider."""
|
||||
return await get_provider(device_type).check_health(url, http_client, prev_health)
|
||||
|
||||
@@ -22,7 +22,7 @@ import socket
|
||||
import struct
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import numpy as np
|
||||
@@ -142,7 +142,7 @@ def _build_set_power_payload(on: bool, duration_ms: int = 0) -> bytes:
|
||||
return struct.pack("<HI", 65535 if on else 0, duration_ms & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def _parse_state_service_reply(raw: bytes) -> Optional[dict]:
|
||||
def _parse_state_service_reply(raw: bytes) -> dict | None:
|
||||
"""Parse a LIFX StateService (discovery) reply.
|
||||
|
||||
Returns ``{"mac": "aabbccddeeff", "service": int, "port": int}`` or
|
||||
@@ -192,8 +192,8 @@ class LIFXClient(LEDClient):
|
||||
self._port = port
|
||||
self._led_count = led_count
|
||||
self._min_interval_s = max(0.0, min_interval_s)
|
||||
self._transport: Optional[asyncio.DatagramTransport] = None
|
||||
self._protocol: Optional[_LIFXProtocol] = None
|
||||
self._transport: asyncio.DatagramTransport | None = None
|
||||
self._protocol: _LIFXProtocol | None = None
|
||||
self._connected = False
|
||||
self._next_tx_at: float = 0.0
|
||||
self._sequence: int = 0
|
||||
@@ -211,7 +211,7 @@ class LIFXClient(LEDClient):
|
||||
return self._connected and self._transport is not None
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
@@ -257,7 +257,7 @@ class LIFXClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Average the strip → HSBK → SetColor."""
|
||||
@@ -279,7 +279,7 @@ class LIFXClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
"""Synchronous variant — same shape, runs on the hot loop."""
|
||||
@@ -318,7 +318,7 @@ class LIFXClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Send GetService and wait briefly for a StateService reply."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -50,7 +50,7 @@ class MockClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self._connected:
|
||||
@@ -64,7 +64,7 @@ class MockClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
return DeviceHealth(
|
||||
online=True,
|
||||
|
||||
@@ -6,7 +6,7 @@ an ``mqtt_source_id`` so different devices can talk to different brokers.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -81,7 +81,7 @@ class MQTTLEDClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if self._runtime is None:
|
||||
@@ -111,7 +111,7 @@ class MQTTLEDClient(LEDClient):
|
||||
prev_health=None,
|
||||
*,
|
||||
mqtt_manager=None,
|
||||
mqtt_source_id: Optional[str] = None,
|
||||
mqtt_source_id: str | None = None,
|
||||
) -> DeviceHealth:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
@@ -137,7 +137,7 @@ class NanoleafClient(LEDClient):
|
||||
self._led_count = led_count
|
||||
self._min_interval_s = max(0.0, min_interval_s)
|
||||
self._request_timeout_s = request_timeout_s
|
||||
self._http: Optional[httpx.AsyncClient] = None
|
||||
self._http: httpx.AsyncClient | None = None
|
||||
self._connected = False
|
||||
self._next_tx_at: float = 0.0
|
||||
|
||||
@@ -156,7 +156,7 @@ class NanoleafClient(LEDClient):
|
||||
return self._connected and self._http is not None
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
def _state_url(self) -> str:
|
||||
@@ -194,7 +194,7 @@ class NanoleafClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Average the strip and PUT a single HSB state update."""
|
||||
@@ -252,7 +252,7 @@ class NanoleafClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""GET ``/api/v1/<token>/info``. Without a token we can't authenticate,
|
||||
so we fall back to GET ``/api/v1`` which returns 401 when the host is
|
||||
|
||||
@@ -21,7 +21,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import numpy as np
|
||||
@@ -82,8 +82,8 @@ class OPCClient(LEDClient):
|
||||
self._led_count = led_count
|
||||
self._channel = channel & 0xFF
|
||||
self._connect_timeout_s = connect_timeout_s
|
||||
self._writer: Optional[asyncio.StreamWriter] = None
|
||||
self._reader: Optional[asyncio.StreamReader] = None
|
||||
self._writer: asyncio.StreamWriter | None = None
|
||||
self._reader: asyncio.StreamReader | None = None
|
||||
self._connected = False
|
||||
|
||||
@property
|
||||
@@ -103,7 +103,7 @@ class OPCClient(LEDClient):
|
||||
return self._connected and self._writer is not None
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
@@ -144,7 +144,7 @@ class OPCClient(LEDClient):
|
||||
return np.zeros_like(pixels)
|
||||
return ((pixels.astype(np.uint16) * brightness) // 255).astype(np.uint8)
|
||||
|
||||
def _as_numpy(self, pixels: Union[List[Tuple[int, int, int]], np.ndarray]) -> np.ndarray:
|
||||
def _as_numpy(self, pixels: List[Tuple[int, int, int]] | np.ndarray) -> np.ndarray:
|
||||
if isinstance(pixels, np.ndarray):
|
||||
arr = pixels
|
||||
else:
|
||||
@@ -159,7 +159,7 @@ class OPCClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self.is_connected:
|
||||
@@ -175,7 +175,7 @@ class OPCClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
"""Synchronous hot-path write. Drain runs implicitly when the OS buffer
|
||||
@@ -197,7 +197,7 @@ class OPCClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Open a TCP connection and close it. OPC has no protocol-level
|
||||
ping; reachable TCP is the strongest signal we get."""
|
||||
|
||||
@@ -5,7 +5,7 @@ import socket
|
||||
import struct
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -24,7 +24,7 @@ def parse_openrgb_url(url: str) -> Tuple[str, int, int, List[str]]:
|
||||
When *zone_names* is non-empty, only LEDs in those zones are addressed.
|
||||
Multiple zones are separated by ``+``.
|
||||
"""
|
||||
zones_str: Optional[str] = None
|
||||
zones_str: str | None = None
|
||||
|
||||
if url.startswith("openrgb://"):
|
||||
url = url[len("openrgb://") :]
|
||||
@@ -87,16 +87,16 @@ class OpenRGBLEDClient(LEDClient):
|
||||
self._client: Any = None # openrgb.OpenRGBClient
|
||||
self._device: Any = None # openrgb.Device
|
||||
self._connected = False
|
||||
self._device_name: Optional[str] = None
|
||||
self._device_led_count: Optional[int] = None
|
||||
self._device_name: str | None = None
|
||||
self._device_led_count: int | None = None
|
||||
|
||||
# Background sender thread — decouples processing loop from blocking TCP writes
|
||||
self._send_lock = threading.Lock()
|
||||
self._send_event = threading.Event()
|
||||
self._send_pending: Optional[Tuple[np.ndarray, int]] = None # (pixels, brightness)
|
||||
self._send_thread: Optional[threading.Thread] = None
|
||||
self._send_pending: Tuple[np.ndarray, int] | None = None # (pixels, brightness)
|
||||
self._send_thread: threading.Thread | None = None
|
||||
self._send_stop = threading.Event()
|
||||
self._last_sent_pixels: Optional[np.ndarray] = None
|
||||
self._last_sent_pixels: np.ndarray | None = None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Connect to OpenRGB server and access the target device."""
|
||||
@@ -207,12 +207,12 @@ class OpenRGBLEDClient(LEDClient):
|
||||
return self._connected and self._client is not None
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._device_led_count
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Send pixel colors to the OpenRGB device (async wrapper)."""
|
||||
@@ -232,7 +232,7 @@ class OpenRGBLEDClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
"""Non-blocking fire-and-forget send for the processing hot loop.
|
||||
@@ -377,7 +377,7 @@ class OpenRGBLEDClient(LEDClient):
|
||||
finally:
|
||||
comms.lock.release()
|
||||
|
||||
async def snapshot_device_state(self) -> Optional[dict]:
|
||||
async def snapshot_device_state(self) -> dict | None:
|
||||
"""Save the active mode index before streaming."""
|
||||
if self._device is None:
|
||||
return None
|
||||
@@ -387,7 +387,7 @@ class OpenRGBLEDClient(LEDClient):
|
||||
logger.warning(f"Could not snapshot OpenRGB device state: {e}")
|
||||
return None
|
||||
|
||||
async def restore_device_state(self, state: Optional[dict]) -> None:
|
||||
async def restore_device_state(self, state: dict | None) -> None:
|
||||
"""Restore the original mode after streaming stops."""
|
||||
if not state or self._device is None:
|
||||
return
|
||||
@@ -404,7 +404,7 @@ class OpenRGBLEDClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check OpenRGB server reachability via raw TCP socket connect.
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ zones individually, so it doesn't reduce.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def average_color(
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
) -> Tuple[int, int, int]:
|
||||
"""Reduce an N-pixel strip to one average RGB triple.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ desktop COM ports on desktop, USB devices on Android.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Protocol
|
||||
from typing import List, Protocol
|
||||
|
||||
from ledgrab.utils import get_logger
|
||||
from ledgrab.utils.platform import is_android
|
||||
@@ -119,7 +119,7 @@ def port_exists(device: str) -> bool:
|
||||
|
||||
def open_transport(
|
||||
url: str,
|
||||
baud_rate: Optional[int] = None,
|
||||
baud_rate: int | None = None,
|
||||
timeout: float = 1.0,
|
||||
) -> SerialTransport:
|
||||
"""Construct an unopened transport for ``url``. Caller invokes ``open()``."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -155,7 +155,7 @@ class SPIClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
if not self._connected:
|
||||
@@ -205,7 +205,7 @@ class SPIClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self._connected:
|
||||
@@ -222,7 +222,7 @@ class SPIClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check if the SPI/GPIO device is accessible."""
|
||||
import platform
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -90,7 +90,7 @@ class USBHIDClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self.is_connected:
|
||||
@@ -144,7 +144,7 @@ class USBHIDClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Check if the HID device is present."""
|
||||
try:
|
||||
|
||||
@@ -17,7 +17,7 @@ import json
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import numpy as np
|
||||
@@ -82,8 +82,8 @@ class WiZClient(LEDClient):
|
||||
self._port = port
|
||||
self._led_count = led_count
|
||||
self._min_interval_s = max(0.0, min_interval_s)
|
||||
self._transport: Optional[asyncio.DatagramTransport] = None
|
||||
self._protocol: Optional[_WiZProtocol] = None
|
||||
self._transport: asyncio.DatagramTransport | None = None
|
||||
self._protocol: _WiZProtocol | None = None
|
||||
self._connected = False
|
||||
self._next_tx_at: float = 0.0
|
||||
|
||||
@@ -100,7 +100,7 @@ class WiZClient(LEDClient):
|
||||
return self._connected and self._transport is not None
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
@@ -137,7 +137,7 @@ class WiZClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Average the pixel strip to one color and push ``setPilot``."""
|
||||
@@ -161,7 +161,7 @@ class WiZClient(LEDClient):
|
||||
|
||||
def send_pixels_fast(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> None:
|
||||
"""Synchronous variant for the hot path. Same shape as send_pixels."""
|
||||
@@ -205,7 +205,7 @@ class WiZClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Send a getPilot and wait briefly for any reply on a one-shot socket."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Tuple, Optional, Dict, Any
|
||||
from typing import List, Tuple, Dict, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
@@ -106,10 +106,10 @@ class WLEDClient(LEDClient):
|
||||
parsed = urlparse(self.url)
|
||||
self.host = parsed.hostname or parsed.netloc.split(":")[0]
|
||||
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
self._ddp_client: Optional[DDPClient] = None
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
self._ddp_client: DDPClient | None = None
|
||||
self._connected = False
|
||||
self._pre_connect_state: Optional[dict] = None
|
||||
self._pre_connect_state: dict | None = None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Establish connection to WLED device.
|
||||
@@ -208,7 +208,7 @@ class WLEDClient(LEDClient):
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
json_data: Dict[str, Any] | None = None,
|
||||
retry: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
"""Make HTTP request to WLED device with retry logic.
|
||||
@@ -498,7 +498,7 @@ class WLEDClient(LEDClient):
|
||||
|
||||
# ===== LEDClient abstraction methods =====
|
||||
|
||||
async def snapshot_device_state(self) -> Optional[dict]:
|
||||
async def snapshot_device_state(self) -> dict | None:
|
||||
"""Snapshot WLED state (on, lor, AudioReactive).
|
||||
|
||||
If connect() already captured a pre-mutation snapshot, returns that
|
||||
@@ -525,7 +525,7 @@ class WLEDClient(LEDClient):
|
||||
logger.warning(f"Could not snapshot WLED state: {e}")
|
||||
return None
|
||||
|
||||
async def restore_device_state(self, state: Optional[dict]) -> None:
|
||||
async def restore_device_state(self, state: dict | None) -> None:
|
||||
"""Restore WLED state after streaming."""
|
||||
if not state:
|
||||
return
|
||||
@@ -541,7 +541,7 @@ class WLEDClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""WLED health check via GET /json/info (+ /json/cfg for LED type)."""
|
||||
url = url.rstrip("/")
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||
|
||||
import httpx
|
||||
from zeroconf import ServiceStateChange
|
||||
@@ -47,7 +47,7 @@ class WLEDDeviceProvider(LEDDeviceProvider):
|
||||
"""Provider for WLED LED controllers."""
|
||||
|
||||
def __init__(self):
|
||||
self._http_client: Optional[httpx.AsyncClient] = None
|
||||
self._http_client: httpx.AsyncClient | None = None
|
||||
self._client_lock = asyncio.Lock()
|
||||
# Per-base-URL state cache: base -> (expires_at, json_state_dict)
|
||||
self._state_cache: Dict[str, tuple] = {}
|
||||
@@ -186,7 +186,7 @@ class WLEDDeviceProvider(LEDDeviceProvider):
|
||||
|
||||
async def _enrich_device(
|
||||
self, url: str, fallback_name: str
|
||||
) -> tuple[str, Optional[str], Optional[int], str]:
|
||||
) -> tuple[str, str | None, int | None, str]:
|
||||
"""Probe a WLED device's /json/info to get name, version, LED count, MAC.
|
||||
|
||||
Reuses the shared HTTP client so discovery probes share connection-pool state.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -85,7 +85,7 @@ class WSLEDClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
if not self._connected:
|
||||
@@ -123,7 +123,7 @@ class WSLEDClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
return DeviceHealth(
|
||||
online=True,
|
||||
|
||||
@@ -22,7 +22,7 @@ import json
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import numpy as np
|
||||
@@ -77,8 +77,8 @@ class YeelightClient(LEDClient):
|
||||
self._led_count = led_count
|
||||
self._min_interval_s = max(0.0, min_interval_s)
|
||||
self._connect_timeout_s = connect_timeout_s
|
||||
self._reader: Optional[asyncio.StreamReader] = None
|
||||
self._writer: Optional[asyncio.StreamWriter] = None
|
||||
self._reader: asyncio.StreamReader | None = None
|
||||
self._writer: asyncio.StreamWriter | None = None
|
||||
self._connected = False
|
||||
self._next_tx_at: float = 0.0
|
||||
self._req_id: int = 0
|
||||
@@ -93,7 +93,7 @@ class YeelightClient(LEDClient):
|
||||
return self._connected and self._writer is not None
|
||||
|
||||
@property
|
||||
def device_led_count(self) -> Optional[int]:
|
||||
def device_led_count(self) -> int | None:
|
||||
return self._led_count or None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
@@ -141,7 +141,7 @@ class YeelightClient(LEDClient):
|
||||
|
||||
async def send_pixels(
|
||||
self,
|
||||
pixels: Union[List[Tuple[int, int, int]], np.ndarray],
|
||||
pixels: List[Tuple[int, int, int]] | np.ndarray,
|
||||
brightness: int = 255,
|
||||
) -> bool:
|
||||
"""Average the pixel strip to one color and ``set_rgb``.
|
||||
@@ -185,7 +185,7 @@ class YeelightClient(LEDClient):
|
||||
cls,
|
||||
url: str,
|
||||
http_client,
|
||||
prev_health: Optional[DeviceHealth] = None,
|
||||
prev_health: DeviceHealth | None = None,
|
||||
) -> DeviceHealth:
|
||||
"""Health check: open the TCP socket to the bulb and close it."""
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -230,7 +230,7 @@ _DISCOVER_REQUEST = (
|
||||
).encode("ascii")
|
||||
|
||||
|
||||
def _parse_ssdp_response(raw: bytes) -> Optional[dict]:
|
||||
def _parse_ssdp_response(raw: bytes) -> dict | None:
|
||||
"""Parse a Yeelight discovery response into a ``{header: value}`` dict.
|
||||
|
||||
Returns ``None`` when the payload doesn't look like a Yeelight reply
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Auto-crop postprocessing filter."""
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -52,7 +52,7 @@ class AutoCropFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
threshold = self.options.get("threshold", 15)
|
||||
min_bar_size = self.options.get("min_bar_size", 20)
|
||||
min_aspect_ratio = float(self.options.get("min_aspect_ratio", 0.0))
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -23,8 +23,8 @@ class FilterOptionDef:
|
||||
min_value: Any
|
||||
max_value: Any
|
||||
step: Any
|
||||
choices: Optional[List[Dict[str, str]]] = None # for "select": [{value, label}]
|
||||
max_length: Optional[int] = None # for "string" type
|
||||
choices: List[Dict[str, str]] | None = None # for "select": [{value, label}]
|
||||
max_length: int | None = None # for "string" type
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = {
|
||||
@@ -76,7 +76,7 @@ class PostprocessingFilter(ABC):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def process_image(self, image: np.ndarray, image_pool: "ImagePool") -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: "ImagePool") -> np.ndarray | None:
|
||||
"""Process image.
|
||||
|
||||
Args:
|
||||
@@ -89,7 +89,7 @@ class PostprocessingFilter(ABC):
|
||||
"""
|
||||
...
|
||||
|
||||
def process_strip(self, strip: np.ndarray) -> Optional[np.ndarray]:
|
||||
def process_strip(self, strip: np.ndarray) -> np.ndarray | None:
|
||||
"""Process a 1D LED strip array (N, 3) uint8.
|
||||
|
||||
Default implementation reshapes to (1, N, 3), calls process_image
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Brightness postprocessing filter."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -36,7 +36,7 @@ class BrightnessFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
if self.options["value"] == 1.0:
|
||||
return None
|
||||
image[:] = self._lut[image]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Color correction postprocessing filter."""
|
||||
|
||||
import math
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -118,7 +118,7 @@ class ColorCorrectionFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
if self._is_neutral:
|
||||
return None
|
||||
image[:] = self._lut[image]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Contrast postprocessing filter."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -42,7 +42,7 @@ class ContrastFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
if self.options["value"] == 1.0:
|
||||
return None
|
||||
image[:] = self._lut[image]
|
||||
|
||||
@@ -5,7 +5,7 @@ instantiated at runtime: the store expands it into the referenced
|
||||
template's filters when building the processing pipeline.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -37,10 +37,10 @@ class CSSFilterTemplateFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
# Never called — expanded at pipeline build time.
|
||||
return None
|
||||
|
||||
def process_strip(self, strip: np.ndarray) -> Optional[np.ndarray]:
|
||||
def process_strip(self, strip: np.ndarray) -> np.ndarray | None:
|
||||
# Never called — expanded at pipeline build time.
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Downscaler postprocessing filter."""
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -32,7 +32,7 @@ class DownscalerFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
factor = self.options["factor"]
|
||||
if factor >= 1.0:
|
||||
return None
|
||||
|
||||
@@ -5,7 +5,7 @@ instantiated at runtime: ``LiveStreamManager`` expands it into the
|
||||
referenced template's filters when building the processing pipeline.
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -37,6 +37,6 @@ class FilterTemplateFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
# Never called — expanded at pipeline build time by LiveStreamManager.
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Flip postprocessing filter."""
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -40,7 +40,7 @@ class FlipFilter(PostprocessingFilter):
|
||||
),
|
||||
]
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
h = self.options.get("horizontal", False)
|
||||
v = self.options.get("vertical", False)
|
||||
if not h and not v:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Frame interpolation postprocessing filter."""
|
||||
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -38,21 +38,21 @@ class FrameInterpolationFilter(PostprocessingFilter):
|
||||
|
||||
def __init__(self, options: Dict[str, Any]):
|
||||
super().__init__(options)
|
||||
self._frame_a: Optional[np.ndarray] = None # frame N-1
|
||||
self._frame_b: Optional[np.ndarray] = None # frame N (latest source)
|
||||
self._frame_a: np.ndarray | None = None # frame N-1
|
||||
self._frame_b: np.ndarray | None = None # frame N (latest source)
|
||||
self._time_a: float = 0.0
|
||||
self._time_b: float = 0.0
|
||||
self._sig_b: Optional[bytes] = None # 64-byte signature of frame_b input
|
||||
self._sig_b: bytes | None = None # 64-byte signature of frame_b input
|
||||
# Pre-allocated uint16 scratch buffers for blending
|
||||
self._u16_a: Optional[np.ndarray] = None
|
||||
self._u16_b: Optional[np.ndarray] = None
|
||||
self._blend_shape: Optional[tuple] = None
|
||||
self._u16_a: np.ndarray | None = None
|
||||
self._u16_b: np.ndarray | None = None
|
||||
self._blend_shape: tuple | None = None
|
||||
|
||||
@classmethod
|
||||
def get_options_schema(cls) -> List[FilterOptionDef]:
|
||||
return []
|
||||
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> Optional[np.ndarray]:
|
||||
def process_image(self, image: np.ndarray, image_pool: ImagePool) -> np.ndarray | None:
|
||||
"""Return interpolated blend on idle ticks; update state on new source frames.
|
||||
|
||||
Returns:
|
||||
@@ -61,11 +61,11 @@ class FrameInterpolationFilter(PostprocessingFilter):
|
||||
"""
|
||||
return self._blend(image, lambda shape: image_pool.acquire(*shape))
|
||||
|
||||
def process_strip(self, strip: np.ndarray) -> Optional[np.ndarray]:
|
||||
def process_strip(self, strip: np.ndarray) -> np.ndarray | None:
|
||||
"""Frame interpolation for 1D LED strips — allocates directly."""
|
||||
return self._blend(strip, lambda shape: np.empty(shape, dtype=np.uint8))
|
||||
|
||||
def _blend(self, data: np.ndarray, alloc_fn) -> Optional[np.ndarray]:
|
||||
def _blend(self, data: np.ndarray, alloc_fn) -> np.ndarray | None:
|
||||
"""Shared blend logic for both images and strips."""
|
||||
now = time.perf_counter()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user